[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/blank.yml",
    "content": "name: Blank Issue\ndescription: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL.\n\nbody:\n    - type: markdown\n      attributes:\n          value: |\n              ![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)\n\n              GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.\n\n    - type: textarea\n      id: content\n      attributes:\n          label: Content\n      validations:\n          required: true\n\n    - type: checkboxes\n      id: agreement-check\n      attributes:\n          label: Request Agreement\n          options:\n              - label: I have read the requirements for opening an issue above\n                required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug/Crash Report\ndescription: Create a bug or crash report for Vencord. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL.\nlabels: [bug]\ntitle: \"[Bug] <title>\"\n\nbody:\n    - type: markdown\n      attributes:\n          value: |\n              ![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)\n\n              GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.\n\n    - type: textarea\n      id: bug-description\n      attributes:\n          label: What happens when the bug or crash occurs?\n          description: Where does this bug or crash occur, when does it occur, etc.\n          placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...\n      validations:\n          required: true\n\n    - type: textarea\n      id: expected-behaviour\n      attributes:\n          label: What is the expected behaviour?\n          description: Simply detail what the expected behaviour is.\n          placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...\n      validations:\n          required: true\n\n    - type: textarea\n      id: steps-to-take\n      attributes:\n          label: How do you recreate this bug or crash?\n          description: Give us a list of steps in order to recreate the bug or crash.\n          placeholder: |\n              1. Do ...\n              2. Then ...\n              3. Do this ..., ... and then ...\n              4. Observe \"the bug\" or \"the crash\"\n      validations:\n          required: true\n\n    - type: textarea\n      id: crash-log\n      attributes:\n          label: Errors\n          description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the \"```\".\n          value: |\n              ```\n              Replace this text with your crash-log.\n              ```\n      validations:\n          required: false\n\n    - type: checkboxes\n      id: agreement-check\n      attributes:\n          label: Request Agreement\n          description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable\n          options:\n              - label: I am using Discord Stable or tried on Stable and this bug happens there as well\n                required: true\n              - label: I am a Vencord Developer\n                required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Vencord Support Server\n    url: https://discord.gg/D9uwnFnqmd\n    about: If you need help regarding Vencord, please join our support server!\n  - name: Vencord Installer\n    url: https://github.com/Vencord/Installer\n    about: You can find the Vencord Installer here\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build DevBuild\non:\n    push:\n        branches:\n            - main\n        paths:\n            - .github/workflows/build.yml\n            - src/**\n            - browser/**\n            - scripts/build/**\n            - package.json\n            - pnpm-lock.yaml\nenv:\n    FORCE_COLOR: true\n\njobs:\n    Build:\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v4\n\n            - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json\n\n            - name: Use Node.js 20\n              uses: actions/setup-node@v4\n              with:\n                  node-version: 20\n                  cache: \"pnpm\"\n\n            - name: Install dependencies\n              run: pnpm install --frozen-lockfile\n\n            - name: Build web\n              run: pnpm buildWebStandalone\n\n            - name: Build\n              run: pnpm build --standalone\n\n            - name: Generate plugin list\n              run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json\n\n            - name: Clean up obsolete files\n              run: |\n                  rm -rf dist/*-unpacked dist/vendor Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map\n\n            - name: Get some values needed for the release\n              id: release_values\n              run: |\n                  echo \"release_tag=$(git rev-parse --short HEAD)\" >> $GITHUB_ENV\n\n            - name: Upload DevBuild as release\n              if: github.repository == 'Vendicated/Vencord'\n              run: |\n                  gh release upload devbuild --clobber dist/*\n                  gh release edit devbuild --title \"DevBuild $RELEASE_TAG\"\n              env:\n                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n                  RELEASE_TAG: ${{ env.release_tag }}\n\n            - name: Upload DevBuild to builds repo\n              if: github.repository == 'Vendicated/Vencord'\n              run: |\n                  git config --global user.name \"$USERNAME\"\n                  git config --global user.email actions@github.com\n\n                  git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload\n                  cd upload\n\n                  GLOBIGNORE=.git:.gitignore:README.md:LICENSE\n                  rm -rf *\n                  cp -r ../dist/* .\n\n                  git add -A\n                  git commit -m \"Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA\"\n                  git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git\n              env:\n                  API_TOKEN: ${{ secrets.BUILDS_TOKEN }}\n                  GH_REPO: Vencord/builds\n                  USERNAME: GitHub-Actions\n"
  },
  {
    "path": ".github/workflows/codeberg-mirror.yml",
    "content": "name: Sync to Codeberg\nconcurrency:\n    group: ${{ github.ref }}\n    cancel-in-progress: true\non:\n    push:\n    workflow_dispatch:\n    schedule:\n        - cron: \"0 */6 * * *\"\n\njobs:\n    codeberg:\n        if: github.repository == 'Vendicated/Vencord'\n        runs-on: ubuntu-latest\n        steps:\n            - uses: actions/checkout@v4\n              with:\n                  fetch-depth: 0\n            - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1\n              with:\n                  target_repo_url: \"git@codeberg.org:Vee/cord.git\"\n                  ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Release Browser Extension\non:\n    push:\n        tags:\n            - v*\n\njobs:\n    Publish:\n        if: github.repository == 'Vendicated/Vencord'\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v4\n\n            - name: check that tag matches package.json version\n              run: |\n                  pkg_version=\"v$(jq -r .version < package.json)\"\n                  if [[ \"${{ github.ref_name }}\" != \"$pkg_version\" ]]; then\n                      echo \"Tag ${{ github.ref_name }} does not match package.json version $pkg_version\" >&2\n                      exit 1\n                  fi\n\n            - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json\n\n            - name: Use Node.js 19\n              uses: actions/setup-node@v4\n              with:\n                  node-version: 20\n                  cache: \"pnpm\"\n\n            - name: Install dependencies\n              run: pnpm install --frozen-lockfile\n\n            - name: Build web\n              run: pnpm buildWebStandalone\n\n            - name: Publish extension\n              run: |\n                  cd dist/chromium-unpacked\n                  pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish\n              env:\n                  EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}\n                  CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}\n                  CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}\n                  REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/reportBrokenPlugins.yml",
    "content": "name: Test Patches\non:\n    workflow_dispatch:\n        inputs:\n            discord_branch:\n                type: choice\n                description: \"Discord Branch to test patches on\"\n                options:\n                    - both\n                    - stable\n                    - canary\n                default: both\n            webhook_url:\n                type: string\n                description: \"Webhook URL that the report will be posted to. This will be visible for everyone, so DO NOT pass sensitive webhooks like discord webhook. This is meant to be used by Venbot.\"\n                required: false\n    # schedule:\n    #   # Every day at midnight\n    #   - cron: 0 0 * * *\n\njobs:\n    TestPlugins:\n        if: github.repository == 'Vendicated/Vencord'\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v4\n              if: ${{ github.event_name == 'schedule' }}\n              with:\n                  ref: dev\n\n            - uses: actions/checkout@v4\n              if: ${{ github.event_name == 'workflow_dispatch' }}\n\n            - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json\n\n            - name: Use Node.js 20\n              uses: actions/setup-node@v4\n              with:\n                  node-version: 20\n                  cache: \"pnpm\"\n\n            - name: Install dependencies\n              run: |\n                  pnpm install --frozen-lockfile\n\n            - name: Build Vencord Reporter Version\n              run: pnpm buildReporter\n\n            - name: Run Reporter\n              timeout-minutes: 10\n              run: |\n                  export PATH=\"$PWD/node_modules/.bin:$PATH\"\n                  export CHROMIUM_BIN=/usr/bin/google-chrome\n\n                  esbuild scripts/generateReport.ts > dist/report.mjs\n\n                  stable_output_file=$(mktemp)\n                  canary_output_file=$(mktemp)\n\n                  pids=\"\"\n\n                  branch=\"${{ inputs.discord_branch }}\"\n                  if [[ \"${{ github.event_name }}\" = \"schedule\" ]]; then\n                    branch=\"both\"\n                  fi\n\n                  if [[ \"$branch\" = \"both\" || \"$branch\" = \"stable\" ]]; then\n                    node dist/report.mjs > \"$stable_output_file\" &\n                    pids+=\" $!\"\n                  fi\n\n                  if [[ \"$branch\" = \"both\" || \"$branch\" = \"canary\" ]]; then\n                    USE_CANARY=true node dist/report.mjs > \"$canary_output_file\" &\n                    pids+=\" $!\"\n                  fi\n\n                  exit_code=0\n                  for pid in $pids; do\n                      if ! wait \"$pid\"; then\n                        exit_code=1\n                      fi\n                  done\n\n                  cat \"$stable_output_file\" \"$canary_output_file\" >> $GITHUB_STEP_SUMMARY\n                  exit $exit_code\n              env:\n                  WEBHOOK_URL: ${{ inputs.webhook_url || secrets.DISCORD_WEBHOOK }}\n                  WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\non:\n    push:\n    pull_request:\n        branches:\n            - main\n            - dev\njobs:\n    test:\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v4\n            - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json\n\n            - name: Use Node.js 20\n              uses: actions/setup-node@v4\n              with:\n                  node-version: 20\n                  cache: \"pnpm\"\n\n            - name: Install dependencies\n              run: pnpm install --frozen-lockfile\n\n            - name: Lint & Test if desktop version compiles\n              run: pnpm test\n\n            - name: Test if web version compiles\n              run: pnpm buildWeb\n\n            - name: Test if plugin structure is valid\n              run: pnpm generatePluginJson\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\nnode_modules\n\n*.exe\nvencord_installer\n\n.idea\n.DS_Store\n\nyarn.lock\nbun.lock\npackage-lock.json\n\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n*.tsbuildinfo\n\nsrc/userplugins\n\nExtensionCache/\n/settings\n"
  },
  {
    "path": ".npmrc",
    "content": "strict-peer-dependencies=false\npackage-manager-strict=false\n"
  },
  {
    "path": ".stylelintrc.json",
    "content": "{\n    \"extends\": \"stylelint-config-standard\",\n    \"rules\": {\n        \"selector-class-pattern\": [\n            \"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$\",\n            {\n                \"message\": \"Expected class selector to be kebab-case with camelCase segments\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"dbaeumer.vscode-eslint\",\n        \"EditorConfig.EditorConfig\",\n        \"GregorBiswanger.json2ts\",\n        \"stylelint.vscode-stylelint\",\n        \"Vendicated.vencord-companion\"\n    ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // this allows you to debug Vencord from VSCode.\n    // How to use:\n    // You need to run Discord via the command line to pass some flags to it.\n    // If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),\n    //     add the --inspect flag\n    // To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag\n    //\n    // Now launch the desired configuration in VSCode and start Discord with the flags.\n    // For example, to debug both process, run Electron: All then launch Discord with\n    // discord --remote-debugging-port=9223 --inspect\n\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Electron: Main\",\n            \"type\": \"node\",\n            \"request\": \"attach\",\n            \"port\": 9229,\n            \"timeout\": 30000\n        },\n        {\n            \"name\": \"Electron: Renderer\",\n            \"type\": \"chrome\",\n            \"request\": \"attach\",\n            \"port\": 9223,\n            \"timeout\": 30000,\n            \"webRoot\": \"${workspaceFolder}/src\"\n        }\n    ],\n    \"compounds\": [\n        {\n            \"name\": \"Electron: All\",\n            \"configurations\": [\"Electron: Main\", \"Electron: Renderer\"]\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.formatOnSave\": true,\n    \"editor.codeActionsOnSave\": {\n        \"source.fixAll.eslint\": \"explicit\"\n    },\n    \"[typescript]\": {\n        \"editor.defaultFormatter\": \"vscode.typescript-language-features\"\n    },\n    \"[typescriptreact]\": {\n        \"editor.defaultFormatter\": \"vscode.typescript-language-features\"\n    },\n    \"javascript.format.semicolons\": \"insert\",\n    \"typescript.format.semicolons\": \"insert\",\n    \"typescript.preferences.quoteStyle\": \"double\",\n    \"javascript.preferences.quoteStyle\": \"double\",\n    \"gitlens.remotes\": [\n        {\n            \"domain\": \"codeberg.org\",\n            \"type\": \"Gitea\"\n        }\n    ],\n    \"css.format.spaceAroundSelectorSeparator\": true,\n    \"[css]\": {\n        \"editor.defaultFormatter\": \"vscode.css-language-features\"\n    }\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Build\",\n            \"type\": \"shell\",\n            \"command\": \"pnpm build\",\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            }\n        },\n        {\n            \"label\": \"Watch\",\n            \"type\": \"shell\",\n            \"command\": \"pnpm watch\",\n            \"problemMatcher\": [],\n            \"group\": {\n                \"kind\": \"build\"\n            }\n        }\n    ]\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nOur community is welcoming to everyone, regardless of their characteristics.\n\nAs such, we expect you to treat everyone with respect and contribute to an open and welcoming community.\n\nDO\n- have empathy and be nice to others\n- be respectful of differing opinions, even if you disagree\n- give and accept constructive criticism\n\nDON'T\n- use offensive or derogatory language\n- troll or spam\n- personally attack or harass others\n\nRepetitive violations of these guidelines might get your access to the repository restricted.\n\nIf you feel like a user is violating these guidelines or feel treated unfairly, please refrain from vigilantism\nand instead report the issue to a moderator! The best way is joining our [official Discord community](https://vencord.dev/discord)\nand opening a modmail ticket.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Vencord\n\nVencord is a community project and welcomes any kind of contribution from anyone!\n\nWe have development documentation for new contributors, which can be found at <https://docs.vencord.dev>.\n\nAll contributions should be made in accordance with our [Code of Conduct](./CODE_OF_CONDUCT.md).\n\n## How to contribute\n\nContributions can be sent via pull requests. If you're new to Git, check [this guide](https://opensource.com/article/19/7/create-pull-request-github).\n\nPull requests can be made either to the `main` or the `dev` branch. However, unless you're an advanced user, I recommend sticking to `main`. This is because the dev branch might contain unstable changes and be force pushed frequently, which could cause conflicts in your pull request.\n\n## Write a plugin\n\nWriting a plugin is the primary way to contribute.\n\nBefore starting your plugin:\n- Check existing pull requests to see if someone is already working on a similar plugin\n- Check our [plugin requests tracker](https://github.com/Vencord/plugin-requests/issues) to see if there is an existing request, or if the same idea has been rejected\n- If there isn't an existing request, [open one](https://github.com/Vencord/plugin-requests/issues/new?assignees=&labels=&projects=&template=request.yml) yourself\n  and include that you'd like to work on this yourself. Then wait for feedback to see if the idea even has any chance of being accepted. Or maybe others have some ideas to improve it!\n- Familarise yourself with our plugin rules below to ensure your plugin is not banned\n\n### Plugin Rules\n\n- No simple slash command plugins like `/cat`. Instead, make a [user installable Discord bot](https://discord.com/developers/docs/change-log#userinstallable-apps-preview)\n- No simple text replace plugins like Let me Google that for you. The TextReplace plugin can do this\n- No raw DOM manipulation. Use proper patches and React\n- No FakeDeafen or FakeMute\n- No StereoMic\n- No plugins that simply hide or redesign ui elements. This can be done with CSS\n- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)\n- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)\n- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones\n- No plugins that require the user to enter their own API key\n- Do not introduce new dependencies unless absolutely necessary and warranted\n\n## Improve Vencord itself\n\nIf you have any ideas on how to improve Vencord itself, or want to propose a new plugin API, feel free to open a feature request so we can discuss.\n\nOr if you notice any bugs or typos, feel free to fix them!\n\n## Contribute to our Documentation\n\nThe source code of our documentation is available at <https://github.com/Vencord/Docs>\n\nIf you see anything outdated, incorrect or lacking, please fix it!\nIf you think a new page should be added, feel free to suggest it via an issue and we can discuss.\n\n## Help out users in our Discord community\n\nWe have an open support channel in our [Discord community](https://vencord.dev/discord).\nHelping out users there is always appreciated! The more, the merrier.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# Vencord\n\n![](https://img.shields.io/github/package-json/v/Vendicated/Vencord?style=for-the-badge&logo=github&logoColor=d3869b&label=&color=1d2021&labelColor=282828)\n[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Vee/cord)\n\nThe cutest Discord client mod\n\n![](https://github.com/user-attachments/assets/3fac98c0-c411-4d2a-97a3-13b7da8687a2)\n\n## Features\n\n-   Easy to install\n-   [100+ built in plugins](https://vencord.dev/plugins)\n-   Fairly lightweight despite the many inbuilt plugins\n-   Excellent Browser Support: Run Vencord in your Browser via extension or UserScript\n-   Works on any Discord branch: Stable, Canary or PTB all work\n-   Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)\n-   Privacy friendly: blocks Discord analytics & crash reporting out of the box and has no telemetry\n-   Maintained very actively, broken plugins are usually fixed within 12 hours\n-   Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)\n\n\n## Installing / Uninstalling\n\nVisit https://vencord.dev/download\n\n## Join our Support/Community Server\n\nhttps://discord.gg/D9uwnFnqmd\n\n## Sponsors\n\n|     **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!**     |\n| :------------------------------------------------------------------------------------------: |\n|   [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated)    |\n| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |\n\n\n## Star History\n\n<a href=\"https://star-history.com/#Vendicated/Vencord&Timeline\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline&theme=dark\" />\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline\" />\n    <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline\" />\n  </picture>\n</a>\n\n## Disclaimer\n\nDiscord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.\nMention of it does not imply any affiliation with or endorsement by Discord Inc.\n\n<details>\n<summary>Using Vencord violates Discord's terms of service</summary>\n\nClient modifications are against Discord’s Terms of Service.\n\nHowever, Discord is pretty indifferent about them and there are no known cases of users getting banned for using client mods! So you should generally be fine as long as you don’t use any plugins that implement abusive behaviour. But no worries, all inbuilt plugins are safe to use!\n\nRegardless, if your account is very important to you and it getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Vencord), just to be safe\n\nAdditionally, make sure not to post screenshots with Vencord in a server where you might get banned for it\n\n</details>\n"
  },
  {
    "path": "browser/GMPolyfill.js",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nfunction parseHeaders(headers) {\n    const result = new Headers();\n    if (!headers)\n        return result;\n\n    const headersArr = headers.trim().split(\"\\n\");\n    for (var i = 0; i < headersArr.length; i++) {\n        var row = headersArr[i];\n        var index = row.indexOf(\":\")\n            , key = row.slice(0, index).trim().toLowerCase()\n            , value = row.slice(index + 1).trim();\n\n        result.append(key, value);\n    }\n    return result;\n}\n\nfunction blobTo(to, blob) {\n    if (to === \"arrayBuffer\" && blob.arrayBuffer) return blob.arrayBuffer();\n    return new Promise((resolve, reject) => {\n        var fileReader = new FileReader();\n        fileReader.onload = event => resolve(event.target.result);\n        if (to === \"arrayBuffer\") fileReader.readAsArrayBuffer(blob);\n        else if (to === \"text\") fileReader.readAsText(blob, \"utf-8\");\n        else reject(\"unknown to\");\n    });\n}\n\nfunction GM_fetch(url, opt) {\n    return new Promise((resolve, reject) => {\n        // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest\n        const options = opt || {};\n        options.url = url;\n        options.data = options.body;\n        options.responseType = \"blob\";\n        options.onload = resp => {\n            var blob = resp.response;\n            resp.blob = () => Promise.resolve(blob);\n            resp.arrayBuffer = () => blobTo(\"arrayBuffer\", blob);\n            resp.text = () => blobTo(\"text\", blob);\n            resp.json = async () => JSON.parse(await blobTo(\"text\", blob));\n            resp.headers = parseHeaders(resp.responseHeaders);\n            resp.ok = resp.status >= 200 && resp.status < 300;\n            resolve(resp);\n        };\n        options.ontimeout = () => reject(\"fetch timeout\");\n        options.onerror = () => reject(\"fetch error\");\n        options.onabort = () => reject(\"fetch abort\");\n        GM_xmlhttpRequest(options);\n    });\n}\nexport const fetch = GM_fetch;\n"
  },
  {
    "path": "browser/Vencord.ts",
    "content": "/*!\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./VencordNativeStub\";\n\nexport * from \"../src/Vencord\";\n"
  },
  {
    "path": "browser/VencordNativeStub.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/// <reference path=\"../src/modules.d.ts\" />\n/// <reference path=\"../src/globals.d.ts\" />\n\n// Be very careful with imports in this file to avoid circular dependency issues.\n// Only import pure modules that don't import other parts of Vencord.\nimport monacoHtmlLocal from \"file://monacoWin.html?minify\";\nimport * as DataStore from \"@api/DataStore\";\nimport type { Settings } from \"@api/Settings\";\nimport { getThemeInfo } from \"@main/themes\";\nimport { debounce } from \"@shared/debounce\";\nimport { localStorage } from \"@utils/localStorage\";\nimport { getStylusWebStoreUrl } from \"@utils/web\";\nimport { EXTENSION_BASE_URL, metaReady, RENDERER_CSS_URL } from \"@utils/web-metadata\";\n\n// listeners for ipc.on\nconst cssListeners = new Set<(css: string) => void>();\nconst NOOP = () => { };\nconst NOOP_ASYNC = async () => { };\n\nconst setCssDebounced = debounce((css: string) => VencordNative.quickCss.set(css));\n\nconst themeStore = DataStore.createStore(\"VencordThemes\", \"VencordThemeData\");\n\n// probably should make this less cursed at some point\nwindow.VencordNative = {\n    themes: {\n        uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),\n        deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),\n        getThemesList: () => DataStore.entries(themeStore).then(entries =>\n            entries.map(([name, css]) => getThemeInfo(css, name.toString()))\n        ),\n        getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),\n        getSystemValues: async () => ({}),\n\n        openFolder: async () => Promise.reject(\"themes:openFolder is not supported on web\"),\n    },\n\n    native: {\n        getVersions: () => ({}),\n        openExternal: async (url) => void open(url, \"_blank\"),\n        getRendererCss: async () => {\n            if (IS_USERSCRIPT)\n                // need to wait for next tick for _vcUserScriptRendererCss to be set\n                return Promise.resolve().then(() => window._vcUserScriptRendererCss);\n\n            await metaReady;\n\n            return fetch(RENDERER_CSS_URL)\n                .then(res => res.text());\n        },\n        onRendererCssUpdate: NOOP,\n    },\n\n    updater: {\n        getRepo: async () => ({ ok: true, value: \"https://github.com/Vendicated/Vencord\" }),\n        getUpdates: async () => ({ ok: true, value: [] }),\n        update: async () => ({ ok: true, value: false }),\n        rebuild: async () => ({ ok: true, value: true }),\n    },\n\n    quickCss: {\n        get: () => DataStore.get(\"VencordQuickCss\").then(s => s ?? \"\"),\n        set: async (css: string) => {\n            await DataStore.set(\"VencordQuickCss\", css);\n            cssListeners.forEach(l => l(css));\n        },\n        addChangeListener(cb) {\n            cssListeners.add(cb);\n        },\n        addThemeChangeListener: NOOP,\n        openFile: NOOP_ASYNC,\n        async openEditor() {\n            if (IS_USERSCRIPT) {\n                const shouldOpenWebStore = confirm(\"QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\\n\\nDo you want to open the Stylus web store page?\");\n                if (shouldOpenWebStore) {\n                    window.open(getStylusWebStoreUrl(), \"_blank\");\n                }\n                return;\n            }\n\n            const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;\n            const win = open(\"about:blank\", \"VencordQuickCss\", features);\n            if (!win) {\n                alert(\"Failed to open QuickCSS popup. Make sure to allow popups!\");\n                return;\n            }\n\n            win.baseUrl = EXTENSION_BASE_URL;\n            win.setCss = setCssDebounced;\n            win.getCurrentCss = () => VencordNative.quickCss.get();\n            win.getTheme = this.getEditorTheme;\n\n            win.document.write(monacoHtmlLocal);\n        },\n        getEditorTheme: () => {\n            const { getTheme, Theme } = require(\"@utils/discord\");\n\n            return getTheme() === Theme.Light\n                ? \"vs-light\"\n                : \"vs-dark\";\n        }\n    },\n\n    settings: {\n        get: () => {\n            try {\n                return JSON.parse(localStorage.getItem(\"VencordSettings\") || \"{}\");\n            } catch (e) {\n                console.error(\"Failed to parse settings from localStorage: \", e);\n                return {};\n            }\n        },\n        set: async (s: Settings) => localStorage.setItem(\"VencordSettings\", JSON.stringify(s)),\n        openFolder: async () => Promise.reject(\"settings:openFolder is not supported on web\"),\n    },\n\n    pluginHelpers: {} as any,\n    csp: {} as any,\n};\n"
  },
  {
    "path": "browser/background.js",
    "content": "/**\n * @template T\n * @param {T[]} arr\n * @param {(v: T) => boolean} predicate\n */\nfunction removeFirst(arr, predicate) {\n    const idx = arr.findIndex(predicate);\n    if (idx !== -1) arr.splice(idx, 1);\n}\n\nchrome.webRequest.onHeadersReceived.addListener(\n    ({ responseHeaders, type, url }) => {\n        if (!responseHeaders) return;\n\n        if (type === \"main_frame\" && url.includes(\"discord.com\")) {\n            // In main frame requests, the CSP needs to be removed to enable fetching of custom css\n            // as desired by the user\n            removeFirst(responseHeaders, h => h.name.toLowerCase() === \"content-security-policy\");\n        } else if (type === \"stylesheet\" && url.startsWith(\"https://raw.githubusercontent.com/\")) {\n            // Most users will load css from GitHub, but GitHub doesn't set the correct content type,\n            // so we fix it here\n            removeFirst(responseHeaders, h => h.name.toLowerCase() === \"content-type\");\n            responseHeaders.push({\n                name: \"Content-Type\",\n                value: \"text/css\"\n            });\n        }\n        return { responseHeaders };\n    },\n    { urls: [\"https://raw.githubusercontent.com/*\", \"*://*.discord.com/*\"], types: [\"main_frame\", \"stylesheet\"] },\n    [\"blocking\", \"responseHeaders\"]\n);\n"
  },
  {
    "path": "browser/content.js",
    "content": "if (typeof browser === \"undefined\") {\n    var browser = chrome;\n}\n\ndocument.addEventListener(\n    \"DOMContentLoaded\",\n    () => {\n        window.postMessage({\n            type: \"vencord:meta\",\n            meta: {\n                EXTENSION_VERSION: browser.runtime.getManifest().version,\n                EXTENSION_BASE_URL: browser.runtime.getURL(\"\"),\n                RENDERER_CSS_URL: browser.runtime.getURL(\"dist/Vencord.css\"),\n            }\n        });\n    },\n    { once: true }\n);\n"
  },
  {
    "path": "browser/manifest.json",
    "content": "{\n    \"manifest_version\": 3,\n    \"minimum_chrome_version\": \"111\",\n\n    \"name\": \"Vencord Web\",\n    \"description\": \"The cutest Discord mod now in your browser\",\n    \"author\": \"Vendicated\",\n    \"homepage_url\": \"https://github.com/Vendicated/Vencord\",\n    \"icons\": {\n        \"128\": \"icon.png\"\n    },\n\n    \"host_permissions\": [\n        \"*://*.discord.com/*\",\n        \"https://raw.githubusercontent.com/*\"\n    ],\n\n    \"permissions\": [\"declarativeNetRequest\"],\n\n    \"content_scripts\": [\n        {\n            \"run_at\": \"document_start\",\n            \"matches\": [\"*://*.discord.com/*\"],\n            \"js\": [\"content.js\"],\n            \"all_frames\": true,\n            \"world\": \"ISOLATED\"\n        },\n        {\n            \"run_at\": \"document_start\",\n            \"matches\": [\"*://*.discord.com/*\"],\n            \"js\": [\"dist/Vencord.js\"],\n            \"all_frames\": true,\n            \"world\": \"MAIN\"\n        }\n    ],\n\n    \"web_accessible_resources\": [\n        {\n            \"resources\": [\"dist/*\", \"vendor/*\"],\n            \"matches\": [\"*://*.discord.com/*\"]\n        }\n    ],\n\n    \"declarative_net_request\": {\n        \"rule_resources\": [\n            {\n                \"id\": \"modifyResponseHeaders\",\n                \"enabled\": true,\n                \"path\": \"modifyResponseHeaders.json\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "browser/manifestv2.json",
    "content": "{\n    \"manifest_version\": 2,\n    \"minimum_chrome_version\": \"91\",\n\n    \"name\": \"Vencord Web\",\n    \"description\": \"The cutest Discord mod now in your browser\",\n    \"author\": \"Vendicated\",\n    \"homepage_url\": \"https://github.com/Vendicated/Vencord\",\n    \"icons\": {\n        \"128\": \"icon.png\"\n    },\n\n    \"permissions\": [\n        \"webRequest\",\n        \"webRequestBlocking\",\n        \"*://*.discord.com/*\",\n        \"https://raw.githubusercontent.com/*\"\n    ],\n\n    \"content_scripts\": [\n        {\n            \"run_at\": \"document_start\",\n            \"matches\": [\"*://*.discord.com/*\"],\n            \"js\": [\"content.js\"],\n            \"all_frames\": true,\n            \"world\": \"ISOLATED\"\n        },\n        {\n            \"run_at\": \"document_start\",\n            \"matches\": [\"*://*.discord.com/*\"],\n            \"js\": [\"dist/Vencord.js\"],\n            \"all_frames\": true,\n            \"world\": \"MAIN\"\n        }\n    ],\n\n    \"background\": {\n        \"scripts\": [\"background.js\"]\n    },\n\n    \"web_accessible_resources\": [\"dist/Vencord.js\", \"dist/Vencord.css\"],\n\n    \"browser_specific_settings\": {\n        \"gecko\": {\n            \"id\": \"vencord-firefox@vendicated.dev\",\n            \"strict_min_version\": \"128.0\"\n        }\n    }\n}\n"
  },
  {
    "path": "browser/modifyResponseHeaders.json",
    "content": "[\n    {\n        \"id\": 1,\n        \"action\": {\n            \"type\": \"modifyHeaders\",\n            \"responseHeaders\": [\n                {\n                    \"header\": \"content-security-policy\",\n                    \"operation\": \"remove\"\n                },\n                {\n                    \"header\": \"content-security-policy-report-only\",\n                    \"operation\": \"remove\"\n                }\n            ]\n        },\n        \"condition\": {\n            \"resourceTypes\": [\"main_frame\", \"sub_frame\"],\n            \"urlFilter\": \"||discord.com^\"\n        }\n    },\n    {\n        \"id\": 2,\n        \"action\": {\n            \"type\": \"modifyHeaders\",\n            \"responseHeaders\": [\n                {\n                    \"header\": \"content-type\",\n                    \"operation\": \"set\",\n                    \"value\": \"text/css\"\n                }\n            ]\n        },\n        \"condition\": {\n            \"resourceTypes\": [\"stylesheet\"],\n            \"urlFilter\": \"||raw.githubusercontent.com^\"\n        }\n    }\n]\n"
  },
  {
    "path": "browser/monaco.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./patch-worker\";\n\nimport * as monaco from \"monaco-editor/esm/vs/editor/editor.main.js\";\n\ndeclare global {\n    const baseUrl: string;\n    const getCurrentCss: () => Promise<string>;\n    const setCss: (css: string) => void;\n    const getTheme: () => string;\n}\n\nconst BASE = \"/vendor/monaco/vs\";\n\nself.MonacoEnvironment = {\n    getWorkerUrl(_moduleId: unknown, label: string) {\n        const path = label === \"css\" ? \"/language/css/css.worker.js\" : \"/editor/editor.worker.js\";\n        return new URL(BASE + path, baseUrl).toString();\n    }\n};\n\ngetCurrentCss().then(css => {\n    const editor = monaco.editor.create(\n        document.getElementById(\"container\")!,\n        {\n            value: css,\n            language: \"css\",\n            theme: getTheme(),\n        }\n    );\n    editor.onDidChangeModelContent(() =>\n        setCss(editor.getValue())\n    );\n    window.addEventListener(\"resize\", () => {\n        // make monaco re-layout\n        editor.layout();\n    });\n});\n"
  },
  {
    "path": "browser/monacoWin.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\" />\n        <title>Vencord QuickCSS Editor</title>\n        <style>\n            html,\n            body,\n            #container {\n                position: absolute;\n                left: 0;\n                top: 0;\n                width: 100%;\n                height: 100%;\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n        </style>\n    </head>\n\n    <body>\n        <div id=\"container\"></div>\n\n        <script>\n            const script = document.createElement(\"script\");\n            script.src = new URL(\"/vendor/monaco/index.js\", baseUrl);\n\n            const style = document.createElement(\"link\");\n            style.type = \"text/css\";\n            style.rel = \"stylesheet\";\n            style.href = new URL(\"/vendor/monaco/index.css\", baseUrl);\n\n            document.body.append(style, script);\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "browser/patch-worker.js",
    "content": "/*\nCopyright 2013 Rob Wu <gwnRob@gmail.com>\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n// Target: Chrome 20+\n\n// W3-compliant Worker proxy.\n// This module replaces the global Worker object.\n// When invoked, the default Worker object is called.\n// If this call fails with SECURITY_ERR, the script is fetched\n//  using async XHR, and transparently proxies all calls and\n//  setters/getters to the new Worker object.\n// Note: This script does not magically circumvent the Same origin policy.\n\n(function () {\n    'use strict';\n    var Worker_ = window.Worker;\n    var URL = window.URL || window.webkitURL;\n    // Create dummy worker for the following purposes:\n    // 1. Don't override the global Worker object if the fallback isn't\n    //    going to work (future API changes?)\n    // 2. Use it to trigger early validation of postMessage calls\n    // Note: Blob constructor is supported since Chrome 20, but since\n    // some of the used Chrome APIs are only supported as of Chrome 20,\n    //  I don't bother adding a BlobBuilder fallback.\n    var dummyWorker = new Worker_(\n        URL.createObjectURL(new Blob([], { type: 'text/javascript' })));\n    window.Worker = function Worker(scriptURL) {\n        if (arguments.length === 0) {\n            throw new TypeError('Not enough arguments');\n        }\n        try {\n            return new Worker_(scriptURL);\n        } catch (e) {\n            if (e.code === 18/*DOMException.SECURITY_ERR*/) {\n                return new WorkerXHR(scriptURL);\n            } else {\n                throw e;\n            }\n        }\n    };\n    // Bind events and replay queued messages\n    function bindWorker(worker, workerURL) {\n        if (worker._terminated) {\n            return;\n        }\n        worker.Worker = new Worker_(workerURL);\n        worker.Worker.onerror = worker._onerror;\n        worker.Worker.onmessage = worker._onmessage;\n        var o;\n        while ((o = worker._replayQueue.shift())) {\n            worker.Worker[o.method].apply(worker.Worker, o.arguments);\n        }\n        while ((o = worker._messageQueue.shift())) {\n            worker.Worker.postMessage.apply(worker.Worker, o);\n        }\n    }\n    function WorkerXHR(scriptURL) {\n        var worker = this;\n        var x = new XMLHttpRequest();\n        x.responseType = 'blob';\n        x.onload = function () {\n            // http://stackoverflow.com/a/10372280/938089\n            var workerURL = URL.createObjectURL(x.response);\n            bindWorker(worker, workerURL);\n        };\n        x.open('GET', scriptURL);\n        x.send();\n        worker._replayQueue = [];\n        worker._messageQueue = [];\n    }\n    WorkerXHR.prototype = {\n        constructor: Worker_,\n        terminate: function () {\n            if (!this._terminated) {\n                this._terminated = true;\n                if (this.Worker)\n                    this.Worker.terminate();\n            }\n        },\n        postMessage: function (message, transfer) {\n            if (!(this instanceof WorkerXHR))\n                throw new TypeError('Illegal invocation');\n            if (this.Worker) {\n                this.Worker.postMessage.apply(this.Worker, arguments);\n            } else {\n                // Trigger validation:\n                dummyWorker.postMessage(message);\n                // Alright, push the valid message to the queue.\n                this._messageQueue.push(arguments);\n            }\n        }\n    };\n    // Implement the EventTarget interface\n    [\n        'addEventListener',\n        'removeEventListener',\n        'dispatchEvent'\n    ].forEach(function (method) {\n        WorkerXHR.prototype[method] = function () {\n            if (!(this instanceof WorkerXHR)) {\n                throw new TypeError('Illegal invocation');\n            }\n            if (this.Worker) {\n                this.Worker[method].apply(this.Worker, arguments);\n            } else {\n                this._replayQueue.push({ method: method, arguments: arguments });\n            }\n        };\n    });\n    Object.defineProperties(WorkerXHR.prototype, {\n        onmessage: {\n            get: function () { return this._onmessage || null; },\n            set: function (func) {\n                this._onmessage = typeof func === 'function' ? func : null;\n            }\n        },\n        onerror: {\n            get: function () { return this._onerror || null; },\n            set: function (func) {\n                this._onerror = typeof func === 'function' ? func : null;\n            }\n        }\n    });\n})();"
  },
  {
    "path": "browser/userscript.meta.js",
    "content": "// ==UserScript==\n// @name            Vencord\n// @description     A Discord client mod - Web version\n// @version         %version%\n// @author          Vendicated (https://github.com/Vendicated)\n// @namespace       https://github.com/Vendicated/Vencord\n// @supportURL      https://github.com/Vendicated/Vencord\n// @icon            https://raw.githubusercontent.com/Vendicated/Vencord/refs/heads/main/browser/icon.png\n// @license         GPL-3.0\n// @match           *://*.discord.com/*\n// @grant           GM_xmlhttpRequest\n// @grant           unsafeWindow\n// @run-at          document-start\n// @compatible      chrome Chrome + Tampermonkey or Violentmonkey\n// @compatible      firefox Firefox Tampermonkey\n// @compatible      opera Opera + Tampermonkey or Violentmonkey\n// @compatible      edge Edge + Tampermonkey or Violentmonkey\n// @compatible      safari Safari + Tampermonkey or Violentmonkey\n// ==/UserScript==\n\n\n// this UserScript DOES NOT work on Firefox with Violentmonkey or Greasemonkey due to a bug that makes it impossible\n// to overwrite stuff on the window on sites that use CSP. Use Tampermonkey or use a chromium based browser\n// https://github.com/violentmonkey/violentmonkey/issues/997\n\n// this is a compiled and minified version of Vencord. For the source code, visit the GitHub repo\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport stylistic from \"@stylistic/eslint-plugin\";\nimport pathAlias from \"eslint-plugin-path-alias\";\nimport react from \"eslint-plugin-react\";\nimport header from \"eslint-plugin-simple-header\";\nimport simpleImportSort from \"eslint-plugin-simple-import-sort\";\nimport unusedImports from \"eslint-plugin-unused-imports\";\nimport tseslint from \"typescript-eslint\";\n\nexport default tseslint.config(\n    { ignores: [\"dist\", \"browser\", \"packages/vencord-types\"] },\n    {\n        files: [\"src/**/*.{tsx,ts,mts,mjs,js,jsx}\", \"eslint.config.mjs\"],\n        settings: {\n            react: {\n                version: \"18\"\n            }\n        },\n        ...react.configs.flat.recommended,\n        rules: {\n            ...react.configs.flat.recommended.rules,\n            \"react/react-in-jsx-scope\": \"off\",\n            \"react/prop-types\": \"off\",\n            \"react/display-name\": \"off\",\n            \"react/no-unescaped-entities\": \"off\",\n        }\n    },\n    {\n        files: [\"src/**/*.{tsx,ts,mts,mjs,js,jsx}\", \"eslint.config.mjs\"],\n        plugins: {\n            \"simple-header\": header,\n            \"@stylistic\": stylistic,\n            \"@typescript-eslint\": tseslint.plugin,\n            \"simple-import-sort\": simpleImportSort,\n            \"unused-imports\": unusedImports,\n            \"path-alias\": pathAlias\n        },\n        settings: {\n            \"import/resolver\": {\n                map: [\n                    [\"@webpack\", \"./src/webpack\"],\n                    [\"@webpack/common\", \"./src/webpack/common\"],\n                    [\"@utils\", \"./src/utils\"],\n                    [\"@api\", \"./src/api\"],\n                    [\"@components\", \"./src/components\"]\n                ]\n            }\n        },\n        languageOptions: {\n            parser: tseslint.parser,\n            parserOptions: {\n                project: [\"./tsconfig.json\"],\n                tsconfigRootDir: import.meta.dirname\n            }\n        },\n        rules: {\n            /*\n             * Since it's only been a month and Vencord has already been stolen\n             * by random skids who rebranded it to \"AlphaCord\" and erased all license\n             * information\n             */\n            \"simple-header/header\": [\n                \"error\",\n                {\n                    \"files\": [\"scripts/header-new.txt\", \"scripts/header-old.txt\"],\n                    \"templates\": { \"author\": [\".*\", \"Vendicated and contributors\"] }\n                }\n            ],\n\n            // Style Rules\n            \"@stylistic/jsx-quotes\": [\"error\", \"prefer-double\"],\n            \"@stylistic/quotes\": [\"error\", \"double\", { \"avoidEscape\": true }],\n            \"@stylistic/no-mixed-spaces-and-tabs\": \"error\",\n            \"@stylistic/arrow-parens\": [\"error\", \"as-needed\"],\n            \"@stylistic/eol-last\": [\"error\", \"always\"],\n            \"@stylistic/no-multi-spaces\": \"error\",\n            \"@stylistic/no-trailing-spaces\": \"error\",\n            \"@stylistic/no-whitespace-before-property\": \"error\",\n            \"@stylistic/semi\": [\"error\", \"always\"],\n            \"@stylistic/semi-style\": [\"error\", \"last\"],\n            \"@stylistic/space-in-parens\": [\"error\", \"never\"],\n            \"@stylistic/block-spacing\": [\"error\", \"always\"],\n            \"@stylistic/object-curly-spacing\": [\"error\", \"always\"],\n            \"@stylistic/spaced-comment\": [\"error\", \"always\", { \"markers\": [\"!\"] }],\n            \"@stylistic/no-extra-semi\": \"error\",\n\n            // TS Rules\n            \"@stylistic/function-call-spacing\": [\"error\", \"never\"],\n\n            // ESLint Rules\n            \"yoda\": \"error\",\n            \"eqeqeq\": [\"error\", \"always\", { \"null\": \"ignore\" }],\n            \"prefer-destructuring\": [\"error\", {\n                \"VariableDeclarator\": { \"array\": false, \"object\": true },\n                \"AssignmentExpression\": { \"array\": false, \"object\": false }\n            }],\n            \"operator-assignment\": [\"error\", \"always\"],\n            \"no-useless-computed-key\": \"error\",\n            \"no-unneeded-ternary\": [\"error\", { \"defaultAssignment\": false }],\n            \"no-invalid-regexp\": \"error\",\n            \"no-constant-condition\": [\"error\", { \"checkLoops\": false }],\n            \"no-duplicate-imports\": \"error\",\n            \"@typescript-eslint/dot-notation\": [\n                \"error\",\n                {\n                    \"allowPrivateClassPropertyAccess\": true,\n                    \"allowProtectedClassPropertyAccess\": true\n                }\n            ],\n            \"no-useless-escape\": [\n                \"error\",\n                {\n                    \"allowRegexCharacters\": [\"i\"]\n                }\n            ],\n            \"no-fallthrough\": \"error\",\n            \"for-direction\": \"error\",\n            \"no-async-promise-executor\": \"error\",\n            \"no-cond-assign\": \"error\",\n            \"no-dupe-else-if\": \"error\",\n            \"no-duplicate-case\": \"error\",\n            \"no-irregular-whitespace\": \"error\",\n            \"no-loss-of-precision\": \"error\",\n            \"no-misleading-character-class\": \"error\",\n            \"no-prototype-builtins\": \"error\",\n            \"no-regex-spaces\": \"error\",\n            \"no-shadow-restricted-names\": \"error\",\n            \"no-unexpected-multiline\": \"error\",\n            \"no-unsafe-optional-chaining\": \"error\",\n            \"no-useless-backreference\": \"error\",\n            \"use-isnan\": \"error\",\n            \"prefer-const\": [\"error\", { destructuring: \"all\" }],\n            \"prefer-spread\": \"error\",\n\n            // Plugin Rules\n            \"simple-import-sort/imports\": \"error\",\n            \"simple-import-sort/exports\": \"error\",\n            \"unused-imports/no-unused-imports\": \"error\",\n            \"path-alias/no-relative\": \"error\"\n        }\n    }\n);\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"vencord\",\n    \"private\": \"true\",\n    \"version\": \"1.14.6\",\n    \"description\": \"The cutest Discord client mod\",\n    \"homepage\": \"https://github.com/Vendicated/Vencord#readme\",\n    \"bugs\": {\n        \"url\": \"https://github.com/Vendicated/Vencord/issues\"\n    },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/Vendicated/Vencord.git\"\n    },\n    \"license\": \"GPL-3.0-or-later\",\n    \"author\": \"Vendicated\",\n    \"scripts\": {\n        \"build\": \"node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs\",\n        \"buildStandalone\": \"pnpm build --standalone\",\n        \"buildWeb\": \"node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs\",\n        \"buildWebStandalone\": \"pnpm buildWeb --standalone\",\n        \"buildReporter\": \"pnpm buildWebStandalone --reporter --skip-extension\",\n        \"buildReporterDesktop\": \"pnpm build --reporter\",\n        \"watch\": \"pnpm build --watch\",\n        \"dev\": \"pnpm watch\",\n        \"watchWeb\": \"pnpm buildWeb --watch\",\n        \"generatePluginJson\": \"tsx scripts/generatePluginList.ts\",\n        \"generateTypes\": \"tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false\",\n        \"inject\": \"node scripts/runInstaller.mjs -- --install\",\n        \"uninject\": \"node scripts/runInstaller.mjs -- --uninstall\",\n        \"lint\": \"eslint\",\n        \"lint-styles\": \"stylelint \\\"src/**/*.css\\\" --ignore-pattern src/userplugins\",\n        \"lint:fix\": \"pnpm lint --fix\",\n        \"test\": \"pnpm buildStandalone && pnpm testTsc && pnpm lint && pnpm lint-styles && pnpm generatePluginJson\",\n        \"testWeb\": \"pnpm lint && pnpm buildWeb && pnpm testTsc\",\n        \"testTsc\": \"tsc --noEmit\"\n    },\n    \"dependencies\": {\n        \"@intrnl/xxhash64\": \"^0.1.2\",\n        \"@vap/core\": \"0.0.12\",\n        \"@vap/shiki\": \"0.10.5\",\n        \"fflate\": \"^0.8.2\",\n        \"gifenc\": \"github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3\",\n        \"monaco-editor\": \"^0.54.0\",\n        \"nanoid\": \"^5.1.6\",\n        \"virtual-merge\": \"^1.0.1\"\n    },\n    \"devDependencies\": {\n        \"@stylistic/eslint-plugin\": \"^5.6.0\",\n        \"@types/chrome\": \"^0.1.30\",\n        \"@types/lodash\": \"^4.17.20\",\n        \"@types/node\": \"^24.10.1\",\n        \"@types/react\": \"^19.0.10\",\n        \"@types/react-dom\": \"^19.0.4\",\n        \"@types/yazl\": \"^3.3.0\",\n        \"@vencord/discord-types\": \"link:packages/discord-types\",\n        \"diff\": \"^8.0.2\",\n        \"esbuild\": \"^0.27.0\",\n        \"eslint\": \"9.39.1\",\n        \"eslint-import-resolver-alias\": \"^1.1.2\",\n        \"eslint-plugin-path-alias\": \"2.1.0\",\n        \"eslint-plugin-react\": \"^7.37.5\",\n        \"eslint-plugin-simple-header\": \"^1.2.1\",\n        \"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n        \"eslint-plugin-unused-imports\": \"^4.3.0\",\n        \"highlight.js\": \"11.11.1\",\n        \"html-minifier-terser\": \"^7.2.0\",\n        \"moment\": \"^2.22.2\",\n        \"p-limit\": \"^7.3.0\",\n        \"puppeteer-core\": \"^24.30.0\",\n        \"standalone-electron-types\": \"^34.2.0\",\n        \"stylelint\": \"^16.25.0\",\n        \"stylelint-config-standard\": \"^39.0.1\",\n        \"svgo\": \"^4.0.0\",\n        \"ts-patch\": \"^3.3.0\",\n        \"ts-pattern\": \"^5.6.0\",\n        \"tsx\": \"^4.20.6\",\n        \"type-fest\": \"^5.2.0\",\n        \"typescript\": \"^5.9.3\",\n        \"typescript-eslint\": \"^8.47.0\",\n        \"typescript-transform-paths\": \"^3.5.5\",\n        \"zip-local\": \"^0.3.5\"\n    },\n    \"packageManager\": \"pnpm@10.4.1\",\n    \"pnpm\": {\n        \"patchedDependencies\": {\n            \"eslint-plugin-path-alias@2.1.0\": \"patches/eslint-plugin-path-alias@2.1.0.patch\"\n        },\n        \"peerDependencyRules\": {\n            \"ignoreMissing\": [\n                \"eslint-plugin-import\",\n                \"eslint\"\n            ]\n        },\n        \"allowedDeprecatedVersions\": {\n            \"source-map-resolve\": \"*\",\n            \"resolve-url\": \"*\",\n            \"source-map-url\": \"*\",\n            \"urix\": \"*\",\n            \"q\": \"*\"\n        },\n        \"onlyBuiltDependencies\": [\n            \"esbuild\"\n        ]\n    },\n    \"engines\": {\n        \"node\": \">=18\"\n    }\n}\n"
  },
  {
    "path": "packages/discord-types/.npmignore",
    "content": "node_modules\n"
  },
  {
    "path": "packages/discord-types/CONTRIBUTING.md",
    "content": "Hint: https://docs.discord.food is an incredible resource and allows you to copy paste complete enums and interfaces\n"
  },
  {
    "path": "packages/discord-types/LICENSE",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "packages/discord-types/README.md",
    "content": "# Discord Types\n\nThis package provides TypeScript types for the Webpack modules of Discord's web app.\n\nWhile it was primarily created for Vencord, other client mods could also benefit from this, so it is published as a standalone package!\n\n## Installation\n\n```bash\nnpm install -D @vencord/discord-types\nyarn add -D @vencord/discord-types\npnpm add -D @vencord/discord-types\n```\n\n## Example Usage\n\n```ts\nimport type { UserStore } from \"@vencord/discord-types\";\n\nconst userStore: UserStore = findStore(\"UserStore\"); // findStore is up to you to implement, this library only provides types and no runtime code\n```\n\n## Enums\n\nThis library also exports some const enums that you can use from Typescript code:\n```ts\nimport { ApplicationCommandType } from \"@vencord/discord-types/enums\";\n\nconsole.log(ApplicationCommandType.CHAT_INPUT); // 1\n```\n\n### License\n\nThis package is licensed under the [LGPL-3.0](./LICENSE) (or later) license.\n\nA very short summary of the license is that you can use this package as a library in both open source and closed source projects,\nsimilar to an MIT-licensed project.\nHowever, if you modify the code of this package, you must release source code of your modified version under the same license.\n\n### Credit\n\nThis package was inspired by Swishilicous' [discord-types](https://www.npmjs.com/package/discord-types) package.\n"
  },
  {
    "path": "packages/discord-types/enums/activity.ts",
    "content": "export const enum ActivityType {\n    PLAYING = 0,\n    STREAMING = 1,\n    LISTENING = 2,\n    WATCHING = 3,\n    CUSTOM_STATUS = 4,\n    COMPETING = 5,\n    HANG_STATUS = 6\n}\n\nexport const enum ActivityFlags {\n    INSTANCE = 1 << 0,\n    JOIN = 1 << 1,\n    /** @deprecated */\n    SPECTATE = 1 << 2,\n    /** @deprecated */\n    JOIN_REQUEST = 1 << 3,\n    SYNC = 1 << 4,\n    PLAY = 1 << 5,\n    PARTY_PRIVACY_FRIENDS = 1 << 6,\n    PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7,\n    EMBEDDED = 1 << 8,\n    CONTEXTLESS = 1 << 9\n}\n\nexport const enum ActivityStatusDisplayType {\n    NAME = 0,\n    STATE = 1,\n    DETAILS = 2\n}\n"
  },
  {
    "path": "packages/discord-types/enums/channel.ts",
    "content": "export const enum ChannelType {\n    GUILD_TEXT = 0,\n    DM = 1,\n    GUILD_VOICE = 2,\n    GROUP_DM = 3,\n    GUILD_CATEGORY = 4,\n    GUILD_ANNOUNCEMENT = 5,\n    ANNOUNCEMENT_THREAD = 10,\n    PUBLIC_THREAD = 11,\n    PRIVATE_THREAD = 12,\n    GUILD_STAGE_VOICE = 13,\n    GUILD_DIRECTORY = 14,\n    GUILD_FORUM = 15,\n    GUILD_MEDIA = 16\n}\n"
  },
  {
    "path": "packages/discord-types/enums/commands.ts",
    "content": "export const enum ApplicationCommandOptionType {\n    SUB_COMMAND = 1,\n    SUB_COMMAND_GROUP = 2,\n    STRING = 3,\n    INTEGER = 4,\n    BOOLEAN = 5,\n    USER = 6,\n    CHANNEL = 7,\n    ROLE = 8,\n    MENTIONABLE = 9,\n    NUMBER = 10,\n    ATTACHMENT = 11,\n}\n\nexport const enum ApplicationCommandInputType {\n    BUILT_IN = 0,\n    BUILT_IN_TEXT = 1,\n    BUILT_IN_INTEGRATION = 2,\n    BOT = 3,\n    PLACEHOLDER = 4,\n}\n\nexport const enum ApplicationCommandType {\n    CHAT_INPUT = 1,\n    USER = 2,\n    MESSAGE = 3,\n}\n\nexport const enum ApplicationIntegrationType {\n    GUILD_INSTALL = 0,\n    USER_INSTALL = 1\n}\n"
  },
  {
    "path": "packages/discord-types/enums/index.ts",
    "content": "export * from \"./activity\";\nexport * from \"./channel\";\nexport * from \"./commands\";\nexport * from \"./messages\";\nexport * from \"./misc\";\nexport * from \"./user\";\n"
  },
  {
    "path": "packages/discord-types/enums/messages.ts",
    "content": "export const enum StickerType {\n    /** an official sticker in a pack */\n    STANDARD = 1,\n    /** a sticker uploaded to a guild for the guild's members */\n    GUILD = 2\n}\n\nexport const enum StickerFormatType {\n    PNG = 1,\n    APNG = 2,\n    LOTTIE = 3,\n    GIF = 4\n}\n\nexport const enum MessageType {\n    /**\n     * A default message (see below)\n     *\n     * Value: 0\n     * Name: DEFAULT\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    DEFAULT = 0,\n    /**\n     * A message sent when a user is added to a group DM or thread\n     *\n     * Value: 1\n     * Name: RECIPIENT_ADD\n     * Rendered Content: \"{author} added {mentions [0] } to the {group/thread}.\"\n     * Deletable: false\n     */\n    RECIPIENT_ADD = 1,\n    /**\n     * A message sent when a user is removed from a group DM or thread\n     *\n     * Value: 2\n     * Name: RECIPIENT_REMOVE\n     * Rendered Content: \"{author} removed {mentions [0] } from the {group/thread}.\"\n     * Deletable: false\n     */\n    RECIPIENT_REMOVE = 2,\n    /**\n     * A message sent when a user creates a call in a private channel\n     *\n     * Value: 3\n     * Name: CALL\n     * Rendered Content: participated ? \"{author} started a call{ended ? \" that lasted {duration}\" : \" — Join the call\"}.\" : \"You missed a call from {author} that lasted {duration}.\"\n     * Deletable: false\n     */\n    CALL = 3,\n    /**\n     * A message sent when a group DM or thread's name is changed\n     *\n     * Value: 4\n     * Name: CHANNEL_NAME_CHANGE\n     * Rendered Content: \"{author} changed the {is_forum ? \"post title\" : \"channel name\"}: {content} \"\n     * Deletable: false\n     */\n    CHANNEL_NAME_CHANGE = 4,\n    /**\n     * A message sent when a group DM's icon is changed\n     *\n     * Value: 5\n     * Name: CHANNEL_ICON_CHANGE\n     * Rendered Content: \"{author} changed the channel icon.\"\n     * Deletable: false\n     */\n    CHANNEL_ICON_CHANGE = 5,\n    /**\n     * A message sent when a message is pinned in a channel\n     *\n     * Value: 6\n     * Name: CHANNEL_PINNED_MESSAGE\n     * Rendered Content: \"{author} pinned a message to this channel.\"\n     * Deletable: true\n     */\n    CHANNEL_PINNED_MESSAGE = 6,\n    /**\n     * A message sent when a user joins a guild\n     *\n     * Value: 7\n     * Name: USER_JOIN\n     * Rendered Content: See user join message type , obtained via the formula timestamp_ms % 13\n     * Deletable: true\n     */\n    USER_JOIN = 7,\n    /**\n     * A message sent when a user subscribes to (boosts) a guild\n     *\n     * Value: 8\n     * Name: PREMIUM_GUILD_SUBSCRIPTION\n     * Rendered Content: \"{author} just boosted the server{content ? \" {content} times\"}!\"\n     * Deletable: true\n     */\n    PREMIUM_GUILD_SUBSCRIPTION = 8,\n    /**\n     * A message sent when a user subscribes to (boosts) a guild to tier 1\n     *\n     * Value: 9\n     * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_1\n     * Rendered Content: \"{author} just boosted the server{content ? \" {content} times\"}! {guild} has achieved Level 1! \"\n     * Deletable: true\n     */\n    PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,\n    /**\n     * A message sent when a user subscribes to (boosts) a guild to tier 2\n     *\n     * Value: 10\n     * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_2\n     * Rendered Content: \"{author} just boosted the server{content ? \" {content} times\"}! {guild} has achieved Level 2! \"\n     * Deletable: true\n     */\n    PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,\n    /**\n     * A message sent when a user subscribes to (boosts) a guild to tier 3\n     *\n     * Value: 11\n     * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_3\n     * Rendered Content: \"{author} just boosted the server{content ? \" {content} times\"}! {guild} has achieved Level 3! \"\n     * Deletable: true\n     */\n    PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,\n    /**\n     * A message sent when a news channel is followed\n     *\n     * Value: 12\n     * Name: CHANNEL_FOLLOW_ADD\n     * Rendered Content: \"{author} has added {content} to this channel. Its most important updates will show up here.\"\n     * Deletable: true\n     */\n    CHANNEL_FOLLOW_ADD = 12,\n    /**\n     * A message sent when a guild is disqualified from discovery\n     *\n     * Value: 14\n     * Name: GUILD_DISCOVERY_DISQUALIFIED\n     * Rendered Content: \"This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.\"\n     * Deletable: true\n     */\n    GUILD_DISCOVERY_DISQUALIFIED = 14,\n    /**\n     * A message sent when a guild requalifies for discovery\n     *\n     * Value: 15\n     * Name: GUILD_DISCOVERY_REQUALIFIED\n     * Rendered Content: \"This server is eligible for Server Discovery again and has been automatically relisted!\"\n     * Deletable: true\n     */\n    GUILD_DISCOVERY_REQUALIFIED = 15,\n    /**\n     * A message sent when a guild has failed discovery requirements for a week\n     *\n     * Value: 16\n     * Name: GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING\n     * Rendered Content: \"This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.\"\n     * Deletable: true\n     */\n    GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16,\n    /**\n     * A message sent when a guild has failed discovery requirements for 3 weeks\n     *\n     * Value: 17\n     * Name: GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING\n     * Rendered Content: \"This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.\"\n     * Deletable: true\n     */\n    GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17,\n    /**\n     * A message sent when a thread is created\n     *\n     * Value: 18\n     * Name: THREAD_CREATED\n     * Rendered Content: \"{author} started a thread: {content} . See all threads.\"\n     * Deletable: true\n     */\n    THREAD_CREATED = 18,\n    /**\n     * A message sent when a user replies to a message\n     *\n     * Value: 19\n     * Name: REPLY\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    REPLY = 19,\n    /**\n     * A message sent when a user uses a slash command\n     *\n     * Value: 20\n     * Name: CHAT_INPUT_COMMAND\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    CHAT_INPUT_COMMAND = 20,\n    /**\n     * A message sent when a thread starter message is added to a thread\n     *\n     * Value: 21\n     * Name: THREAD_STARTER_MESSAGE\n     * Rendered Content: \"{referenced_message?.content}\" ?? \"Sorry, we couldn't load the first message in this thread\"\n     * Deletable: false\n     */\n    THREAD_STARTER_MESSAGE = 21,\n    /**\n     * A message sent to remind users to invite friends to a guild\n     *\n     * Value: 22\n     * Name: GUILD_INVITE_REMINDER\n     * Rendered Content: \"Wondering who to invite?\\nStart by inviting anyone who can help you build the server!\"\n     * Deletable: true\n     */\n    GUILD_INVITE_REMINDER = 22,\n    /**\n     * A message sent when a user uses a context menu command\n     *\n     * Value: 23\n     * Name: CONTEXT_MENU_COMMAND\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    CONTEXT_MENU_COMMAND = 23,\n    /**\n     * A message sent when auto moderation takes an action\n     *\n     * Value: 24\n     * Name: AUTO_MODERATION_ACTION\n     * Rendered Content: Special embed rendered from embeds[0]\n     * Deletable: true 1\n     */\n    AUTO_MODERATION_ACTION = 24,\n    /**\n     * A message sent when a user purchases or renews a role subscription\n     *\n     * Value: 25\n     * Name: ROLE_SUBSCRIPTION_PURCHASE\n     * Rendered Content: \"{author} {is_renewal ? \"renewed\" : \"joined\"} {role_subscription.tier_name} and has been a subscriber of {guild} for {role_subscription.total_months_subscribed} month(?s)!\"\n     * Deletable: true\n     */\n    ROLE_SUBSCRIPTION_PURCHASE = 25,\n    /**\n     * A message sent when a user is upsold to a premium interaction\n     *\n     * Value: 26\n     * Name: INTERACTION_PREMIUM_UPSELL\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    INTERACTION_PREMIUM_UPSELL = 26,\n    /**\n     * A message sent when a stage channel starts\n     *\n     * Value: 27\n     * Name: STAGE_START\n     * Rendered Content: \"{author} started {content} \"\n     * Deletable: true\n     */\n    STAGE_START = 27,\n    /**\n     * A message sent when a stage channel ends\n     *\n     * Value: 28\n     * Name: STAGE_END\n     * Rendered Content: \"{author} ended {content} \"\n     * Deletable: true\n     */\n    STAGE_END = 28,\n    /**\n     * A message sent when a user starts speaking in a stage channel\n     *\n     * Value: 29\n     * Name: STAGE_SPEAKER\n     * Rendered Content: \"{author} is now a speaker.\"\n     * Deletable: true\n     */\n    STAGE_SPEAKER = 29,\n    /**\n     * A message sent when a user raises their hand in a stage channel\n     *\n     * Value: 30\n     * Name: STAGE_RAISE_HAND\n     * Rendered Content: \"{author} requested to speak.\"\n     * Deletable: true\n     */\n    STAGE_RAISE_HAND = 30,\n    /**\n     * A message sent when a stage channel's topic is changed\n     *\n     * Value: 31\n     * Name: STAGE_TOPIC\n     * Rendered Content: \"{author} changed the Stage topic: {content} \"\n     * Deletable: true\n     */\n    STAGE_TOPIC = 31,\n    /**\n     * A message sent when a user purchases an application premium subscription\n     *\n     * Value: 32\n     * Name: GUILD_APPLICATION_PREMIUM_SUBSCRIPTION\n     * Rendered Content: \"{author} upgraded {application ?? \"a deleted application\"} to premium for this server!\"\n     * Deletable: true\n     */\n    GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32,\n    /**\n     * A message sent when a user gifts a premium (Nitro) referral\n     *\n     * Value: 35\n     * Name: PREMIUM_REFERRAL\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    PREMIUM_REFERRAL = 35,\n    /**\n     * A message sent when a user enabled lockdown for the guild\n     *\n     * Value: 36\n     * Name: GUILD_INCIDENT_ALERT_MODE_ENABLED\n     * Rendered Content: \"{author} enabled security actions until {content}.\"\n     * Deletable: true\n     */\n    GUILD_INCIDENT_ALERT_MODE_ENABLED = 36,\n    /**\n     * A message sent when a user disables lockdown for the guild\n     *\n     * Value: 37\n     * Name: GUILD_INCIDENT_ALERT_MODE_DISABLED\n     * Rendered Content: \"{author} disabled security actions.\"\n     * Deletable: true\n     */\n    GUILD_INCIDENT_ALERT_MODE_DISABLED = 37,\n    /**\n     * A message sent when a user reports a raid for the guild\n     *\n     * Value: 38\n     * Name: GUILD_INCIDENT_REPORT_RAID\n     * Rendered Content: \"{author} reported a raid in {guild}.\"\n     * Deletable: true\n     */\n    GUILD_INCIDENT_REPORT_RAID = 38,\n    /**\n     * A message sent when a user reports a false alarm for the guild\n     *\n     * Value: 39\n     * Name: GUILD_INCIDENT_REPORT_FALSE_ALARM\n     * Rendered Content: \"{author} reported a false alarm in {guild}.\"\n     * Deletable: true\n     */\n    GUILD_INCIDENT_REPORT_FALSE_ALARM = 39,\n    /**\n     * A message sent when no one sends a message in the current channel for 1 hour\n     *\n     * Value: 40\n     * Name: GUILD_DEADCHAT_REVIVE_PROMPT\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    GUILD_DEADCHAT_REVIVE_PROMPT = 40,\n    /**\n     * A message sent when a user buys another user a gift\n     *\n     * Value: 41\n     * Name: CUSTOM_GIFT\n     * Rendered Content: Special embed rendered from embeds[0].url and gift_info\n     * Deletable: true\n     */\n    CUSTOM_GIFT = 41,\n    /**\n     * Value: 42\n     * Name: GUILD_GAMING_STATS_PROMPT\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    GUILD_GAMING_STATS_PROMPT = 42,\n    /**\n     * A message sent when a user purchases a guild product\n     *\n     * Value: 44\n     * Name: PURCHASE_NOTIFICATION\n     * Rendered Content: \"{author} has purchased {purchase_notification.guild_product_purchase.product_name}!\"\n     * Deletable: true\n     */\n    PURCHASE_NOTIFICATION = 44,\n    /**\n     * A message sent when a poll is finalized\n     *\n     * Value: 46\n     * Name: POLL_RESULT\n     * Rendered Content: Special embed rendered from embeds[0]\n     * Deletable: true\n     */\n    POLL_RESULT = 46,\n    /**\n     * A message sent by the Discord Updates account when a new changelog is posted\n     *\n     * Value: 47\n     * Name: CHANGELOG\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    CHANGELOG = 47,\n    /**\n     * A message sent when a Nitro promotion is triggered\n     *\n     * Value: 48\n     * Name: NITRO_NOTIFICATION\n     * Rendered Content: Special embed rendered from content\n     * Deletable: true\n     */\n    NITRO_NOTIFICATION = 48,\n    /**\n     * A message sent when a voice channel is linked to a lobby\n     *\n     * Value: 49\n     * Name: CHANNEL_LINKED_TO_LOBBY\n     * Rendered Content: \"{content}\"\n     * Deletable: true\n     */\n    CHANNEL_LINKED_TO_LOBBY = 49,\n    /**\n     * A local-only ephemeral message sent when a user is prompted to gift Nitro to a friend on their friendship anniversary\n     *\n     * Value: 50\n     * Name: GIFTING_PROMPT\n     * Rendered Content: Special embed\n     * Deletable: true\n     */\n    GIFTING_PROMPT = 50,\n    /**\n     * A local-only message sent when a user receives an in-game message NUX\n     *\n     * Value: 51\n     * Name: IN_GAME_MESSAGE_NUX\n     * Rendered Content: \"{author} messaged you from {application.name}. In-game chat may not include rich messaging features such as images, polls, or apps. Learn More \"\n     * Deletable: true\n     */\n    IN_GAME_MESSAGE_NUX = 51,\n    /**\n     * A message sent when a user accepts a guild join request\n     *\n     * Value: 52\n     * Name: GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION 2\n     * Rendered Content: \"{join_request.user}'s application to {content} was approved! Welcome!\"\n     * Deletable: true\n     */\n    GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION = 52,\n    /**\n     * A message sent when a user rejects a guild join request\n     *\n     * Value: 53\n     * Name: GUILD_JOIN_REQUEST_REJECT_NOTIFICATION 2\n     * Rendered Content: \"{join_request.user}'s application to {content} was rejected.\"\n     * Deletable: true\n     */\n    GUILD_JOIN_REQUEST_REJECT_NOTIFICATION = 53,\n    /**\n     * A message sent when a user withdraws a guild join request\n     *\n     * Value: 54\n     * Name: GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION 2\n     * Rendered Content: \"{join_request.user}'s application to {content} has been withdrawn.\"\n     * Deletable: true\n     */\n    GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION = 54,\n    /**\n     * A message sent when a user upgrades to HD streaming\n     *\n     * Value: 55\n     * Name: HD_STREAMING_UPGRADED\n     * Rendered Content: \"{author} activated HD Splash Potion \"\n     * Deletable: true\n     */\n    HD_STREAMING_UPGRADED = 55,\n    /**\n     * A message sent when a user resolves a moderation report by deleting the offending message\n     *\n     * Value: 58\n     * Name: REPORT_TO_MOD_DELETED_MESSAGE\n     * Rendered Content: \"{author} deleted the message\"\n     * Deletable: true\n     */\n    REPORT_TO_MOD_DELETED_MESSAGE = 58,\n    /**\n     * A message sent when a user resolves a moderation report by timing out the offending user\n     *\n     * Value: 59\n     * Name: REPORT_TO_MOD_TIMEOUT_USER\n     * Rendered Content: \"{author} timed out {mentions [0] }\"\n     * Deletable: true\n     */\n    REPORT_TO_MOD_TIMEOUT_USER = 59,\n    /**\n     * A message sent when a user resolves a moderation report by kicking the offending user\n     *\n     * Value: 60\n     * Name: REPORT_TO_MOD_KICK_USER\n     * Rendered Content: \"{author} kicked {mentions [0] }\"\n     * Deletable: true\n     */\n    REPORT_TO_MOD_KICK_USER = 60,\n    /**\n     * A message sent when a user resolves a moderation report by banning the offending user\n     *\n     * Value: 61\n     * Name: REPORT_TO_MOD_BAN_USER\n     * Rendered Content: \"{author} banned {mentions [0] }\"\n     * Deletable: true\n     */\n    REPORT_TO_MOD_BAN_USER = 61,\n    /**\n     * A message sent when a user resolves a moderation report\n     *\n     * Value: 62\n     * Name: REPORT_TO_MOD_CLOSED_REPORT\n     * Rendered Content: \"{author} resolved this flag\"\n     * Deletable: true\n     */\n    REPORT_TO_MOD_CLOSED_REPORT = 62,\n    /**\n     * A message sent when a user adds a new emoji to a guild\n     *\n     * Value: 63\n     * Name: EMOJI_ADDED\n     * Rendered Content: \"{author} added a new emoji, {content} :{emoji.name}: \"\n     * Deletable: true\n     */\n    EMOJI_ADDED = 63,\n}\n\nexport const enum MessageFlags {\n    /**\n     * Message has been published to subscribed channels (via Channel Following)\n     *\n     * Value: 1 << 0\n     */\n    CROSSPOSTED = 1 << 0,\n    /**\n     * Message originated from a message in another channel (via Channel Following)\n     */\n    IS_CROSSPOST = 1 << 1,\n    /**\n     * Embeds will not be included when serializing this message\n     */\n    SUPPRESS_EMBEDS = 1 << 2,\n    /**\n     * Source message for this crosspost has been deleted (via Channel Following)\n     */\n    SOURCE_MESSAGE_DELETED = 1 << 3,\n    /**\n     * Message came from the urgent message system\n     */\n    URGENT = 1 << 4,\n    /**\n     * Message has an associated thread, with the same ID as the message\n     */\n    HAS_THREAD = 1 << 5,\n    /**\n     * Message is only visible to the user who invoked the interaction\n     */\n    EPHEMERAL = 1 << 6,\n    /**\n     * Message is an interaction response and the bot is \"thinking\"\n     */\n    LOADING = 1 << 7,\n    /**\n     * Some roles were not mentioned and added to the thread\n     */\n    FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8,\n    /**\n     * Message is hidden from the guild's feed\n     */\n    GUILD_FEED_HIDDEN = 1 << 9,\n    /**\n     * Message contains a link that impersonates Discord\n     */\n    SHOULD_SHOW_LINK_NOT_DISCORD_WARNING = 1 << 10,\n    /**\n     * Message will not trigger push and desktop notifications\n     */\n    SUPPRESS_NOTIFICATIONS = 1 << 12,\n    /**\n     * Message's audio attachment is rendered as a voice message\n     */\n    IS_VOICE_MESSAGE = 1 << 13,\n    /**\n     * Message has a forwarded message snapshot attached\n     */\n    HAS_SNAPSHOT = 1 << 14,\n    /**\n     * Message contains components from version 2 of the UI kit\n     */\n    IS_COMPONENTS_V2 = 1 << 15,\n    /**\n     * Message was triggered by the social layer integration\n     */\n    SENT_BY_SOCIAL_LAYER_INTEGRATION = 1 << 16,\n}\n"
  },
  {
    "path": "packages/discord-types/enums/misc.ts",
    "content": "export const enum CloudUploadPlatform {\n    REACT_NATIVE = 0,\n    WEB = 1,\n}\n\nexport const enum DraftType {\n    ChannelMessage = 0,\n    ThreadSettings = 1,\n    FirstThreadMessage = 2,\n    ApplicationLauncherCommand = 3,\n    Poll = 4,\n    SlashCommand = 5,\n    ForwardContextMessage = 6,\n}\n\nexport const enum GuildScheduledEventStatus {\n    SCHEDULED = 1,\n    ACTIVE = 2,\n    COMPLETED = 3,\n    CANCELED = 4,\n}\n\nexport const enum GuildScheduledEventEntityType {\n    STAGE_INSTANCE = 1,\n    VOICE = 2,\n    EXTERNAL = 3,\n}\n\nexport const enum GuildScheduledEventPrivacyLevel {\n    GUILD_ONLY = 2,\n}\n\nexport const enum ParticipantType {\n    STREAM = 0,\n    HIDDEN_STREAM = 1,\n    USER = 2,\n    ACTIVITY = 3,\n}\n\nexport const enum RTCPlatform {\n    DESKTOP = 0,\n    MOBILE = 1,\n    XBOX = 2,\n    PLAYSTATION = 3,\n}\n\nexport const enum VideoSourceType {\n    VIDEO = 0,\n    CAMERA_PREVIEW = 1,\n}\n\nexport const enum EmojiIntention {\n    REACTION = 0,\n    STATUS = 1,\n    COMMUNITY_CONTENT = 2,\n    CHAT = 3,\n    GUILD_STICKER_RELATED_EMOJI = 4,\n    GUILD_ROLE_BENEFIT_EMOJI = 5,\n    SOUNDBOARD = 6,\n    VOICE_CHANNEL_TOPIC = 7,\n    GIFT = 8,\n    AUTO_SUGGESTION = 9,\n    POLLS = 10,\n    PROFILE = 11,\n    MESSAGE_CONFETTI = 12,\n    GUILD_PROFILE = 13,\n    CHANNEL_NAME = 14,\n    DEFAULT_REACT_EMOJI = 15,\n}\n\nexport const enum LoadState {\n    NOT_LOADED = 0,\n    LOADING = 1,\n    LOADED = 2,\n    ERROR = 3,\n}\n\nexport const enum ConnectionStatsFlags {\n    TRANSPORT = 1,\n    OUTBOUND = 2,\n    INBOUND = 4,\n    ALL = 7,\n}\n\nexport const enum SpeakingFlags {\n    NONE = 0,\n    VOICE = 1,\n    SOUNDSHARE = 2,\n    PRIORITY = 4,\n    HIDDEN = 8,\n}\n\nexport const enum GoLiveQualityMode {\n    AUTO = 1,\n    FULL = 2,\n}\n\nexport const enum VoiceProcessingStateReason {\n    CPU_OVERUSE = 1,\n    FAILED = 2,\n    VAD_CPU_OVERUSE = 3,\n    INITIALIZED = 4,\n}\n"
  },
  {
    "path": "packages/discord-types/enums/user.ts",
    "content": "export const enum RelationshipType {\n    NONE = 0,\n    FRIEND = 1,\n    BLOCKED = 2,\n    INCOMING_REQUEST = 3,\n    OUTGOING_REQUEST = 4,\n    IMPLICIT = 5,\n    SUGGESTION = 6\n}\n\nexport enum GiftIntentType {\n    FRIEND_ANNIVERSARY = 0\n}\n\nexport const enum ReadStateType {\n    CHANNEL = 0,\n    GUILD_EVENT = 1,\n    NOTIFICATION_CENTER = 2,\n    GUILD_HOME = 3,\n    GUILD_ONBOARDING_QUESTION = 4,\n    MESSAGE_REQUESTS = 5,\n}\n"
  },
  {
    "path": "packages/discord-types/package.json",
    "content": "{\n    \"name\": \"@vencord/discord-types\",\n    \"author\": \"Vencord Contributors\",\n    \"private\": false,\n    \"description\": \"Typescript definitions for the webpack modules of the Discord Web app\",\n    \"version\": \"1.0.0\",\n    \"license\": \"LGPL-3.0-or-later\",\n    \"types\": \"src/index.d.ts\",\n    \"type\": \"module\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/Vendicated/Vencord.git\",\n        \"directory\": \"packages/discord-types\"\n    },\n    \"dependencies\": {\n        \"moment\": \"^2.22.2\",\n        \"type-fest\": \"^4.41.0\"\n    },\n    \"peerDependencies\": {\n        \"@types/react\": \"^19.0.10\"\n    }\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/Activity.d.ts",
    "content": "import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from \"../../enums\";\n\nexport interface ActivityAssets {\n    large_image?: string;\n    large_text?: string;\n    large_url?: string;\n    small_image?: string;\n    small_text?: string;\n    small_url?: string;\n}\n\nexport interface ActivityButton {\n    label: string;\n    url: string;\n}\n\nexport interface Activity {\n    name: string;\n    application_id: string;\n    type: ActivityType;\n    state?: string;\n    state_url?: string;\n    details?: string;\n    details_url?: string;\n    url?: string;\n    flags: ActivityFlags;\n    status_display_type?: ActivityStatusDisplayType;\n    timestamps?: {\n        start?: number;\n        end?: number;\n    };\n    assets?: ActivityAssets;\n    buttons?: string[];\n    metadata?: {\n        button_urls?: Array<string>;\n    };\n    party?: {\n        id?: string;\n        size?: [number, number];\n    };\n}\n\nexport type OnlineStatus = \"online\" | \"idle\" | \"dnd\" | \"invisible\" | \"offline\" | \"unknown\" | \"streaming\";\n"
  },
  {
    "path": "packages/discord-types/src/common/Application.d.ts",
    "content": "import { Guild } from \"./Guild\";\nimport { User } from \"./User\";\n\nexport interface ApplicationExecutable {\n    os: \"win32\" | \"darwin\" | \"linux\";\n    name: string;\n    isLauncher: boolean;\n}\n\nexport interface ApplicationThirdPartySku {\n    id: string;\n    sku: string;\n    distributor: string;\n}\n\nexport interface ApplicationDeveloper {\n    id: string;\n    name: string;\n}\n\nexport interface ApplicationInstallParams {\n    permissions: string | null;\n    scopes: string[];\n}\n\nexport interface Application {\n    id: string;\n    name: string;\n    icon: string | null;\n    description: string;\n    type: number | null;\n    coverImage: string | null;\n    primarySkuId: string | undefined;\n    bot: User | null;\n    splash: string | undefined;\n    thirdPartySkus: ApplicationThirdPartySku[];\n    isMonetized: boolean;\n    isVerified: boolean;\n    roleConnectionsVerificationUrl: string | undefined;\n    parentId: string | undefined;\n    connectionEntrypointUrl: string | undefined;\n    overlay: boolean;\n    overlayWarn: boolean;\n    overlayCompatibilityHook: boolean;\n    overlayMethods: number;\n    hook: boolean;\n    aliases: string[];\n    publishers: ApplicationDeveloper[];\n    developers: ApplicationDeveloper[];\n    storeListingSkuId: string | undefined;\n    guildId: string | null;\n    guild: Guild | undefined;\n    executables: ApplicationExecutable[];\n    hashes: string[];\n    eulaId: string | undefined;\n    slug: string | undefined;\n    flags: number;\n    maxParticipants: number | undefined;\n    tags: string[];\n    embeddedActivityConfig: Record<string, unknown> | undefined;\n    team: ApplicationTeam | undefined;\n    integrationTypesConfig: Record<string, Record<string, unknown>>;\n    storefront_available: boolean;\n    termsOfServiceUrl: string | undefined;\n    privacyPolicyUrl: string | undefined;\n    isDiscoverable: boolean;\n    customInstallUrl: string | undefined;\n    installParams: ApplicationInstallParams | undefined;\n    directoryEntry: Record<string, unknown> | undefined;\n    categories: string[] | undefined;\n    linkedGames: string[] | undefined;\n    deepLinkUri: string | undefined;\n}\n\nexport interface ApplicationTeam {\n    id: string;\n    name: string;\n    icon: string | null;\n    members: ApplicationTeamMember[];\n    ownerUserId: string;\n}\n\nexport interface ApplicationTeamMember {\n    user: User;\n    teamId: string;\n    membershipState: number;\n    permissions: string[];\n    role: string;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/Channel.d.ts",
    "content": "import { DiscordRecord } from \"./Record\";\n\nexport class Channel extends DiscordRecord {\n    constructor(channel: object);\n    application_id: number | undefined;\n    bitrate: number;\n    defaultAutoArchiveDuration: number | undefined;\n    flags: number;\n    guild_id: string;\n    icon: string;\n    id: string;\n    lastMessageId: string;\n    lastPinTimestamp: string | undefined;\n    member: unknown;\n    memberCount: number | undefined;\n    memberIdsPreview: string[] | undefined;\n    memberListId: unknown;\n    messageCount: number | undefined;\n    name: string;\n    nicks: Record<string, unknown>;\n    nsfw: boolean;\n    originChannelId: unknown;\n    ownerId: string;\n    parent_id: string;\n    permissionOverwrites: {\n        [role: string]: {\n            id: string;\n            type: number;\n            deny: bigint;\n            allow: bigint;\n        };\n    };\n    position: number;\n    rateLimitPerUser: number;\n    rawRecipients: {\n        id: string;\n        avatar: string;\n        username: string;\n        public_flags: number;\n        discriminator: string;\n    }[];\n    recipients: string[];\n    rtcRegion: string;\n    threadMetadata: {\n        locked: boolean;\n        archived: boolean;\n        invitable: boolean;\n        createTimestamp: string | undefined;\n        autoArchiveDuration: number;\n        archiveTimestamp: string | undefined;\n    };\n    topic: string;\n    type: number;\n    userLimit: number;\n    videoQualityMode: undefined;\n\n    get accessPermissions(): bigint;\n    get lastActiveTimestamp(): number;\n\n    computeLurkerPermissionsAllowList(): unknown;\n    getApplicationId(): unknown;\n    getGuildId(): string;\n    getRecipientId(): unknown;\n    hasFlag(flag: number): boolean;\n    isActiveThread(): boolean;\n    isArchivedThread(): boolean;\n    isCategory(): boolean;\n    isDM(): boolean;\n    isDirectory(): boolean;\n    isForumChannel(): boolean;\n    isGroupDM(): boolean;\n    isGuildStageVoice(): boolean;\n    isGuildVoice(): boolean;\n    isListenModeCapable(): boolean;\n    isManaged(): boolean;\n    isMultiUserDM(): boolean;\n    isNSFW(): boolean;\n    isOwner(): boolean;\n    isPrivate(): boolean;\n    isSystemDM(): boolean;\n    isThread(): boolean;\n    isVocal(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/Guild.d.ts",
    "content": "import { Role } from './Role';\nimport { DiscordRecord } from './Record';\n\n// copy(Object.keys(findByProps(\"CREATOR_MONETIZABLE\")).map(JSON.stringify).join(\"|\"))\nexport type GuildFeatures =\n    \"INVITE_SPLASH\" | \"VIP_REGIONS\" | \"VANITY_URL\" | \"MORE_EMOJI\" | \"MORE_STICKERS\" | \"MORE_SOUNDBOARD\" | \"VERIFIED\" | \"COMMERCE\" | \"DISCOVERABLE\" | \"COMMUNITY\" | \"FEATURABLE\" | \"NEWS\" | \"HUB\" | \"PARTNERED\" | \"ANIMATED_ICON\" | \"BANNER\" | \"ENABLED_DISCOVERABLE_BEFORE\" | \"WELCOME_SCREEN_ENABLED\" | \"MEMBER_VERIFICATION_GATE_ENABLED\" | \"PREVIEW_ENABLED\" | \"ROLE_SUBSCRIPTIONS_ENABLED\" | \"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE\" | \"CREATOR_MONETIZABLE\" | \"CREATOR_MONETIZABLE_PROVISIONAL\" | \"CREATOR_MONETIZABLE_WHITEGLOVE\" | \"CREATOR_MONETIZABLE_DISABLED\" | \"CREATOR_MONETIZABLE_RESTRICTED\" | \"CREATOR_STORE_PAGE\" | \"CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING\" | \"PRODUCTS_AVAILABLE_FOR_PURCHASE\" | \"GUILD_WEB_PAGE_VANITY_URL\" | \"THREADS_ENABLED\" | \"THREADS_ENABLED_TESTING\" | \"NEW_THREAD_PERMISSIONS\" | \"ROLE_ICONS\" | \"TEXT_IN_STAGE_ENABLED\" | \"TEXT_IN_VOICE_ENABLED\" | \"HAS_DIRECTORY_ENTRY\" | \"ANIMATED_BANNER\" | \"LINKED_TO_HUB\" | \"EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT\" | \"GUILD_HOME_DEPRECATION_OVERRIDE\" | \"GUILD_HOME_TEST\" | \"GUILD_HOME_OVERRIDE\" | \"GUILD_ONBOARDING\" | \"GUILD_ONBOARDING_EVER_ENABLED\" | \"GUILD_ONBOARDING_HAS_PROMPTS\" | \"GUILD_SERVER_GUIDE\" | \"INTERNAL_EMPLOYEE_ONLY\" | \"AUTO_MODERATION\" | \"INVITES_DISABLED\" | \"BURST_REACTIONS\" | \"SOUNDBOARD\" | \"SHARD\" | \"ACTIVITY_FEED_ENABLED_BY_USER\" | \"ACTIVITY_FEED_DISABLED_BY_USER\" | \"SUMMARIES_ENABLED_GA\" | \"LEADERBOARD_ENABLED\" | \"SUMMARIES_ENABLED_BY_USER\" | \"SUMMARIES_OPT_OUT_EXPERIENCE\" | \"CHANNEL_ICON_EMOJIS_GENERATED\" | \"NON_COMMUNITY_RAID_ALERTS\" | \"RAID_ALERTS_DISABLED\" | \"AUTOMOD_TRIGGER_USER_PROFILE\" | \"ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY\" | \"GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE\" | \"CLAN\" | \"MEMBER_VERIFICATION_MANUAL_APPROVAL\" | \"FORWARDING_DISABLED\" | \"MEMBER_VERIFICATION_ROLLOUT_TEST\" | \"AUDIO_BITRATE_128_KBPS\" | \"AUDIO_BITRATE_256_KBPS\" | \"AUDIO_BITRATE_384_KBPS\" | \"VIDEO_BITRATE_ENHANCED\" | \"MAX_FILE_SIZE_50_MB\" | \"MAX_FILE_SIZE_100_MB\" | \"GUILD_TAGS\" | \"ENHANCED_ROLE_COLORS\" | \"PREMIUM_TIER_3_OVERRIDE\" | \"REPORT_TO_MOD_PILOT\" | \"TIERLESS_BOOSTING_SYSTEM_MESSAGE\";\nexport type GuildPremiumFeatures =\n    \"ANIMATED_ICON\" | \"STAGE_CHANNEL_VIEWERS_150\" | \"ROLE_ICONS\" | \"GUILD_TAGS\" | \"BANNER\" | \"MAX_FILE_SIZE_50_MB\" | \"VIDEO_QUALITY_720_60FPS\" | \"STAGE_CHANNEL_VIEWERS_50\" | \"VIDEO_QUALITY_1080_60FPS\" | \"MAX_FILE_SIZE_100_MB\" | \"VANITY_URL\" | \"VIDEO_BITRATE_ENHANCED\" | \"STAGE_CHANNEL_VIEWERS_300\" | \"AUDIO_BITRATE_128_KBPS\" | \"ANIMATED_BANNER\" | \"TIERLESS_BOOSTING\" | \"ENHANCED_ROLE_COLORS\" | \"INVITE_SPLASH\" | \"AUDIO_BITRATE_256_KBPS\" | \"AUDIO_BITRATE_384_KBPS\";\n\nexport class Guild extends DiscordRecord {\n    constructor(guild: object);\n    afkChannelId: string | undefined;\n    afkTimeout: number;\n    applicationCommandCounts: {\n        0: number;\n        1: number;\n        2: number;\n    };\n    application_id: unknown;\n    banner: string | undefined;\n    defaultMessageNotifications: number;\n    description: string | undefined;\n    discoverySplash: string | undefined;\n    explicitContentFilter: number;\n    features: Set<GuildFeatures>;\n    homeHeader: string | undefined;\n    hubType: unknown;\n    icon: string | undefined;\n    id: string;\n    joinedAt: Date;\n    latestOnboardingQuestionId: string | undefined;\n    maxMembers: number;\n    maxStageVideoChannelUsers: number;\n    maxVideoChannelUsers: number;\n    mfaLevel: number;\n    moderatorReporting: unknown;\n    name: string;\n    nsfwLevel: number;\n    ownerConfiguredContentLevel: number;\n    ownerId: string;\n    preferredLocale: string;\n    premiumFeatures: {\n        additionalEmojiSlots: number;\n        additionalSoundSlots: number;\n        additionalStickerSlots: number;\n        features: Array<GuildPremiumFeatures>;\n    };\n    premiumProgressBarEnabled: boolean;\n    premiumSubscriberCount: number;\n    premiumTier: 0 | 1 | 2 | 3;\n    profile: {\n        badge: string | undefined;\n        tag: string | undefined;\n    } | undefined;\n    publicUpdatesChannelId: string | undefined;\n    roles: Record<string, Role>;\n    rulesChannelId: string | undefined;\n    safetyAlertsChannelId: string | undefined;\n    splash: string | undefined;\n    systemChannelFlags: number;\n    systemChannelId: string | undefined;\n    vanityURLCode: string | undefined;\n    verificationLevel: number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/GuildMember.d.ts",
    "content": "export interface GuildMember {\n    avatar: string | undefined;\n    avatarDecoration: string | undefined;\n    banner: string | undefined;\n    bio: string;\n    colorRoleId: string | undefined;\n    colorString: string;\n    colorStrings: {\n        primaryColor: string | undefined;\n        secondaryColor: string | undefined;\n        tertiaryColor: string | undefined;\n    };\n    communicationDisabledUntil: string | undefined;\n    flags: number;\n    fullProfileLoadedTimestamp: number;\n    guildId: string;\n    highestRoleId: string;\n    hoistRoleId: string;\n    iconRoleId: string;\n    isPending: boolean | undefined;\n    joinedAt: string | undefined;\n    nick: string | undefined;\n    premiumSince: string | undefined;\n    roles: string[];\n    userId: string;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/Record.d.ts",
    "content": "type Updater = (value: any) => any;\n\n/**\n * Common Record class extended by various Discord data structures, like User, Channel, Guild, etc.\n */\nexport class DiscordRecord {\n    toJS(): Record<string, any>;\n\n    set(key: string, value: any): this;\n    merge(data: Record<string, any>): this;\n    update(key: string, defaultValueOrUpdater: Updater | any, updater?: Updater): this;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/Role.d.ts",
    "content": "export interface Role {\n    color: number;\n    colorString: string | undefined;\n    colorStrings: {\n        primaryColor: string | undefined;\n        secondaryColor: string | undefined;\n        tertiaryColor: string | undefined;\n    };\n    colors: {\n        primary_color: number | undefined;\n        secondary_color: number | undefined;\n        tertiary_color: number | undefined;\n    };\n    flags: number;\n    hoist: boolean;\n    icon: string | undefined;\n    id: string;\n    managed: boolean;\n    mentionable: boolean;\n    name: string;\n    originalPosition: number;\n    permissions: bigint;\n    position: number;\n    /**\n     * probably incomplete\n     */\n    tags: {\n        bot_id: string;\n        integration_id: string;\n        premium_subscriber: unknown;\n    } | undefined;\n    unicodeEmoji: string | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/User.d.ts",
    "content": "// TODO: a lot of optional params can also be null, not just undef\n\nimport { DiscordRecord } from \"./Record\";\n\nexport class User extends DiscordRecord {\n    constructor(user: object);\n    accentColor: number;\n    avatar: string;\n    banner: string | null | undefined;\n    bio: string;\n    bot: boolean;\n    desktop: boolean;\n    discriminator: string;\n    email: string | undefined;\n    flags: number;\n    globalName: string | undefined;\n    guildMemberAvatars: Record<string, string>;\n    id: string;\n    mfaEnabled: boolean;\n    mobile: boolean;\n    nsfwAllowed: boolean | undefined;\n    phone: string | undefined;\n    premiumType: number | undefined;\n    premiumUsageFlags: number;\n    publicFlags: number;\n    purchasedFlags: number;\n    system: boolean;\n    username: string;\n    verified: boolean;\n\n    get createdAt(): Date;\n    get hasPremiumPerks(): boolean;\n    get tag(): string;\n    get usernameNormalized(): string;\n\n    addGuildAvatarHash(guildId: string, avatarHash: string): User;\n    getAvatarSource(guildId: string, canAnimate?: boolean): { uri: string; };\n    getAvatarURL(guildId?: string | null, t?: unknown, canAnimate?: boolean): string;\n    hasAvatarForGuild(guildId: string): boolean;\n    hasDisabledPremium(): boolean;\n    hasFlag(flag: number): boolean;\n    hasFreePremium(): boolean;\n    hasHadSKU(e: unknown): boolean;\n    hasPremiumUsageFlag(flag: number): boolean;\n    hasPurchasedFlag(flag: number): boolean;\n    hasUrgentMessages(): boolean;\n    isClaimed(): boolean;\n    isLocalBot(): boolean;\n    isNonUserBot(): boolean;\n    isPhoneVerified(): boolean;\n    isStaff(): boolean;\n    isSystemUser(): boolean;\n    isVerifiedBot(): boolean;\n    removeGuildAvatarHash(guildId: string): User;\n    toString(): string;\n}\n\nexport interface UserJSON {\n    avatar: string;\n    avatarDecoration: unknown | undefined;\n    discriminator: string;\n    id: string;\n    publicFlags: number;\n    username: string;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/index.d.ts",
    "content": "export * from \"./Activity\";\nexport * from \"./Application\";\nexport * from \"./Channel\";\nexport * from \"./Guild\";\nexport * from \"./GuildMember\";\nexport * from \"./messages\";\nexport * from \"./Role\";\nexport * from \"./User\";\nexport * from \"./Record\";\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/Commands.d.ts",
    "content": "import { Channel } from \"../Channel\";\nimport { Guild } from \"../Guild\";\nimport { Promisable } from \"type-fest\";\nimport { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from \"../../../enums\";\n\nexport interface CommandContext {\n    channel: Channel;\n    guild?: Guild;\n}\n\nexport interface CommandOption {\n    name: string;\n    displayName?: string;\n    type: ApplicationCommandOptionType;\n    description: string;\n    displayDescription?: string;\n    required?: boolean;\n    options?: CommandOption[];\n    choices?: Array<ChoicesOption>;\n}\n\nexport interface ChoicesOption {\n    label: string;\n    value: string;\n    name: string;\n    displayName?: string;\n}\n\nexport interface CommandReturnValue {\n    content: string;\n    // TODO: implement\n    // cancel?: boolean;\n}\n\nexport interface CommandArgument {\n    type: ApplicationCommandOptionType;\n    name: string;\n    value: string;\n    focused: undefined;\n    options: CommandArgument[];\n}\n\nexport interface Command {\n    id?: string;\n    applicationId?: string;\n    type?: ApplicationCommandType;\n    inputType?: ApplicationCommandInputType;\n    plugin?: string;\n\n    name: string;\n    untranslatedName?: string;\n    displayName?: string;\n    description: string;\n    untranslatedDescription?: string;\n    displayDescription?: string;\n\n    options?: CommandOption[];\n    predicate?(ctx: CommandContext): boolean;\n\n    execute(args: CommandArgument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/Embed.d.ts",
    "content": "export interface Embed {\n    author?: {\n        name: string;\n        url: string;\n        iconURL: string | undefined;\n        iconProxyURL: string | undefined;\n    };\n    color: string;\n    fields: [];\n    id: string;\n    image?: {\n        height: number;\n        width: number;\n        url: string;\n        proxyURL: string;\n    };\n    provider?: {\n        name: string;\n        url: string | undefined;\n    };\n    rawDescription: string;\n    rawTitle: string;\n    referenceId: unknown;\n    timestamp: string;\n    thumbnail?: {\n        height: number;\n        proxyURL: string | undefined;\n        url: string;\n        width: number;\n    };\n    type: string;\n    url: string | undefined;\n    video?: {\n        height: number;\n        width: number;\n        url: string;\n        proxyURL: string | undefined;\n    };\n}\n\nexport interface EmbedJSON {\n    author?: {\n        name: string;\n        url: string;\n        icon_url: string;\n        proxy_icon_url: string;\n    };\n    title: string;\n    color: string;\n    description: string;\n    type: string;\n    url: string | undefined;\n    provider?: {\n        name: string;\n        url: string;\n    };\n    timestamp: string;\n    thumbnail?: {\n        height: number;\n        width: number;\n        url: string;\n        proxy_url: string | undefined;\n    };\n    video?: {\n        height: number;\n        width: number;\n        url: string;\n        proxy_url: string | undefined;\n    };\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/Emoji.d.ts",
    "content": "/** Union type for both custom (guild) emojis and unicode emojis. */\nexport type Emoji = CustomEmoji | UnicodeEmoji;\n\n/**\n * Custom emoji uploaded to a guild.\n */\nexport interface CustomEmoji {\n    /** Discriminator for custom emojis. */\n    type: 1;\n    /** Whether the emoji is animated (GIF). */\n    animated: boolean;\n    /** Whether the emoji is available for use. */\n    available: boolean;\n    /** Guild id this emoji belongs to. */\n    guildId: string;\n    /** Unique emoji id (snowflake). */\n    id: string;\n    /** Whether the emoji is managed by an integration (e.g. Twitch). */\n    managed: boolean;\n    /** Emoji name without colons. */\n    name: string;\n    /** Original name before any modifications. */\n    originalName?: string;\n    /** Whether the emoji requires colons to use. */\n    require_colons: boolean;\n    /** Role ids that can use this emoji (empty array means everyone). */\n    roles: string[];\n    /** Version number, incremented when emoji is updated. */\n    version?: number;\n}\n\n/**\n * Built-in unicode emoji.\n */\nexport interface UnicodeEmoji {\n    /** Discriminator for unicode emojis. */\n    type: 0;\n    /** Skin tone variant emojis keyed by diversity surrogate code (e.g. \"1f3fb\" for light skin). */\n    diversityChildren: Record<string, UnicodeEmoji>;\n    /** Raw emoji data from Discord's emoji dataset. */\n    emojiObject: EmojiObject;\n    /** Index position in the emoji list. */\n    index: number;\n    /** Unicode surrogate pair(s) for this emoji. */\n    surrogates: string;\n    /** Unique name identifier for this emoji. */\n    uniqueName: string;\n    /** Whether to render using sprite sheet. */\n    useSpriteSheet: boolean;\n    /** Original name if renamed in context. */\n    originalName?: string;\n    /** Emoji id when used in custom emoji context. */\n    id?: string;\n    /** Guild id when used in guild context. */\n    guildId?: string;\n    /** Formatted string of all emoji names. */\n    get allNamesString(): string;\n    /** Always false for unicode emojis. */\n    get animated(): false;\n    /** Default skin tone variant or undefined if no diversity. */\n    get defaultDiversityChild(): UnicodeEmoji | undefined;\n    /** Whether this emoji supports skin tone modifiers. */\n    get hasDiversity(): boolean | undefined;\n    /** Whether this emoji is a skin tone variant of another. */\n    get hasDiversityParent(): boolean | undefined;\n    /** Whether this emoji supports multiple diversity modifiers (e.g. handshake with two skin tones). */\n    get hasMultiDiversity(): boolean | undefined;\n    /** Whether this emoji is a multi-diversity variant of another. */\n    get hasMultiDiversityParent(): boolean | undefined;\n    /** Always true for unicode emojis. */\n    get managed(): true;\n    /** Primary emoji name. */\n    get name(): string;\n    /** All names/aliases for this emoji. */\n    get names(): string[];\n    /** Surrogate sequence with optional diversity modifier. */\n    get optionallyDiverseSequence(): string | undefined;\n    /** Unicode version when this emoji was added. */\n    get unicodeVersion(): number;\n    /** CDN url for emoji image. */\n    get url(): string;\n    /**\n     * Iterates over all diversity variants of this emoji.\n     * @param callback Function called for each diversity variant.\n     */\n    forEachDiversity(callback: (emoji: UnicodeEmoji) => void): void;\n    /**\n     * Iterates over all names/aliases of this emoji.\n     * @param callback Function called for each name.\n     */\n    forEachName(callback: (name: string) => void): void;\n}\n\n/**\n * Raw emoji data from Discord's emoji dataset.\n */\nexport interface EmojiObject {\n    /** All names/aliases for this emoji. */\n    names: string[];\n    /** Unicode surrogate pair(s). */\n    surrogates: string;\n    /** Unicode version when this emoji was added. */\n    unicodeVersion: number;\n    /** Index in the sprite sheet for rendering. */\n    spriteIndex?: number;\n    /** Whether this emoji supports multiple skin tone modifiers. */\n    hasMultiDiversity?: boolean;\n    /** Whether this emoji is a diversity variant with a multi-diversity parent. */\n    hasMultiDiversityParent?: boolean;\n    /** Skin tone modifier codes for this variant (e.g. [\"1f3fb\"] or [\"1f3fb\", \"1f3fc\"]). */\n    diversity?: string[];\n    /** Sprite indices of diversity children for parent emojis. */\n    diversityChildren?: number[];\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/Message.d.ts",
    "content": "import { CommandOption } from './Commands';\nimport { User, UserJSON } from '../User';\nimport { Embed, EmbedJSON } from './Embed';\nimport { DiscordRecord } from \"../Record\";\nimport { ApplicationIntegrationType, MessageFlags, MessageType, StickerFormatType } from \"../../../enums\";\n\n/*\n * TODO: looks like discord has moved over to Date instead of Moment;\n */\nexport class Message extends DiscordRecord {\n    constructor(message: object);\n    activity: unknown;\n    application: unknown;\n    applicationId: string | unknown;\n    attachments: MessageAttachment[];\n    author: User;\n    blocked: boolean;\n    bot: boolean;\n    call: {\n        duration: moment.Duration;\n        endedTimestamp: moment.Moment;\n        participants: string[];\n    };\n    channel_id: string;\n    /**\n     * NOTE: not fully typed\n     */\n    codedLinks: {\n        code?: string;\n        type: string;\n    }[];\n    colorString: unknown;\n    components: unknown[];\n    content: string;\n    customRenderedContent: unknown;\n    editedTimestamp: Date;\n    embeds: Embed[];\n    flags: MessageFlags;\n    giftCodes: string[];\n    id: string;\n    interaction: {\n        id: string;\n        name: string;\n        type: number;\n        user: User;\n    }[] | undefined;\n    interactionData: {\n        application_command: {\n            application_id: string;\n            default_member_permissions: unknown;\n            default_permission: boolean;\n            description: string;\n            dm_permission: unknown;\n            id: string;\n            name: string;\n            options: CommandOption[];\n            permissions: unknown[];\n            type: number;\n            version: string;\n        };\n        attachments: MessageAttachment[];\n        guild_id: string | undefined;\n        id: string;\n        name: string;\n        options: {\n            focused: unknown;\n            name: string;\n            type: number;\n            value: string;\n        }[];\n        type: number;\n        version: string;\n    }[];\n    interactionMetadata?: {\n        id: string;\n        type: number;\n        name?: string;\n        command_type?: number;\n        ephemerality_reason?: number;\n        user: User;\n        authorizing_integration_owners: Record<ApplicationIntegrationType, string>;\n        original_response_message_id?: string;\n        interacted_message_id?: string;\n        target_user?: User;\n        target_message_id?: string;\n    };\n    interactionError: unknown[];\n    isSearchHit: boolean;\n    loggingName: unknown;\n    mentionChannels: string[];\n    mentionEveryone: boolean;\n    mentionRoles: string[];\n    mentioned: boolean;\n    mentions: string[];\n    messageReference: {\n        guild_id?: string;\n        channel_id: string;\n        message_id: string;\n    } | undefined;\n    messageSnapshots: {\n        message: Message;\n    }[];\n    nick: unknown; // probably a string\n    nonce: string | undefined;\n    pinned: boolean;\n    reactions: MessageReaction[];\n    state: string;\n    stickerItems: {\n        format_type: StickerFormatType;\n        id: string;\n        name: string;\n    }[];\n    stickers: unknown[];\n    timestamp: moment.Moment;\n    tts: boolean;\n    type: MessageType;\n    webhookId: string | undefined;\n\n    /**\n     *  Doesn't actually update the original message; it just returns a new message instance with the added reaction.\n     */\n    addReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;\n    /**\n     * Searches each reaction and if the provided string has an index above -1 it'll return the reaction object.\n     */\n    getReaction(name: string): MessageReaction;\n    /**\n     * Doesn't actually update the original message; it just returns the message instance without the reaction searched with the provided emoji object.\n     */\n    removeReactionsForEmoji(emoji: ReactionEmoji): Message;\n    /**\n     * Doesn't actually update the original message; it just returns the message instance without the reaction.\n     */\n    removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;\n\n    getChannelId(): string;\n    hasFlag(flag: MessageFlags): boolean;\n    isCommandType(): boolean;\n    isEdited(): boolean;\n    isSystemDM(): boolean;\n\n    /** Vencord added */\n    deleted?: boolean;\n}\n\n/** A smaller Message object found in FluxDispatcher and elsewhere. */\nexport interface MessageJSON {\n    attachments: MessageAttachment[];\n    author: UserJSON;\n    channel_id: string;\n    components: unknown[];\n    content: string;\n    edited_timestamp: string;\n    embeds: EmbedJSON[];\n    flags: number;\n    guild_id: string | undefined;\n    id: string;\n    loggingName: unknown;\n    member: {\n        avatar: string | undefined;\n        communication_disabled_until: string | undefined;\n        deaf: boolean;\n        hoisted_role: string | undefined;\n        is_pending: boolean;\n        joined_at: string;\n        mute: boolean;\n        nick: string | boolean;\n        pending: boolean;\n        premium_since: string | undefined;\n        roles: string[];\n    } | undefined;\n    mention_everyone: boolean;\n    mention_roles: string[];\n    mentions: UserJSON[];\n    message_reference: {\n        guild_id?: string;\n        channel_id: string;\n        message_id: string;\n    } | undefined;\n    nonce: string | undefined;\n    pinned: boolean;\n    referenced_message: MessageJSON | undefined;\n    state: string;\n    timestamp: string;\n    tts: boolean;\n    type: number;\n}\n\nexport interface MessageAttachment {\n    filename: string;\n    id: string;\n    proxy_url: string;\n    size: number;\n    spoiler: boolean;\n    url: string;\n    content_type?: string;\n    width?: number;\n    height?: number;\n}\n\nexport interface ReactionEmoji {\n    id: string | undefined;\n    name: string;\n    animated: boolean;\n}\n\nexport interface MessageReaction {\n    count: number;\n    emoji: ReactionEmoji;\n    me: boolean;\n}\n\n// Object.keys(findByProps(\"REPLYABLE\")).map(JSON.stringify).join(\"|\")\nexport type MessageTypeSets = Record<\n    \"UNDELETABLE\" | \"GUILD_DISCOVERY_STATUS\" | \"USER_MESSAGE\" | \"NOTIFIABLE_SYSTEM_MESSAGE\" | \"REPLYABLE\" | \"FORWARDABLE\" | \"REFERENCED_MESSAGE_AVAILABLE\" | \"AVAILABLE_IN_GUILD_FEED\" | \"DEADCHAT_PROMPTS\" | \"NON_COLLAPSIBLE\" | \"NON_PARSED\" | \"AUTOMOD_INCIDENT_ACTIONS\" | \"SELF_MENTIONABLE_SYSTEM\" | \"SCHEDULABLE\",\n    Set<MessageType>\n>;\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/Sticker.d.ts",
    "content": "import { StickerFormatType, StickerType } from \"../../../enums\";\n\ninterface BaseSticker {\n    asset: string;\n    available: boolean;\n    description: string;\n    format_type: StickerFormatType;\n    id: string;\n    name: string;\n    sort_value?: number;\n    /** a comma separated string */\n    tags: string;\n}\n\nexport interface PackSticker extends BaseSticker {\n    pack_id: string;\n    type: StickerType.STANDARD;\n}\n\nexport interface GuildSticker extends BaseSticker {\n    guild_id: string;\n    type: StickerType.GUILD;\n}\n\nexport type Sticker = PackSticker | GuildSticker;\n\nexport interface PremiumStickerPack {\n    banner_asset_id?: string;\n    cover_sticker_id?: string;\n    description: string;\n    id: string;\n    name: string;\n    sku_id: string;\n    stickers: PackSticker[];\n}\n"
  },
  {
    "path": "packages/discord-types/src/common/messages/index.d.ts",
    "content": "export * from \"./Commands\";\nexport * from \"./Message\";\nexport * from \"./Embed\";\nexport * from \"./Emoji\";\nexport * from \"./Sticker\";\n"
  },
  {
    "path": "packages/discord-types/src/components.d.ts",
    "content": "import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from \"react\";\n\n\n// #region Old compability\n\nexport type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;\nexport type Margins = Record<\"marginTop16\" | \"marginTop8\" | \"marginBottom8\" | \"marginTop20\" | \"marginBottom20\", string>;\n\n// copy(find(m => Array.isArray(m) && m.includes(\"heading-sm/normal\")).map(JSON.stringify).join(\"|\"))\nexport type TextVariant = \"heading-sm/normal\" | \"heading-sm/medium\" | \"heading-sm/semibold\" | \"heading-sm/bold\" | \"heading-sm/extrabold\" | \"heading-md/normal\" | \"heading-md/medium\" | \"heading-md/semibold\" | \"heading-md/bold\" | \"heading-md/extrabold\" | \"heading-lg/normal\" | \"heading-lg/medium\" | \"heading-lg/semibold\" | \"heading-lg/bold\" | \"heading-lg/extrabold\" | \"heading-xl/normal\" | \"heading-xl/medium\" | \"heading-xl/semibold\" | \"heading-xl/bold\" | \"heading-xl/extrabold\" | \"heading-xxl/normal\" | \"heading-xxl/medium\" | \"heading-xxl/semibold\" | \"heading-xxl/bold\" | \"heading-xxl/extrabold\" | \"text-xxs/normal\" | \"text-xxs/medium\" | \"text-xxs/semibold\" | \"text-xxs/bold\" | \"text-xs/normal\" | \"text-xs/medium\" | \"text-xs/semibold\" | \"text-xs/bold\" | \"text-sm/normal\" | \"text-sm/medium\" | \"text-sm/semibold\" | \"text-sm/bold\" | \"text-md/normal\" | \"text-md/medium\" | \"text-md/semibold\" | \"text-md/bold\" | \"text-lg/normal\" | \"text-lg/medium\" | \"text-lg/semibold\" | \"text-lg/bold\";\n\nexport type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {\n    variant?: TextVariant;\n    tag?: \"div\" | \"span\" | \"p\" | \"strong\" | HeadingTag;\n}>;\n\nexport type Text = ComponentType<TextProps>;\n\nexport interface ButtonProps extends PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, \"size\">> {\n    /** Button.Looks.FILLED */\n    look?: string;\n    /** Button.Colors.BRAND */\n    color?: string;\n    /** Button.Sizes.MEDIUM */\n    size?: string;\n\n    className?: string;\n}\n\nexport type Button = ComponentType<ButtonProps> & {\n    Colors: Record<\"BRAND\" | \"RED\" | \"GREEN\" | \"PRIMARY\" | \"LINK\" | \"WHITE\" | \"TRANSPARENT\" | \"CUSTOM\", string>;\n    Looks: Record<\"FILLED\" | \"LINK\", string>;\n    Sizes: Record<\"NONE\" | \"SMALL\" | \"MEDIUM\" | \"LARGE\" | \"XLARGE\" | \"MIN\", string>;\n};\n\n// #endregion\n\nexport interface TooltipChildrenProps {\n    onClick(): void;\n    onMouseEnter(): void;\n    onMouseLeave(): void;\n    onContextMenu(): void;\n    onFocus(): void;\n    onBlur(): void;\n    \"aria-label\"?: string;\n}\n\nexport interface TooltipProps {\n    text: ReactNode | ComponentType;\n    children: FunctionComponent<TooltipChildrenProps>;\n    \"aria-label\"?: string;\n\n    allowOverflow?: boolean;\n    forceOpen?: boolean;\n    hide?: boolean;\n    hideOnClick?: boolean;\n    shouldShow?: boolean;\n    spacing?: number;\n\n    /** Tooltip.Colors.BLACK */\n    color?: string;\n    /** TooltipPositions.TOP */\n    position?: PopoutPosition;\n\n    tooltipClassName?: string;\n    tooltipContentClassName?: string;\n}\n\nexport type Tooltip = ComponentType<TooltipProps> & {\n    Colors: Record<\"BLACK\" | \"BRAND\" | \"CUSTOM\" | \"GREEN\" | \"GREY\" | \"PRIMARY\" | \"RED\" | \"YELLOW\", string>;\n};\n\nexport type TooltipPositions = Record<\"BOTTOM\" | \"CENTER\" | \"LEFT\" | \"RIGHT\" | \"TOP\" | \"WINDOW_CENTER\", string>;\n\nexport type TooltipContainer = ComponentType<PropsWithChildren<{\n    text: ReactNode;\n    element?: \"div\" | \"span\";\n    \"aria-label\"?: string | false;\n\n    delay?: number;\n    /** Tooltip.Colors.BLACK */\n    color?: string;\n    /** TooltipPositions.TOP */\n    position?: PopoutPosition;\n    spacing?: number;\n\n    className?: string;\n    tooltipClassName?: string | null;\n    tooltipContentClassName?: string | null;\n\n    allowOverflow?: boolean;\n    forceOpen?: boolean;\n    hideOnClick?: boolean;\n    disableTooltipPointerEvents?: boolean;\n}>>;\n\nexport type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {\n    editable?: boolean;\n    outline?: boolean;\n    /** Card.Types.PRIMARY */\n    type?: string;\n}>> & {\n    Types: Record<\"BRAND\" | \"CUSTOM\" | \"DANGER\" | \"PRIMARY\" | \"SUCCESS\" | \"WARNING\", string>;\n};\n\nexport type ComboboxPopout = ComponentType<PropsWithChildren<{\n    value: Set<any>;\n    placeholder: string;\n    children(query: string): ReactNode[];\n\n    onChange(value: any): void;\n    itemToString?: (item: any) => string;\n    onClose?(): void;\n\n    className?: string;\n    listClassName?: string;\n\n    autoFocus?: boolean;\n    multiSelect?: boolean;\n    maxVisibleItems?: number;\n    showScrollbar?: boolean;\n\n}>>;\n\nexport type CheckboxAligns = {\n    CENTER: \"center\";\n    TOP: \"top\";\n};\n\nexport type CheckboxTypes = {\n    DEFAULT: \"default\";\n    INVERTED: \"inverted\";\n    GHOST: \"ghost\";\n    ROW: \"row\";\n};\n\nexport type Checkbox = ComponentType<PropsWithChildren<{\n    value: boolean;\n    onChange(event: PointerEvent, value: boolean): void;\n\n    align?: \"center\" | \"top\";\n    disabled?: boolean;\n    displayOnly?: boolean;\n    readOnly?: boolean;\n    reverse?: boolean;\n    shape?: string;\n    size?: number;\n    type?: \"default\" | \"inverted\" | \"ghost\" | \"row\";\n}>> & {\n    Shapes: Record<\"BOX\" | \"ROUND\" | \"SMALL_BOX\", string>;\n    Aligns: CheckboxAligns;\n    Types: CheckboxTypes;\n};\n\nexport type Timestamp = ComponentType<PropsWithChildren<{\n    timestamp: Date;\n    isEdited?: boolean;\n\n    className?: string;\n    id?: string;\n\n    cozyAlt?: boolean;\n    compact?: boolean;\n    isInline?: boolean;\n    isVisibleOnlyOnHover?: boolean;\n}>>;\n\nexport type TextInput = ComponentType<PropsWithChildren<{\n    name?: string;\n    onChange?(value: string, name?: string): void;\n    placeholder?: string;\n    editable?: boolean;\n    /** defaults to 999. Pass null to disable this default */\n    maxLength?: number | null;\n    error?: string;\n\n    inputClassName?: string;\n    inputPrefix?: string;\n    inputRef?: Ref<HTMLInputElement>;\n    prefixElement?: ReactNode;\n\n    focusProps?: any;\n\n    /** TextInput.Sizes.DEFAULT */\n    size?: string;\n} & Omit<HTMLProps<HTMLInputElement>, \"onChange\" | \"maxLength\">>> & {\n    Sizes: Record<\"DEFAULT\" | \"MINI\", string>;\n};\n\n// FIXME: this is wrong, it's not actually just HTMLTextAreaElement\nexport type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, \"onChange\"> & {\n    onChange(v: string): void;\n    inputRef?: Ref<HTMLTextAreaElement>;\n}>;\n\ninterface SelectOption {\n    disabled?: boolean;\n    value: any;\n    label: string;\n    key?: React.Key;\n    default?: boolean;\n}\n\nexport type Select = ComponentType<PropsWithChildren<{\n    placeholder?: string;\n    options: ReadonlyArray<SelectOption>; // TODO\n\n    /**\n     * - 0 ~ Filled\n     * - 1 ~ Custom\n     */\n    look?: 0 | 1;\n    className?: string;\n    popoutClassName?: string;\n    popoutPosition?: PopoutPosition;\n    optionClassName?: string;\n\n    autoFocus?: boolean;\n    isDisabled?: boolean;\n    clearable?: boolean;\n    closeOnSelect?: boolean;\n    hideIcon?: boolean;\n\n    select(value: any): void;\n    isSelected(value: any): boolean;\n    serialize(value: any): string;\n    clear?(): void;\n\n    maxVisibleItems?: number;\n    popoutWidth?: number;\n\n    onClose?(): void;\n    onOpen?(): void;\n\n    renderOptionLabel?(option: SelectOption): ReactNode;\n    /** discord stupid this gets all options instead of one yeah */\n    renderOptionValue?(option: SelectOption[]): ReactNode;\n\n    \"aria-label\"?: boolean;\n    \"aria-labelledby\"?: boolean;\n}>>;\n\nexport type SearchableSelect = ComponentType<PropsWithChildren<{\n    placeholder?: string;\n    options: ReadonlyArray<SelectOption>; // TODO\n    value?: any;\n\n    /**\n     * - 0 ~ Filled\n     * - 1 ~ Custom\n     */\n    look?: 0 | 1;\n    className?: string;\n    popoutClassName?: string;\n    wrapperClassName?: string;\n    popoutPosition?: PopoutPosition;\n    optionClassName?: string;\n\n    autoFocus?: boolean;\n    isDisabled?: boolean;\n    clearable?: boolean;\n    closeOnSelect?: boolean;\n    clearOnSelect?: boolean;\n    multi?: boolean;\n\n    onChange(value: any): void;\n    onSearchChange?(value: string): void;\n\n    onClose?(): void;\n    onOpen?(): void;\n    onBlur?(): void;\n\n    renderOptionPrefix?(option: SelectOption): ReactNode;\n    renderOptionSuffix?(option: SelectOption): ReactNode;\n\n    filter?(option: SelectOption[], query: string): SelectOption[];\n\n    centerCaret?: boolean;\n    debounceTime?: number;\n    maxVisibleItems?: number;\n    popoutWidth?: number;\n\n    \"aria-labelledby\"?: boolean;\n}>>;\n\nexport type Slider = ComponentClass<PropsWithChildren<{\n    initialValue: number;\n    defaultValue?: number;\n    keyboardStep?: number;\n    maxValue?: number;\n    minValue?: number;\n    markers?: number[];\n    stickToMarkers?: boolean;\n\n    /** 0 above, 1 below */\n    markerPosition?: 0 | 1;\n    orientation?: \"horizontal\" | \"vertical\";\n\n    getAriaValueText?(currentValue: number): string;\n    renderMarker?(marker: number): ReactNode;\n    onMarkerRender?(marker: number): ReactNode;\n    onValueRender?(value: number): ReactNode;\n    onValueChange?(value: number): void;\n    asValueChanges?(value: number): void;\n\n    className?: string;\n    disabled?: boolean;\n    handleSize?: number;\n    mini?: boolean;\n    hideBubble?: boolean;\n\n    fillStyles?: CSSProperties;\n    barStyles?: CSSProperties;\n    grabberStyles?: CSSProperties;\n    grabberClassName?: string;\n    barClassName?: string;\n\n    \"aria-hidden\"?: boolean;\n    \"aria-label\"?: string;\n    \"aria-labelledby\"?: string;\n    \"aria-describedby\"?: string;\n}>>;\n\ndeclare enum PopoutAnimation {\n    NONE = \"1\",\n    TRANSLATE = \"2\",\n    SCALE = \"3\",\n    FADE = \"4\"\n}\n\ntype PopoutPosition = \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\" | \"window_center\";\n\nexport type Popout = ComponentType<{\n    children(\n        thing: {\n            \"aria-controls\": string;\n            \"aria-expanded\": boolean;\n            onClick(event: MouseEvent<HTMLElement>): void;\n            onKeyDown(event: KeyboardEvent<HTMLElement>): void;\n            onMouseDown(event: MouseEvent<HTMLElement>): void;\n        },\n        data: {\n            isShown: boolean;\n            position: PopoutPosition;\n        }\n    ): ReactNode;\n    shouldShow?: boolean;\n    targetElementRef: RefObject<any>;\n    renderPopout(args: {\n        closePopout(): void;\n        isPositioned: boolean;\n        nudge: number;\n        position: PopoutPosition;\n        setPopoutRef(ref: any): void;\n        updatePosition(): void;\n    }): ReactNode;\n\n    onRequestOpen?(): void;\n    onRequestClose?(): void;\n\n    /** \"center\" and others */\n    align?: \"left\" | \"right\" | \"center\";\n    /** Popout.Animation */\n    animation?: PopoutAnimation;\n    autoInvert?: boolean;\n    nudgeAlignIntoViewport?: boolean;\n    /** \"bottom\" and others */\n    position?: PopoutPosition;\n    positionKey?: string;\n    spacing?: number;\n}> & {\n    Animation: typeof PopoutAnimation;\n};\n\nexport type Dialog = ComponentType<JSX.IntrinsicElements[\"div\"]>;\n\ntype Resolve = (data: { theme: \"light\" | \"dark\", saturation: number; }) => {\n    hex(): string;\n    hsl(): string;\n    int(): number;\n    spring(): string;\n};\n\nexport type useToken = (color: {\n    css: string;\n    resolve: Resolve;\n}) => ReturnType<Resolve>;\n\nexport type Paginator = ComponentType<{\n    currentPage: number;\n    maxVisiblePages: number;\n    pageSize: number;\n    totalCount: number;\n\n    onPageChange?(page: number): void;\n    hideMaxPage?: boolean;\n}>;\n\nexport type MaskedLink = ComponentType<PropsWithChildren<{\n    href: string;\n    rel?: string;\n    target?: string;\n    title?: string,\n    className?: string;\n    tabIndex?: number;\n    onClick?(): void;\n    trusted?: boolean;\n    messageId?: string;\n    channelId?: string;\n}>>;\n\nexport interface ScrollerBaseProps {\n    className?: string;\n    style?: CSSProperties;\n    dir?: \"ltr\";\n    paddingFix?: boolean;\n    onClose?(): void;\n    onScroll?(): void;\n}\n\nexport type ScrollerThin = ComponentType<PropsWithChildren<ScrollerBaseProps & {\n    orientation?: \"horizontal\" | \"vertical\" | \"auto\";\n    fade?: boolean;\n}>>;\n\ninterface BaseListItem {\n    anchorId: any;\n    listIndex: number;\n    offsetTop: number;\n    section: number;\n}\ninterface ListSection extends BaseListItem {\n    type: \"section\";\n}\ninterface ListRow extends BaseListItem {\n    type: \"row\";\n    row: number;\n    rowIndex: number;\n}\n\nexport type ListScrollerThin = ComponentType<ScrollerBaseProps & {\n    sections: number[];\n    renderSection?: (item: ListSection) => React.ReactNode;\n    renderRow: (item: ListRow) => React.ReactNode;\n    renderFooter?: (item: any) => React.ReactNode;\n    renderSidebar?: (listVisible: boolean, sidebarVisible: boolean) => React.ReactNode;\n    wrapSection?: (section: number, children: React.ReactNode) => React.ReactNode;\n\n    sectionHeight: number;\n    rowHeight: number;\n    footerHeight?: number;\n    sidebarHeight?: number;\n\n    chunkSize?: number;\n\n    paddingTop?: number;\n    paddingBottom?: number;\n    fade?: boolean;\n    onResize?: Function;\n    getAnchorId?: any;\n\n    innerTag?: string;\n    innerId?: string;\n    innerClassName?: string;\n    innerRole?: string;\n    innerAriaLabel?: string;\n    // Yes, Discord uses this casing\n    innerAriaMultiselectable?: boolean;\n    innerAriaOrientation?: \"vertical\" | \"horizontal\";\n}>;\n\nexport type Clickable = <T extends \"a\" | \"div\" | \"span\" | \"li\" = \"div\">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {\n    tag?: T;\n}) => ReactNode;\n\nexport type Avatar = ComponentType<PropsWithChildren<{\n    className?: string;\n\n    src?: string;\n    size?: \"SIZE_16\" | \"SIZE_20\" | \"SIZE_24\" | \"SIZE_32\" | \"SIZE_40\" | \"SIZE_48\" | \"SIZE_56\" | \"SIZE_80\" | \"SIZE_120\";\n\n    statusColor?: string;\n    statusTooltip?: string;\n    statusBackdropColor?: string;\n\n    isMobile?: boolean;\n    isTyping?: boolean;\n    isSpeaking?: boolean;\n\n    typingIndicatorRef?: unknown;\n\n    \"aria-hidden\"?: boolean;\n    \"aria-label\"?: string;\n}>>;\n\ntype FocusLock = ComponentType<PropsWithChildren<{\n    containerRef: Ref<HTMLElement>;\n}>>;\n\nexport type Icon = ComponentType<JSX.IntrinsicElements[\"svg\"] & {\n    size?: string;\n    colorClass?: string;\n} & Record<string, any>>;\n\nexport type ColorPicker = ComponentType<{\n    color: number | null;\n    showEyeDropper?: boolean;\n    suggestedColors?: string[];\n    label?: ReactNode;\n    onChange(value: number | null): void;\n}>;\n"
  },
  {
    "path": "packages/discord-types/src/flux.d.ts",
    "content": "import { FluxStore } from \"./stores/FluxStore\";\n\nexport class FluxEmitter {\n    constructor();\n\n    changeSentinel: number;\n    changedStores: Set<FluxStore>;\n    isBatchEmitting: boolean;\n    isDispatching: boolean;\n    isPaused: boolean;\n    pauseTimer: NodeJS.Timeout | null;\n    reactChangedStores: Set<FluxStore>;\n\n    batched(batch: (...args: any[]) => void): void;\n    destroy(): void;\n    emit(): void;\n    emitNonReactOnce(): void;\n    emitReactOnce(): void;\n    getChangeSentinel(): number;\n    getIsPaused(): boolean;\n    injectBatchEmitChanges(batch: (...args: any[]) => void): void;\n    markChanged(store: FluxStore): void;\n    pause(): void;\n    resume(): void;\n}\n\nexport interface Flux {\n    Store: typeof FluxStore;\n    Emitter: FluxEmitter;\n}\n"
  },
  {
    "path": "packages/discord-types/src/fluxEvents.d.ts",
    "content": "/*\nfunction makeFluxEventList() {\n    // prefill MESSAGE_CREATE so that typescript infers this is a String Set\n    // without explicitly typing so that this function is also valid javascript\n    const events = new Set([\"MESSAGE_CREATE\"]);\n\n    const { nodes } = Vencord.Webpack.Common.FluxDispatcher._actionHandlers._dependencyGraph;\n    for (const nodeId in nodes) {\n        for (const event in nodes[nodeId].actionHandler) {\n            events.add(event);\n        }\n    }\n    for (const event in Vencord.Webpack.Common.FluxDispatcher._subscriptions) {\n        events.add(event);\n    }\n\n    return Array.from(events, e => JSON.stringify(e)).sort().join(\"|\");\n}\ncopy(makeFluxEventList())\n*/\n\nexport type FluxEvents = \"ACCESSIBILITY_COLORBLIND_TOGGLE\" | \"ACCESSIBILITY_DARK_SIDEBAR_TOGGLE\" | \"ACCESSIBILITY_DESATURATE_ROLES_TOGGLE\" | \"ACCESSIBILITY_FORCED_COLORS_MODAL_SEEN\" | \"ACCESSIBILITY_KEYBOARD_MODE_DISABLE\" | \"ACCESSIBILITY_KEYBOARD_MODE_ENABLE\" | \"ACCESSIBILITY_LOW_CONTRAST_TOGGLE\" | \"ACCESSIBILITY_RESET_TO_DEFAULT\" | \"ACCESSIBILITY_SET_ALWAYS_SHOW_LINK_DECORATIONS\" | \"ACCESSIBILITY_SET_CONTRAST\" | \"ACCESSIBILITY_SET_FONT_SIZE\" | \"ACCESSIBILITY_SET_MESSAGE_GROUP_SPACING\" | \"ACCESSIBILITY_SET_PREFERS_REDUCED_MOTION\" | \"ACCESSIBILITY_SET_ROLE_STYLE\" | \"ACCESSIBILITY_SET_SATURATION\" | \"ACCESSIBILITY_SET_SYNC_FORCED_COLORS\" | \"ACCESSIBILITY_SET_ZOOM\" | \"ACCESSIBILITY_SUBMIT_BUTTON_TOGGLE\" | \"ACCESSIBILITY_SYNC_PROFILE_THEME_WITH_USER_THEME_TOGGLE\" | \"ACCESSIBILITY_SYSTEM_COLOR_PREFERENCES_CHANGED\" | \"ACCESSIBILITY_SYSTEM_PREFERS_CONTRAST_CHANGED\" | \"ACCESSIBILITY_SYSTEM_PREFERS_CROSSFADES_CHANGED\" | \"ACCESSIBILITY_SYSTEM_PREFERS_REDUCED_MOTION_CHANGED\" | \"ACKNOWLEDGE_CHANNEL_SAFETY_WARNING_TOOLTIP\" | \"ACK_APPROVED_GUILD_JOIN_REQUEST\" | \"ACTIVE_AV_ERRORS_CHANGED\" | \"ACTIVE_BOGO_PROMOTION_FETCH\" | \"ACTIVE_BOGO_PROMOTION_FETCH_FAIL\" | \"ACTIVE_BOGO_PROMOTION_FETCH_SUCCESS\" | \"ACTIVE_CHANNELS_FETCH_FAILURE\" | \"ACTIVE_CHANNELS_FETCH_START\" | \"ACTIVE_CHANNELS_FETCH_SUCCESS\" | \"ACTIVE_OUTBOUND_PROMOTIONS_FETCH\" | \"ACTIVE_OUTBOUND_PROMOTIONS_FETCH_FAIL\" | \"ACTIVE_OUTBOUND_PROMOTIONS_FETCH_SUCCESS\" | \"ACTIVITY_INVITE_EDUCATION_DISMISS\" | \"ACTIVITY_INVITE_MODAL_CLOSE\" | \"ACTIVITY_INVITE_MODAL_OPEN\" | \"ACTIVITY_INVITE_MODAL_QUERY\" | \"ACTIVITY_INVITE_MODAL_SEND\" | \"ACTIVITY_JOIN\" | \"ACTIVITY_JOIN_FAILED\" | \"ACTIVITY_JOIN_LOADING\" | \"ACTIVITY_LAUNCH_FAIL\" | \"ACTIVITY_LAYOUT_MODE_UPDATE\" | \"ACTIVITY_METADATA_UPDATE\" | \"ACTIVITY_PLAY\" | \"ACTIVITY_POPOUT_WINDOW_OPEN\" | \"ACTIVITY_SCREEN_ORIENTATION_UPDATE\" | \"ACTIVITY_START\" | \"ACTIVITY_SYNC\" | \"ACTIVITY_SYNC_STOP\" | \"ACTIVITY_UPDATE_FAIL\" | \"ACTIVITY_UPDATE_START\" | \"ACTIVITY_UPDATE_SUCCESS\" | \"ADD_STICKER_PREVIEW\" | \"ADMIN_ONBOARDING_GUIDE_HIDE\" | \"ADYEN_CASH_APP_PAY_SUBMIT_SUCCESS\" | \"ADYEN_CREATE_CASH_APP_PAY_COMPONENT_SUCCESS\" | \"ADYEN_CREATE_CLIENT_SUCCESS\" | \"ADYEN_TEARDOWN_CLIENT\" | \"AFK\" | \"AGE_GATE_FAILURE_MODAL_OPEN\" | \"AGE_GATE_LOGOUT_UNDERAGE_NEW_USER\" | \"AGE_GATE_MODAL_CLOSE\" | \"AGE_GATE_MODAL_OPEN\" | \"AGE_GATE_SUCCESS_MODAL_OPEN\" | \"APEX_EXPERIMENT_CLEAR_SERVER_ASSIGNMENTS\" | \"APEX_EXPERIMENT_OVERRIDE_CLEAR\" | \"APEX_EXPERIMENT_OVERRIDE_CREATE\" | \"APEX_EXPERIMENT_OVERRIDE_DELETE\" | \"APPLICATIONS_FETCH\" | \"APPLICATIONS_FETCH_FAIL\" | \"APPLICATIONS_FETCH_SUCCESS\" | \"APPLICATION_ACTIVITY_STATISTICS_FETCH_FAIL\" | \"APPLICATION_ACTIVITY_STATISTICS_FETCH_START\" | \"APPLICATION_ACTIVITY_STATISTICS_FETCH_SUCCESS\" | \"APPLICATION_ASSETS_FETCH\" | \"APPLICATION_ASSETS_FETCH_SUCCESS\" | \"APPLICATION_ASSETS_UPDATE\" | \"APPLICATION_BRANCHES_FETCH_FAIL\" | \"APPLICATION_BRANCHES_FETCH_SUCCESS\" | \"APPLICATION_BUILD_FETCH_START\" | \"APPLICATION_BUILD_FETCH_SUCCESS\" | \"APPLICATION_BUILD_NOT_FOUND\" | \"APPLICATION_BUILD_SIZE_FETCH_FAIL\" | \"APPLICATION_BUILD_SIZE_FETCH_START\" | \"APPLICATION_BUILD_SIZE_FETCH_SUCCESS\" | \"APPLICATION_COMMAND_AUTOCOMPLETE_REQUEST\" | \"APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE\" | \"APPLICATION_COMMAND_EXECUTE_BAD_VERSION\" | \"APPLICATION_COMMAND_INDEX_FETCH_FAILURE\" | \"APPLICATION_COMMAND_INDEX_FETCH_REQUEST\" | \"APPLICATION_COMMAND_INDEX_FETCH_SUCCESS\" | \"APPLICATION_COMMAND_SET_ACTIVE_COMMAND\" | \"APPLICATION_COMMAND_SET_PREFERRED_COMMAND\" | \"APPLICATION_COMMAND_UPDATE_CHANNEL_STATE\" | \"APPLICATION_COMMAND_UPDATE_OPTIONS\" | \"APPLICATION_COMMAND_USED\" | \"APPLICATION_DIRECTORY_FETCH_APPLICATION\" | \"APPLICATION_DIRECTORY_FETCH_APPLICATION_FAILURE\" | \"APPLICATION_DIRECTORY_FETCH_APPLICATION_SUCCESS\" | \"APPLICATION_DIRECTORY_FETCH_CATEGORIES_SUCCESS\" | \"APPLICATION_DIRECTORY_FETCH_COLLECTIONS\" | \"APPLICATION_DIRECTORY_FETCH_COLLECTIONS_FAILURE\" | \"APPLICATION_DIRECTORY_FETCH_COLLECTIONS_SUCCESS\" | \"APPLICATION_DIRECTORY_FETCH_SEARCH\" | \"APPLICATION_DIRECTORY_FETCH_SEARCH_FAILURE\" | \"APPLICATION_DIRECTORY_FETCH_SEARCH_SUCCESS\" | \"APPLICATION_DIRECTORY_FETCH_SIMILAR_APPLICATIONS\" | \"APPLICATION_DIRECTORY_FETCH_SIMILAR_APPLICATIONS_FAILURE\" | \"APPLICATION_DIRECTORY_FETCH_SIMILAR_APPLICATIONS_SUCCESS\" | \"APPLICATION_FETCH\" | \"APPLICATION_FETCH_FAIL\" | \"APPLICATION_FETCH_SUCCESS\" | \"APPLICATION_STORE_ACCEPT_EULA\" | \"APPLICATION_STORE_ACCEPT_STORE_TERMS\" | \"APPLICATION_STORE_CLEAR_DATA\" | \"APPLICATION_STORE_DIRECTORY_LAYOUT_FETCHING\" | \"APPLICATION_STORE_DIRECTORY_LAYOUT_FETCH_FAILED\" | \"APPLICATION_STORE_DIRECTORY_LAYOUT_FETCH_SUCCESS\" | \"APPLICATION_STORE_LOCATION_CHANGE\" | \"APPLICATION_STORE_MATURE_AGREE\" | \"APPLICATION_STORE_RESET_NAVIGATION\" | \"APPLICATION_SUBSCRIPTIONS_CHANNEL_NOTICE_DISMISSED\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS_FAILURE\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS_SUCCESS\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS_FAILURE\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS_SUCCESS\" | \"APPLICATION_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN_SUCCESS\" | \"APPLICATION_UPDATE\" | \"APPLIED_BOOSTS_COOLDOWN_FETCH_SUCCESS\" | \"APPLIED_GUILD_BOOST_COUNT_RESET\" | \"APPLIED_GUILD_BOOST_COUNT_UPDATE\" | \"APP_DM_OPEN\" | \"APP_ICON_EDITOR_RESET\" | \"APP_ICON_TRACK_IMPRESSION\" | \"APP_ICON_UPDATED\" | \"APP_LAUNCHER_ADD_FAILED_APP_DM_LOAD\" | \"APP_LAUNCHER_DISMISS\" | \"APP_LAUNCHER_REMOVE_FAILED_APP_DM_LOAD\" | \"APP_LAUNCHER_SET_ACTIVE_COMMAND\" | \"APP_LAUNCHER_SHOW\" | \"APP_RECOMMENDATIONS_FETCH_RECOMMENDATIONS\" | \"APP_RECOMMENDATIONS_FETCH_RECOMMENDATIONS_FAILURE\" | \"APP_RECOMMENDATIONS_FETCH_RECOMMENDATIONS_SUCCESS\" | \"APP_STATE_UPDATE\" | \"APP_VIEW_SET_HOME_LINK\" | \"AUDIO_INPUT_DETECTED\" | \"AUDIO_RESET\" | \"AUDIO_SET_ACTIVE_INPUT_PROFILE\" | \"AUDIO_SET_ATTENUATION\" | \"AUDIO_SET_AUTOMATIC_GAIN_CONTROL\" | \"AUDIO_SET_BYPASS_SYSTEM_INPUT_PROCESSING\" | \"AUDIO_SET_DEBUG_LOGGING\" | \"AUDIO_SET_DISPLAY_SILENCE_WARNING\" | \"AUDIO_SET_ECHO_CANCELLATION\" | \"AUDIO_SET_INPUT_DEVICE\" | \"AUDIO_SET_INPUT_VOLUME\" | \"AUDIO_SET_KRISP_MODEL_OVERRIDE\" | \"AUDIO_SET_KRISP_SUPPRESSION_LEVEL\" | \"AUDIO_SET_LOCAL_PAN\" | \"AUDIO_SET_LOCAL_VIDEO_DISABLED\" | \"AUDIO_SET_LOCAL_VOLUME\" | \"AUDIO_SET_LOOPBACK\" | \"AUDIO_SET_MODE\" | \"AUDIO_SET_NOISE_CANCELLATION\" | \"AUDIO_SET_NOISE_SUPPRESSION\" | \"AUDIO_SET_OUTPUT_DEVICE\" | \"AUDIO_SET_OUTPUT_VOLUME\" | \"AUDIO_SET_QOS\" | \"AUDIO_SET_SELF_MUTE\" | \"AUDIO_SET_SIDECHAIN_COMPRESSION\" | \"AUDIO_SET_SIDECHAIN_COMPRESSION_STRENGTH\" | \"AUDIO_SET_SUBSYSTEM\" | \"AUDIO_SET_TEMPORARY_SELF_MUTE\" | \"AUDIO_TOGGLE_LOCAL_MUTE\" | \"AUDIO_TOGGLE_LOCAL_SOUNDBOARD_MUTE\" | \"AUDIO_TOGGLE_SELF_DEAF\" | \"AUDIO_TOGGLE_SELF_MUTE\" | \"AUDIO_VOLUME_CHANGE\" | \"AUDIT_LOG_FETCH_FAIL\" | \"AUDIT_LOG_FETCH_NEXT_PAGE_FAIL\" | \"AUDIT_LOG_FETCH_NEXT_PAGE_START\" | \"AUDIT_LOG_FETCH_NEXT_PAGE_SUCCESS\" | \"AUDIT_LOG_FETCH_START\" | \"AUDIT_LOG_FETCH_SUCCESS\" | \"AUDIT_LOG_FILTER_BY_ACTION\" | \"AUDIT_LOG_FILTER_BY_TARGET\" | \"AUDIT_LOG_FILTER_BY_USER\" | \"AUTHENTICATOR_CREATE\" | \"AUTHENTICATOR_DELETE\" | \"AUTHENTICATOR_UPDATE\" | \"AUTH_INVITE_UPDATE\" | \"AUTH_SESSION_CHANGE\" | \"AUTO_MODERATION_MENTION_RAID_DETECTION\" | \"AUTO_MODERATION_MENTION_RAID_NOTICE_DISMISS\" | \"AUTO_UPDATER_QUIT_AND_INSTALL\" | \"BACKGROUND_SYNC\" | \"BACKGROUND_SYNC_CHANNEL_MESSAGES\" | \"BASIC_GUILD_FETCH\" | \"BASIC_GUILD_FETCH_FAILURE\" | \"BASIC_GUILD_FETCH_SUCCESS\" | \"BILLING_CREATE_REFERRAL_SUCCESS\" | \"BILLING_IP_COUNTRY_CODE_FAILURE\" | \"BILLING_IP_COUNTRY_CODE_FETCH_START\" | \"BILLING_IP_LOCATION_FAILURE\" | \"BILLING_IP_LOCATION_FETCH_START\" | \"BILLING_LOCALIZED_PRICING_PROMO_FAILURE\" | \"BILLING_MOST_RECENT_SUBSCRIPTION_FETCH_SUCCESS\" | \"BILLING_NITRO_AFFINITY_FETCHED\" | \"BILLING_NITRO_AFFINITY_FETCH_SUCCEEDED\" | \"BILLING_PAYMENTS_FETCH_SUCCESS\" | \"BILLING_PAYMENT_FETCH_SUCCESS\" | \"BILLING_PAYMENT_SOURCES_FETCH_FAIL\" | \"BILLING_PAYMENT_SOURCES_FETCH_START\" | \"BILLING_PAYMENT_SOURCES_FETCH_SUCCESS\" | \"BILLING_PAYMENT_SOURCE_CREATE_FAIL\" | \"BILLING_PAYMENT_SOURCE_CREATE_START\" | \"BILLING_PAYMENT_SOURCE_CREATE_SUCCESS\" | \"BILLING_PAYMENT_SOURCE_FETCH_SUCCESS\" | \"BILLING_PAYMENT_SOURCE_REMOVE_CLEAR_ERROR\" | \"BILLING_PAYMENT_SOURCE_REMOVE_FAIL\" | \"BILLING_PAYMENT_SOURCE_REMOVE_START\" | \"BILLING_PAYMENT_SOURCE_REMOVE_SUCCESS\" | \"BILLING_PAYMENT_SOURCE_UPDATE_CLEAR_ERROR\" | \"BILLING_PAYMENT_SOURCE_UPDATE_FAIL\" | \"BILLING_PAYMENT_SOURCE_UPDATE_START\" | \"BILLING_PAYMENT_SOURCE_UPDATE_SUCCESS\" | \"BILLING_POPUP_BRIDGE_CALLBACK\" | \"BILLING_POPUP_BRIDGE_STATE_UPDATE\" | \"BILLING_PREVIOUS_PREMIUM_SUBSCRIPTION_FETCH_SUCCESS\" | \"BILLING_PURCHASE_TOKEN_AUTH_CLEAR_STATE\" | \"BILLING_REFERRALS_REMAINING_FETCH_FAIL\" | \"BILLING_REFERRALS_REMAINING_FETCH_START\" | \"BILLING_REFERRALS_REMAINING_FETCH_SUCCESS\" | \"BILLING_REFERRAL_RESOLVE_FAIL\" | \"BILLING_REFERRAL_RESOLVE_SUCCESS\" | \"BILLING_REFERRAL_TRIAL_OFFER_UPDATE\" | \"BILLING_SET_IP_COUNTRY_CODE\" | \"BILLING_SET_IP_LOCATION\" | \"BILLING_SET_LOCALIZED_PRICING_PROMO\" | \"BILLING_SUBSCRIPTION_CANCEL_FAIL\" | \"BILLING_SUBSCRIPTION_CANCEL_START\" | \"BILLING_SUBSCRIPTION_CANCEL_SUCCESS\" | \"BILLING_SUBSCRIPTION_FETCH_FAIL\" | \"BILLING_SUBSCRIPTION_FETCH_START\" | \"BILLING_SUBSCRIPTION_FETCH_SUCCESS\" | \"BILLING_SUBSCRIPTION_RESET\" | \"BILLING_SUBSCRIPTION_REWARD_ELIGIBILITY_FETCH_FAILURE\" | \"BILLING_SUBSCRIPTION_REWARD_ELIGIBILITY_FETCH_START\" | \"BILLING_SUBSCRIPTION_REWARD_ELIGIBILITY_FETCH_SUCCESS\" | \"BILLING_SUBSCRIPTION_UPDATE_FAIL\" | \"BILLING_SUBSCRIPTION_UPDATE_START\" | \"BILLING_SUBSCRIPTION_UPDATE_SUCCESS\" | \"BILLING_USER_OFFER_ACKNOWLEDGED_SUCCESS\" | \"BILLING_USER_OFFER_FETCH_FAIL\" | \"BILLING_USER_OFFER_FETCH_START\" | \"BILLING_USER_OFFER_FETCH_SUCCESS\" | \"BILLING_USER_TRIAL_OFFER_ACKNOWLEDGED_SUCCESS\" | \"BILLING_USER_TRIAL_OFFER_FETCH_SUCCESS\" | \"BOOSTED_GUILD_GRACE_PERIOD_NOTICE_DISMISS\" | \"BRAINTREE_CREATE_CLIENT_SUCCESS\" | \"BRAINTREE_CREATE_PAYPAL_CLIENT_SUCCESS\" | \"BRAINTREE_CREATE_VENMO_CLIENT_SUCCESS\" | \"BRAINTREE_TEARDOWN_PAYPAL_CLIENT\" | \"BRAINTREE_TEARDOWN_VENMO_CLIENT\" | \"BRAINTREE_TOKENIZE_PAYPAL_FAIL\" | \"BRAINTREE_TOKENIZE_PAYPAL_START\" | \"BRAINTREE_TOKENIZE_PAYPAL_SUCCESS\" | \"BRAINTREE_TOKENIZE_VENMO_FAIL\" | \"BRAINTREE_TOKENIZE_VENMO_START\" | \"BRAINTREE_TOKENIZE_VENMO_SUCCESS\" | \"BROWSER_HANDOFF_BEGIN\" | \"BROWSER_HANDOFF_FROM_APP\" | \"BROWSER_HANDOFF_SET_USER\" | \"BROWSER_HANDOFF_UNAVAILABLE\" | \"BUILD_OVERRIDE_RESOLVED\" | \"BULK_ACK\" | \"BULK_CLEAR_RECENTS\" | \"BURST_REACTION_ANIMATION_ADD\" | \"BURST_REACTION_EFFECT_CLEAR\" | \"BURST_REACTION_EFFECT_PLAY\" | \"BURST_REACTION_PICKER_ANIMATION_ADD\" | \"BURST_REACTION_PICKER_ANIMATION_CLEAR\" | \"CACHED_EMOJIS_LOADED\" | \"CACHED_STICKERS_LOADED\" | \"CACHE_LOADED\" | \"CACHE_LOADED_LAZY\" | \"CACHE_LOADED_LAZY_NO_CACHE\" | \"CALL_CHAT_TOASTS_SET_ENABLED\" | \"CALL_CONNECT\" | \"CALL_CONNECT_MULTIPLE\" | \"CALL_CREATE\" | \"CALL_DELETE\" | \"CALL_ENQUEUE_RING\" | \"CALL_UPDATE\" | \"CANDIDATE_GAMES_CHANGE\" | \"CATEGORY_COLLAPSE\" | \"CATEGORY_COLLAPSE_ALL\" | \"CATEGORY_EXPAND\" | \"CATEGORY_EXPAND_ALL\" | \"CERTIFIED_DEVICES_SET\" | \"CHANGE_LOG_FETCH_FAILED\" | \"CHANGE_LOG_FETCH_SUCCESS\" | \"CHANGE_LOG_LOCK\" | \"CHANGE_LOG_MARK_SEEN\" | \"CHANGE_LOG_RESOLVED\" | \"CHANGE_LOG_SET_CONFIG\" | \"CHANGE_LOG_SET_OVERRIDE\" | \"CHANGE_LOG_UNLOCK\" | \"CHANNEL_ACK\" | \"CHANNEL_CALL_POPOUT_WINDOW_OPEN\" | \"CHANNEL_COLLAPSE\" | \"CHANNEL_CREATE\" | \"CHANNEL_DELETE\" | \"CHANNEL_FOLLOWER_CREATED\" | \"CHANNEL_FOLLOWER_STATS_FETCH_FAILURE\" | \"CHANNEL_FOLLOWER_STATS_FETCH_SUCCESS\" | \"CHANNEL_FOLLOWING_PUBLISH_BUMP_DISMISSED\" | \"CHANNEL_FOLLOWING_PUBLISH_BUMP_HIDE_PERMANENTLY\" | \"CHANNEL_LOCAL_ACK\" | \"CHANNEL_MEMBER_COUNT_UPDATE\" | \"CHANNEL_MUTE_EXPIRED\" | \"CHANNEL_PERMISSIONS_DELETE_OVERWRITE_SUCCESS\" | \"CHANNEL_PERMISSIONS_PUT_OVERWRITE_SUCCESS\" | \"CHANNEL_PINS_ACK\" | \"CHANNEL_PINS_UPDATE\" | \"CHANNEL_PRELOAD\" | \"CHANNEL_RECIPIENT_ADD\" | \"CHANNEL_RECIPIENT_REMOVE\" | \"CHANNEL_RTC_ACTIVE_CHANNELS\" | \"CHANNEL_RTC_JUMP_TO_VOICE_CHANNEL_MESSAGE\" | \"CHANNEL_RTC_SELECT_PARTICIPANT\" | \"CHANNEL_RTC_UPDATE_CHAT_OPEN\" | \"CHANNEL_RTC_UPDATE_LAYOUT\" | \"CHANNEL_RTC_UPDATE_PARTCIPANTS_LIST_OPEN\" | \"CHANNEL_RTC_UPDATE_PARTICIPANTS_OPEN\" | \"CHANNEL_RTC_UPDATE_STAGE_STREAM_SIZE\" | \"CHANNEL_RTC_UPDATE_STAGE_VIDEO_LIMIT_BOOST_UPSELL_DISMISSED\" | \"CHANNEL_RTC_UPDATE_VOICE_PARTICIPANTS_HIDDEN\" | \"CHANNEL_SAFETY_WARNING_FEEDBACK\" | \"CHANNEL_SELECT\" | \"CHANNEL_SETTINGS_CLOSE\" | \"CHANNEL_SETTINGS_INIT\" | \"CHANNEL_SETTINGS_LOADED_INVITES\" | \"CHANNEL_SETTINGS_OVERWRITE_SELECT\" | \"CHANNEL_SETTINGS_PERMISSIONS_INIT\" | \"CHANNEL_SETTINGS_PERMISSIONS_SAVE_SUCCESS\" | \"CHANNEL_SETTINGS_PERMISSIONS_SELECT_PERMISSION\" | \"CHANNEL_SETTINGS_PERMISSIONS_SET_ADVANCED_MODE\" | \"CHANNEL_SETTINGS_PERMISSIONS_SUBMITTING\" | \"CHANNEL_SETTINGS_PERMISSIONS_UPDATE_PERMISSION\" | \"CHANNEL_SETTINGS_SET_SECTION\" | \"CHANNEL_SETTINGS_SUBMIT\" | \"CHANNEL_SETTINGS_SUBMIT_FAILURE\" | \"CHANNEL_SETTINGS_SUBMIT_SUCCESS\" | \"CHANNEL_SETTINGS_UPDATE\" | \"CHANNEL_STATUSES\" | \"CHANNEL_TOGGLE_MEMBERS_SECTION\" | \"CHANNEL_TOGGLE_SUMMARIES_SECTION\" | \"CHANNEL_UPDATES\" | \"CHECKING_FOR_UPDATES\" | \"CHECKOUT_RECOVERY_STATUS_FETCH\" | \"CHECKOUT_RECOVERY_STATUS_FETCH_FAILURE\" | \"CHECKOUT_RECOVERY_STATUS_FETCH_SUCCESS\" | \"CHECK_LAUNCHABLE_GAME\" | \"CLEAR_CACHES\" | \"CLEAR_CHANNEL_SAFETY_WARNINGS\" | \"CLEAR_CONSUMED_ENTITLEMENT\" | \"CLEAR_CONVERSATION_SUMMARIES\" | \"CLEAR_INTERACTION_MODAL_STATE\" | \"CLEAR_LAST_SESSION_VOICE_CHANNEL_ID\" | \"CLEAR_MENTIONS\" | \"CLEAR_MESSAGES\" | \"CLEAR_OLDEST_UNREAD_MESSAGE\" | \"CLEAR_PENDING_CHANNEL_AND_ROLE_UPDATES\" | \"CLEAR_REMOTE_DISCONNECT_VOICE_CHANNEL_ID\" | \"CLEAR_STICKER_PREVIEW\" | \"CLEAR_THEME_OVERRIDE\" | \"CLEAR_VIDEO_STREAM_READY_TIMEOUT\" | \"CLICKER_GAME_ADD_POINTS\" | \"CLICKER_GAME_PURCHASE_ITEM\" | \"CLICKER_GAME_PURCHASE_ITEM_UPGRADE\" | \"CLICKER_GAME_REDEEM_PRIZE_FAIL\" | \"CLICKER_GAME_REDEEM_PRIZE_START\" | \"CLICKER_GAME_REDEEM_PRIZE_SUCCESS\" | \"CLICKER_GAME_RESET\" | \"CLICKER_GAME_SET_MUTED\" | \"CLICKER_GAME_SET_VOLUME\" | \"CLICKER_GAME_UNLOCK_ACHIEVEMENT\" | \"CLICKER_GAME_UPDATE_ITEM_METADATA\" | \"CLIENT_THEMES_EDITOR_CLOSE\" | \"CLIPS_ALLOW_VOICE_RECORDING_UPDATE\" | \"CLIPS_CLASSIFY_HARDWARE\" | \"CLIPS_CLEAR_CLIPS_SESSION\" | \"CLIPS_CLEAR_NEW_CLIP_IDS\" | \"CLIPS_DELETE_CLIP\" | \"CLIPS_DISMISS_EDUCATION\" | \"CLIPS_INIT\" | \"CLIPS_INIT_FAILURE\" | \"CLIPS_LOAD_DIRECTORY_SUCCESS\" | \"CLIPS_RESTART\" | \"CLIPS_SAVE_ANIMATION_END\" | \"CLIPS_SAVE_CLIP\" | \"CLIPS_SAVE_CLIP_ERROR\" | \"CLIPS_SAVE_CLIP_PLACEHOLDER\" | \"CLIPS_SAVE_CLIP_PLACEHOLDER_ERROR\" | \"CLIPS_SAVE_CLIP_START\" | \"CLIPS_SETTINGS_UPDATE\" | \"CLIPS_SHOW_CALL_WARNING\" | \"CLIPS_UPDATE_METADATA\" | \"CLOSE_AGE_VERIFICATION_MODAL\" | \"CLOSE_SUSPENDED_USER\" | \"COLLECTIBLES_CATEGORIES_FETCH\" | \"COLLECTIBLES_CATEGORIES_FETCH_FAILURE\" | \"COLLECTIBLES_CATEGORIES_FETCH_SUCCESS\" | \"COLLECTIBLES_CLAIM\" | \"COLLECTIBLES_CLAIM_FAILURE\" | \"COLLECTIBLES_CLAIM_SUCCESS\" | \"COLLECTIBLES_MARKETING_FETCH\" | \"COLLECTIBLES_MARKETING_FETCH_SUCCESS\" | \"COLLECTIBLES_PRODUCT_DETAILS_OPEN\" | \"COLLECTIBLES_PRODUCT_FETCH\" | \"COLLECTIBLES_PRODUCT_FETCH_FAILURE\" | \"COLLECTIBLES_PRODUCT_FETCH_SUCCESS\" | \"COLLECTIBLES_PURCHASES_FETCH\" | \"COLLECTIBLES_PURCHASES_FETCH_FAILURE\" | \"COLLECTIBLES_PURCHASES_FETCH_SUCCESS\" | \"COLLECTIBLES_SET_SHOP_HOME_CONFIG_OVERRIDE\" | \"COLLECTIBLES_SHOP_CLOSE\" | \"COLLECTIBLES_SHOP_HOME_FETCH\" | \"COLLECTIBLES_SHOP_HOME_FETCH_FAILURE\" | \"COLLECTIBLES_SHOP_HOME_FETCH_SUCCESS\" | \"COLLECTIBLES_SHOP_OPEN\" | \"COLLECTIBLES_SKIP_NUM_CATEGORIES\" | \"COMMANDS_MIGRATION_NOTICE_DISMISSED\" | \"COMMANDS_MIGRATION_OVERVIEW_TOOLTIP_DISMISSED\" | \"COMMANDS_MIGRATION_TOGGLE_TOOLTIP_DISMISSED\" | \"COMMANDS_MIGRATION_UPDATE_SUCCESS\" | \"COMPLETE_NEW_MEMBER_ACTION\" | \"CONNECTED_DEVICE_DONT_SWITCH\" | \"CONNECTED_DEVICE_IGNORE\" | \"CONNECTED_DEVICE_NEVER_SHOW_MODAL\" | \"CONNECTED_DEVICE_SWITCH\" | \"CONNECTIONS_GRID_MODAL_HIDE\" | \"CONNECTIONS_GRID_MODAL_SHOW\" | \"CONNECTION_CLOSED\" | \"CONNECTION_OPEN\" | \"CONNECTION_OPEN_STATE_UPDATE\" | \"CONNECTION_OPEN_SUPPLEMENTAL\" | \"CONNECTION_RESUMED\" | \"CONSOLE_COMMAND_UPDATE\" | \"CONSUMABLES_CLEAR_ERROR\" | \"CONSUMABLES_ENTITLEMENT_FETCH_COMPLETED\" | \"CONSUMABLES_ENTITLEMENT_FETCH_FAILED\" | \"CONSUMABLES_ENTITLEMENT_FETCH_STARTED\" | \"CONSUMABLES_PRICE_FETCH_FAILED\" | \"CONSUMABLES_PRICE_FETCH_STARTED\" | \"CONSUMABLES_PRICE_FETCH_SUCCEEDED\" | \"CONTENT_INVENTORY_CLEAR_DELETE_HISTORY_ERROR\" | \"CONTENT_INVENTORY_CLEAR_FEED\" | \"CONTENT_INVENTORY_DEBUG_CLEAR_IMPRESSIONS\" | \"CONTENT_INVENTORY_DEBUG_LOG_IMPRESSIONS\" | \"CONTENT_INVENTORY_DEBUG_TOGGLE_FAST_IMPRESSION_CAPPING\" | \"CONTENT_INVENTORY_DEBUG_TOGGLE_IMPRESSION_CAPPING\" | \"CONTENT_INVENTORY_DELETE_OUTBOX_ENTRY_FAILURE\" | \"CONTENT_INVENTORY_DELETE_OUTBOX_ENTRY_START\" | \"CONTENT_INVENTORY_DELETE_OUTBOX_ENTRY_SUCCESS\" | \"CONTENT_INVENTORY_FETCH_OUTBOX_FAILURE\" | \"CONTENT_INVENTORY_FETCH_OUTBOX_START\" | \"CONTENT_INVENTORY_FETCH_OUTBOX_SUCCESS\" | \"CONTENT_INVENTORY_FORCE_SHOW_GAME_SHARING\" | \"CONTENT_INVENTORY_INBOX_STALE\" | \"CONTENT_INVENTORY_MANUAL_REFRESH\" | \"CONTENT_INVENTORY_SET_FEED\" | \"CONTENT_INVENTORY_SET_FEED_STATE\" | \"CONTENT_INVENTORY_SET_FILTERS\" | \"CONTENT_INVENTORY_TOGGLE_FEED_HIDDEN\" | \"CONTENT_INVENTORY_TRACK_ITEM_IMPRESSIONS\" | \"CONTEXT_MENU_CLOSE\" | \"CONTEXT_MENU_OPEN\" | \"CONVERSATION_SUMMARY_UPDATE\" | \"CREATE_PENDING_REPLY\" | \"CREATE_PENDING_SCHEDULED_MESSAGE\" | \"CREATE_REFERRALS_SUCCESS\" | \"CREATE_SHALLOW_PENDING_REPLY\" | \"CREATOR_MONETIZATION_NAG_ACTIVATE_ELIGIBLITY_FETCH_SUCCESS\" | \"CREATOR_MONETIZATION_PRICE_TIERS_FETCH\" | \"CREATOR_MONETIZATION_PRICE_TIERS_FETCH_FAILURE\" | \"CREATOR_MONETIZATION_PRICE_TIERS_FETCH_SUCCESS\" | \"CURRENT_BUILD_OVERRIDE_RESOLVED\" | \"CURRENT_USER_UPDATE\" | \"CUSTOM_ACTIVITY_LINK_FETCH_SUCCESS\" | \"DCF_DAILY_CAP_OVERRIDE\" | \"DCF_EVENT_LOGGED\" | \"DCF_HANDLE_DC_DISMISSED\" | \"DCF_HANDLE_DC_SHOWN\" | \"DCF_NEW_USER_MIN_AGE_REQUIRED_OVERRIDE\" | \"DCF_OVERRIDE_LAST_DC_DISMISSED\" | \"DCF_RESET\" | \"DECAY_READ_STATES\" | \"DELETED_ENTITY_IDS\" | \"DELETE_PENDING_REPLY\" | \"DELETE_PENDING_SCHEDULED_MESSAGE\" | \"DELETE_SUMMARY\" | \"DETECTABLE_GAME_SUPPLEMENTAL_FETCH\" | \"DETECTABLE_GAME_SUPPLEMENTAL_FETCH_FAILURE\" | \"DETECTABLE_GAME_SUPPLEMENTAL_FETCH_SUCCESS\" | \"DETECTED_OFF_PLATFORM_PREMIUM_PERKS_DISMISS\" | \"DEVELOPER_ACTIVITY_SHELF_FETCH_FAIL\" | \"DEVELOPER_ACTIVITY_SHELF_FETCH_START\" | \"DEVELOPER_ACTIVITY_SHELF_FETCH_SUCCESS\" | \"DEVELOPER_ACTIVITY_SHELF_MARK_ACTIVITY_USED\" | \"DEVELOPER_ACTIVITY_SHELF_SET_ACTIVITY_URL_OVERRIDE\" | \"DEVELOPER_ACTIVITY_SHELF_TOGGLE_USE_ACTIVITY_URL_OVERRIDE\" | \"DEVELOPER_ACTIVITY_SHELF_UPDATE_FILTER\" | \"DEVELOPER_OPTIONS_UPDATE_SETTINGS\" | \"DEVELOPER_TEST_MODE_AUTHORIZATION_FAIL\" | \"DEVELOPER_TEST_MODE_AUTHORIZATION_START\" | \"DEVELOPER_TEST_MODE_AUTHORIZATION_SUCCESS\" | \"DEVELOPER_TEST_MODE_RESET\" | \"DEVELOPER_TEST_MODE_RESET_ERROR\" | \"DEV_TOOLS_DESIGN_TOGGLE_SET\" | \"DEV_TOOLS_DESIGN_TOGGLE_WEB_SET\" | \"DEV_TOOLS_DEV_SETTING_SET\" | \"DEV_TOOLS_FRIENDS_LIST_GIFT_INTENTS_SHOWN_RESET\" | \"DEV_TOOLS_FRIENDS_TAB_BADGE_COOLDOWN_RESET\" | \"DEV_TOOLS_GIFT_MESSAGE_COOLDOWN_RESET\" | \"DEV_TOOLS_SETTINGS_UPDATE\" | \"DEV_TOOLS_SET_FRIEND_ANNIVERSARY_COUNT\" | \"DISABLE_AUTOMATIC_ACK\" | \"DISCOVER_CHECKLIST_FETCH_FAILURE\" | \"DISCOVER_CHECKLIST_FETCH_START\" | \"DISCOVER_CHECKLIST_FETCH_SUCCESS\" | \"DISMISS_CHANNEL_SAFETY_WARNINGS\" | \"DISMISS_FAVORITE_SUGGESTION\" | \"DISMISS_MEDIA_POST_SHARE_PROMPT\" | \"DISPATCH_APPLICATION_ADD_TO_INSTALLATIONS\" | \"DISPATCH_APPLICATION_CANCEL\" | \"DISPATCH_APPLICATION_ERROR\" | \"DISPATCH_APPLICATION_INSTALL\" | \"DISPATCH_APPLICATION_INSTALL_SCRIPTS_PROGRESS_UPDATE\" | \"DISPATCH_APPLICATION_LAUNCH_SETUP_COMPLETE\" | \"DISPATCH_APPLICATION_LAUNCH_SETUP_START\" | \"DISPATCH_APPLICATION_MOVE_UP\" | \"DISPATCH_APPLICATION_REMOVE_FINISHED\" | \"DISPATCH_APPLICATION_REPAIR\" | \"DISPATCH_APPLICATION_STATE_UPDATE\" | \"DISPATCH_APPLICATION_UNINSTALL\" | \"DISPATCH_APPLICATION_UPDATE\" | \"DISPLAYED_INVITE_SHOW\" | \"DOMAIN_MIGRATION_FAILURE\" | \"DOMAIN_MIGRATION_SKIP\" | \"DOMAIN_MIGRATION_START\" | \"DRAFT_CHANGE\" | \"DRAFT_CLEAR\" | \"DRAFT_SAVE\" | \"EMAIL_SETTINGS_FETCH_SUCCESS\" | \"EMAIL_SETTINGS_UPDATE\" | \"EMAIL_SETTINGS_UPDATE_SUCCESS\" | \"EMBEDDED_ACTIVITY_CLOSE\" | \"EMBEDDED_ACTIVITY_DEFERRED_OPEN\" | \"EMBEDDED_ACTIVITY_FETCH_SHELF\" | \"EMBEDDED_ACTIVITY_FETCH_SHELF_FAIL\" | \"EMBEDDED_ACTIVITY_FETCH_SHELF_SUCCESS\" | \"EMBEDDED_ACTIVITY_LAUNCH_FAIL\" | \"EMBEDDED_ACTIVITY_LAUNCH_START\" | \"EMBEDDED_ACTIVITY_LAUNCH_SUCCESS\" | \"EMBEDDED_ACTIVITY_OPEN\" | \"EMBEDDED_ACTIVITY_SET_CONFIG\" | \"EMBEDDED_ACTIVITY_SET_FOCUSED_LAYOUT\" | \"EMBEDDED_ACTIVITY_SET_ORIENTATION_LOCK_STATE\" | \"EMBEDDED_ACTIVITY_SET_PANEL_MODE\" | \"EMBEDDED_ACTIVITY_UPDATE\" | \"EMBEDDED_ACTIVITY_UPDATE_POPOUT_WINDOW_LAYOUT\" | \"EMBEDDED_ACTIVITY_UPDATE_V2\" | \"EMOJI_AUTOSUGGESTION_UPDATE\" | \"EMOJI_DELETE\" | \"EMOJI_FETCH_FAILURE\" | \"EMOJI_FETCH_SUCCESS\" | \"EMOJI_INTERACTION_INITIATED\" | \"EMOJI_TRACK_USAGE\" | \"EMOJI_UPLOAD_START\" | \"EMOJI_UPLOAD_STOP\" | \"ENABLE_AUTOMATIC_ACK\" | \"ENTITLEMENTS_FETCH_FOR_USER_FAIL\" | \"ENTITLEMENTS_FETCH_FOR_USER_START\" | \"ENTITLEMENTS_FETCH_FOR_USER_SUCCESS\" | \"ENTITLEMENTS_GIFTABLE_FETCH_SUCCESS\" | \"ENTITLEMENT_CREATE\" | \"ENTITLEMENT_DELETE\" | \"ENTITLEMENT_FETCH_APPLICATION_FAIL\" | \"ENTITLEMENT_FETCH_APPLICATION_START\" | \"ENTITLEMENT_FETCH_APPLICATION_SUCCESS\" | \"ENTITLEMENT_UPDATE\" | \"EVENT_DIRECTORY_FETCH_FAILURE\" | \"EVENT_DIRECTORY_FETCH_START\" | \"EVENT_DIRECTORY_FETCH_SUCCESS\" | \"EXPERIMENTS_FETCH\" | \"EXPERIMENTS_FETCH_FAILURE\" | \"EXPERIMENTS_FETCH_SUCCESS\" | \"EXPERIMENT_OVERRIDE_BUCKET\" | \"FAMILY_CENTER_FETCH_START\" | \"FAMILY_CENTER_HANDLE_TAB_SELECT\" | \"FAMILY_CENTER_INITIAL_LOAD\" | \"FAMILY_CENTER_LINKED_USERS_FETCH_SUCCESS\" | \"FAMILY_CENTER_LINK_CODE_FETCH_SUCCESS\" | \"FAMILY_CENTER_REQUEST_LINK_REMOVE_SUCCESS\" | \"FAMILY_CENTER_REQUEST_LINK_SUCCESS\" | \"FAMILY_CENTER_REQUEST_LINK_UPDATE_SUCCESS\" | \"FAMILY_CENTER_TEEN_ACTIVITY_FETCH_SUCCESS\" | \"FAMILY_CENTER_TEEN_ACTIVITY_MORE_FETCH_SUCCESS\" | \"FEEDBACK_OVERRIDE_CLEAR\" | \"FEEDBACK_OVERRIDE_SET\" | \"FETCH_AUTH_SESSIONS_SUCCESS\" | \"FETCH_CHAT_WALLPAPERS_FAILURE\" | \"FETCH_CHAT_WALLPAPERS_START\" | \"FETCH_CHAT_WALLPAPERS_SUCCESS\" | \"FETCH_GUILD_EVENT\" | \"FETCH_GUILD_EVENTS_FOR_GUILD\" | \"FETCH_GUILD_MEMBER_SUPPLEMENTAL_SUCCESS\" | \"FETCH_INTEGRATION_APPLICATION_IDS_FOR_MY_GUILDS\" | \"FETCH_INTEGRATION_APPLICATION_IDS_FOR_MY_GUILDS_FAILURE\" | \"FETCH_INTEGRATION_APPLICATION_IDS_FOR_MY_GUILDS_SUCCESS\" | \"FETCH_SCHEDULED_MESSAGES\" | \"FETCH_SCHEDULED_MESSAGES_FAILURE\" | \"FETCH_SCHEDULED_MESSAGES_SUCCESS\" | \"FINGERPRINT\" | \"FORCE_INVISIBLE\" | \"FORGOT_PASSWORD_REQUEST\" | \"FORGOT_PASSWORD_SENT\" | \"FORUM_SEARCH_CLEAR\" | \"FORUM_SEARCH_FAILURE\" | \"FORUM_SEARCH_QUERY_UPDATED\" | \"FORUM_SEARCH_START\" | \"FORUM_SEARCH_SUCCESS\" | \"FORUM_UNREADS\" | \"FRIENDS_LIST_GIFT_INTENTS_SHOWN\" | \"FRIENDS_SET_INITIAL_SECTION\" | \"FRIENDS_SET_SECTION\" | \"FRIENDS_TAB_BADGE_DISMISS\" | \"FRIEND_INVITES_FETCH_REQUEST\" | \"FRIEND_INVITES_FETCH_RESPONSE\" | \"FRIEND_INVITE_CREATE_FAILURE\" | \"FRIEND_INVITE_CREATE_REQUEST\" | \"FRIEND_INVITE_CREATE_SUCCESS\" | \"FRIEND_INVITE_REVOKE_REQUEST\" | \"FRIEND_INVITE_REVOKE_SUCCESS\" | \"FRIEND_SUGGESTION_CREATE\" | \"FRIEND_SUGGESTION_DELETE\" | \"GAMES_DATABASE_FETCH\" | \"GAMES_DATABASE_FETCH_FAIL\" | \"GAMES_DATABASE_UPDATE\" | \"GAME_CLOUD_SYNC_COMPLETE\" | \"GAME_CLOUD_SYNC_CONFLICT\" | \"GAME_CLOUD_SYNC_ERROR\" | \"GAME_CLOUD_SYNC_START\" | \"GAME_CLOUD_SYNC_UPDATE\" | \"GAME_CONSOLE_FETCH_DEVICES_FAIL\" | \"GAME_CONSOLE_FETCH_DEVICES_START\" | \"GAME_CONSOLE_FETCH_DEVICES_SUCCESS\" | \"GAME_CONSOLE_SELECT_DEVICE\" | \"GAME_DETECTION_DEBUGGING_START\" | \"GAME_DETECTION_DEBUGGING_STOP\" | \"GAME_DETECTION_DEBUGGING_TICK\" | \"GAME_DETECTION_WATCH_CANDIDATE_GAMES_START\" | \"GAME_ICON_UPDATE\" | \"GAME_INVITE_CLEAR_UNSEEN\" | \"GAME_INVITE_CREATE\" | \"GAME_INVITE_DELETE\" | \"GAME_INVITE_DELETE_MANY\" | \"GAME_INVITE_UPDATE_STATUS\" | \"GAME_LAUNCHABLE_UPDATE\" | \"GAME_LAUNCH_FAIL\" | \"GAME_LAUNCH_START\" | \"GAME_LAUNCH_SUCCESS\" | \"GAME_PROFILE_OPEN\" | \"GAME_RELATIONSHIP_ADD\" | \"GAME_RELATIONSHIP_REMOVE\" | \"GENERIC_PUSH_NOTIFICATION_SENT\" | \"GIFT_CODES_FETCH\" | \"GIFT_CODES_FETCH_FAILURE\" | \"GIFT_CODES_FETCH_SUCCESS\" | \"GIFT_CODE_CREATE\" | \"GIFT_CODE_CREATE_SUCCESS\" | \"GIFT_CODE_REDEEM\" | \"GIFT_CODE_REDEEM_FAILURE\" | \"GIFT_CODE_REDEEM_SUCCESS\" | \"GIFT_CODE_RESOLVE\" | \"GIFT_CODE_RESOLVE_FAILURE\" | \"GIFT_CODE_RESOLVE_SUCCESS\" | \"GIFT_CODE_REVOKE_SUCCESS\" | \"GIFT_CODE_UPDATE\" | \"GIFT_INTENT_FLOW_PURCHASED_GIFT\" | \"GIF_PICKER_INITIALIZE\" | \"GIF_PICKER_QUERY\" | \"GIF_PICKER_QUERY_FAILURE\" | \"GIF_PICKER_QUERY_SUCCESS\" | \"GIF_PICKER_SUGGESTIONS_SUCCESS\" | \"GIF_PICKER_TRENDING_FETCH_SUCCESS\" | \"GIF_PICKER_TRENDING_SEARCH_TERMS_SUCCESS\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_CLEAR\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_COUNT_FAILURE\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_COUNT_START\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_COUNT_SUCCESS\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_FAILURE\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_LAYOUT_RESET\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_START\" | \"GLOBAL_DISCOVERY_SERVERS_SEARCH_SUCCESS\" | \"GUILD_ACK\" | \"GUILD_ANALYTICS_ENGAGEMENT_OVERVIEW_FETCH_FAILURE\" | \"GUILD_ANALYTICS_ENGAGEMENT_OVERVIEW_FETCH_SUCCESS\" | \"GUILD_ANALYTICS_GROWTH_ACTIVATION_OVERVIEW_FETCH_FAILURE\" | \"GUILD_ANALYTICS_GROWTH_ACTIVATION_OVERVIEW_FETCH_SUCCESS\" | \"GUILD_ANALYTICS_GROWTH_ACTIVATION_RETENTION_FETCH_FAILURE\" | \"GUILD_ANALYTICS_GROWTH_ACTIVATION_RETENTION_FETCH_SUCCESS\" | \"GUILD_APPLICATIONS_FETCH_SUCCESS\" | \"GUILD_APPLICATION_COMMAND_INDEX_UPDATE\" | \"GUILD_APPLIED_BOOSTS_FETCH_SUCCESS\" | \"GUILD_APPLIED_BOOSTS_UPDATE\" | \"GUILD_APPLY_BOOST_FAIL\" | \"GUILD_APPLY_BOOST_START\" | \"GUILD_APPLY_BOOST_SUCCESS\" | \"GUILD_BAN_ADD\" | \"GUILD_BAN_REMOVE\" | \"GUILD_BOOST_SLOTS_FETCH\" | \"GUILD_BOOST_SLOTS_FETCH_SUCCESS\" | \"GUILD_BOOST_SLOT_CREATE\" | \"GUILD_BOOST_SLOT_UPDATE\" | \"GUILD_BOOST_SLOT_UPDATE_SUCCESS\" | \"GUILD_CREATE\" | \"GUILD_DELETE\" | \"GUILD_DIRECTORY_ADMIN_ENTRIES_FETCH_SUCCESS\" | \"GUILD_DIRECTORY_CACHED_SEARCH\" | \"GUILD_DIRECTORY_CATEGORY_SELECT\" | \"GUILD_DIRECTORY_COUNTS_FETCH_SUCCESS\" | \"GUILD_DIRECTORY_ENTRY_CREATE\" | \"GUILD_DIRECTORY_ENTRY_DELETE\" | \"GUILD_DIRECTORY_ENTRY_UPDATE\" | \"GUILD_DIRECTORY_FETCH_FAILURE\" | \"GUILD_DIRECTORY_FETCH_START\" | \"GUILD_DIRECTORY_FETCH_SUCCESS\" | \"GUILD_DIRECTORY_SEARCH_CLEAR\" | \"GUILD_DIRECTORY_SEARCH_FAILURE\" | \"GUILD_DIRECTORY_SEARCH_START\" | \"GUILD_DIRECTORY_SEARCH_SUCCESS\" | \"GUILD_DISCOVERY_CATEGORY_ADD\" | \"GUILD_DISCOVERY_CATEGORY_DELETE\" | \"GUILD_DISCOVERY_CATEGORY_FETCH_SUCCESS\" | \"GUILD_DISCOVERY_CATEGORY_UPDATE_FAIL\" | \"GUILD_DISCOVERY_METADATA_FETCH_FAIL\" | \"GUILD_DISCOVERY_SLUG_FETCH_FAIL\" | \"GUILD_DISCOVERY_SLUG_FETCH_SUCCESS\" | \"GUILD_EMOJIS_UPDATE\" | \"GUILD_FEATURE_ACK\" | \"GUILD_FOLDER_COLLAPSE\" | \"GUILD_FOLDER_CREATE_LOCAL\" | \"GUILD_FOLDER_DELETE_LOCAL\" | \"GUILD_FOLDER_EDIT_LOCAL\" | \"GUILD_GEO_RESTRICTED\" | \"GUILD_HOME_SETTINGS_FETCH_FAIL\" | \"GUILD_HOME_SETTINGS_FETCH_START\" | \"GUILD_HOME_SETTINGS_FETCH_SUCCESS\" | \"GUILD_HOME_SETTINGS_TOGGLE_ENABLED\" | \"GUILD_HOME_SETTINGS_UPDATE_FAIL\" | \"GUILD_HOME_SETTINGS_UPDATE_START\" | \"GUILD_HOME_SETTINGS_UPDATE_SUCCESS\" | \"GUILD_IDENTITY_SETTINGS_CLEAR_ERRORS\" | \"GUILD_IDENTITY_SETTINGS_INIT\" | \"GUILD_IDENTITY_SETTINGS_RESET_ALL_PENDING\" | \"GUILD_IDENTITY_SETTINGS_RESET_AND_CLOSE_FORM\" | \"GUILD_IDENTITY_SETTINGS_RESET_PENDING_MEMBER_CHANGES\" | \"GUILD_IDENTITY_SETTINGS_RESET_PENDING_PROFILE_CHANGES\" | \"GUILD_IDENTITY_SETTINGS_SET_GUILD\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_AVATAR\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_AVATAR_DECORATION\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_BANNER\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_BIO\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_NICKNAME\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_PROFILE_EFFECT_ID\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_PRONOUNS\" | \"GUILD_IDENTITY_SETTINGS_SET_PENDING_THEME_COLORS\" | \"GUILD_IDENTITY_SETTINGS_SUBMIT\" | \"GUILD_IDENTITY_SETTINGS_SUBMIT_FAILURE\" | \"GUILD_IDENTITY_SETTINGS_SUBMIT_SUCCESS\" | \"GUILD_INTEGRATIONS_UPDATE\" | \"GUILD_JOIN\" | \"GUILD_JOIN_REQUESTS_BULK_ACTION\" | \"GUILD_JOIN_REQUESTS_FETCH_FAILURE\" | \"GUILD_JOIN_REQUESTS_FETCH_START\" | \"GUILD_JOIN_REQUESTS_FETCH_SUCCESS\" | \"GUILD_JOIN_REQUESTS_SET_APPLICATION_TAB\" | \"GUILD_JOIN_REQUESTS_SET_SELECTED\" | \"GUILD_JOIN_REQUESTS_SET_SORT_ORDER\" | \"GUILD_JOIN_REQUEST_BY_ID_FETCH_SUCCESS\" | \"GUILD_JOIN_REQUEST_CREATE\" | \"GUILD_JOIN_REQUEST_DELETE\" | \"GUILD_JOIN_REQUEST_UPDATE\" | \"GUILD_LOCAL_RING_START\" | \"GUILD_MEMBERS_CHUNK_BATCH\" | \"GUILD_MEMBERS_REQUEST\" | \"GUILD_MEMBER_ADD\" | \"GUILD_MEMBER_LIST_UPDATE\" | \"GUILD_MEMBER_PROFILE_UPDATE\" | \"GUILD_MEMBER_REMOVE\" | \"GUILD_MEMBER_REMOVE_LOCAL\" | \"GUILD_MEMBER_UPDATE\" | \"GUILD_MEMBER_UPDATE_LOCAL\" | \"GUILD_MOVE_BY_ID\" | \"GUILD_MUTE_EXPIRED\" | \"GUILD_NEW_MEMBER_ACTIONS_DELETE_SUCCESS\" | \"GUILD_NEW_MEMBER_ACTIONS_FETCH_FAIL\" | \"GUILD_NEW_MEMBER_ACTIONS_FETCH_START\" | \"GUILD_NEW_MEMBER_ACTIONS_FETCH_SUCCESS\" | \"GUILD_NEW_MEMBER_ACTION_UPDATE_SUCCESS\" | \"GUILD_NSFW_AGREE\" | \"GUILD_ONBOARDING_COMPLETE\" | \"GUILD_ONBOARDING_PROMPTS_FETCH_FAILURE\" | \"GUILD_ONBOARDING_PROMPTS_FETCH_START\" | \"GUILD_ONBOARDING_PROMPTS_FETCH_SUCCESS\" | \"GUILD_ONBOARDING_PROMPTS_LOCAL_UPDATE\" | \"GUILD_ONBOARDING_SELECT_OPTION\" | \"GUILD_ONBOARDING_SET_STEP\" | \"GUILD_ONBOARDING_START\" | \"GUILD_ONBOARDING_UPDATE_RESPONSES_SUCCESS\" | \"GUILD_POWERUPS_ACK_NOTIFICATION\" | \"GUILD_POWERUPS_RESET_NOTIFICATIONS\" | \"GUILD_POWERUP_CATALOG_FETCH_SUCCESS\" | \"GUILD_POWERUP_ENTITLEMENTS_CREATE\" | \"GUILD_POWERUP_ENTITLEMENTS_DELETE\" | \"GUILD_PRODUCTS_FETCH\" | \"GUILD_PRODUCTS_FETCH_FAILURE\" | \"GUILD_PRODUCTS_FETCH_SUCCESS\" | \"GUILD_PRODUCT_CREATE\" | \"GUILD_PRODUCT_DELETE\" | \"GUILD_PRODUCT_FETCH\" | \"GUILD_PRODUCT_FETCH_FAILURE\" | \"GUILD_PRODUCT_FETCH_SUCCESS\" | \"GUILD_PRODUCT_UPDATE\" | \"GUILD_PROFILE_FETCH\" | \"GUILD_PROFILE_FETCH_FAILURE\" | \"GUILD_PROFILE_FETCH_SUCCESS\" | \"GUILD_PROFILE_UPDATE\" | \"GUILD_PROFILE_UPDATE_FAILURE\" | \"GUILD_PROFILE_UPDATE_SUCCESS\" | \"GUILD_PROFILE_UPDATE_VISIBILITY\" | \"GUILD_PROFILE_UPDATE_VISIBILITY_FAILURE\" | \"GUILD_PROFILE_UPDATE_VISIBILITY_SUCCESS\" | \"GUILD_PROGRESS_COMPLETED_SEEN\" | \"GUILD_PROGRESS_DISMISS\" | \"GUILD_PROGRESS_INITIALIZE\" | \"GUILD_PROMPT_VIEWED\" | \"GUILD_RESOURCE_CHANNEL_UPDATE_SUCCESS\" | \"GUILD_RING_START\" | \"GUILD_RING_STOP\" | \"GUILD_ROLE_CONNECTIONS_CONFIGURATIONS_FETCH_SUCCESS\" | \"GUILD_ROLE_CONNECTIONS_MODAL_SHOW\" | \"GUILD_ROLE_CONNECTION_ELIGIBILITY_FETCH_SUCCESS\" | \"GUILD_ROLE_CREATE\" | \"GUILD_ROLE_DELETE\" | \"GUILD_ROLE_MEMBER_ADD\" | \"GUILD_ROLE_MEMBER_BULK_ADD\" | \"GUILD_ROLE_MEMBER_COUNT_FETCH_SUCCESS\" | \"GUILD_ROLE_MEMBER_COUNT_UPDATE\" | \"GUILD_ROLE_MEMBER_REMOVE\" | \"GUILD_ROLE_SUBSCRIPTIONS_CREATE_LISTING\" | \"GUILD_ROLE_SUBSCRIPTIONS_DELETE_GROUP_LISTING\" | \"GUILD_ROLE_SUBSCRIPTIONS_DELETE_LISTING\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS_FAILURE\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS_SUCCESS\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN_SUCCESS\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_ABORTED\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_FAILURE\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_SUCCESS\" | \"GUILD_ROLE_SUBSCRIPTIONS_FETCH_TEMPLATES\" | \"GUILD_ROLE_SUBSCRIPTIONS_STASH_TEMPLATE_CHANNELS\" | \"GUILD_ROLE_SUBSCRIPTIONS_UPDATE_GROUP_LISTING\" | \"GUILD_ROLE_SUBSCRIPTIONS_UPDATE_LISTING\" | \"GUILD_ROLE_SUBSCRIPTIONS_UPDATE_SUBSCRIPTIONS_SETTINGS\" | \"GUILD_ROLE_SUBSCRIPTIONS_UPDATE_SUBSCRIPTION_TRIAL\" | \"GUILD_ROLE_UPDATE\" | \"GUILD_SCHEDULED_EVENT_CREATE\" | \"GUILD_SCHEDULED_EVENT_DELETE\" | \"GUILD_SCHEDULED_EVENT_EXCEPTIONS_DELETE\" | \"GUILD_SCHEDULED_EVENT_EXCEPTION_CREATE\" | \"GUILD_SCHEDULED_EVENT_EXCEPTION_DELETE\" | \"GUILD_SCHEDULED_EVENT_EXCEPTION_UPDATE\" | \"GUILD_SCHEDULED_EVENT_RSVPS_FETCH_SUCESS\" | \"GUILD_SCHEDULED_EVENT_UPDATE\" | \"GUILD_SCHEDULED_EVENT_USERS_FETCH_SUCCESS\" | \"GUILD_SCHEDULED_EVENT_USER_ADD\" | \"GUILD_SCHEDULED_EVENT_USER_COUNTS_FETCH_SUCCESS\" | \"GUILD_SCHEDULED_EVENT_USER_REMOVE\" | \"GUILD_SEARCH_RECENT_MEMBERS\" | \"GUILD_SETTINGS_CANCEL_CHANGES\" | \"GUILD_SETTINGS_CLOSE\" | \"GUILD_SETTINGS_DEFAULT_CHANNELS_RESET\" | \"GUILD_SETTINGS_DEFAULT_CHANNELS_SAVE_FAILED\" | \"GUILD_SETTINGS_DEFAULT_CHANNELS_SAVE_SUCCESS\" | \"GUILD_SETTINGS_DEFAULT_CHANNELS_SUBMIT\" | \"GUILD_SETTINGS_DEFAULT_CHANNELS_TOGGLE\" | \"GUILD_SETTINGS_INIT\" | \"GUILD_SETTINGS_JOIN_RULES_APPLY_SET_PENDING_FORM_FIELDS\" | \"GUILD_SETTINGS_JOIN_RULES_INVITE_SET_PENDING_RULES\" | \"GUILD_SETTINGS_JOIN_RULES_SET_CONTENT_LEVEL\" | \"GUILD_SETTINGS_JOIN_RULES_SET_SELECTED_TYPE\" | \"GUILD_SETTINGS_LOADED_BANS\" | \"GUILD_SETTINGS_LOADED_BANS_BATCH\" | \"GUILD_SETTINGS_LOADED_INTEGRATIONS\" | \"GUILD_SETTINGS_LOADED_INVITES\" | \"GUILD_SETTINGS_ONBOARDING_ADD_NEW_MEMBER_ACTION\" | \"GUILD_SETTINGS_ONBOARDING_ADD_RESOURCE_CHANNEL\" | \"GUILD_SETTINGS_ONBOARDING_DELETE_NEW_MEMBER_ACTION\" | \"GUILD_SETTINGS_ONBOARDING_DELETE_RESOURCE_CHANNEL\" | \"GUILD_SETTINGS_ONBOARDING_DISMISS_RESOURCE_CHANNEL_SUGGESTION\" | \"GUILD_SETTINGS_ONBOARDING_EDUCATION_UPSELL_DISMISSED\" | \"GUILD_SETTINGS_ONBOARDING_HOME_SETTINGS_RESET\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_EDIT\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_ERRORS\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_RESET\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_SAVE_FAILED\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_SAVE_SUCCESS\" | \"GUILD_SETTINGS_ONBOARDING_PROMPTS_SUBMIT\" | \"GUILD_SETTINGS_ONBOARDING_REORDER_NEW_MEMBER_ACTION\" | \"GUILD_SETTINGS_ONBOARDING_REORDER_RESOURCE_CHANNEL\" | \"GUILD_SETTINGS_ONBOARDING_SET_MODE\" | \"GUILD_SETTINGS_ONBOARDING_STEP\" | \"GUILD_SETTINGS_ONBOARDING_UPDATE_NEW_MEMBER_ACTION\" | \"GUILD_SETTINGS_ONBOARDING_UPDATE_RESOURCE_CHANNEL\" | \"GUILD_SETTINGS_ONBOARDING_UPDATE_WELCOME_MESSAGE\" | \"GUILD_SETTINGS_OPEN\" | \"GUILD_SETTINGS_PROFILE_UPDATE\" | \"GUILD_SETTINGS_ROLES_CLEAR_PERMISSIONS\" | \"GUILD_SETTINGS_ROLES_INIT\" | \"GUILD_SETTINGS_ROLES_ROLE_STYLE_UPDATE\" | \"GUILD_SETTINGS_ROLES_SAVE_FAIL\" | \"GUILD_SETTINGS_ROLES_SAVE_SUCCESS\" | \"GUILD_SETTINGS_ROLES_SORT_UPDATE\" | \"GUILD_SETTINGS_ROLES_SUBMITTING\" | \"GUILD_SETTINGS_ROLES_UPDATE_COLOR\" | \"GUILD_SETTINGS_ROLES_UPDATE_COLORS\" | \"GUILD_SETTINGS_ROLES_UPDATE_DESCRIPTION\" | \"GUILD_SETTINGS_ROLES_UPDATE_NAME\" | \"GUILD_SETTINGS_ROLES_UPDATE_PERMISSIONS\" | \"GUILD_SETTINGS_ROLES_UPDATE_PERMISSION_SET\" | \"GUILD_SETTINGS_ROLES_UPDATE_ROLE_CONNECTION_CONFIGURATIONS\" | \"GUILD_SETTINGS_ROLES_UPDATE_ROLE_ICON\" | \"GUILD_SETTINGS_ROLES_UPDATE_SETTINGS\" | \"GUILD_SETTINGS_ROLE_SELECT\" | \"GUILD_SETTINGS_SAFETY_PAGE\" | \"GUILD_SETTINGS_SAFETY_SET_SUBSECTION\" | \"GUILD_SETTINGS_SAVE_ROUTE_STACK\" | \"GUILD_SETTINGS_SET_MFA_SUCCESS\" | \"GUILD_SETTINGS_SET_SEARCH_QUERY\" | \"GUILD_SETTINGS_SET_SECTION\" | \"GUILD_SETTINGS_SET_VANITY_URL\" | \"GUILD_SETTINGS_SET_WIDGET\" | \"GUILD_SETTINGS_SUBMIT\" | \"GUILD_SETTINGS_SUBMIT_FAILURE\" | \"GUILD_SETTINGS_SUBMIT_SUCCESS\" | \"GUILD_SETTINGS_UPDATE\" | \"GUILD_SETTINGS_VANITY_URL_ERROR\" | \"GUILD_SETTINGS_VANITY_URL_RESET\" | \"GUILD_SETTINGS_VANITY_URL_SET\" | \"GUILD_SETTINGS_WIDGET_UPDATE\" | \"GUILD_SOUNDBOARD_FETCH\" | \"GUILD_SOUNDBOARD_SOUNDS_UPDATE\" | \"GUILD_SOUNDBOARD_SOUND_CREATE\" | \"GUILD_SOUNDBOARD_SOUND_DELETE\" | \"GUILD_SOUNDBOARD_SOUND_PLAY_END\" | \"GUILD_SOUNDBOARD_SOUND_PLAY_LOCALLY\" | \"GUILD_SOUNDBOARD_SOUND_PLAY_START\" | \"GUILD_SOUNDBOARD_SOUND_UPDATE\" | \"GUILD_STICKERS_CREATE_SUCCESS\" | \"GUILD_STICKERS_FETCH_SUCCESS\" | \"GUILD_STICKERS_UPDATE\" | \"GUILD_STOP_LURKING\" | \"GUILD_STOP_LURKING_FAILURE\" | \"GUILD_SUBSCRIPTIONS\" | \"GUILD_SUBSCRIPTIONS_ADD_MEMBER_UPDATES\" | \"GUILD_SUBSCRIPTIONS_CHANNEL\" | \"GUILD_SUBSCRIPTIONS_FLUSH\" | \"GUILD_SUBSCRIPTIONS_MEMBERS_ADD\" | \"GUILD_SUBSCRIPTIONS_MEMBERS_REMOVE\" | \"GUILD_SUBSCRIPTIONS_REMOVE_MEMBER_UPDATES\" | \"GUILD_TAG_CHANGED_COACHMARK_SEEN\" | \"GUILD_TEMPLATE_ACCEPT\" | \"GUILD_TEMPLATE_ACCEPT_FAILURE\" | \"GUILD_TEMPLATE_ACCEPT_SUCCESS\" | \"GUILD_TEMPLATE_CREATE_SUCCESS\" | \"GUILD_TEMPLATE_DELETE_SUCCESS\" | \"GUILD_TEMPLATE_DIRTY_TOOLTIP_HIDE\" | \"GUILD_TEMPLATE_DIRTY_TOOLTIP_REFRESH\" | \"GUILD_TEMPLATE_LOAD_FOR_GUILD_SUCCESS\" | \"GUILD_TEMPLATE_MODAL_HIDE\" | \"GUILD_TEMPLATE_MODAL_SHOW\" | \"GUILD_TEMPLATE_PROMOTION_TOOLTIP_HIDE\" | \"GUILD_TEMPLATE_RESOLVE\" | \"GUILD_TEMPLATE_RESOLVE_FAILURE\" | \"GUILD_TEMPLATE_RESOLVE_SUCCESS\" | \"GUILD_TEMPLATE_SYNC_SUCCESS\" | \"GUILD_TOGGLE_COLLAPSE_MUTED\" | \"GUILD_TOP_READ_CHANNELS_FETCH_SUCCESS\" | \"GUILD_UNAPPLY_BOOST_FAIL\" | \"GUILD_UNAPPLY_BOOST_START\" | \"GUILD_UNAPPLY_BOOST_SUCCESS\" | \"GUILD_UNAVAILABLE\" | \"GUILD_UNLOCKED_POWERUPS_FETCH_SUCCESS\" | \"GUILD_UPDATE\" | \"GUILD_UPDATE_DISCOVERY_METADATA\" | \"GUILD_UPDATE_DISCOVERY_METADATA_FAIL\" | \"GUILD_UPDATE_DISCOVERY_METADATA_FROM_SERVER\" | \"GUILD_VERIFICATION_CHECK\" | \"HABITUAL_DND_CLEAR\" | \"HIDE_ACTION_SHEET\" | \"HIDE_ACTION_SHEET_QUICK_SWITCHER\" | \"HIDE_KEYBOARD_SHORTCUTS\" | \"HIGH_FIVE_COMPLETE\" | \"HIGH_FIVE_COMPLETE_CLEAR\" | \"HIGH_FIVE_QUEUE\" | \"HIGH_FIVE_REMOVE\" | \"HIGH_FIVE_SET_ENABLED\" | \"HOTSPOT_HIDE\" | \"HOTSPOT_OVERRIDE_CLEAR\" | \"HOTSPOT_OVERRIDE_SET\" | \"HYPESQUAD_ONLINE_MEMBERSHIP_JOIN_SUCCESS\" | \"HYPESQUAD_ONLINE_MEMBERSHIP_LEAVE_SUCCESS\" | \"IDLE\" | \"IMPERSONATE_STOP\" | \"IMPERSONATE_UPDATE\" | \"INBOX_OPEN\" | \"INCOMING_CALL_MOVE\" | \"INITIALIZE_MEMBER_SAFETY_STORE\" | \"INITIATE_AGE_VERIFICATION\" | \"INSTALLATION_LOCATION_ADD\" | \"INSTALLATION_LOCATION_FETCH_METADATA\" | \"INSTALLATION_LOCATION_REMOVE\" | \"INSTALLATION_LOCATION_UPDATE\" | \"INSTANT_INVITE_CLEAR\" | \"INSTANT_INVITE_CREATE\" | \"INSTANT_INVITE_CREATE_FAILURE\" | \"INSTANT_INVITE_CREATE_SUCCESS\" | \"INSTANT_INVITE_REVOKE_SUCCESS\" | \"INTEGRATION_CREATE\" | \"INTEGRATION_DELETE\" | \"INTEGRATION_PERMISSION_SETTINGS_APPLICATION_PERMISSIONS_FETCH_FAILURE\" | \"INTEGRATION_PERMISSION_SETTINGS_CLEAR\" | \"INTEGRATION_PERMISSION_SETTINGS_COMMANDS_FETCH_FAILURE\" | \"INTEGRATION_PERMISSION_SETTINGS_COMMANDS_FETCH_SUCCESS\" | \"INTEGRATION_PERMISSION_SETTINGS_COMMAND_UPDATE\" | \"INTEGRATION_PERMISSION_SETTINGS_EDIT\" | \"INTEGRATION_PERMISSION_SETTINGS_INIT\" | \"INTEGRATION_PERMISSION_SETTINGS_RESET\" | \"INTEGRATION_QUERY\" | \"INTEGRATION_QUERY_FAILURE\" | \"INTEGRATION_QUERY_SUCCESS\" | \"INTEGRATION_SETTINGS_INIT\" | \"INTEGRATION_SETTINGS_SAVE_FAILURE\" | \"INTEGRATION_SETTINGS_SAVE_SUCCESS\" | \"INTEGRATION_SETTINGS_SET_SECTION\" | \"INTEGRATION_SETTINGS_START_EDITING_COMMAND\" | \"INTEGRATION_SETTINGS_START_EDITING_INTEGRATION\" | \"INTEGRATION_SETTINGS_START_EDITING_WEBHOOK\" | \"INTEGRATION_SETTINGS_STOP_EDITING_COMMAND\" | \"INTEGRATION_SETTINGS_STOP_EDITING_INTEGRATION\" | \"INTEGRATION_SETTINGS_STOP_EDITING_WEBHOOK\" | \"INTEGRATION_SETTINGS_SUBMITTING\" | \"INTEGRATION_SETTINGS_UPDATE_INTEGRATION\" | \"INTEGRATION_SETTINGS_UPDATE_WEBHOOK\" | \"INTERACTION_CREATE\" | \"INTERACTION_FAILURE\" | \"INTERACTION_IFRAME_MODAL_CLOSE\" | \"INTERACTION_IFRAME_MODAL_CREATE\" | \"INTERACTION_IFRAME_MODAL_KEY_CREATE\" | \"INTERACTION_MODAL_CREATE\" | \"INTERACTION_QUEUE\" | \"INTERACTION_SUCCESS\" | \"INVITE_ACCEPT\" | \"INVITE_ACCEPT_FAILURE\" | \"INVITE_ACCEPT_SUCCESS\" | \"INVITE_APP_NOT_OPENED\" | \"INVITE_APP_OPENED\" | \"INVITE_APP_OPENING\" | \"INVITE_MODAL_CLOSE\" | \"INVITE_MODAL_ERROR\" | \"INVITE_MODAL_OPEN\" | \"INVITE_RESOLVE\" | \"INVITE_RESOLVE_FAILURE\" | \"INVITE_RESOLVE_SUCCESS\" | \"INVITE_SUGGESTIONS_SEARCH\" | \"KEYBINDS_ADD_KEYBIND\" | \"KEYBINDS_DELETE_KEYBIND\" | \"KEYBINDS_ENABLE_ALL_KEYBINDS\" | \"KEYBINDS_REGISTER_GLOBAL_KEYBIND_ACTIONS\" | \"KEYBINDS_SET_KEYBIND\" | \"KEYBOARD_NAVIGATION_EXPLAINER_MODAL_SEEN\" | \"LAB_FEATURE_TOGGLE\" | \"LAYER_POP\" | \"LAYER_POP_ALL\" | \"LAYER_PUSH\" | \"LAYOUT_CREATE\" | \"LAYOUT_CREATE_WIDGETS\" | \"LAYOUT_DELETE_ALL_WIDGETS\" | \"LAYOUT_DELETE_WIDGET\" | \"LAYOUT_SET_PINNED\" | \"LAYOUT_SET_TOP_WIDGET\" | \"LAYOUT_SET_WIDGET_META\" | \"LAYOUT_UPDATE_WIDGET\" | \"LIBRARY_APPLICATIONS_TEST_MODE_ENABLED\" | \"LIBRARY_APPLICATION_ACTIVE_BRANCH_UPDATE\" | \"LIBRARY_APPLICATION_ACTIVE_LAUNCH_OPTION_UPDATE\" | \"LIBRARY_APPLICATION_FILTER_UPDATE\" | \"LIBRARY_APPLICATION_FLAGS_UPDATE_START\" | \"LIBRARY_APPLICATION_FLAGS_UPDATE_SUCCESS\" | \"LIBRARY_APPLICATION_UPDATE\" | \"LIBRARY_FETCH_SUCCESS\" | \"LIBRARY_TABLE_ACTIVE_ROW_ID_UPDATE\" | \"LIBRARY_TABLE_SORT_UPDATE\" | \"LIVE_CHANNEL_NOTICE_HIDE\" | \"LOAD_ARCHIVED_THREADS\" | \"LOAD_ARCHIVED_THREADS_FAIL\" | \"LOAD_ARCHIVED_THREADS_SUCCESS\" | \"LOAD_CHANNELS\" | \"LOAD_DATA_HARVEST_TYPE_FAILURE\" | \"LOAD_DATA_HARVEST_TYPE_START\" | \"LOAD_FORUM_POSTS\" | \"LOAD_FRIEND_SUGGESTIONS_FAILURE\" | \"LOAD_FRIEND_SUGGESTIONS_SUCCESS\" | \"LOAD_GUILD_AFFINITIES_SUCCESS\" | \"LOAD_ICYMI_HYDRATED\" | \"LOAD_INVITE_SUGGESTIONS\" | \"LOAD_MESSAGES\" | \"LOAD_MESSAGES_AROUND_SUCCESS\" | \"LOAD_MESSAGES_FAILURE\" | \"LOAD_MESSAGES_SUCCESS\" | \"LOAD_MESSAGES_SUCCESS_CACHED\" | \"LOAD_MESSAGE_INTERACTION_DATA_SUCCESS\" | \"LOAD_MESSAGE_REQUESTS_SUPPLEMENTAL_DATA_ERROR\" | \"LOAD_MESSAGE_REQUESTS_SUPPLEMENTAL_DATA_SUCCESS\" | \"LOAD_NOTIFICATION_CENTER_ITEMS\" | \"LOAD_NOTIFICATION_CENTER_ITEMS_FAILURE\" | \"LOAD_NOTIFICATION_CENTER_ITEMS_SUCCESS\" | \"LOAD_PINNED_MESSAGES\" | \"LOAD_PINNED_MESSAGES_FAILURE\" | \"LOAD_PINNED_MESSAGES_SUCCESS\" | \"LOAD_RECENT_MENTIONS\" | \"LOAD_RECENT_MENTIONS_FAILURE\" | \"LOAD_RECENT_MENTIONS_SUCCESS\" | \"LOAD_REGIONS\" | \"LOAD_RELATIONSHIPS_FAILURE\" | \"LOAD_RELATIONSHIPS_SUCCESS\" | \"LOAD_THREADS_SUCCESS\" | \"LOAD_USER_AFFINITIES_V2\" | \"LOAD_USER_AFFINITIES_V2_FAILURE\" | \"LOAD_USER_AFFINITIES_V2_SUCCESS\" | \"LOCAL_ACTIVITY_UPDATE\" | \"LOCAL_MESSAGES_LOADED\" | \"LOCAL_MESSAGE_CREATE\" | \"LOGIN\" | \"LOGIN_ACCOUNT_DISABLED\" | \"LOGIN_ACCOUNT_SCHEDULED_FOR_DELETION\" | \"LOGIN_ATTEMPTED\" | \"LOGIN_FAILURE\" | \"LOGIN_MFA\" | \"LOGIN_MFA_STEP\" | \"LOGIN_PASSWORD_RECOVERY_PHONE_VERIFICATION\" | \"LOGIN_PHONE_IP_AUTHORIZATION_REQUIRED\" | \"LOGIN_RESET\" | \"LOGIN_STATUS_RESET\" | \"LOGIN_SUCCESS\" | \"LOGIN_SUSPENDED_USER\" | \"LOGOUT\" | \"LOGOUT_AUTH_SESSIONS_SUCCESS\" | \"MASKED_LINK_ADD_TRUSTED_DOMAIN\" | \"MASKED_LINK_ADD_TRUSTED_PROTOCOL\" | \"MAX_MEMBER_COUNT_NOTICE_DISMISS\" | \"MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS\" | \"MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS_ERROR\" | \"MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS_START\" | \"MEDIA_ENGINE_CONNECTION_STATS\" | \"MEDIA_ENGINE_CONNECTION_STATS_HISTORY_RESET\" | \"MEDIA_ENGINE_DEVICES\" | \"MEDIA_ENGINE_INTERACTION_REQUIRED\" | \"MEDIA_ENGINE_NOISE_CANCELLATION_ERROR\" | \"MEDIA_ENGINE_NOISE_CANCELLATION_ERROR_RESET\" | \"MEDIA_ENGINE_PERMISSION\" | \"MEDIA_ENGINE_SET_AEC_DUMP\" | \"MEDIA_ENGINE_SET_AUDIO_ENABLED\" | \"MEDIA_ENGINE_SET_ENABLE_HARDWARE_MUTE_NOTICE\" | \"MEDIA_ENGINE_SET_EXPERIMENTAL_ENCODERS\" | \"MEDIA_ENGINE_SET_EXPERIMENTAL_SOUNDSHARE\" | \"MEDIA_ENGINE_SET_GO_LIVE_SOURCE\" | \"MEDIA_ENGINE_SET_HARDWARE_ENCODING\" | \"MEDIA_ENGINE_SET_OPEN_H264\" | \"MEDIA_ENGINE_SET_USE_SYSTEM_SCREENSHARE_PICKER\" | \"MEDIA_ENGINE_SET_VIDEO_DEVICE\" | \"MEDIA_ENGINE_SET_VIDEO_ENABLED\" | \"MEDIA_ENGINE_SET_VIDEO_HOOK\" | \"MEDIA_ENGINE_SOUNDSHARE_FAILED\" | \"MEDIA_ENGINE_SOUNDSHARE_TRANSMITTING\" | \"MEDIA_ENGINE_VIDEO_SOURCE_QUALITY_CHANGED\" | \"MEDIA_ENGINE_VIDEO_STATE_CHANGED\" | \"MEDIA_ENGINE_VOICE_ACTIVITY_DETECTION_ERROR\" | \"MEDIA_PLAYBACK_POSITION_UPDATE\" | \"MEDIA_PLAYBACK_RATE_UPDATE\" | \"MEDIA_POST_EMBED_FETCH\" | \"MEDIA_POST_EMBED_FETCH_FAILURE\" | \"MEDIA_POST_EMBED_FETCH_SUCCESS\" | \"MEDIA_SESSION_JOINED\" | \"MEMBER_SAFETY_GUILD_MEMBER_SEARCH_SUCCESS\" | \"MEMBER_SAFETY_GUILD_MEMBER_UPDATE_BATCH\" | \"MEMBER_SAFETY_NEW_MEMBER_TIMESTAMP_REFRESH\" | \"MEMBER_SAFETY_PAGINATION_TOKEN_UPDATE\" | \"MEMBER_SAFETY_PAGINATION_UPDATE\" | \"MEMBER_SAFETY_SEARCH_STATE_UPDATE\" | \"MEMBER_VERIFICATION_FORM_FETCH_FAIL\" | \"MEMBER_VERIFICATION_FORM_UPDATE\" | \"MESSAGE_ACK\" | \"MESSAGE_ACKED\" | \"MESSAGE_CREATE\" | \"MESSAGE_DELETE\" | \"MESSAGE_DELETE_BULK\" | \"MESSAGE_EDIT_FAILED_AUTOMOD\" | \"MESSAGE_END_EDIT\" | \"MESSAGE_EXPLICIT_CONTENT_FP_CREATE\" | \"MESSAGE_EXPLICIT_CONTENT_FP_SUBMIT\" | \"MESSAGE_EXPLICIT_CONTENT_SCAN_TIMEOUT\" | \"MESSAGE_GIFT_INTENT_SHOWN\" | \"MESSAGE_LENGTH_UPSELL\" | \"MESSAGE_NOTIFICATION_SHOWN\" | \"MESSAGE_PREVIEWS_LOADED\" | \"MESSAGE_PREVIEWS_LOCALLY_LOADED\" | \"MESSAGE_REACTION_ADD\" | \"MESSAGE_REACTION_ADD_MANY\" | \"MESSAGE_REACTION_ADD_USERS\" | \"MESSAGE_REACTION_REMOVE\" | \"MESSAGE_REACTION_REMOVE_ALL\" | \"MESSAGE_REACTION_REMOVE_EMOJI\" | \"MESSAGE_REMINDER_DUE\" | \"MESSAGE_REQUEST_ACCEPT_OPTIMISTIC\" | \"MESSAGE_REQUEST_ACK\" | \"MESSAGE_REQUEST_CLEAR_ACK\" | \"MESSAGE_REVEAL\" | \"MESSAGE_SEND_FAILED\" | \"MESSAGE_SEND_FAILED_AUTOMOD\" | \"MESSAGE_START_EDIT\" | \"MESSAGE_UPDATE\" | \"MESSAGE_UPDATE_EDIT\" | \"MFA_CLEAR_BACKUP_CODES\" | \"MFA_DISABLE_SUCCESS\" | \"MFA_ENABLE_SUCCESS\" | \"MFA_SEEN_BACKUP_CODE_PROMPT\" | \"MFA_SEND_VERIFICATION_KEY\" | \"MFA_SMS_TOGGLE\" | \"MFA_SMS_TOGGLE_COMPLETE\" | \"MFA_VIEW_BACKUP_CODES\" | \"MFA_WEBAUTHN_CREDENTIALS_LOADED\" | \"MOBILE_NATIVE_UPDATE_CHECK_FINISHED\" | \"MOBILE_WEB_SIDEBAR_CLOSE\" | \"MOBILE_WEB_SIDEBAR_OPEN\" | \"MODAL_POP\" | \"MODAL_PUSH\" | \"MOD_VIEW_SEARCH_FINISH\" | \"MULTI_ACCOUNT_INVALIDATE_PUSH_SYNC_TOKENS\" | \"MULTI_ACCOUNT_MOBILE_EXPERIMENT_UPDATE\" | \"MULTI_ACCOUNT_MOVE_ACCOUNT\" | \"MULTI_ACCOUNT_REMOVE_ACCOUNT\" | \"MULTI_ACCOUNT_UPDATE_PUSH_SYNC_TOKEN\" | \"MULTI_ACCOUNT_VALIDATE_TOKEN_FAILURE\" | \"MULTI_ACCOUNT_VALIDATE_TOKEN_REQUEST\" | \"MULTI_ACCOUNT_VALIDATE_TOKEN_SUCCESS\" | \"MUTUAL_FRIENDS_FETCH_FAILURE\" | \"MUTUAL_FRIENDS_FETCH_START\" | \"MUTUAL_FRIENDS_FETCH_SUCCESS\" | \"NATIVE_APP_MODAL_OPENED\" | \"NATIVE_APP_MODAL_OPENING\" | \"NATIVE_APP_MODAL_OPEN_FAILED\" | \"NATIVE_SCREEN_SHARE_PICKER_CANCEL\" | \"NATIVE_SCREEN_SHARE_PICKER_ERROR\" | \"NATIVE_SCREEN_SHARE_PICKER_PRESENT\" | \"NATIVE_SCREEN_SHARE_PICKER_RELEASE\" | \"NATIVE_SCREEN_SHARE_PICKER_UPDATE\" | \"NEWLY_ADDED_EMOJI_SEEN_ACKNOWLEDGED\" | \"NEWLY_ADDED_EMOJI_SEEN_PENDING\" | \"NEWLY_ADDED_EMOJI_SEEN_UPDATED\" | \"NEW_PAYMENT_SOURCE_ADDRESS_INFO_UPDATE\" | \"NEW_PAYMENT_SOURCE_CARD_INFO_UPDATE\" | \"NEW_PAYMENT_SOURCE_CLEAR_ERROR\" | \"NEW_PAYMENT_SOURCE_STRIPE_PAYMENT_REQUEST_UPDATE\" | \"NOTICE_DISABLE\" | \"NOTICE_DISMISS\" | \"NOTICE_SHOW\" | \"NOTIFICATIONS_SET_DESKTOP_TYPE\" | \"NOTIFICATIONS_SET_DISABLED_SOUNDS\" | \"NOTIFICATIONS_SET_DISABLE_UNREAD_BADGE\" | \"NOTIFICATIONS_SET_NOTIFY_MESSAGES_IN_SELECTED_CHANNEL\" | \"NOTIFICATIONS_SET_PERMISSION_STATE\" | \"NOTIFICATIONS_SET_TASKBAR_FLASH\" | \"NOTIFICATIONS_SET_TTS_TYPE\" | \"NOTIFICATIONS_TOGGLE_ALL_DISABLED\" | \"NOTIFICATION_CENTER_CLEAR_GUILD_MENTIONS\" | \"NOTIFICATION_CENTER_ITEMS_ACK\" | \"NOTIFICATION_CENTER_ITEMS_ACK_FAILURE\" | \"NOTIFICATION_CENTER_ITEMS_LOCAL_ACK\" | \"NOTIFICATION_CENTER_ITEM_COMPLETED\" | \"NOTIFICATION_CENTER_ITEM_CREATE\" | \"NOTIFICATION_CENTER_ITEM_DELETE\" | \"NOTIFICATION_CENTER_ITEM_DELETE_FAILURE\" | \"NOTIFICATION_CENTER_REFRESH\" | \"NOTIFICATION_CENTER_SET_ACTIVE\" | \"NOTIFICATION_CENTER_SET_TAB\" | \"NOTIFICATION_CENTER_TAB_FOCUSED\" | \"NOTIFICATION_CLICK\" | \"NOTIFICATION_CREATE\" | \"NOTIFICATION_SETTINGS_UPDATE\" | \"NOW_PLAYING_MOUNTED\" | \"NOW_PLAYING_UNMOUNTED\" | \"NUF_COMPLETE\" | \"NUF_NEW_USER\" | \"OAUTH2_TOKEN_REVOKE\" | \"ONLINE_GUILD_MEMBER_COUNT_UPDATE\" | \"OUTBOUND_PROMOTIONS_SEEN\" | \"OUTBOUND_PROMOTION_NOTICE_DISMISS\" | \"OVERLAY_ACTIVATE_REGION\" | \"OVERLAY_CALL_PRIVATE_CHANNEL\" | \"OVERLAY_CRASHED\" | \"OVERLAY_DEACTIVATE_ALL_REGIONS\" | \"OVERLAY_DISABLE_EXTERNAL_LINK_ALERT\" | \"OVERLAY_FOCUSED\" | \"OVERLAY_FORCE_RENDER_MODE\" | \"OVERLAY_INCOMPATIBLE_APP\" | \"OVERLAY_INITIALIZE\" | \"OVERLAY_JOIN_GAME\" | \"OVERLAY_MESSAGE_EVENT_ACTION\" | \"OVERLAY_NOTIFICATION_EVENT\" | \"OVERLAY_READY\" | \"OVERLAY_RELOAD\" | \"OVERLAY_RENDER_DEBUG_CLEAR_TRACKED_PIDS\" | \"OVERLAY_RENDER_DEBUG_MODE\" | \"OVERLAY_SELECT_CALL\" | \"OVERLAY_SELECT_CHANNEL\" | \"OVERLAY_SET_ASSOCIATED_GAME\" | \"OVERLAY_SET_AVATAR_SIZE_MODE\" | \"OVERLAY_SET_CLICK_ZONES\" | \"OVERLAY_SET_DISABLE_CLICKABLE_REGIONS\" | \"OVERLAY_SET_DISPLAY_NAME_MODE\" | \"OVERLAY_SET_DISPLAY_USER_MODE\" | \"OVERLAY_SET_ENABLED\" | \"OVERLAY_SET_GAME_INVITE_NOTIFICATION\" | \"OVERLAY_SET_GPU_BOOST_REQUESTED\" | \"OVERLAY_SET_INPUT_LOCKED\" | \"OVERLAY_SET_INVITE_MESSAGE\" | \"OVERLAY_SET_LIMITED_INTERACTION_OVERRIDE\" | \"OVERLAY_SET_NOTIFICATION_DISABLED_SETTING\" | \"OVERLAY_SET_NOTIFICATION_POSITION_MODE\" | \"OVERLAY_SET_NOT_IDLE\" | \"OVERLAY_SET_PREVIEW_IN_GAME_MODE\" | \"OVERLAY_SET_SHOW_KEYBIND_INDICATORS\" | \"OVERLAY_SET_TEXT_WIDGET_OPACITY\" | \"OVERLAY_SOUNDBOARD_SOUNDS_FETCH_REQUEST\" | \"OVERLAY_START_SESSION\" | \"OVERLAY_SUCCESSFULLY_SHOWN\" | \"OVERLAY_UPDATE_OVERLAY_METHOD\" | \"OVERLAY_UPDATE_OVERLAY_STATE\" | \"OVERLAY_WIDGET_CHANGED\" | \"PASSIVE_UPDATE_V2\" | \"PASSWORDLESS_FAILURE\" | \"PASSWORDLESS_START\" | \"PASSWORD_UPDATED\" | \"PAYMENT_AUTHENTICATION_CLEAR_ERROR\" | \"PAYMENT_AUTHENTICATION_ERROR\" | \"PAYMENT_UPDATE\" | \"PERMISSION_CLEAR_ELEVATED_PROCESS\" | \"PERMISSION_CLEAR_PTT_ADMIN_WARNING\" | \"PERMISSION_CLEAR_SUPPRESS_WARNING\" | \"PERMISSION_CLEAR_VAD_WARNING\" | \"PERMISSION_CONTINUE_NONELEVATED_PROCESS\" | \"PERMISSION_REQUEST_ELEVATED_PROCESS\" | \"PHONE_SET_COUNTRY_CODE\" | \"PICTURE_IN_PICTURE_CLOSE\" | \"PICTURE_IN_PICTURE_HIDE\" | \"PICTURE_IN_PICTURE_MOVE\" | \"PICTURE_IN_PICTURE_OPEN\" | \"PICTURE_IN_PICTURE_RESIZE\" | \"PICTURE_IN_PICTURE_SHOW\" | \"PICTURE_IN_PICTURE_UPDATE_RECT\" | \"PICTURE_IN_PICTURE_UPDATE_SELECTED_WINDOW\" | \"POGGERMODE_ACHIEVEMENT_UNLOCK\" | \"POGGERMODE_SETTINGS_UPDATE\" | \"POGGERMODE_TEMPORARILY_DISABLED\" | \"POGGERMODE_UPDATE_COMBO\" | \"POGGERMODE_UPDATE_MESSAGE_COMBO\" | \"POPOUT_WINDOW_ADD_STYLESHEET\" | \"POPOUT_WINDOW_CLOSE\" | \"POPOUT_WINDOW_OPEN\" | \"POPOUT_WINDOW_SET_ALWAYS_ON_TOP\" | \"POST_CONNECTION_OPEN\" | \"POTIONS_SET_CONFETTI_MODE\" | \"POTIONS_TRIGGER_MESSAGE_CONFETTI\" | \"PREMIUM_MARKETING_DATA_READY\" | \"PREMIUM_MARKETING_PREVIEW\" | \"PREMIUM_PAYMENT_ERROR_CLEAR\" | \"PREMIUM_PAYMENT_MODAL_CLOSE\" | \"PREMIUM_PAYMENT_MODAL_OPEN\" | \"PREMIUM_PAYMENT_SUBSCRIBE_FAIL\" | \"PREMIUM_PAYMENT_SUBSCRIBE_START\" | \"PREMIUM_PAYMENT_SUBSCRIBE_SUCCESS\" | \"PREMIUM_PAYMENT_UPDATE_FAIL\" | \"PREMIUM_PAYMENT_UPDATE_SUCCESS\" | \"PREMIUM_REQUIRED_MODAL_CLOSE\" | \"PREMIUM_REQUIRED_MODAL_OPEN\" | \"PRESENCES_REPLACE\" | \"PRESENCE_SUBSCRIPTIONS_ADD\" | \"PRESENCE_UPDATES\" | \"PRIVATE_CHANNEL_RECIPIENTS_ADD_USER\" | \"PRIVATE_CHANNEL_RECIPIENTS_INVITE_CLOSE\" | \"PRIVATE_CHANNEL_RECIPIENTS_INVITE_OPEN\" | \"PRIVATE_CHANNEL_RECIPIENTS_INVITE_QUERY\" | \"PRIVATE_CHANNEL_RECIPIENTS_INVITE_SELECT\" | \"PRIVATE_CHANNEL_RECIPIENTS_REMOVE_USER\" | \"PROFILE_CUSTOMIZATION_OPEN_PREVIEW_MODAL\" | \"PROFILE_EFFECTS_SET_TRY_IT_OUT\" | \"PROXY_BLOCKED_REQUEST\" | \"PUBLIC_UPSELL_NOTICE_DISMISS\" | \"PURCHASED_ITEMS_FESTIVITY_FETCH_WOW_MOMENT_MEDIA_FAILURE\" | \"PURCHASED_ITEMS_FESTIVITY_FETCH_WOW_MOMENT_MEDIA_SUCCESS\" | \"PURCHASED_ITEMS_FESTIVITY_IS_FETCHING_WOW_MOMENT_MEDIA\" | \"PURCHASED_ITEMS_FESTIVITY_SET_CAN_PLAY_WOW_MOMENT\" | \"PUSH_NOTIFICATION_CLICK\" | \"QUESTS_CLAIM_REWARD_BEGIN\" | \"QUESTS_CLAIM_REWARD_FAILURE\" | \"QUESTS_CLAIM_REWARD_SUCCESS\" | \"QUESTS_DELIVERY_OVERRIDE\" | \"QUESTS_DISMISS_CONTENT_BEGIN\" | \"QUESTS_DISMISS_CONTENT_FAILURE\" | \"QUESTS_DISMISS_CONTENT_SUCCESS\" | \"QUESTS_DISMISS_PROGRESS_TRACKING_FAILURE_NOTICE\" | \"QUESTS_ENROLL_BEGIN\" | \"QUESTS_ENROLL_FAILURE\" | \"QUESTS_ENROLL_SUCCESS\" | \"QUESTS_FETCH_CLAIMED_QUESTS_BEGIN\" | \"QUESTS_FETCH_CLAIMED_QUESTS_FAILURE\" | \"QUESTS_FETCH_CLAIMED_QUESTS_SUCCESS\" | \"QUESTS_FETCH_CURRENT_QUESTS_BEGIN\" | \"QUESTS_FETCH_CURRENT_QUESTS_FAILURE\" | \"QUESTS_FETCH_CURRENT_QUESTS_SUCCESS\" | \"QUESTS_FETCH_QUEST_TO_DELIVER_BEGIN\" | \"QUESTS_FETCH_QUEST_TO_DELIVER_FAILURE\" | \"QUESTS_FETCH_QUEST_TO_DELIVER_SUCCESS\" | \"QUESTS_FETCH_REWARD_CODE_BEGIN\" | \"QUESTS_FETCH_REWARD_CODE_FAILURE\" | \"QUESTS_FETCH_REWARD_CODE_SUCCESS\" | \"QUESTS_PREVIEW_UPDATE_SUCCESS\" | \"QUESTS_SELECT_TASK_PLATFORM\" | \"QUESTS_SEND_HEARTBEAT_FAILURE\" | \"QUESTS_SEND_HEARTBEAT_SUCCESS\" | \"QUESTS_UPDATE_OPTIMISTIC_PROGRESS\" | \"QUESTS_USER_COMPLETION_UPDATE\" | \"QUESTS_USER_STATUS_UPDATE\" | \"QUEUE_INTERACTION_COMPONENT_STATE\" | \"QUICKSWITCHER_HIDE\" | \"QUICKSWITCHER_SEARCH\" | \"QUICKSWITCHER_SELECT\" | \"QUICKSWITCHER_SHOW\" | \"QUICKSWITCHER_SWITCH_TO\" | \"RECEIVE_CHANNEL_AFFINITIES\" | \"RECEIVE_CHANNEL_SUMMARIES\" | \"RECEIVE_CHANNEL_SUMMARIES_BULK\" | \"RECEIVE_CHANNEL_SUMMARY\" | \"RECENT_MENTION_DELETE\" | \"RECOMPUTE_READ_STATES\" | \"REFERRALS_FETCH_ELIGIBLE_USER_FAIL\" | \"REFERRALS_FETCH_ELIGIBLE_USER_START\" | \"REFERRALS_FETCH_ELIGIBLE_USER_SUCCESS\" | \"REGISTER\" | \"REGISTER_SUCCESS\" | \"RELATIONSHIP_ADD\" | \"RELATIONSHIP_IGNORE_USER_SUCCESS\" | \"RELATIONSHIP_PENDING_INCOMING_REMOVED\" | \"RELATIONSHIP_REMOVE\" | \"RELATIONSHIP_UPDATE\" | \"REMOTE_COMMAND\" | \"REMOTE_SESSION_CONNECT\" | \"REMOTE_SESSION_DISCONNECT\" | \"REMOVE_AUTOMOD_MESSAGE_NOTICE\" | \"REPORT_AV_ERROR\" | \"REPORT_TO_MOD_REPORT_MESSAGE_SUCCESS\" | \"REQUEST_CHANNEL_AFFINITIES\" | \"REQUEST_CHANNEL_SUMMARIES\" | \"REQUEST_CHANNEL_SUMMARIES_BULK\" | \"REQUEST_CHANNEL_SUMMARY\" | \"REQUEST_FORUM_UNREADS\" | \"REQUEST_SOUNDBOARD_SOUNDS\" | \"RESET_NOTIFICATION_CENTER\" | \"RESET_PAYMENT_ID\" | \"RESET_PREVIEW_CLIENT_THEME\" | \"RESET_SOCKET\" | \"RESORT_THREADS\" | \"RPC_APP_AUTHENTICATED\" | \"RPC_APP_CONNECTED\" | \"RPC_APP_DISCONNECTED\" | \"RPC_NOTIFICATION_CREATE\" | \"RPC_SERVER_READY\" | \"RTC_CONNECTION_CLIENT_CONNECT\" | \"RTC_CONNECTION_CLIENT_DISCONNECT\" | \"RTC_CONNECTION_FLAGS\" | \"RTC_CONNECTION_LOSS_RATE\" | \"RTC_CONNECTION_PING\" | \"RTC_CONNECTION_PLATFORM\" | \"RTC_CONNECTION_REMOTE_VIDEO_SINK_WANTS\" | \"RTC_CONNECTION_ROSTER_MAP_UPDATE\" | \"RTC_CONNECTION_SECURE_FRAMES_UPDATE\" | \"RTC_CONNECTION_STATE\" | \"RTC_CONNECTION_UPDATE_ID\" | \"RTC_CONNECTION_USERS_MERGED\" | \"RTC_CONNECTION_VIDEO\" | \"RTC_DEBUG_MODAL_CLOSE\" | \"RTC_DEBUG_MODAL_OPEN\" | \"RTC_DEBUG_MODAL_OPEN_REPLAY\" | \"RTC_DEBUG_MODAL_OPEN_REPLAY_AT_PATH\" | \"RTC_DEBUG_MODAL_SET_SECTION\" | \"RTC_DEBUG_MODAL_UPDATE_VIDEO_OUTPUT\" | \"RTC_DEBUG_POPOUT_WINDOW_OPEN\" | \"RTC_DEBUG_SET_RECORDING_FLAG\" | \"RTC_DEBUG_SET_SIMULCAST_OVERRIDE\" | \"RTC_LATENCY_TEST_COMPLETE\" | \"RUNNING_GAMES_CHANGE\" | \"RUNNING_GAME_ADD_OVERRIDE\" | \"RUNNING_GAME_DELETE_ENTRY\" | \"RUNNING_GAME_EDIT_NAME\" | \"RUNNING_GAME_TOGGLE_DETECTION\" | \"RUNNING_GAME_TOGGLE_OVERLAY\" | \"RUNNING_STREAMER_TOOLS_CHANGE\" | \"SAFETY_HUB_APPEAL_CLOSE\" | \"SAFETY_HUB_APPEAL_OPEN\" | \"SAFETY_HUB_APPEAL_SIGNAL_CUSTOM_INPUT_CHANGE\" | \"SAFETY_HUB_APPEAL_SIGNAL_SELECT\" | \"SAFETY_HUB_AUTOMATED_UNDERAGE_APPEAL_MODAL_CLOSE\" | \"SAFETY_HUB_AUTOMATED_UNDERAGE_APPEAL_MODAL_OPEN\" | \"SAFETY_HUB_AUTOMATED_UNDERAGE_APPEAL_START_POLL\" | \"SAFETY_HUB_AUTOMATED_UNDERAGE_APPEAL_SUBMIT_SUCCESS\" | \"SAFETY_HUB_CHECK_AUTOMATED_UNDERAGE_APPEAL_FAILURE\" | \"SAFETY_HUB_CHECK_AUTOMATED_UNDERAGE_APPEAL_START\" | \"SAFETY_HUB_CHECK_AUTOMATED_UNDERAGE_APPEAL_SUCCESS\" | \"SAFETY_HUB_FETCH_CLASSIFICATION_FAILURE\" | \"SAFETY_HUB_FETCH_CLASSIFICATION_START\" | \"SAFETY_HUB_FETCH_CLASSIFICATION_SUCCESS\" | \"SAFETY_HUB_FETCH_FAILURE\" | \"SAFETY_HUB_FETCH_START\" | \"SAFETY_HUB_FETCH_SUCCESS\" | \"SAFETY_HUB_REQUEST_AUTOMATED_UNDERAGE_APPEAL_FAILURE\" | \"SAFETY_HUB_REQUEST_AUTOMATED_UNDERAGE_APPEAL_START\" | \"SAFETY_HUB_REQUEST_AUTOMATED_UNDERAGE_APPEAL_SUCCESS\" | \"SAFETY_HUB_REQUEST_REVIEW_FAILURE\" | \"SAFETY_HUB_REQUEST_REVIEW_START\" | \"SAFETY_HUB_REQUEST_REVIEW_SUCCESS\" | \"SAVED_MESSAGES_UPDATE\" | \"SAVED_MESSAGE_CREATE\" | \"SAVED_MESSAGE_DELETE\" | \"SAVE_LAST_NON_VOICE_ROUTE\" | \"SAVE_LAST_ROUTE\" | \"SCHEDULED_MESSAGES_CREATE_SUCCESS\" | \"SCHEDULED_MESSAGES_DELETE_FAILURE\" | \"SCHEDULED_MESSAGES_DELETE_START\" | \"SCHEDULED_MESSAGES_DELETE_SUCCESS\" | \"SEARCH_ADD_HISTORY\" | \"SEARCH_AUTOCOMPLETE_QUERY_UPDATE\" | \"SEARCH_CLEAR_HISTORY\" | \"SEARCH_EDITOR_STATE_CHANGE\" | \"SEARCH_EDITOR_STATE_CLEAR\" | \"SEARCH_ENSURE_SEARCH_STATE\" | \"SEARCH_FINISH\" | \"SEARCH_INDEXING\" | \"SEARCH_MESSAGES_CLEAR_ALL\" | \"SEARCH_MESSAGES_FAILURE\" | \"SEARCH_MESSAGES_INDEXING\" | \"SEARCH_MESSAGES_START\" | \"SEARCH_MESSAGES_SUCCESS\" | \"SEARCH_RECENT_MESSAGES_CLEAR\" | \"SEARCH_REMOVE_HISTORY\" | \"SEARCH_RESULTS_QUERY_UPDATE\" | \"SEARCH_SCREEN_OPEN\" | \"SEARCH_SET_SHOW_BLOCKED_RESULTS\" | \"SEARCH_SET_SHOW_NO_RESULTS_ALT\" | \"SEARCH_START\" | \"SECURE_FRAMES_SETTINGS_UPDATE\" | \"SECURE_FRAMES_TRANSIENT_KEY_CREATE\" | \"SECURE_FRAMES_TRANSIENT_KEY_DELETE\" | \"SECURE_FRAMES_UPLOADED_KEY_VERSION_ADD\" | \"SECURE_FRAMES_UPLOADED_KEY_VERSION_CLEAR\" | \"SECURE_FRAMES_USER_VERIFIED_KEYS_DELETE\" | \"SECURE_FRAMES_VERIFIED_KEY_CREATE\" | \"SECURE_FRAMES_VERIFIED_KEY_DELETE\" | \"SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE\" | \"SELECT_HOME_RESOURCE_CHANNEL\" | \"SELECT_NEW_MEMBER_ACTION_CHANNEL\" | \"SELF_PRESENCE_STORE_UPDATE\" | \"SESSIONS_REPLACE\" | \"SET_CHANNEL_BITRATE\" | \"SET_CHANNEL_VIDEO_QUALITY_MODE\" | \"SET_CONSENT_REQUIRED\" | \"SET_CREATED_AT_OVERRIDE\" | \"SET_GUILD_FOLDER_EXPANDED\" | \"SET_GUILD_LEADERBOARD\" | \"SET_HIGHLIGHTED_SUMMARY\" | \"SET_INTERACTION_COMPONENT_STATE\" | \"SET_LOCATION_METADATA\" | \"SET_NATIVE_PERMISSION\" | \"SET_PENDING_REPLY_SHOULD_MENTION\" | \"SET_PREMIUM_TYPE_OVERRIDE\" | \"SET_PREVIOUS_GO_LIVE_SETTINGS\" | \"SET_RECENTLY_ACTIVE_COLLAPSED\" | \"SET_RECENT_MENTIONS_FILTER\" | \"SET_RECENT_MENTIONS_STALE\" | \"SET_RPC_NOTIFICATION_SETTINGS\" | \"SET_SELECTED_SUMMARY\" | \"SET_SOUNDPACK\" | \"SET_STREAM_APP_INTENT\" | \"SET_SUMMARY_FEEDBACK\" | \"SET_THEME_OVERRIDE\" | \"SET_TTS_SPEECH_RATE\" | \"SET_USER_LEADERBOARD_LAST_UPDATE_REQUESTED\" | \"SET_VAD_PERMISSION\" | \"SHARED_CANVAS_CLEAR_DRAWABLES\" | \"SHARED_CANVAS_DRAW_LINE_POINT\" | \"SHARED_CANVAS_SET_DRAW_MODE\" | \"SHARED_CANVAS_UPDATE_EMOJI_HOSE\" | \"SHARED_CANVAS_UPDATE_LINE_POINTS\" | \"SHOW_ACTION_SHEET\" | \"SHOW_ACTION_SHEET_QUICK_SWITCHER\" | \"SHOW_KEYBOARD_SHORTCUTS\" | \"SIDEBAR_CLOSE\" | \"SIDEBAR_CLOSE_GUILD\" | \"SIDEBAR_CREATE_THREAD\" | \"SIDEBAR_VIEW_CHANNEL\" | \"SIDEBAR_VIEW_GUILD\" | \"SKUS_FETCH_SUCCESS\" | \"SKU_FETCH_FAIL\" | \"SKU_FETCH_START\" | \"SKU_FETCH_SUCCESS\" | \"SKU_PURCHASE_AWAIT_CONFIRMATION\" | \"SKU_PURCHASE_CLEAR_ERROR\" | \"SKU_PURCHASE_FAIL\" | \"SKU_PURCHASE_MODAL_CLOSE\" | \"SKU_PURCHASE_MODAL_OPEN\" | \"SKU_PURCHASE_PREVIEW_FETCH\" | \"SKU_PURCHASE_PREVIEW_FETCH_FAILURE\" | \"SKU_PURCHASE_PREVIEW_FETCH_SUCCESS\" | \"SKU_PURCHASE_SHOW_CONFIRMATION_STEP\" | \"SKU_PURCHASE_START\" | \"SKU_PURCHASE_SUCCESS\" | \"SKU_PURCHASE_UPDATE_IS_GIFT\" | \"SLOWMODE_RESET_COOLDOWN\" | \"SLOWMODE_SET_COOLDOWN\" | \"SOUNDBOARD_FETCH_DEFAULT_SOUNDS\" | \"SOUNDBOARD_FETCH_DEFAULT_SOUNDS_SUCCESS\" | \"SOUNDBOARD_MUTE_JOIN_SOUND\" | \"SOUNDBOARD_SET_OVERLAY_ENABLED\" | \"SOUNDBOARD_SOUNDS_RECEIVED\" | \"SPEAKING\" | \"SPEAKING_MESSAGE\" | \"SPEAK_MESSAGE\" | \"SPEAK_TEXT\" | \"SPELLCHECK_LEARN_WORD\" | \"SPELLCHECK_TOGGLE\" | \"SPELLCHECK_UNLEARN_WORD\" | \"SPOTIFY_ACCOUNT_ACCESS_TOKEN\" | \"SPOTIFY_ACCOUNT_ACCESS_TOKEN_REVOKE\" | \"SPOTIFY_NEW_TRACK\" | \"SPOTIFY_PLAYER_PAUSE\" | \"SPOTIFY_PLAYER_PLAY\" | \"SPOTIFY_PLAYER_STATE\" | \"SPOTIFY_PROFILE_UPDATE\" | \"SPOTIFY_SET_ACTIVE_DEVICE\" | \"SPOTIFY_SET_DEVICES\" | \"SPOTIFY_SET_PROTOCOL_REGISTERED\" | \"STAGE_INSTANCE_CREATE\" | \"STAGE_INSTANCE_DELETE\" | \"STAGE_INSTANCE_UPDATE\" | \"STAGE_MUSIC_MUTE\" | \"STAGE_MUSIC_PLAY\" | \"START_SESSION\" | \"STATUS_PAGE_INCIDENT\" | \"STATUS_PAGE_SCHEDULED_MAINTENANCE\" | \"STATUS_PAGE_SCHEDULED_MAINTENANCE_ACK\" | \"STICKER_FETCH_SUCCESS\" | \"STICKER_PACKS_FETCH_START\" | \"STICKER_PACKS_FETCH_SUCCESS\" | \"STICKER_PACK_FETCH_SUCCESS\" | \"STICKER_TRACK_USAGE\" | \"STOP_SPEAKING\" | \"STORE_LISTINGS_FETCH_FAIL\" | \"STORE_LISTINGS_FETCH_START\" | \"STORE_LISTINGS_FETCH_SUCCESS\" | \"STORE_LISTING_FETCH_SUCCESS\" | \"STREAMER_MODE_UPDATE\" | \"STREAMING_UPDATE\" | \"STREAM_CLOSE\" | \"STREAM_CREATE\" | \"STREAM_DELETE\" | \"STREAM_LAYOUT_UPDATE\" | \"STREAM_PREVIEW_FETCH_FAIL\" | \"STREAM_PREVIEW_FETCH_START\" | \"STREAM_PREVIEW_FETCH_SUCCESS\" | \"STREAM_SERVER_UPDATE\" | \"STREAM_SET_PAUSED\" | \"STREAM_START\" | \"STREAM_STOP\" | \"STREAM_TIMED_OUT\" | \"STREAM_UPDATE\" | \"STREAM_UPDATE_SELF_HIDDEN\" | \"STREAM_UPDATE_SETTINGS\" | \"STREAM_WATCH\" | \"STRIPE_TOKEN_FAILURE\" | \"SUBSCRIPTION_PLANS_FETCH\" | \"SUBSCRIPTION_PLANS_FETCH_FAILURE\" | \"SUBSCRIPTION_PLANS_FETCH_SUCCESS\" | \"SUBSCRIPTION_PLANS_RESET\" | \"SURVEY_FETCHED\" | \"SURVEY_HIDE\" | \"SURVEY_OVERRIDE\" | \"SURVEY_SEEN\" | \"SYSTEM_THEME_CHANGE\" | \"THERMAL_STATE_CHANGE\" | \"THREAD_CREATE\" | \"THREAD_CREATE_LOCAL\" | \"THREAD_DELETE\" | \"THREAD_LIST_SYNC\" | \"THREAD_MEMBERS_UPDATE\" | \"THREAD_MEMBER_LIST_UPDATE\" | \"THREAD_MEMBER_LOCAL_UPDATE\" | \"THREAD_MEMBER_UPDATE\" | \"THREAD_SETTINGS_DRAFT_CHANGE\" | \"THREAD_UPDATE\" | \"TOGGLE_GUILD_EXPANDED_STATE\" | \"TOGGLE_GUILD_FOLDER_EXPAND\" | \"TOGGLE_OVERLAY_CANVAS\" | \"TOGGLE_TOPICS_BAR\" | \"TOP_EMOJIS_FETCH\" | \"TOP_EMOJIS_FETCH_SUCCESS\" | \"TRUNCATE_MENTIONS\" | \"TRUNCATE_MESSAGES\" | \"TRY_ACK\" | \"TUTORIAL_INDICATOR_DISMISS\" | \"TUTORIAL_INDICATOR_HIDE\" | \"TUTORIAL_INDICATOR_SHOW\" | \"TUTORIAL_INDICATOR_SUPPRESS_ALL\" | \"TYPING_START\" | \"TYPING_START_LOCAL\" | \"TYPING_STOP\" | \"TYPING_STOP_LOCAL\" | \"UNSYNCED_USER_SETTINGS_UPDATE\" | \"UNVERIFIED_GAME_UPDATE\" | \"UPCOMING_GUILD_EVENT_NOTICE_HIDE\" | \"UPCOMING_GUILD_EVENT_NOTICE_SEEN\" | \"UPDATE_AVAILABLE\" | \"UPDATE_BACKGROUND_GRADIENT_PRESET\" | \"UPDATE_CHANNEL_DIMENSIONS\" | \"UPDATE_CHANNEL_LIST_DIMENSIONS\" | \"UPDATE_CHANNEL_LIST_SUBTITLES\" | \"UPDATE_CHAT_WALLPAPER_FLAG_COMPLETE\" | \"UPDATE_CHAT_WALLPAPER_FLAG_START\" | \"UPDATE_CHAT_WALLPAPER_OVERRIDES\" | \"UPDATE_CLIENT_PREMIUM_TYPE\" | \"UPDATE_CONSENTS\" | \"UPDATE_DATA_HARVEST_TYPE\" | \"UPDATE_DOWNLOADED\" | \"UPDATE_ERROR\" | \"UPDATE_GUILD_LIST_DIMENSIONS\" | \"UPDATE_MANUALLY\" | \"UPDATE_MOBILE_PENDING_THEME_INDEX\" | \"UPDATE_NOT_AVAILABLE\" | \"UPDATE_STRANGER_STATUS\" | \"UPDATE_THEME_PREFERENCES\" | \"UPDATE_TOKEN\" | \"UPDATE_VISIBLE_MESSAGES\" | \"UPLOAD_ATTACHMENT_ADD_FILES\" | \"UPLOAD_ATTACHMENT_CLEAR_ALL_FILES\" | \"UPLOAD_ATTACHMENT_POP_FILE\" | \"UPLOAD_ATTACHMENT_REMOVE_FILE\" | \"UPLOAD_ATTACHMENT_REMOVE_FILES\" | \"UPLOAD_ATTACHMENT_SET_FILE\" | \"UPLOAD_ATTACHMENT_SET_UPLOADS\" | \"UPLOAD_ATTACHMENT_UPDATE_FILE\" | \"UPLOAD_CANCEL_REQUEST\" | \"UPLOAD_COMPLETE\" | \"UPLOAD_COMPRESSION_PROGRESS\" | \"UPLOAD_FAIL\" | \"UPLOAD_FILE_UPDATE\" | \"UPLOAD_ITEM_CANCEL_REQUEST\" | \"UPLOAD_PROGRESS\" | \"UPLOAD_RESTORE_FAILED_UPLOAD\" | \"UPLOAD_START\" | \"USER_ACTIVITY_STATISTICS_FETCH_SUCCESS\" | \"USER_APPLICATION_REMOVE\" | \"USER_APPLICATION_UPDATE\" | \"USER_APPLIED_BOOSTS_FETCH_START\" | \"USER_APPLIED_BOOSTS_FETCH_SUCCESS\" | \"USER_AUTHORIZED_APPS_REQUEST\" | \"USER_AUTHORIZED_APPS_UPDATE\" | \"USER_CONNECTIONS_CALLBACK\" | \"USER_CONNECTIONS_INTEGRATION_JOINING\" | \"USER_CONNECTIONS_INTEGRATION_JOINING_ERROR\" | \"USER_CONNECTIONS_UPDATE\" | \"USER_CONNECTION_UPDATE\" | \"USER_GUILD_JOIN_REQUEST_COACHMARK_CLEAR\" | \"USER_GUILD_JOIN_REQUEST_COACHMARK_SHOW\" | \"USER_GUILD_JOIN_REQUEST_COOLDOWN_FETCH\" | \"USER_GUILD_JOIN_REQUEST_UPDATE\" | \"USER_GUILD_SETTINGS_CHANNEL_UPDATE\" | \"USER_GUILD_SETTINGS_CHANNEL_UPDATE_BULK\" | \"USER_GUILD_SETTINGS_FULL_UPDATE\" | \"USER_GUILD_SETTINGS_GUILD_AND_CHANNELS_UPDATE\" | \"USER_GUILD_SETTINGS_GUILD_UPDATE\" | \"USER_GUILD_SETTINGS_REMOVE_PENDING_CHANNEL_UPDATES\" | \"USER_JOIN_REQUEST_GUILDS_FETCH\" | \"USER_NON_CHANNEL_ACK\" | \"USER_NOTE_LOAD_START\" | \"USER_NOTE_UPDATE\" | \"USER_PAYMENT_BROWSER_CHECKOUT_DONE\" | \"USER_PAYMENT_BROWSER_CHECKOUT_STARTED\" | \"USER_PAYMENT_CLIENT_ADD\" | \"USER_PROFILE_EFFECTS_FETCH\" | \"USER_PROFILE_EFFECTS_FETCH_FAILURE\" | \"USER_PROFILE_EFFECTS_FETCH_SUCCESS\" | \"USER_PROFILE_FETCH_FAILURE\" | \"USER_PROFILE_FETCH_START\" | \"USER_PROFILE_FETCH_SUCCESS\" | \"USER_PROFILE_MODAL_CLOSE\" | \"USER_PROFILE_MODAL_OPEN\" | \"USER_PROFILE_PIN_BADGES_ON_CLIENT\" | \"USER_PROFILE_SIDEBAR_TOGGLE_SECTION\" | \"USER_PROFILE_UPDATE_FAILURE\" | \"USER_PROFILE_UPDATE_START\" | \"USER_PROFILE_UPDATE_SUCCESS\" | \"USER_REQUIRED_ACTION_UPDATE\" | \"USER_SETTINGS_ACCOUNT_CLOSE\" | \"USER_SETTINGS_ACCOUNT_INIT\" | \"USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM\" | \"USER_SETTINGS_ACCOUNT_RESET_PENDING_LEGACY_USERNAME_DISABLED\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR_DECORATION\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_BIO\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_LEGACY_USERNAME_DISABLED\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_NAMEPLATE\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_PROFILE_EFFECT_ID\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS\" | \"USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS\" | \"USER_SETTINGS_ACCOUNT_SET_SINGLE_TRY_IT_OUT_COLLECTIBLES_ITEM\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_AVATAR\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_AVATAR_DECORATION\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_BANNER\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_PRESET\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_PROFILE_EFFECT_ID\" | \"USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_THEME_COLORS\" | \"USER_SETTINGS_ACCOUNT_SUBMIT\" | \"USER_SETTINGS_ACCOUNT_SUBMIT_FAILURE\" | \"USER_SETTINGS_ACCOUNT_SUBMIT_SUCCESS\" | \"USER_SETTINGS_CLEAR_ERRORS\" | \"USER_SETTINGS_LOCALE_OVERRIDE\" | \"USER_SETTINGS_MODAL_CLEAR_SCROLL_POSITION\" | \"USER_SETTINGS_MODAL_CLEAR_SUBSECTION\" | \"USER_SETTINGS_MODAL_CLOSE\" | \"USER_SETTINGS_MODAL_INIT\" | \"USER_SETTINGS_MODAL_OPEN\" | \"USER_SETTINGS_MODAL_RESET\" | \"USER_SETTINGS_MODAL_SET_SECTION\" | \"USER_SETTINGS_MODAL_SUBMIT\" | \"USER_SETTINGS_MODAL_SUBMIT_COMPLETE\" | \"USER_SETTINGS_MODAL_SUBMIT_FAILURE\" | \"USER_SETTINGS_MODAL_UPDATE_ACCOUNT\" | \"USER_SETTINGS_OVERRIDE_APPLY\" | \"USER_SETTINGS_OVERRIDE_CLEAR\" | \"USER_SETTINGS_PROTO_ENQUEUE_UPDATE\" | \"USER_SETTINGS_PROTO_LOAD_IF_NECESSARY\" | \"USER_SETTINGS_PROTO_UPDATE\" | \"USER_SETTINGS_PROTO_UPDATE_EDIT_INFO\" | \"USER_SETTINGS_RESET_ALL_PENDING\" | \"USER_SETTINGS_RESET_ALL_TRY_IT_OUT\" | \"USER_SETTINGS_RESET_PENDING_ACCOUNT_CHANGES\" | \"USER_SETTINGS_RESET_PENDING_AVATAR_DECORATION\" | \"USER_SETTINGS_RESET_PENDING_PRIMARY_GUILD_CHANGES\" | \"USER_SETTINGS_RESET_PENDING_PROFILE_CHANGES\" | \"USER_SETTINGS_SET_PENDING_PRIMARY_GUILD_ID\" | \"USER_SOUNDBOARD_SET_VOLUME\" | \"USER_UPDATE\" | \"VIDEO_FILTER_ASSETS_FETCH_SUCCESS\" | \"VIDEO_FILTER_ASSET_DELETE_SUCCESS\" | \"VIDEO_FILTER_ASSET_UPLOAD_SUCCESS\" | \"VIDEO_SAVE_LAST_USED_BACKGROUND_OPTION\" | \"VIDEO_SIZE_UPDATE\" | \"VIDEO_STREAM_READY_TIMEOUT\" | \"VIEW_HISTORY_MARK_VIEW\" | \"VIRTUAL_CURRENCY_BALANCE_FETCH\" | \"VIRTUAL_CURRENCY_BALANCE_FETCH_FAIL\" | \"VIRTUAL_CURRENCY_BALANCE_FETCH_SUCCESS\" | \"VIRTUAL_CURRENCY_BALANCE_UPDATE\" | \"VIRTUAL_CURRENCY_EARNED_ORBS_COACHMARK_CLOSE\" | \"VIRTUAL_CURRENCY_EARNED_ORBS_COACHMARK_OPEN\" | \"VIRTUAL_CURRENCY_ONBOARDING_MODAL_OPEN\" | \"VIRTUAL_CURRENCY_ONBOARDING_MODAL_RESET\" | \"VIRTUAL_CURRENCY_REDEEM_FAIL\" | \"VIRTUAL_CURRENCY_REDEEM_START\" | \"VIRTUAL_CURRENCY_REDEEM_SUCCESS\" | \"VIRTUAL_CURRENCY_SET_BALANCE_PILL_OVERLAY\" | \"VOICE_CATEGORY_COLLAPSE\" | \"VOICE_CATEGORY_EXPAND\" | \"VOICE_CHANNEL_EFFECT_CLEAR\" | \"VOICE_CHANNEL_EFFECT_RECENT_EMOJI\" | \"VOICE_CHANNEL_EFFECT_SEND\" | \"VOICE_CHANNEL_EFFECT_SENT_LOCAL\" | \"VOICE_CHANNEL_EFFECT_TOGGLE_ANIMATION_TYPE\" | \"VOICE_CHANNEL_EFFECT_UPDATE_TIME_STAMP\" | \"VOICE_CHANNEL_SELECT\" | \"VOICE_CHANNEL_STATUS_UPDATE\" | \"VOICE_FILTER_APPLIED\" | \"VOICE_FILTER_APPLY_FAILED\" | \"VOICE_FILTER_CATALOG_FETCH_FAILED\" | \"VOICE_FILTER_CATALOG_FETCH_SUCCESS\" | \"VOICE_FILTER_DEV_TOOLS_SET_UPDATE_TIME\" | \"VOICE_FILTER_DOWNLOAD_FAILED\" | \"VOICE_FILTER_DOWNLOAD_PROGRESS\" | \"VOICE_FILTER_DOWNLOAD_STARTED\" | \"VOICE_FILTER_FILE_READY\" | \"VOICE_FILTER_LAGGING\" | \"VOICE_FILTER_LOOPBACK_TOGGLE\" | \"VOICE_FILTER_NATIVE_MODULE_STATE_CHANGE\" | \"VOICE_FILTER_REQUEST_SWITCH\" | \"VOICE_FILTER_UPDATE_LIMITED_TIME_VOICES\" | \"VOICE_SERVER_UPDATE\" | \"VOICE_STATE_UPDATES\" | \"WAIT_FOR_REMOTE_SESSION\" | \"WEBHOOKS_FETCHING\" | \"WEBHOOKS_UPDATE\" | \"WEBHOOK_CREATE\" | \"WEBHOOK_DELETE\" | \"WEBHOOK_UPDATE\" | \"WELCOME_SCREEN_FETCH_FAIL\" | \"WELCOME_SCREEN_FETCH_START\" | \"WELCOME_SCREEN_FETCH_SUCCESS\" | \"WELCOME_SCREEN_SETTINGS_CLEAR\" | \"WELCOME_SCREEN_SETTINGS_RESET\" | \"WELCOME_SCREEN_SETTINGS_UPDATE\" | \"WELCOME_SCREEN_SUBMIT\" | \"WELCOME_SCREEN_SUBMIT_FAILURE\" | \"WELCOME_SCREEN_SUBMIT_SUCCESS\" | \"WELCOME_SCREEN_UPDATE\" | \"WELCOME_SCREEN_VIEW\" | \"WINDOW_FOCUS\" | \"WINDOW_FULLSCREEN_CHANGE\" | \"WINDOW_HIDDEN\" | \"WINDOW_INIT\" | \"WINDOW_RESIZED\" | \"WINDOW_UNLOAD\" | \"WINDOW_VISIBILITY_CHANGE\" | \"WRITE_CACHES\";\n"
  },
  {
    "path": "packages/discord-types/src/index.d.ts",
    "content": "export * from \"./common\";\nexport * from \"./components\";\nexport * from \"./flux\";\nexport * from \"./fluxEvents\";\nexport * from \"./menu\";\nexport * from \"./modules\";\nexport * from \"./stores\";\nexport * from \"./utils\";\nexport * as Webpack from \"../webpack\";\n"
  },
  {
    "path": "packages/discord-types/src/menu.d.ts",
    "content": "import type { ComponentType, CSSProperties, ForwardRefRenderFunction, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from \"react\";\n\ntype RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;\n\nexport interface Menu {\n    Menu: RC<{\n        navId: string;\n        onClose(): void;\n        className?: string;\n        style?: CSSProperties;\n        hideScroller?: boolean;\n        onSelect?(): void;\n    }>;\n    MenuSeparator: ComponentType;\n    MenuGroup: RC<{\n        label?: string;\n    }>;\n    MenuItem: RC<{\n        id: string;\n        label: ReactNode;\n        action?(e: MouseEvent): void;\n        icon?: ComponentType<any>;\n\n        color?: string;\n        render?: ComponentType<any>;\n        onChildrenScroll?: Function;\n        childRowHeight?: number;\n        listClassName?: string;\n        disabled?: boolean;\n    }>;\n    MenuCheckboxItem: RC<{\n        id: string;\n        label: string;\n        checked: boolean;\n        action?(e: MouseEvent): void;\n        disabled?: boolean;\n    }>;\n    MenuRadioItem: RC<{\n        id: string;\n        group: string;\n        label: string;\n        checked: boolean;\n        action?(e: MouseEvent): void;\n        disabled?: boolean;\n    }>;\n    MenuControlItem: RC<{\n        id: string;\n        interactive?: boolean;\n        label?: string;\n        control: ForwardRefRenderFunction<any, any>;\n    }>;\n    MenuSliderControl: RC<{\n        minValue: number,\n        maxValue: number,\n        value: number,\n        onChange(value: number): void,\n        renderValue?(value: number): string,\n    }>;\n    MenuSearchControl: RC<{\n        query: string;\n        onChange(query: string): void;\n        placeholder?: string;\n    }>;\n}\n\nexport interface ContextMenuApi {\n    closeContextMenu(): void;\n    openContextMenu(\n        event: UIEvent,\n        render?: Menu[\"Menu\"],\n        options?: { enableSpellCheck?: boolean; },\n        renderLazy?: () => Promise<Menu[\"Menu\"]>\n    ): void;\n    openContextMenuLazy(\n        event: UIEvent,\n        renderLazy?: () => Promise<Menu[\"Menu\"]>,\n        options?: { enableSpellCheck?: boolean; }\n    ): void;\n}\n\n"
  },
  {
    "path": "packages/discord-types/src/modules/CloudUpload.d.ts",
    "content": "import EventEmitter from \"events\";\nimport { CloudUploadPlatform } from \"../../enums\";\n\ninterface BaseUploadItem {\n    platform: CloudUploadPlatform;\n    id?: string;\n    origin?: string;\n    isThumbnail?: boolean;\n    clip?: unknown;\n}\n\nexport interface ReactNativeUploadItem extends BaseUploadItem {\n    platform: CloudUploadPlatform.REACT_NATIVE;\n    uri: string;\n    filename?: string;\n    mimeType?: string;\n    durationSecs?: number;\n    waveform?: string;\n    isRemix?: boolean;\n}\n\nexport interface WebUploadItem extends BaseUploadItem {\n    platform: CloudUploadPlatform.WEB;\n    file: File;\n}\n\nexport type CloudUploadItem = ReactNativeUploadItem | WebUploadItem;\n\nexport class CloudUpload extends EventEmitter {\n    constructor(item: CloudUploadItem, channelId: string, reactNativeFileIndex?: number);\n\n    channelId: string;\n    classification: string;\n    clip: unknown;\n    contentHash: unknown;\n    currentSize: number;\n    description: string | null;\n    durationSecs: number | undefined;\n    etag: string | undefined;\n    error: unknown;\n    filename: string;\n    id: string;\n    isImage: boolean;\n    isRemix: boolean | undefined;\n    isThumbnail: boolean;\n    isVideo: boolean;\n    item: {\n        file: File;\n        platform: CloudUploadPlatform;\n        origin: string;\n    };\n    loaded: number;\n    mimeType: string;\n    origin: string;\n    postCompressionSize: number | undefined;\n    preCompressionSize: number;\n    responseUrl: string;\n    sensitive: boolean;\n    spoiler: boolean;\n    startTime: number;\n    status: \"NOT_STARTED\" | \"STARTED\" | \"UPLOADING\" | \"ERROR\" | \"COMPLETED\" | \"CANCELLED\" | \"REMOVED_FROM_MSG_DRAFT\";\n    uniqueId: string;\n    uploadedFilename: string;\n    waveform: string | undefined;\n\n    // there are many more methods than just these but I didn't find them particularly useful\n    upload(): Promise<void>;\n    cancel(): void;\n    delete(): Promise<void>;\n    getSize(): number;\n    maybeConvertToWebP(): Promise<void>;\n    removeFromMsgDraft(): void;\n}\n"
  },
  {
    "path": "packages/discord-types/src/modules/index.d.ts",
    "content": "export * from \"./CloudUpload\";\n"
  },
  {
    "path": "packages/discord-types/src/stores/AccessibilityStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport type ReducedMotionPreference = \"auto\" | \"reduce\" | \"no-preference\";\nexport type ForcedColorsPreference = \"none\" | \"active\";\nexport type ContrastPreference = \"no-preference\" | \"more\" | \"less\" | \"custom\";\nexport type RoleStyle = \"username\" | \"dot\" | \"hidden\";\n\nexport interface AccessibilityState {\n    fontSize: number;\n    zoom: number;\n    keyboardModeEnabled: boolean;\n    contrastMode: string;\n    colorblindMode: boolean;\n    lowContrastMode: boolean;\n    saturation: number;\n    contrast: number;\n    desaturateUserColors: boolean;\n    forcedColorsModalSeen: boolean;\n    keyboardNavigationExplainerModalSeen: boolean;\n    messageGroupSpacing: number | null;\n    systemPrefersReducedMotion: ReducedMotionPreference;\n    systemPrefersCrossfades: boolean;\n    prefersReducedMotion: ReducedMotionPreference;\n    systemForcedColors: ForcedColorsPreference;\n    syncForcedColors: boolean;\n    systemPrefersContrast: ContrastPreference;\n    alwaysShowLinkDecorations: boolean;\n    roleStyle: RoleStyle;\n    displayNameStylesEnabled: boolean;\n    submitButtonEnabled: boolean;\n    syncProfileThemeWithUserTheme: boolean;\n    enableCustomCursor: boolean;\n    switchIconsEnabled: boolean;\n}\n\nexport class AccessibilityStore extends FluxStore {\n    get fontScale(): number;\n    get fontSize(): number;\n    get isFontScaledUp(): boolean;\n    get isFontScaledDown(): boolean;\n    get fontScaleClass(): string;\n    get zoom(): number;\n    get isZoomedIn(): boolean;\n    get isZoomedOut(): boolean;\n    get keyboardModeEnabled(): boolean;\n    get colorblindMode(): boolean;\n    get lowContrastMode(): boolean;\n    get saturation(): number;\n    get contrast(): number;\n    get desaturateUserColors(): boolean;\n    get forcedColorsModalSeen(): boolean;\n    get keyboardNavigationExplainerModalSeen(): boolean;\n    get messageGroupSpacing(): number;\n    get isMessageGroupSpacingIncreased(): boolean;\n    get isMessageGroupSpacingDecreased(): boolean;\n    get isSubmitButtonEnabled(): boolean;\n    get syncProfileThemeWithUserTheme(): boolean;\n    get systemPrefersReducedMotion(): ReducedMotionPreference;\n    get rawPrefersReducedMotion(): ReducedMotionPreference;\n    get useReducedMotion(): boolean;\n    get systemForcedColors(): ForcedColorsPreference;\n    get syncForcedColors(): boolean;\n    get useForcedColors(): boolean;\n    get systemPrefersContrast(): ContrastPreference;\n    get systemPrefersCrossfades(): boolean;\n    get alwaysShowLinkDecorations(): boolean;\n    get enableCustomCursor(): boolean;\n    get roleStyle(): RoleStyle;\n    get displayNameStylesEnabled(): boolean;\n    get isHighContrastModeEnabled(): boolean;\n    get isSwitchIconsEnabled(): boolean;\n    getUserAgnosticState(): AccessibilityState;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ActiveJoinedThreadsStore.d.ts",
    "content": "import { Channel, FluxStore } from \"..\";\n\nexport interface ThreadJoined {\n    channel: Channel;\n    joinTimestamp: number;\n}\n\nexport type ThreadsForParent = Record<string, ThreadJoined>;\nexport type ThreadsForGuild = Record<string, ThreadsForParent>;\nexport type AllActiveJoinedThreads = Record<string, ThreadsForGuild>;\n\nexport interface NewThreadCounts {\n    [parentChannelId: string]: number;\n}\n\nexport class ActiveJoinedThreadsStore extends FluxStore {\n    computeAllActiveJoinedThreads(guildId?: string | null): Channel[];\n    getActiveJoinedRelevantThreadsForGuild(guildId: string): ThreadsForGuild;\n    getActiveJoinedRelevantThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent;\n    getActiveJoinedThreadsForGuild(guildId: string): ThreadsForGuild;\n    getActiveJoinedThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent;\n    getActiveJoinedUnreadThreadsForGuild(guildId: string): ThreadsForGuild;\n    getActiveJoinedUnreadThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent;\n    getActiveThreadCount(guildId: string, parentChannelId: string): number;\n    getActiveUnjoinedThreadsForGuild(guildId: string): ThreadsForGuild;\n    getActiveUnjoinedThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent;\n    getActiveUnjoinedUnreadThreadsForGuild(guildId: string): ThreadsForGuild;\n    getActiveUnjoinedUnreadThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent;\n    getAllActiveJoinedThreads(): AllActiveJoinedThreads;\n    getNewThreadCount(guildId: string, parentChannelId: string): number;\n    getNewThreadCountsForGuild(guildId: string): NewThreadCounts;\n    hasActiveJoinedUnreadThreads(guildId: string, parentChannelId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ApplicationStore.d.ts",
    "content": "import { Application, FluxStore } from \"..\";\n\nexport interface ApplicationStoreState {\n    botUserIdToAppUsage: Record<string, ApplicationUsage>;\n}\n\nexport interface ApplicationUsage {\n    applicationId: string;\n    lastUsedMs: number;\n}\n\nexport class ApplicationStore extends FluxStore {\n    getState(): ApplicationStoreState;\n    getApplication(applicationId: string): Application;\n    getApplicationByName(name: string): Application | undefined;\n    getApplicationLastUpdated(applicationId: string): number | undefined;\n    getGuildApplication(guildId: string, type: number): Application | undefined;\n    getGuildApplicationIds(guildId: string): string[];\n    getAppIdForBotUserId(botUserId: string): string | undefined;\n    getFetchingOrFailedFetchingIds(): string[];\n    isFetchingApplication(applicationId: string): boolean;\n    didFetchingApplicationFail(applicationId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/AuthenticationStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport class AuthenticationStore extends FluxStore {\n    /**\n     * Gets the id of the current user\n     */\n    getId(): string;\n    getSessionId(): string;\n    // This Store has a lot more methods related to everything Auth, but they really should\n    // not be needed, so they are not typed\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/CallStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface Call {\n    channelId: string;\n    messageId: string | null;\n    region: string | null;\n    ringing: string[];\n    unavailable: boolean;\n    regionUpdated: boolean;\n}\n\nexport interface CallStoreState {\n    calls: Record<string, Call>;\n    enqueuedRings: Record<string, string[]>;\n}\n\nexport class CallStore extends FluxStore {\n    getCall(channelId: string): Call;\n    getCalls(): Call[];\n    getMessageId(channelId: string): string | null;\n    isCallActive(channelId: string, messageId?: string): boolean;\n    isCallUnavailable(channelId: string): boolean;\n    getInternalState(): CallStoreState;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ChannelRTCStore.d.ts",
    "content": "import { FluxStore, User, VoiceState } from \"..\";\nimport { ParticipantType, RTCPlatform } from \"../../enums\";\n\nexport type RTCLayout = \"normal\" | \"minimum\" | \"no-chat\" | \"full-screen\" | \"haven\";\nexport type RTCMode = \"video\" | \"voice\";\nexport type RTCLayoutContext = \"OVERLAY\" | \"APP\" | \"POPOUT\" | \"CALL_TILE_POPOUT\";\nexport type ParticipantFilterType = \"VIDEO\" | \"STREAM\" | \"FILTERED\" | \"SPEAKING\" | \"ACTIVITY\" | \"NOT_POPPED_OUT\";\n\nexport interface StreamResolution {\n    height: number;\n    width: number;\n}\n\nexport interface Stream {\n    channelId: string;\n    guildId: string | null;\n    ownerId: string;\n    streamType: string;\n}\n\nexport interface BaseParticipant {\n    id: string;\n    type: ParticipantType;\n    isPoppedOut?: boolean;\n}\n\nexport interface UserParticipant extends BaseParticipant {\n    type: ParticipantType.USER;\n    user: User;\n    voiceState: VoiceState | null;\n    voicePlatform: RTCPlatform | null;\n    speaking: boolean;\n    voiceDb: number;\n    latched: boolean;\n    lastSpoke: number;\n    soundsharing: boolean;\n    ringing: boolean;\n    userNick: string;\n    // TODO: type\n    userAvatarDecoration: any | null;\n    localVideoDisabled: boolean;\n    userVideo?: boolean;\n    streamId?: string;\n}\n\nexport interface StreamParticipant extends BaseParticipant {\n    type: ParticipantType.STREAM | ParticipantType.HIDDEN_STREAM;\n    user: User;\n    userNick: string;\n    userVideo: boolean;\n    stream: Stream;\n    maxResolution?: StreamResolution;\n    maxFrameRate?: number;\n    streamId?: string;\n}\n\nexport interface ActivityParticipant extends BaseParticipant {\n    type: ParticipantType.ACTIVITY;\n    applicationId: string;\n    activityType: number;\n    activityUrl: string;\n    participants: string[];\n    guildId: string | null;\n    sortKey: string;\n}\n\nexport type Participant = UserParticipant | StreamParticipant | ActivityParticipant;\n\nexport interface SelectedParticipantStats {\n    view_mode_grid_duration_ms?: number;\n    view_mode_focus_duration_ms?: number;\n    view_mode_toggle_count?: number;\n}\n\nexport interface ChannelRTCState {\n    voiceParticipantsHidden: Record<string, boolean>;\n}\n\nexport class ChannelRTCStore extends FluxStore {\n    getActivityParticipants(channelId: string): ActivityParticipant[];\n    getAllChatOpen(): Record<string, boolean>;\n    getChatOpen(channelId: string): boolean;\n    getFilteredParticipants(channelId: string): Participant[];\n    getGuildRingingUsers(channelId: string): Set<string>;\n    getLayout(channelId: string, context?: RTCLayoutContext): RTCLayout;\n    getMode(channelId: string): RTCMode;\n    getParticipant(channelId: string, participantId: string): Participant | null;\n    getParticipants(channelId: string): Participant[];\n    getParticipantsListOpen(channelId: string): boolean;\n    getParticipantsOpen(channelId: string): boolean;\n    getParticipantsVersion(channelId: string): number;\n    getSelectedParticipant(channelId: string): Participant | null;\n    getSelectedParticipantId(channelId: string): string | null;\n    getSelectedParticipantStats(channelId: string): SelectedParticipantStats;\n    getSpeakingParticipants(channelId: string): UserParticipant[];\n    getStageStreamSize(channelId: string): StreamResolution | undefined;\n    getStageVideoLimitBoostUpsellDismissed(channelId: string): boolean | undefined;\n    getState(): ChannelRTCState;\n    getStreamParticipants(channelId: string): StreamParticipant[];\n    getUserParticipantCount(channelId: string): number;\n    getVideoParticipants(channelId: string): UserParticipant[];\n    getVoiceParticipantsHidden(channelId: string): boolean;\n    isFullscreenInContext(): boolean;\n    isParticipantPoppedOut(channelId: string, participantId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ChannelStore.d.ts",
    "content": "import { Channel, FluxStore } from \"..\";\n\nexport class ChannelStore extends FluxStore {\n    getChannel(channelId: string): Channel;\n    getBasicChannel(channelId: string): Channel | undefined;\n    hasChannel(channelId: string): boolean;\n\n    getChannelIds(guildId?: string | null): string[];\n    getMutableBasicGuildChannelsForGuild(guildId: string): Record<string, Channel>;\n    getMutableGuildChannelsForGuild(guildId: string): Record<string, Channel>;\n    getAllThreadsForGuild(guildId: string): Channel[];\n    getAllThreadsForParent(channelId: string): Channel[];\n    getSortedLinkedChannelsForGuild(guildId: string): Channel[];\n\n    getDMFromUserId(userId: string): string;\n    getDMChannelFromUserId(userId: string): Channel | undefined;\n    getDMUserIds(): string[];\n    getMutableDMsByUserIds(): Record<string, string>;\n    getMutablePrivateChannels(): Record<string, Channel>;\n    getSortedPrivateChannels(): Channel[];\n\n    getGuildChannelsVersion(guildId: string): number;\n    getPrivateChannelsVersion(): number;\n    getInitialOverlayState(): Record<string, Channel>;\n\n    getDebugInfo(): {\n        loadedGuildIds: string[];\n        pendingGuildLoads: string[];\n        guildSizes: string[];\n    };\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/DraftStore.d.ts",
    "content": "import { FluxStore } from \"..\";\nimport { DraftType } from \"../../enums\";\n\nexport interface Draft {\n    timestamp: number;\n    draft: string;\n}\n\nexport interface ThreadSettingsDraft {\n    timestamp: number;\n    parentMessageId?: string;\n    name?: string;\n    isPrivate?: boolean;\n    parentChannelId?: string;\n    location?: string;\n}\n\nexport type ChannelDrafts = {\n    [DraftType.ThreadSettings]: ThreadSettingsDraft;\n} & {\n    [key in Exclude<DraftType, DraftType.ThreadSettings>]: Draft;\n};\n\nexport type UserDrafts = Partial<Record<string, ChannelDrafts>>;\nexport type DraftState = Partial<Record<string, UserDrafts>>;\n\nexport class DraftStore extends FluxStore {\n    getState(): DraftState;\n    getRecentlyEditedDrafts(type: DraftType): Array<Draft & { channelId: string; }>;\n    getDraft(channelId: string, type: DraftType): string;\n\n    getThreadSettings(channelId: string): ThreadSettingsDraft | null | undefined;\n    getThreadDraftWithParentMessageId(parentMessageId: string): ThreadSettingsDraft | null | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/EmojiStore.d.ts",
    "content": "import { Channel, CustomEmoji, Emoji, FluxStore } from \"..\";\nimport { EmojiIntention, LoadState } from \"../../enums\";\n\n/** Emoji picker category names. */\nexport type EmojiCategory =\n    | \"top guild emoji\"\n    | \"favorites\"\n    | \"recent\"\n    | \"custom\"\n    | \"people\"\n    | \"nature\"\n    | \"food\"\n    | \"activity\"\n    | \"travel\"\n    | \"objects\"\n    | \"symbols\"\n    | \"flags\";\n\n/**\n * Tracks usage statistics for a single emoji to compute frecency scores.\n */\nexport interface EmojiUsageRecord {\n    /** Total number of times this emoji has been used. */\n    totalUses: number;\n    /** Array of recent usage timestamps in milliseconds. */\n    recentUses: number[];\n    /** Computed frecency score combining frequency and recency, -1 when dirty. */\n    frecency: number;\n    /** Raw score before frecency computation. */\n    score: number;\n}\n\n/**\n * Options for tracking emoji usage.\n */\nexport interface TrackOptions {\n    /** Timestamp of the usage in milliseconds. */\n    timestamp?: number;\n    /** Number of uses since last track call. */\n    usesSinceLastTrack?: number;\n}\n\n/**\n * Frecency tracker for emoji usage, combines frequency and recency to rank emojis.\n * Used by both regular emoji picker and reaction emoji picker.\n */\nexport interface EmojiFrecency {\n    /** True when data has been modified and needs recomputation. */\n    dirty: boolean;\n    /** Cached array of frequently used emojis after computation. */\n    _frequently: Emoji[];\n    /** Maximum number of frequently used items to track (default 42). */\n    numFrequentlyItems: number;\n    /** Maximum number of recent usage samples to keep per emoji (default 10). */\n    maxSamples: number;\n    /** Computes bonus score for frecency calculation (returns 100). */\n    computeBonus: () => number;\n    /**\n     * Computes weight multiplier based on recency index.\n     * Returns 100 for index <= 3, 70 for <= 15, 50 for <= 30, 30 for <= 45, 10 for <= 80.\n     */\n    computeWeight: (index: number) => number;\n    /**\n     * Computes frecency score for an emoji.\n     * @param totalUses Total number of times emoji was used.\n     * @param score Raw score value.\n     * @param config Configuration for frecency calculation.\n     */\n    computeFrecency: (totalUses: number, score: number, config: {\n        /** Number of recent uses to consider. */\n        numOfRecentUses?: number;\n        /** Maximum total uses to cap at. */\n        maxTotalUse?: number;\n    }) => number;\n    /** Whether to calculate max total use dynamically. */\n    calculateMaxTotalUse: boolean;\n    /**\n     * Looks up an emoji by name or id.\n     * @param name Emoji name or id to look up.\n     * @returns The emoji if found.\n     */\n    lookupKey: (name: string) => Emoji | undefined;\n    /** Usage history keyed by emoji name (for unicode) or id (for custom). */\n    usageHistory: Record<string, EmojiUsageRecord>;\n    /** Callback invoked after frecency computation completes. */\n    afterCompute: () => void;\n\n    /**\n     * Overwrites the usage history with new data.\n     * @param history New usage history to set.\n     * @param pendingUsages Pending usages to track after overwriting.\n     */\n    overwriteHistory(history: Record<string, EmojiUsageRecord> | null, pendingUsages?: PendingUsage[]): void;\n    /** Marks the frecency data as dirty, requiring recomputation. */\n    markDirty(): void;\n    /** Returns whether the frecency data needs recomputation. */\n    isDirty(): boolean;\n    /**\n     * Tracks usage of an emoji.\n     * @param key Emoji name or id.\n     * @param options Track options including timestamp.\n     */\n    track(key: string, options?: TrackOptions): void;\n    /**\n     * Gets the usage record for an emoji, computing if dirty.\n     * @param key Emoji name or id.\n     * @returns The usage record or null if not found.\n     */\n    getEntry(key: string): EmojiUsageRecord | null;\n    /**\n     * Gets the score for an emoji.\n     * @param key Emoji name or id.\n     * @returns The score or null if not found.\n     */\n    getScore(key: string): number | null;\n    /**\n     * Gets the frecency for an emoji.\n     * @param key Emoji name or id.\n     * @returns The frecency or null if not found.\n     */\n    getFrecency(key: string): number | null;\n    /** Recomputes frecency scores for all emojis. */\n    compute(): void;\n    /** Gets the frequently used emojis, computing if necessary. */\n    get frequently(): Emoji[];\n}\n\n/**\n * Container for a guild's emoji collection with usability checks.\n */\nexport interface GuildEmojis {\n    /** Guild id this emoji collection belongs to. */\n    id: string;\n    /** User id for permission checks. */\n    _userId: string;\n    /** Internal emoji array. */\n    _emojis: CustomEmoji[];\n    /** Fast lookup map of emoji id to emoji. */\n    _emojiMap: Record<string, CustomEmoji>;\n    /** Internal emoticons array. */\n    _emoticons: Emoticon[];\n    /** Internal usable emojis cache. */\n    _usableEmojis: CustomEmoji[];\n    /** Whether user can see server subscription IAP. */\n    _canSeeServerSubIAP: boolean;\n    /** All custom emojis in this guild. */\n    get emojis(): CustomEmoji[];\n    /** Custom emojis the current user can use in this guild. */\n    get usableEmojis(): CustomEmoji[];\n    /** Text emoticons configured for this guild. */\n    get emoticons(): Emoticon[];\n    /**\n     * Gets an emoji by id from this guild.\n     * @param id Emoji id to look up.\n     */\n    getEmoji(id: string): CustomEmoji | undefined;\n    /**\n     * Gets a usable emoji by id from this guild.\n     * @param id Emoji id to look up.\n     */\n    getUsableEmoji(id: string): CustomEmoji | undefined;\n    /**\n     * Checks if an emoji is usable by the current user.\n     * @param emoji Emoji to check.\n     */\n    isUsable(emoji: CustomEmoji): boolean;\n    /** Returns array of all emoji ids in this guild. */\n    emojiIds(): string[];\n}\n\n/**\n * Text emoticon that can be converted to emoji.\n */\nexport interface Emoticon {\n    /** Names/aliases for this emoticon. */\n    names: string[];\n    /** The text representation (e.g. \":)\" or \":D\"). */\n    surrogates: string;\n    /** Whether this emoticon should use sprite sheet rendering. */\n    useSpriteSheet: boolean;\n}\n\n/**\n * Pending emoji usage waiting to be recorded.\n */\nexport interface PendingUsage {\n    /** Emoji key (name for unicode, id for custom). */\n    key: string;\n    /** Timestamp in milliseconds when usage occurred. */\n    timestamp: number;\n}\n\n/**\n * Serializable state for EmojiStore persistence.\n */\nexport interface EmojiStoreState {\n    /** Pending emoji usages not yet committed. */\n    pendingUsages: PendingUsage[];\n    /** Pending reaction emoji usages not yet committed. */\n    emojiReactionPendingUsages: PendingUsage[];\n    /** Guild ids with expanded emoji sections in picker. */\n    expandedSectionsByGuildIds: Set<string>;\n}\n\n/**\n * Context for emoji disambiguation, caching resolved emoji data for a guild context.\n * Provides fast lookup of emojis without triggering data fetches.\n */\nexport interface DisambiguatedEmojiContext {\n    /** User's favorite emojis or null if not loaded. */\n    favorites: Emoji[] | null;\n    /** Set of favorite emoji names and ids for fast lookup, or null if not loaded. */\n    favoriteNamesAndIds: Set<string> | null;\n    /** Top emojis for the current guild or null if not loaded. */\n    topEmojis: Emoji[] | null;\n    /** Current guild id context or null for DMs. */\n    guildId: string | null;\n    /** Regex-escaped emoticon names for matching. */\n    escapedEmoticonNames: string;\n    /** All emojis with disambiguation applied (unique names). */\n    disambiguatedEmoji: Emoji[];\n    /** Compiled regex for matching emoticons or null if none. */\n    emoticonRegex: RegExp | null;\n    /** Frequently used emojis or null if not loaded. */\n    frequentlyUsed: Emoji[] | null;\n    /** Frequently used reaction emojis or null if not loaded. */\n    frequentlyUsedReactionEmojis: Emoji[] | null;\n    /** Set of frequently used reaction emoji names and ids, or null if not loaded. */\n    frequentlyUsedReactionNamesAndIds: Set<string> | null;\n    /** Unicode emoji aliases keyed by alias name, maps to primary name. */\n    unicodeAliases: Record<string, string>;\n    /** Custom emojis keyed by emoji id. */\n    customEmojis: Record<string, CustomEmoji>;\n    /** Custom emojis grouped by guild id. */\n    groupedCustomEmojis: Record<string, CustomEmoji[]>;\n    /** Emoticons keyed by name for fast lookup. */\n    emoticonsByName: Record<string, Emoticon>;\n    /** All emojis keyed by name for fast lookup. */\n    emojisByName: Record<string, Emoji>;\n    /** Custom emojis keyed by id for fast lookup. */\n    emojisById: Record<string, CustomEmoji>;\n    /** Newly added emojis grouped by guild id. */\n    newlyAddedEmoji: Record<string, CustomEmoji[]>;\n    /**\n     * Checks if an emoji is a favorite without triggering a fetch.\n     * @param emoji Emoji to check.\n     */\n    isFavoriteEmojiWithoutFetchingLatest(emoji: Emoji): boolean;\n\n    /** Gets favorite emojis without triggering a fetch. */\n    get favoriteEmojisWithoutFetchingLatest(): Emoji[];\n    /** Gets all disambiguated emojis. */\n    getDisambiguatedEmoji(): Emoji[];\n    /** Gets all custom emojis keyed by name. */\n    getCustomEmoji(): Record<string, CustomEmoji>;\n    /** Gets custom emojis grouped by guild id. */\n    getGroupedCustomEmoji(): Record<string, CustomEmoji[]>;\n    /**\n     * Gets an emoji by name.\n     * @param name Emoji name to look up.\n     */\n    getByName(name: string): Emoji | undefined;\n    /**\n     * Gets an emoticon by name.\n     * @param name Emoticon name to look up.\n     */\n    getEmoticonByName(name: string): Emoticon | undefined;\n    /**\n     * Gets an emoji by id.\n     * @param id Emoji id to look up.\n     */\n    getById(id: string): Emoji | undefined;\n    /**\n     * Gets the regex for matching custom emoticons.\n     * @returns RegExp or null if no emoticons.\n     */\n    getCustomEmoticonRegex(): RegExp | null;\n    /** Gets frequently used emojis without triggering a fetch. */\n    getFrequentlyUsedEmojisWithoutFetchingLatest(): Emoji[];\n    /** Rebuilds the frequently used reaction emojis cache and returns it. */\n    rebuildFrequentlyUsedReactionsEmojisWithoutFetchingLatest(): {\n        frequentlyUsedReactionEmojis: Emoji[];\n        frequentlyUsedReactionNamesAndIds: Set<string>;\n    };\n    /** Gets frequently used reaction emojis without triggering a fetch. */\n    getFrequentlyUsedReactionEmojisWithoutFetchingLatest(): Emoji[];\n    /**\n     * Checks if an emoji is frequently used for reactions.\n     * @param emoji Emoji to check.\n     */\n    isFrequentlyUsedReactionEmojiWithoutFetchingLatest(emoji: Emoji): boolean;\n    /** Rebuilds the favorite emojis cache and returns it. */\n    rebuildFavoriteEmojisWithoutFetchingLatest(): {\n        favorites: Emoji[];\n        favoriteNamesAndIds: Set<string>;\n    };\n    /**\n     * Gets emojis in priority order (favorites, frequent, top) without fetching.\n     * @returns Array of emojis in priority order.\n     */\n    getEmojiInPriorityOrderWithoutFetchingLatest(): Emoji[];\n    /**\n     * Gets top emojis for a guild without triggering a fetch.\n     * @param guildId Guild id to get top emojis for.\n     */\n    getTopEmojiWithoutFetchingLatest(guildId: string): Emoji[];\n    /**\n     * Gets newly added emojis for a specific guild.\n     * @param guildId Guild id.\n     */\n    getNewlyAddedEmojiForGuild(guildId: string): CustomEmoji[];\n    /** Gets escaped custom emoticon names for regex matching. */\n    getEscapedCustomEmoticonNames(): string;\n    /**\n     * Checks if a name matches an emoji name chain.\n     * @param name Name to match.\n     */\n    nameMatchesChain(name: string): boolean;\n}\n\n/**\n * Search options for emoji search.\n */\nexport interface EmojiSearchOptions {\n    /** Channel context for permission checks. */\n    channel: Channel;\n    /** Search query string. */\n    query: string;\n    /** Maximum number of results to return. */\n    count?: number;\n    /** Intention for using the emoji, affects availability filtering. */\n    intention: EmojiIntention;\n    /** Whether to include emojis from guilds the user is not in. */\n    includeExternalGuilds?: boolean;\n    /** Whether to only show unicode emojis in results. */\n    showOnlyUnicode?: boolean;\n    /**\n     * Custom comparator for matching emoji names.\n     * @param name Emoji name to compare.\n     * @returns True if the name matches.\n     */\n    matchComparator?(name: string): boolean;\n}\n\n/**\n * Search results split by availability.\n */\nexport interface EmojiSearchResults {\n    /** Emojis that are locked (require Nitro or permissions). */\n    locked: Emoji[];\n    /** Emojis that are available for use. */\n    unlocked: Emoji[];\n}\n\n/**\n * Metadata about top emojis for a guild.\n */\nexport interface TopEmojisMetadata {\n    /** Array of top emoji ids. */\n    emojiIds: string[];\n    /** Time-to-live for this data in milliseconds. */\n    topEmojisTTL: number;\n}\n\n/**\n * Flux store managing all emoji data including custom guild emojis,\n * unicode emojis, favorites, frecency, and search functionality.\n */\nexport class EmojiStore extends FluxStore {\n    /** Array of emoji category names for the picker. */\n    get categories(): EmojiCategory[];\n    /**\n     * Current skin tone modifier surrogate for emoji diversity.\n     * Empty string for default yellow, or skin tone modifier (🏻🏼🏽🏾🏿).\n     */\n    get diversitySurrogate(): string;\n    /** Frecency tracker for emoji picker usage. */\n    get emojiFrecencyWithoutFetchingLatest(): EmojiFrecency;\n    /** Frecency tracker for reaction emoji usage. */\n    get emojiReactionFrecencyWithoutFetchingLatest(): EmojiFrecency;\n    /** Guild ids with expanded emoji sections in picker. */\n    get expandedSectionsByGuildIds(): Set<string>;\n    /** Current load state of the emoji store. */\n    get loadState(): LoadState;\n\n    /**\n     * Gets a custom emoji by its id.\n     * @param id Emoji id to look up.\n     * @returns The custom emoji if found.\n     */\n    getCustomEmojiById(id?: string | null): CustomEmoji | undefined;\n    /**\n     * Gets a usable custom emoji by its id.\n     * @param id Emoji id to look up.\n     * @returns The custom emoji if found and usable by current user.\n     */\n    getUsableCustomEmojiById(id?: string | null): CustomEmoji | undefined;\n    /**\n     * Gets all guild emoji collections keyed by guild id.\n     * @returns Record of guild id to GuildEmojis.\n     */\n    getGuilds(): Record<string, GuildEmojis>;\n    /**\n     * Gets all custom emojis for a guild.\n     * @param guildId Guild id to get emojis for, or null for all guilds.\n     * @returns Array of custom emojis.\n     */\n    getGuildEmoji(guildId?: string | null): CustomEmoji[];\n    /**\n     * Gets usable custom emojis for a guild.\n     * @param guildId Guild id to get emojis for.\n     * @returns Array of usable custom emojis.\n     */\n    getUsableGuildEmoji(guildId?: string | null): CustomEmoji[];\n    /**\n     * Gets newly added emojis for a guild.\n     * @param guildId Guild id to get emojis for.\n     * @returns Array of newly added custom emojis.\n     */\n    getNewlyAddedEmoji(guildId?: string | null): CustomEmoji[];\n    /**\n     * Gets top emojis for a guild based on usage.\n     * @param guildId Guild id to get emojis for.\n     * @returns Array of top custom emojis.\n     */\n    getTopEmoji(guildId?: string | null): CustomEmoji[];\n    /**\n     * Gets metadata about top emojis for a guild.\n     * @param guildId Guild id to get metadata for.\n     * @returns Metadata including emoji ids and TTL, or undefined if not cached.\n     */\n    getTopEmojisMetadata(guildId?: string | null): TopEmojisMetadata | undefined;\n    /**\n     * Checks if user has any favorite emojis in a guild context.\n     * @param guildId Guild id to check.\n     * @returns True if user has favorites.\n     */\n    hasFavoriteEmojis(guildId?: string | null): boolean;\n    /**\n     * Checks if there are pending emoji usages to be recorded.\n     * @returns True if there are pending usages.\n     */\n    hasPendingUsage(): boolean;\n    /**\n     * Checks if user has any usable custom emojis in any guild.\n     * @returns True if user has usable emojis.\n     */\n    hasUsableEmojiInAnyGuild(): boolean;\n    /** Internal method for ordering search results. */\n    getSearchResultsOrder(...args: any[]): any;\n    /**\n     * Gets the serializable state for persistence.\n     * @returns Current store state.\n     */\n    getState(): EmojiStoreState;\n    /**\n     * Searches for emojis without triggering data fetches.\n     * @param options Search options including query and filters.\n     * @returns Search results split by locked/unlocked.\n     */\n    searchWithoutFetchingLatest(options: EmojiSearchOptions): EmojiSearchResults;\n    /**\n     * Gets the disambiguated emoji context for a guild.\n     * @param guildId Guild id to get context for, or null/undefined for global context.\n     */\n    getDisambiguatedEmojiContext(guildId?: string | null): DisambiguatedEmojiContext;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/FluxStore.d.ts",
    "content": "import { FluxDispatcher, FluxEvents } from \"..\";\n\ntype Callback = () => void;\ntype SyncCallback = () => boolean | void;\n\n/*\n  For some reason, this causes type errors when you try to destructure it:\n  ```ts\n  interface FluxEvent {\n      type: FluxEvents;\n      [key: string]: any;\n  }\n  ```\n */\nexport type FluxEvent = any;\n\nexport type ActionHandler = (event: FluxEvent) => void;\n/** keyed by FluxEvents action type */\nexport type ActionHandlers = Partial<Record<FluxEvents, ActionHandler>>;\n\n/**\n * Base class for all Discord Flux stores.\n * Provides change notification, action handling, and store synchronization.\n */\nexport class FluxStore {\n    /**\n     * @param dispatcher the FluxDispatcher instance to register with\n     * @param actionHandlers handlers for Flux actions, keyed by action type\n     * @param band priority band for action handling (default 2), lower runs first\n     */\n    constructor(dispatcher: FluxDispatcher, actionHandlers?: ActionHandlers, band?: number);\n\n    /** returns displayName if set, otherwise constructor.name */\n    getName(): string;\n\n    /** adds listener to _changeCallbacks, invoked before react listeners and triggers syncWith processing */\n    addChangeListener(callback: Callback): void;\n    /**\n     * adds a listener that auto-removes when callback returns false.\n     * @param callback returning false removes the listener\n     * @param preemptive if true (default), calls callback immediately and skips adding if it returns false\n     */\n    addConditionalChangeListener(callback: () => boolean, preemptive?: boolean): void;\n    /** adds listener to _reactChangeCallbacks, invoked after all regular change listeners complete */\n    addReactChangeListener(callback: Callback): void;\n    removeChangeListener(callback: Callback): void;\n    removeReactChangeListener(callback: Callback): void;\n\n    /** called by dispatcher after action handlers run, marks changed if listeners exist and may resume paused dispatch */\n    doEmitChanges(event: FluxEvent): void;\n    /** marks store as changed for batched listener notification */\n    emitChange(): void;\n\n    /** unique token identifying this store in the dispatcher */\n    getDispatchToken(): string;\n    /** override to set up initial state, called once by initializeIfNeeded */\n    initialize(): void;\n    /** calls initialize() if not already initialized, adds performance mark if init takes >5ms */\n    initializeIfNeeded(): void;\n    /**\n     * sets callback to determine if changes must emit during paused dispatch.\n     * @param callback if omitted, defaults to () => true (always emit)\n     */\n    mustEmitChanges(callback?: ActionHandler): void;\n    /**\n     * registers additional action handlers after construction.\n     * @param actionHandlers handlers keyed by action type\n     * @param band priority band, lower runs first\n     */\n    registerActionHandlers(actionHandlers: ActionHandlers, band?: number): void;\n    /**\n     * syncs this store with other stores, re-emitting when they change.\n     * without timeout: synchronous, callback runs during emitNonReactOnce.\n     * with timeout: debounced, adds regular change listener to each source store.\n     * @param stores stores to sync with\n     * @param callback returning false skips emitChange on this store\n     * @param timeout if provided, debounces the sync callback\n     */\n    syncWith(stores: FluxStore[], callback: SyncCallback, timeout?: number): void;\n    /** adds dispatcher dependencies so this store's handlers run after the specified stores */\n    waitFor(...stores: FluxStore[]): void;\n\n    /** initializes all registered stores, called once at app startup */\n    static initialize(): void;\n    /** clears all registered stores and destroys the change listener system */\n    static destroy(): void;\n    /** returns all registered FluxStore instances */\n    static getAll(): FluxStore[];\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/FriendsStore.d.ts",
    "content": "import { Activity, FluxStore, Guild, User } from \"..\";\nimport { GiftIntentType, RelationshipType } from \"../../enums\";\n\nexport type FriendsSection = \"ADD_FRIEND\" | \"ALL\" | \"ONLINE\" | \"PENDING\" | \"PENDING_IGNORED\" | \"SPAM\" | \"SUGGESTIONS\";\n\nexport type StatusType = \"online\" | \"offline\" | \"idle\" | \"dnd\" | \"invisible\" | \"streaming\" | \"unknown\";\n\nexport interface ApplicationStream {\n    channelId: string;\n    guildId: string | null;\n    ownerId: string;\n    streamType: string;\n}\n\nexport interface FriendsRow {\n    key: string;\n    userId: string;\n    /**\n     * 99 means contact based friend suggestions from FriendSuggestionStore,\n     * shown in SUGGESTIONS tab. different from RelationshipType.SUGGESTION\n     * which is for implicit suggestions in RelationshipStore\n     */\n    type: RelationshipType | 99;\n    status: StatusType;\n    isMobile: boolean;\n    activities: Activity[];\n    applicationStream: ApplicationStream | null;\n    user: User | null;\n    usernameLower: string | null;\n    mutualGuildsLength: number;\n    mutualGuilds: Guild[];\n    nickname: string | null;\n    spam: boolean;\n    giftIntentType: GiftIntentType | undefined;\n    ignoredUser: boolean;\n    applicationId: string | undefined;\n    isGameRelationship: boolean;\n    comparator: [RelationshipType | 99, string | null];\n}\n\nexport interface RelationshipCounts {\n    [RelationshipType.FRIEND]: number;\n    [RelationshipType.INCOMING_REQUEST]: number;\n    [RelationshipType.OUTGOING_REQUEST]: number;\n    [RelationshipType.BLOCKED]: number;\n    /** contact based friend suggestions from FriendSuggestionStore */\n    99: number;\n}\n\nexport interface FriendsRows {\n    _rows: FriendsRow[];\n    reset(): FriendsRows;\n    clone(): FriendsRows;\n    update(updater: (userId: string) => Partial<FriendsRow>): boolean;\n    filter(section: FriendsSection, searchQuery?: string | null): FriendsRow[];\n    getRelationshipCounts(): RelationshipCounts;\n}\n\nexport interface FriendsState {\n    fetching: boolean;\n    section: FriendsSection;\n    rows: FriendsRows;\n}\n\nexport class FriendsStore extends FluxStore {\n    getState(): FriendsState;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildChannelStore.d.ts",
    "content": "import { Channel, FluxStore, ThreadJoined } from \"..\";\nimport { ChannelType } from \"../../enums\";\n\nexport interface ChannelWithComparator {\n    channel: Channel;\n    comparator: number;\n}\n\nexport interface GuildChannels {\n    [ChannelType.GUILD_CATEGORY]: ChannelWithComparator[];\n    id: string;\n    SELECTABLE: ChannelWithComparator[] | ThreadJoined[];\n    VOCAL: ChannelWithComparator[];\n    count: number;\n}\n\nexport interface ChannelNameDisambiguation {\n    id: string;\n    name: string;\n}\n\nexport class GuildChannelStore extends FluxStore {\n    getAllGuilds(): Record<string, GuildChannels>;\n    getChannels(guildId: string): GuildChannels;\n    getDefaultChannel(guildId: string): Channel | null;\n    getDirectoryChannelIds(guildId: string): string[];\n    getFirstChannel(\n        guildId: string,\n        predicate: (item: ChannelWithComparator) => boolean,\n        includeVocal?: boolean\n    ): Channel | null;\n    getFirstChannelOfType(\n        guildId: string,\n        predicate: (item: ChannelWithComparator) => boolean,\n        type: \"SELECTABLE\" | \"VOCAL\" | ChannelType.GUILD_CATEGORY\n    ): Channel | null;\n    getSFWDefaultChannel(guildId: string): Channel | null;\n    getSelectableChannelIds(guildId: string): string[];\n    getSelectableChannels(guildId: string): ChannelWithComparator[];\n    getTextChannelNameDisambiguations(guildId: string): Record<string, ChannelNameDisambiguation>;\n    getVocalChannelIds(guildId: string): string[];\n    hasCategories(guildId: string): boolean;\n    hasChannels(guildId: string): boolean;\n    hasElevatedPermissions(guildId: string): boolean;\n    hasSelectableChannel(guildId: string, channelId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildMemberCountStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport class GuildMemberCountStore extends FluxStore {\n    getMemberCounts(): Record<string, number>;\n    getMemberCount(guildId: string): number;\n    getOnlineCount(guildId: string): number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildMemberStore.d.ts",
    "content": "import { FluxStore, GuildMember } from \"..\";\n\nexport interface PendingRoleUpdates {\n    added: string[];\n    removed: string[];\n}\n\nexport class GuildMemberStore extends FluxStore {\n    /** @returns Format: [guildId-userId: Timestamp (string)] */\n    getCommunicationDisabledUserMap(): Record<string, string>;\n    getCommunicationDisabledVersion(): number;\n\n    getMutableAllGuildsAndMembers(): Record<string, Record<string, GuildMember>>;\n\n    getMember(guildId: string, userId: string): GuildMember | null;\n    getTrueMember(guildId: string, userId: string): GuildMember | null;\n    getMemberIds(guildId: string): string[];\n    getMembers(guildId: string): GuildMember[];\n    getMemberVersion(): number;\n    getMemberRoleWithPendingUpdates(guildId: string, userId: string): string[];\n    getPendingRoleUpdates(guildId: string): PendingRoleUpdates;\n    memberOf(userId: string): string[];\n\n    getCachedSelfMember(guildId: string): GuildMember | null;\n    getSelfMember(guildId: string): GuildMember | null;\n    getSelfMemberJoinedAt(guildId: string): Date | null;\n\n    getNick(guildId: string, userId: string): string | null;\n    getNicknameGuildsMapping(userId: string): Record<string, string[]>;\n    getNicknames(userId: string): string[];\n\n    isMember(guildId: string, userId: string): boolean;\n    isGuestOrLurker(guildId: string, userId: string): boolean;\n    isCurrentUserGuest(guildId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildRoleStore.d.ts",
    "content": "import { FluxStore, Guild, Role } from \"..\";\n\nexport class GuildRoleStore extends FluxStore {\n    getRolesSnapshot(guildId: string): Record<string, Role>;\n    getSortedRoles(guildId: string): Role[];\n\n    getEveryoneRole(guild: Guild): Role;\n    getManyRoles(guildId: string, roleIds: string[]): Role[];\n    getNumRoles(guildId: string): number;\n    getRole(guildId: string, roleId: string): Role;\n    getUnsafeMutableRoles(guildId: string): Record<string, Role>;\n    serializeAllGuildRoles(): Array<{ partitionKey: string; values: Record<string, Role>; }>;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildScheduledEventStore.d.ts",
    "content": "import { FluxStore } from \"..\";\nimport { GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel, GuildScheduledEventStatus } from \"../../enums\";\n\nexport interface GuildScheduledEventEntityMetadata {\n    location?: string;\n}\n\nexport interface GuildScheduledEventRecurrenceRule {\n    start: string;\n    end: string | null;\n    frequency: number;\n    interval: number;\n    byWeekday: number[] | null;\n    byNWeekday: { n: number; day: number; }[] | null;\n    byMonth: number[] | null;\n    byMonthDay: number[] | null;\n    byYearDay: number[] | null;\n    count: number | null;\n}\n\nexport interface GuildScheduledEvent {\n    id: string;\n    guild_id: string;\n    channel_id: string | null;\n    creator_id: string | null;\n    name: string;\n    description: string | null;\n    image: string | null;\n    scheduled_start_time: string;\n    scheduled_end_time: string | null;\n    privacy_level: GuildScheduledEventPrivacyLevel;\n    status: GuildScheduledEventStatus;\n    entity_type: GuildScheduledEventEntityType;\n    entity_id: string | null;\n    entity_metadata: GuildScheduledEventEntityMetadata | null;\n    sku_ids: string[];\n    recurrence_rule: GuildScheduledEventRecurrenceRule | null;\n    // TODO: type\n    guild_scheduled_event_exceptions: any[];\n    auto_start: boolean;\n}\n\nexport interface GuildScheduledEventRsvp {\n    guildScheduledEventId: string;\n    userId: string;\n    interested: boolean;\n}\n\nexport interface GuildScheduledEventUsers {\n    // TODO: finish typing\n    [userId: string]: any;\n}\n\nexport class GuildScheduledEventStore extends FluxStore {\n    getGuildScheduledEvent(eventId: string): GuildScheduledEvent | null;\n    getGuildScheduledEventsForGuild(guildId: string): GuildScheduledEvent[];\n    getGuildScheduledEventsByIndex(status: GuildScheduledEventStatus): GuildScheduledEvent[];\n    getGuildEventCountByIndex(status: GuildScheduledEventStatus): number;\n    getRsvpVersion(): number;\n    getRsvp(eventId: string, recurrenceId: string | null, userId: string | null): GuildScheduledEventRsvp | null;\n    isInterestedInEventRecurrence(eventId: string, recurrenceId: string | null): boolean;\n    getUserCount(eventId: string, recurrenceId: string | null): number;\n    hasUserCount(eventId: string, recurrenceId: string | null): boolean;\n    isActive(eventId: string): boolean;\n    getActiveEventByChannel(channelId: string): GuildScheduledEvent | null;\n    getUsersForGuildEvent(eventId: string, recurrenceId: string | null): GuildScheduledEventUsers;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/GuildStore.d.ts",
    "content": "import { Guild, FluxStore } from \"..\";\n\nexport class GuildStore extends FluxStore {\n    getGuild(guildId: string): Guild;\n    getGuildCount(): number;\n    getGuilds(): Record<string, Guild>;\n    getGuildsArray(): Guild[];\n    getGuildIds(): string[];\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/InstantInviteStore.d.ts",
    "content": "import { FluxStore } from \"..\";\nimport { Invite } from \"./InviteStore\";\n\nexport interface FriendInvite extends Invite {\n    max_age: number;\n    max_uses: number;\n    uses: number;\n    created_at: string;\n    revoked?: boolean;\n}\n\nexport class InstantInviteStore extends FluxStore {\n    getInvite(channelId: string): Invite;\n    getFriendInvite(): FriendInvite | null;\n    getFriendInvitesFetching(): boolean;\n    canRevokeFriendInvite(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/InviteStore.d.ts",
    "content": "import { Channel, FluxStore, Guild, User } from \"..\";\n\nexport interface Invite {\n    code: string;\n    guild: Guild | null;\n    channel: Channel | null;\n    inviter: User | null;\n    approximate_member_count?: number;\n    approximate_presence_count?: number;\n    expires_at?: string | null;\n    flags?: number;\n    target_type?: number;\n    target_user?: User;\n    // TODO: type these\n    target_application?: any;\n    stage_instance?: any;\n    guild_scheduled_event?: any;\n}\n\nexport class InviteStore extends FluxStore {\n    getInvite(code: string): Invite;\n    // TODO: finish typing\n    getInviteError(code: string): any | undefined;\n    getInvites(): Record<string, Invite>;\n    getInviteKeyForGuildId(guildId: string): string | undefined;\n    getFriendMemberIds(code: string): string[] | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/LocaleStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport class LocaleStore extends FluxStore {\n    get locale(): string;\n    get systemLocale(): string;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/MediaEngineStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\n/** Context for media engine settings. */\nexport type MediaEngineContextType = \"default\" | \"stream\";\n\n/** Audio/video device type identifiers. */\nexport type DeviceType = \"audioinput\" | \"audiooutput\" | \"videoinput\";\n\n/** Voice activation mode for microphone input. */\nexport type VoiceMode = \"PUSH_TO_TALK\" | \"VOICE_ACTIVITY\";\n\n/** WebRTC connection state. */\nexport type ConnectionState =\n    | \"DISCONNECTED\"\n    | \"CONNECTING\"\n    | \"CONNECTED\"\n    | \"NO_ROUTE\"\n    | \"ICE_CHECKING\"\n    | \"DTLS_CONNECTING\";\n\n/** Video toggle state indicating why video was enabled/disabled. */\nexport type VideoToggleState =\n    | \"NONE\"\n    | \"video_manual_disable\"\n    | \"video_manual_enable\"\n    | \"video_manual_reenable\"\n    | \"video_auto_disable\"\n    | \"video_auto_enable\"\n    | \"video_auto_downgrade\"\n    | \"video_auto_upgrade\";\n\n/** Quality override for video streams. */\nexport type VideoQualityOverride = \"no_override\" | \"high\" | \"low\";\n\n/** Audio processing subsystem. */\nexport type AudioSubsystem = \"legacy\" | \"standard\" | \"experimental\" | \"automatic\";\n\n/** Media stream types. */\nexport type MediaType = \"audio\" | \"video\" | \"screen\" | \"test\";\n\n/** Keyframe interval calculation mode. */\nexport type KeyframeIntervalMode = \"fixed\" | \"source\";\n\n/** Bandwidth estimation algorithm type. */\nexport type BandwidthEstimationType = \"remb\";\n\n/** Media engine implementation type. */\nexport type MediaEngineType = \"NATIVE\" | \"WEBRTC\" | \"DUMMY\";\n\n/** Desktop capture source types. */\nexport type DesktopSourceType = \"WINDOW\" | \"SCREEN\";\n\n/** HDR capture mode for screen sharing. */\nexport type HdrCaptureMode = \"never\" | \"always\" | \"auto\";\n\n/** Input profile type for voice settings. */\nexport type InputProfile = \"DEFAULT\" | \"CUSTOM\";\n\n/** Media engine feature flags for capability checking. */\nexport type MediaEngineFeature =\n    | \"AUTO_ENABLE\"\n    | \"ATTENUATION\"\n    | \"AUDIO_INPUT_DEVICE\"\n    | \"AUDIO_OUTPUT_DEVICE\"\n    | \"VOICE_PROCESSING\"\n    | \"QOS\"\n    | \"NATIVE_PING\"\n    | \"LEGACY_AUDIO_SUBSYSTEM\"\n    | \"EXPERIMENTAL_AUDIO_SUBSYSTEM\"\n    | \"AUTOMATIC_AUDIO_SUBSYSTEM\"\n    | \"AUDIO_SUBSYSTEM_DEFERRED_SWITCH\"\n    | \"AUDIO_BYPASS_SYSTEM_INPUT_PROCESSING\"\n    | \"DEBUG_LOGGING\"\n    | \"AUTOMATIC_VAD\"\n    | \"VOICE_PANNING\"\n    | \"DIAGNOSTICS\"\n    | \"VIDEO\"\n    | \"DESKTOP_CAPTURE\"\n    | \"DESKTOP_CAPTURE_FORMAT\"\n    | \"DESKTOP_CAPTURE_APPLICATIONS\"\n    | \"SOUNDSHARE\"\n    | \"LOOPBACK\"\n    | \"VIDEO_HOOK\"\n    | \"EXPERIMENTAL_SOUNDSHARE\"\n    | \"WUMPUS_VIDEO\"\n    | \"ELEVATED_HOOK\"\n    | \"HYBRID_VIDEO\"\n    | \"REMOTE_LOCUS_NETWORK_CONTROL\"\n    | \"SCREEN_PREVIEWS\"\n    | \"WINDOW_PREVIEWS\"\n    | \"AUDIO_DEBUG_STATE\"\n    | \"AEC_DUMP\"\n    | \"DISABLE_VIDEO\"\n    | \"CONNECTION_REPLAY\"\n    | \"SIMULCAST\"\n    | \"RTC_REGION_RANKING\"\n    | \"ELECTRON_VIDEO\"\n    | \"MEDIAPIPE\"\n    | \"FIXED_KEYFRAME_INTERVAL\"\n    | \"SAMPLE_PLAYBACK\"\n    | \"FIRST_FRAME_CALLBACK\"\n    | \"REMOTE_USER_MULTI_STREAM\"\n    | \"NOISE_SUPPRESSION\"\n    | \"NOISE_CANCELLATION\"\n    | \"VOICE_FILTERS\"\n    | \"AUTOMATIC_GAIN_CONTROL\"\n    | \"CLIPS\"\n    | \"SPEED_TEST\"\n    | \"IMAGE_QUALITY_MEASUREMENT\"\n    | \"GO_LIVE_HARDWARE\"\n    | \"SCREEN_CAPTURE_KIT\"\n    | \"SCREEN_SOUNDSHARE\"\n    | \"NATIVE_SCREENSHARE_PICKER\"\n    | \"MLS_PAIRWISE_FINGERPRINTS\"\n    | \"OFFLOAD_ADM_CONTROLS\"\n    | \"SIDECHAIN_COMPRESSION\"\n    | \"VAAPI\"\n    | \"GAMESCOPE_CAPTURE\"\n    | \"ASYNC_VIDEO_INPUT_DEVICE_INIT\"\n    | \"ASYNC_CLIPS_SOURCE_DEINIT\"\n    | \"PORT_AWARE_LATENCY_TESTING\";\n\n/** Events emitted by the media engine. */\nexport type MediaEngineEvent =\n    | \"Destroy\"\n    | \"Silence\"\n    | \"Connection\"\n    | \"DeviceChange\"\n    | \"VolumeChange\"\n    | \"VoiceActivity\"\n    | \"WatchdogTimeout\"\n    | \"AudioPermission\"\n    | \"VideoPermission\"\n    | \"DesktopSourceEnd\"\n    | \"ConnectionStats\"\n    | \"VideoInputInitialized\"\n    | \"AudioInputInitialized\"\n    | \"ClipsRecordingRestartNeeded\"\n    | \"ClipsInitFailure\"\n    | \"ClipsRecordingEnded\"\n    | \"NativeScreenSharePickerUpdate\"\n    | \"NativeScreenSharePickerCancel\"\n    | \"NativeScreenSharePickerError\"\n    | \"AudioDeviceModuleError\"\n    | \"VoiceFiltersFailed\"\n    | \"VideoCodecError\"\n    | \"VoiceQueueMetrics\"\n    | \"SystemMicrophoneModeChange\"\n    | \"SelectedDeviceChange\";\n\n/**\n * Audio input or output device.\n */\nexport interface AudioDevice {\n    /** unique device identifier from the system. */\n    id: string;\n    /** device index in enumeration, -1 for default device. */\n    index: number;\n    /** human readable device name. */\n    name: string;\n    /** whether the device is disabled in system settings. */\n    disabled: boolean;\n    /** camera facing direction if applicable, undefined for audio devices. */\n    facing?: string;\n    /** windows device GUID for identification. */\n    guid: string;\n    /** hardware identifier for the device. */\n    hardwareId: string;\n    /** container identifier grouping related devices. */\n    containerId: string;\n    /** audio effects supported by device, undefined if none. */\n    effects?: string[];\n}\n\n/**\n * Video input device (webcam).\n */\nexport interface VideoDevice {\n    /** unique device identifier from the system. */\n    id: string;\n    /** device index in enumeration. */\n    index: number;\n    /** human readable device name. */\n    name: string;\n    /** whether the device is disabled in system settings. */\n    disabled: boolean;\n    /** camera facing direction, \"front\", \"back\", or \"unknown\". */\n    facing?: string;\n    /** windows device GUID for identification. */\n    guid: string;\n    /** hardware identifier for the device. */\n    hardwareId?: string;\n    /** container identifier grouping related devices. */\n    containerId?: string;\n    /** video effects supported by device, undefined if none. */\n    effects?: string[];\n}\n\n/**\n * Quality settings for clips recording.\n */\nexport interface ClipsQuality {\n    /** recording frame rate in fps. */\n    frameRate: number;\n    /** recording resolution height in pixels. */\n    resolution: number;\n}\n\n/**\n * Desktop capture configuration for clips.\n */\nexport interface DesktopDescription {\n    /** desktop source identifier. */\n    id: string;\n    /** soundshare source identifier for audio capture. */\n    soundshareId: number;\n    /** whether to use loopback audio capture. */\n    useLoopback: boolean;\n    /** whether to use video hook for capture. */\n    useVideoHook: boolean;\n    /** whether to use windows graphics capture API. */\n    useGraphicsCapture: boolean;\n    /** whether to use macOS quartz capturer. */\n    useQuartzCapturer: boolean;\n    /** whether to allow macOS screencapturekit. */\n    allowScreenCaptureKit: boolean;\n    /** HDR capture behavior. */\n    hdrCaptureMode: HdrCaptureMode;\n}\n\n/**\n * Source configuration for clips recording.\n */\nexport interface ClipsSource {\n    /** quality settings for the recording. */\n    quality: ClipsQuality;\n    /** desktop capture configuration. */\n    desktopDescription: DesktopDescription;\n}\n\n/**\n * Desktop source for screen sharing.\n */\nexport interface DesktopSource {\n    /** source identifier string. */\n    id: string;\n    /** process id of the source application, null if not applicable. */\n    sourcePid: number | null;\n    /** soundshare identifier for audio capture, null if not capturing audio. */\n    soundshareId: string | null;\n    /** soundshare session identifier, null if not active. */\n    soundshareSession: string | null;\n}\n\n/**\n * Quality settings for go live streaming.\n */\nexport interface GoLiveQuality {\n    /** stream resolution height in pixels. */\n    resolution: number;\n    /** stream frame rate in fps. */\n    frameRate: number;\n}\n\n/**\n * Source configuration for go live streaming.\n */\nexport interface GoLiveSource {\n    /** desktop source being streamed. */\n    desktopSource: DesktopSource;\n    /** quality settings for the stream. */\n    quality: GoLiveQuality;\n}\n\n/**\n * Video stream parameter for simulcast layers.\n */\nexport interface VideoStreamParameter {\n    /** simulcast layer id, e.g. \"100\" for full quality, \"50\" for half. */\n    rid: string;\n    /** type of media stream. */\n    type: MediaType;\n    /** quality percentage 0-100. */\n    quality: number;\n}\n\n/**\n * Stereo panning for a user's audio.\n */\nexport interface LocalPan {\n    /** left channel volume multiplier 0-1, default 1. */\n    left: number;\n    /** right channel volume multiplier 0-1, default 1. */\n    right: number;\n}\n\n/**\n * Voice activity detection and push-to-talk options.\n */\nexport interface ModeOptions {\n    /** VAD threshold in dB, default -60. */\n    threshold: number;\n    /** whether to auto-adjust threshold based on noise floor, default true. */\n    autoThreshold: boolean;\n    /** whether to use krisp for VAD instead of webrtc, default true. */\n    vadUseKrisp: boolean;\n    /** krisp activation threshold 0-1, default 0.8. */\n    vadKrispActivationThreshold: number;\n    /** frames of audio to keep before speech is detected, default 5. */\n    vadLeading: number;\n    /** frames to keep transmitting after speech ends, default 25. */\n    vadTrailing: number;\n    /** PTT release delay in milliseconds, default 20. */\n    delay: number;\n    /** keyboard shortcut keys for PTT, default empty array. */\n    shortcut: string[];\n    /** whether to run VAD before audio processing, default false. */\n    vadDuringPreProcess?: boolean;\n}\n\n/**\n * Options for audio loopback testing.\n */\nexport interface LoopbackOptions {\n    /** whether echo cancellation is enabled. */\n    echoCancellation: boolean;\n    /** whether noise suppression is enabled. */\n    noiseSuppression: boolean;\n    /** whether automatic gain control is enabled. */\n    automaticGainControl: boolean;\n    /** whether krisp noise cancellation is enabled. */\n    noiseCancellation: boolean;\n}\n\n/**\n * Screen capture preview thumbnail.\n */\nexport interface ScreenPreview {\n    /** screen source identifier. */\n    id: string;\n    /** data URL of thumbnail image. */\n    url: string;\n    /** display name, e.g. \"Screen 1\". */\n    name: string;\n}\n\n/**\n * Window capture preview thumbnail.\n */\nexport interface WindowPreview {\n    /** window source identifier. */\n    id: string;\n    /** data URL of thumbnail image. */\n    url: string;\n    /** window title. */\n    name: string;\n}\n\n/**\n * Krisp noise cancellation statistics.\n */\nexport interface NoiseCancellationStats {\n    /** milliseconds of detected voice audio. */\n    voiceMs: number;\n    /** milliseconds of detected music audio. */\n    musicMs: number;\n    /** milliseconds of detected noise audio. */\n    noiseMs: number;\n}\n\n/**\n * MLS signing key for end-to-end encryption.\n */\nexport interface MLSSigningKey {\n    /** raw key bytes. */\n    key: Uint8Array;\n    /** key signature bytes. */\n    signature: Uint8Array;\n}\n\n/**\n * Codec capability info for a single codec.\n */\nexport interface CodecInfo {\n    /** codec name (H264, VP8, VP9, AV1, H265). */\n    name: string;\n    /** whether encoding is supported, false if no hardware/software encoder available. */\n    encode: boolean;\n    /** whether decoding is supported, false if no hardware/software decoder available. */\n    decode: boolean;\n}\n\n/**\n * Metadata for saved clips.\n */\nexport interface ClipMetadata {\n    /** custom name for the clip. */\n    name?: string;\n    /** description text for the clip. */\n    description?: string;\n}\n\n/**\n * Result of saving a clip.\n */\nexport interface SavedClip {\n    /** unique clip identifier. */\n    id: string;\n    /** path where clip was saved. */\n    filepath: string;\n}\n\n/**\n * Result of saving a screenshot.\n */\nexport interface Screenshot {\n    /** path where screenshot was saved. */\n    filepath: string;\n}\n\n/**\n * Settings for video background filters.\n */\nexport interface MediaFilterSettings {\n    /** whether background replacement is enabled. */\n    backgroundEnabled: boolean;\n    /** background blur intensity 0-100. */\n    backgroundBlur: number;\n    /** custom background image id, null for blur only. */\n    backgroundId: string | null;\n}\n\n/**\n * Options for local audio recording.\n */\nexport interface AudioRecordingOptions {\n    /** whether to apply echo cancellation. */\n    echoCancellation: boolean;\n    /** whether to apply noise suppression. */\n    noiseSuppression: boolean;\n}\n\n/**\n * Options for raw audio sample recording.\n */\nexport interface RawSamplesOptions {\n    /** number of audio channels. */\n    channels: number;\n    /** sample rate in hz. */\n    sampleRate: number;\n}\n\n/**\n * Options for creating a voice connection.\n */\nexport interface ConnectionOptions {\n    /** whether to start muted. */\n    selfMute: boolean;\n    /** whether to start deafened. */\n    selfDeaf: boolean;\n    /** whether to start with video enabled. */\n    selfVideo: boolean;\n}\n\n/**\n * Options for creating a replay connection.\n */\nexport interface ReplayConnectionOptions {\n    /** path to the replay file. */\n    filePath: string;\n}\n\n/**\n * Info emitted when video input initializes.\n */\nexport interface VideoInputInitializationInfo {\n    /** device that was initialized. */\n    description: VideoDevice;\n    /** time in seconds until first frame. */\n    timeToFirstFrame: number;\n    /** whether initialization timed out. */\n    initializationTimerExpired: boolean;\n    /** entropy value for the video feed. */\n    entropy: number;\n}\n\n/**\n * Info emitted when audio input initializes.\n */\nexport interface AudioInputInitializationInfo {\n    /** device that was initialized. */\n    description: AudioDevice;\n    /** time in seconds until initialized. */\n    timeToInitialized: number;\n}\n\n/**\n * Video codec error details.\n */\nexport interface VideoCodecErrorInfo {\n    /** whether error occurred during encode or decode. */\n    mode: \"encode\" | \"decode\";\n    /** codec standard name. */\n    codecStandard: string;\n    /** error message text. */\n    message: string;\n    /** implementation name that failed. */\n    implName: string;\n}\n\n/**\n * Codec information for connection setup.\n */\nexport interface ConnectionCodec {\n    /** codec name. */\n    name: string;\n    /** payload type number. */\n    payloadType: number;\n    /** priority order. */\n    priority: number;\n    /** rtx payload type if applicable. */\n    rtxPayloadType?: number;\n}\n\n/**\n * Connection transport initialization options.\n */\nexport interface ConnectionTransportOptions {\n    /** server address. */\n    address: string;\n    /** server port. */\n    port: number;\n    /** audio ssrc. */\n    ssrc: number;\n    /** available encryption modes. */\n    modes: string[];\n    /** stream count. */\n    streamCount?: number;\n    /** audio codecs. */\n    audioCodec?: ConnectionCodec;\n    /** video codecs. */\n    videoCodec?: ConnectionCodec;\n    /** rtx codecs. */\n    rtxCodec?: ConnectionCodec;\n    /** experiment flags. */\n    experiments?: string[];\n}\n\n/**\n * Input mode options for voice activity or push-to-talk.\n */\nexport interface InputModeOptions {\n    /** VAD threshold in dB. */\n    vadThreshold?: number;\n    /** whether to auto-adjust threshold. */\n    vadAutoThreshold?: boolean;\n    /** whether to use krisp for VAD. */\n    vadUseKrisp?: boolean;\n    /** krisp activation threshold. */\n    vadKrispActivationThreshold?: number;\n    /** frames before speech detection. */\n    vadLeading?: number;\n    /** frames after speech ends. */\n    vadTrailing?: number;\n    /** PTT release delay in ms. */\n    pttReleaseDelay?: number;\n}\n\n/**\n * Go live source configuration for streaming.\n */\nexport interface GoLiveSourceOptions {\n    /** quality settings. */\n    quality: GoLiveQuality;\n    /** desktop description if streaming desktop. */\n    desktopDescription?: DesktopDescription;\n    /** camera description if streaming camera. */\n    cameraDescription?: { deviceId: string; };\n}\n\n/**\n * Video stream parameter for simulcast configuration.\n */\nexport interface StreamParameter {\n    /** simulcast rid. */\n    rid: string;\n    /** max bitrate. */\n    maxBitrate?: number;\n    /** max framerate. */\n    maxFrameRate?: number;\n    /** max resolution. */\n    maxResolution?: { width: number; height: number; };\n    /** quality percentage. */\n    quality?: number;\n}\n\n/**\n * Automatic gain control configuration.\n */\nexport interface AutomaticGainControlConfig {\n    /** whether AGC is enabled, default true. */\n    enabled: boolean;\n    /** whether to use AGC2 algorithm, default true. */\n    useAGC2: boolean;\n    /** whether analog gain control is enabled, default false. */\n    enableAnalog: boolean;\n    /** whether digital gain control is enabled, default true. */\n    enableDigital: boolean;\n    /** headroom in decibels, default 5. */\n    headroom_db: number;\n    /** maximum gain in decibels, default 50. */\n    max_gain_db: number;\n    /** initial gain in decibels, default 15. */\n    initial_gain_db: number;\n    /** max gain change per second in decibels, default 6. */\n    max_gain_change_db_per_second: number;\n    /** max output noise level in dbfs, default -50. */\n    max_output_noise_level_dbfs: number;\n    /** fixed gain in decibels, default 0. */\n    fixed_gain_db: number;\n}\n\n/**\n * Active voice/video connection to a channel.\n */\nexport interface MediaEngineConnection {\n    /** context this connection belongs to. */\n    context: MediaEngineContextType;\n    /** unique identifier for this connection. */\n    mediaEngineConnectionId: string;\n    /** user id who owns this connection. */\n    userId: string;\n    /** user id for stream context, undefined in default context. */\n    streamUserId: string | undefined;\n    /** current connection state. */\n    connectionState: ConnectionState;\n    /** whether self is muted. */\n    selfMute: boolean;\n    /** whether self is deafened. */\n    selfDeaf: boolean;\n    /** whether self video is enabled. */\n    selfVideo: boolean;\n    /** whether this connection has been destroyed. */\n    destroyed: boolean;\n    /** audio ssrc for this connection. */\n    audioSSRC: number;\n    /** video ssrc for this connection. */\n    videoSSRC: number;\n    /** local mute states keyed by user id. */\n    localMutes: { [userId: string]: boolean; };\n    /** local volume levels keyed by user id. */\n    localVolumes: { [userId: string]: number; };\n    /** local pan settings keyed by user id. */\n    localPans: { [userId: string]: LocalPan; };\n    /** disabled local video states keyed by user id. */\n    disabledLocalVideos: { [userId: string]: boolean; };\n    /** current voice bitrate in bps, default 64000. */\n    voiceBitrate: number;\n    /** whether video is supported. */\n    videoSupported: boolean;\n    /** video stream parameters. */\n    videoStreamParameters: StreamParameter[];\n    /** soundshare source id. */\n    soundshareId: number | null;\n    /** whether soundshare is active. */\n    soundshareActive: boolean;\n    /** whether echo cancellation is enabled. */\n    echoCancellation: boolean;\n    /** whether noise suppression is enabled. */\n    noiseSuppression: boolean;\n    /** automatic gain control configuration. */\n    automaticGainControl: AutomaticGainControlConfig;\n    /** whether noise cancellation is enabled. */\n    noiseCancellation: boolean;\n    /** whether QoS is enabled. */\n    qos: boolean;\n    /** current input mode. */\n    inputMode: string;\n    /** VAD threshold in dB, default -60. */\n    vadThreshold: number;\n    /** whether VAD auto threshold is enabled, default true. */\n    vadAutoThreshold: boolean;\n    /** PTT release delay in ms, default 20. */\n    pttReleaseDelay: number;\n    /** keyframe interval in ms, default 0. */\n    keyframeInterval: number;\n    /** attenuation factor 0-1, default 1 (no attenuation). */\n    attenuationFactor: number;\n    /** whether to attenuate while self speaking. */\n    attenuateWhileSpeakingSelf: boolean;\n    /** whether to attenuate while others speaking. */\n    attenuateWhileSpeakingOthers: boolean;\n\n    /**\n     * Initializes the connection with transport options.\n     * @param options transport options.\n     */\n    initialize(options: ConnectionTransportOptions): void;\n    /** destroys this connection and cleans up resources. */\n    destroy(): void;\n    /**\n     * Sets codecs for the connection.\n     * @param audioCodec audio codec name.\n     * @param videoCodec video codec name.\n     * @param rtxCodec rtx codec name.\n     */\n    setCodecs(audioCodec: string, videoCodec: string, rtxCodec: string): void;\n    /**\n     * Gets connection statistics.\n     * @returns promise resolving to stats or null.\n     */\n    getStats(): Promise<object | null>;\n    /**\n     * Creates a remote user in the connection.\n     * @param userId user id.\n     * @param audioSSRC audio ssrc.\n     * @param videoSSRC video ssrc.\n     */\n    createUser(userId: string, audioSSRC: number, videoSSRC: number): void;\n    /**\n     * Destroys a remote user from the connection.\n     * @param userId user id.\n     */\n    destroyUser(userId: string): void;\n    /**\n     * Sets self mute state.\n     * @param mute whether to mute.\n     */\n    setSelfMute(mute: boolean): void;\n    /**\n     * Gets self mute state.\n     * @returns true if muted.\n     */\n    getSelfMute(): boolean;\n    /**\n     * Gets self deaf state.\n     * @returns true if deafened.\n     */\n    getSelfDeaf(): boolean;\n    /**\n     * Sets self deaf state.\n     * @param deaf whether to deafen.\n     */\n    setSelfDeaf(deaf: boolean): void;\n    /**\n     * Sets soundshare source for this connection.\n     * @param soundshareId soundshare source id.\n     * @param active whether to enable.\n     */\n    setSoundshareSource(soundshareId: number, active: boolean): void;\n    /**\n     * Sets local mute for a user.\n     * @param userId user to mute.\n     * @param muted whether to mute.\n     */\n    setLocalMute(userId: string, muted: boolean): void;\n    /** performs a fast UDP reconnect. */\n    fastUdpReconnect(): void;\n    /**\n     * Gets number of fast UDP reconnects.\n     * @returns reconnect count or null if unsupported.\n     */\n    getNumFastUdpReconnects(): number | null;\n    /** checks if remote was disconnected. */\n    wasRemoteDisconnected(): void;\n    /**\n     * Disables receiving video from a user.\n     * @param userId user to disable video for.\n     * @param disabled whether to disable.\n     */\n    setLocalVideoDisabled(userId: string, disabled: boolean): void;\n    /**\n     * Sets minimum jitter buffer level.\n     * @param level jitter buffer level.\n     */\n    setMinimumJitterBufferLevel(level: number): void;\n    /**\n     * Sets postpone decode level.\n     * @param level decode level.\n     */\n    setPostponeDecodeLevel(level: number): void;\n    /**\n     * Sets clip recording for a user.\n     * @param userId user id.\n     * @param type clip type.\n     * @param enabled whether enabled.\n     */\n    setClipRecordUser(userId: string, type: string, enabled: boolean): void;\n    /**\n     * Sets clips keyframe interval.\n     * @param interval interval in ms.\n     */\n    setClipsKeyFrameInterval(interval: number): void;\n    /**\n     * Sets viewer side clip.\n     * @param enabled whether enabled.\n     */\n    setViewerSideClip(enabled: boolean): void;\n    /**\n     * Sets remote audio history duration.\n     * @param durationMs duration in ms.\n     */\n    setRemoteAudioHistory(durationMs: number): void;\n    /**\n     * Sets quality decoupling.\n     * @param enabled whether enabled.\n     */\n    setQualityDecoupling(enabled: boolean): void;\n    /**\n     * Gets local volume for a user.\n     * @param userId user id.\n     * @returns volume level.\n     */\n    getLocalVolume(userId: string): number;\n    /**\n     * Sets local volume for a user.\n     * @param userId user to adjust.\n     * @param volume volume level 0-200, 100 is normal.\n     */\n    setLocalVolume(userId: string, volume: number): void;\n    /**\n     * Sets stereo pan for a user.\n     * @param userId user to adjust.\n     * @param left left channel 0-1.\n     * @param right right channel 0-1.\n     */\n    setLocalPan(userId: string, left: number, right: number): void;\n    /**\n     * Checks if currently attenuating.\n     * @returns true if attenuating.\n     */\n    isAttenuating(): boolean;\n    /**\n     * Sets attenuation settings.\n     * @param factor attenuation factor 0-100.\n     * @param whileSpeakingSelf attenuate while self speaking.\n     * @param whileSpeakingOthers attenuate while others speaking.\n     */\n    setAttenuation(factor: number, whileSpeakingSelf: boolean, whileSpeakingOthers: boolean): void;\n    /**\n     * Sets whether user can have priority speaker.\n     * @param userId user id.\n     * @param canHavePriority whether can have priority.\n     */\n    setCanHavePriority(userId: string, canHavePriority: boolean): void;\n    /**\n     * Sets voice bitrate.\n     * @param bitrate bitrate in bps.\n     */\n    setBitRate(bitrate: number): void;\n    /**\n     * Sets voice bitrate.\n     * @param bitrate bitrate in bps.\n     */\n    setVoiceBitRate(bitrate: number): void;\n    /**\n     * Sets camera bitrate.\n     * @param maxBitrate max bitrate.\n     * @param minBitrate min bitrate.\n     * @param targetBitrate target bitrate.\n     */\n    setCameraBitRate(maxBitrate: number, minBitrate: number | null, targetBitrate: number | null): void;\n    /**\n     * Sets echo cancellation.\n     * @param enabled whether enabled.\n     */\n    setEchoCancellation(enabled: boolean): void;\n    /**\n     * Sets noise suppression.\n     * @param enabled whether enabled.\n     */\n    setNoiseSuppression(enabled: boolean): void;\n    /**\n     * Sets automatic gain control.\n     * @param config AGC configuration.\n     */\n    setAutomaticGainControl(config: AutomaticGainControlConfig): void;\n    /**\n     * Sets noise cancellation.\n     * @param enabled whether enabled.\n     */\n    setNoiseCancellation(enabled: boolean): void;\n    /**\n     * Sets noise cancellation during processing.\n     * @param enabled whether enabled.\n     */\n    setNoiseCancellationDuringProcessing(enabled: boolean): void;\n    /**\n     * Sets noise cancellation after processing.\n     * @param enabled whether enabled.\n     */\n    setNoiseCancellationAfterProcessing(enabled: boolean): void;\n    /**\n     * Sets VAD after WebRTC.\n     * @param enabled whether enabled.\n     */\n    setVADAfterWebrtc(enabled: boolean): void;\n    /**\n     * Gets noise cancellation state.\n     * @returns true if enabled.\n     */\n    getNoiseCancellation(): boolean;\n    /**\n     * Gets current voice filter id.\n     * @returns voice filter id or null.\n     */\n    getVoiceFilterId(): string | null;\n    /**\n     * Sets voice filter id.\n     * @param filterId filter id or null.\n     */\n    setVoiceFilterId(filterId: string | null): void;\n    /**\n     * Sets QoS enabled.\n     * @param enabled whether enabled.\n     */\n    setQoS(enabled: boolean): void;\n    /**\n     * Sets soundshare discard rear channels.\n     * @param discard whether to discard.\n     */\n    setSoundshareDiscardRearChannels(discard: boolean): void;\n    /**\n     * Sets input mode.\n     * @param mode input mode.\n     * @param options mode options.\n     */\n    setInputMode(mode: string, options: InputModeOptions): void;\n    /**\n     * Sets silence threshold.\n     * @param threshold threshold value.\n     */\n    setSilenceThreshold(threshold: number): void;\n    /**\n     * Sets force audio input.\n     * @param force whether to force.\n     * @param playTone whether to play tone.\n     * @param isSpeaking whether speaking.\n     */\n    setForceAudioInput(force: boolean, playTone?: boolean, isSpeaking?: boolean): void;\n    /**\n     * Sets speaking flags for a user.\n     * @param userId user id.\n     * @param flags speaking flags.\n     */\n    setSpeakingFlags(userId: string, flags: number): void;\n    /** clears all speaking states. */\n    clearAllSpeaking(): void;\n    /**\n     * Sets encryption mode.\n     * @param mode encryption mode.\n     * @param secretKey secret key.\n     */\n    setEncryption(mode: string, secretKey: Uint8Array): void;\n    /**\n     * Sets reconnect interval.\n     * @param interval interval in ms.\n     */\n    setReconnectInterval(interval: number): void;\n    /**\n     * Sets keyframe interval.\n     * @param interval interval in ms.\n     */\n    setKeyframeInterval(interval: number): void;\n    /**\n     * Sets video quality measurement.\n     * @param enabled whether enabled.\n     */\n    setVideoQualityMeasurement(enabled: boolean): void;\n    /**\n     * Sets video encoder experiments.\n     * @param experiments experiment config.\n     */\n    setVideoEncoderExperiments(experiments: object): void;\n    /**\n     * Sets video broadcast state.\n     * @param broadcast whether broadcasting.\n     */\n    setVideoBroadcast(broadcast: boolean): void;\n    /**\n     * Sets go live source.\n     * @param source source options.\n     */\n    setGoLiveSource(source: GoLiveSourceOptions): void;\n    /** clears go live devices. */\n    clearGoLiveDevices(): void;\n    /** clears the desktop source from this connection. */\n    clearDesktopSource(): void;\n    /**\n     * Sets desktop source status callback.\n     * @param callback callback function.\n     */\n    setDesktopSourceStatusCallback(callback: (status: string) => void): void;\n    /**\n     * Checks if connection has a desktop source.\n     * @returns true if streaming desktop.\n     */\n    hasDesktopSource(): boolean;\n    /**\n     * Sets desktop encoding options.\n     * @param width width.\n     * @param height height.\n     * @param framerate framerate.\n     */\n    setDesktopEncodingOptions(width: number, height: number, framerate: number): void;\n    /**\n     * Sets SDP.\n     * @param sdp SDP string.\n     */\n    setSDP(sdp: string): void;\n    /**\n     * Sets remote video sink wants.\n     * @param wants sink wants config.\n     */\n    setRemoteVideoSinkWants(wants: object): void;\n    /**\n     * Sets local video sink wants.\n     * @param wants sink wants config.\n     */\n    setLocalVideoSinkWants(wants: object): void;\n    /**\n     * Starts samples local playback.\n     * @param id playback id.\n     * @param buffer audio buffer.\n     * @param options playback options.\n     * @param callback completion callback.\n     */\n    startSamplesLocalPlayback(id: string, buffer: AudioBuffer, options: object, callback: (error: number, message: string) => void): void;\n    /** stops all samples local playback. */\n    stopAllSamplesLocalPlayback(): void;\n    /**\n     * Stops samples local playback.\n     * @param id playback id.\n     */\n    stopSamplesLocalPlayback(id: string): void;\n    /**\n     * Sets bandwidth estimation experiments.\n     * @param experiments experiment config.\n     */\n    setBandwidthEstimationExperiments(experiments: object): void;\n    /**\n     * Updates video quality core.\n     * @param options quality options.\n     * @param reason update reason.\n     */\n    updateVideoQualityCore(options: object, reason: string): void;\n    /**\n     * Sets stream parameters.\n     * @param params stream parameters.\n     * @returns promise.\n     */\n    setStreamParameters(params: StreamParameter[]): Promise<void>;\n    /** applies video transport options. */\n    applyVideoTransportOptions(): void;\n    /**\n     * Chooses encryption mode.\n     * @param preferred preferred modes.\n     * @param available available modes.\n     * @returns chosen mode.\n     */\n    chooseEncryptionMode(preferred: string[], available: string[]): string;\n    /**\n     * Gets user options.\n     * @returns user options array.\n     */\n    getUserOptions(): object[];\n    /**\n     * Creates input mode options.\n     * @returns input mode options.\n     */\n    createInputModeOptions(): InputModeOptions;\n    /**\n     * Gets attenuation options.\n     * @returns attenuation options.\n     */\n    getAttenuationOptions(): object;\n    /**\n     * Gets codec params.\n     * @param codec codec name.\n     * @param isHardware whether hardware codec.\n     * @returns codec params.\n     */\n    getCodecParams(codec: string, isHardware: boolean): object;\n    /**\n     * Gets codec options.\n     * @param audioCodec audio codec.\n     * @param videoCodec video codec.\n     * @param rtxCodec rtx codec.\n     * @returns codec options.\n     */\n    getCodecOptions(audioCodec: string, videoCodec: string, rtxCodec: string): object;\n    /**\n     * Gets keyframe interval.\n     * @returns interval in ms.\n     */\n    getKeyFrameInterval(): number;\n    /**\n     * Gets connection transport options.\n     * @returns transport options.\n     */\n    getConnectionTransportOptions(): object;\n    /**\n     * Sets stream (not implemented).\n     * @param stream media stream.\n     */\n    setStream(stream: MediaStream): void;\n    /**\n     * Gets user id by ssrc.\n     * @param ssrc ssrc value.\n     */\n    getUserIdBySsrc(ssrc: number): void;\n    /**\n     * Prepares secure frames transition.\n     * @param transitionId transition id.\n     * @param epoch epoch number.\n     * @param callback callback function.\n     */\n    prepareSecureFramesTransition(transitionId: number, epoch: number, callback: () => void): void;\n    /**\n     * Prepares secure frames epoch.\n     * @param epoch epoch number.\n     * @param data epoch data.\n     * @param callback callback function.\n     */\n    prepareSecureFramesEpoch(epoch: number, data: Uint8Array, callback: () => void): void;\n    /**\n     * Executes secure frames transition.\n     * @param transitionId transition id.\n     */\n    executeSecureFramesTransition(transitionId: number): void;\n    /**\n     * Gets MLS key package.\n     * @param callback callback receiving key package.\n     */\n    getMLSKeyPackage(callback: (keyPackage: Uint8Array) => void): void;\n    /**\n     * Updates MLS external sender.\n     * @param sender external sender data.\n     */\n    updateMLSExternalSender(sender: Uint8Array): void;\n    /**\n     * Processes MLS proposals.\n     * @param proposals proposals data.\n     * @param callback callback function.\n     */\n    processMLSProposals(proposals: Uint8Array, callback: () => void): void;\n    /**\n     * Prepares MLS commit transition.\n     * @param transitionId transition id.\n     * @param commit commit data.\n     * @param callback callback function.\n     */\n    prepareMLSCommitTransition(transitionId: number, commit: Uint8Array, callback: () => void): void;\n    /**\n     * Processes MLS welcome.\n     * @param transitionId transition id.\n     * @param welcome welcome data.\n     * @param callback callback function.\n     */\n    processMLSWelcome(transitionId: number, welcome: Uint8Array, callback: () => void): void;\n    /**\n     * Gets MLS pairwise fingerprint.\n     * @param userId user id.\n     * @param version version.\n     * @param callback callback receiving fingerprint.\n     */\n    getMLSPairwiseFingerprint(userId: string, version: number, callback: (fingerprint: Uint8Array) => void): void;\n    /**\n     * Presents desktop source picker.\n     * @param options picker options.\n     */\n    presentDesktopSourcePicker(options: object): void;\n    /**\n     * Merges users.\n     * @param users user merge data.\n     */\n    mergeUsers(users: object[]): void;\n    /**\n     * Gets whether there is an active video output sink.\n     * @param userId user id.\n     * @returns true if has active sink.\n     */\n    getHasActiveVideoOutputSink(userId: string): boolean;\n    /**\n     * Sets whether there is an active video output sink.\n     * @param userId user id.\n     * @param hasActiveSink whether sink is active.\n     * @param reason reason for the change.\n     */\n    setHasActiveVideoOutputSink(userId: string, hasActiveSink: boolean, reason: string): void;\n    /**\n     * Applies quality constraints to video.\n     * @param constraints quality constraints object.\n     * @param ssrc optional ssrc to apply to.\n     * @returns quality manager result.\n     */\n    applyQualityConstraints(constraints?: object, ssrc?: number): object;\n    /**\n     * Applies video quality mode preset.\n     * @param mode quality mode to apply.\n     */\n    applyVideoQualityMode(mode: number): void;\n    /**\n     * Configures go live simulcast settings.\n     * @param enabled whether simulcast is enabled.\n     * @param options simulcast options.\n     */\n    configureGoLiveSimulcast(enabled: boolean, options: object): void;\n    /**\n     * Emits connection stats.\n     * @returns promise resolving to stats or null.\n     */\n    emitStats(): Promise<object | null>;\n    /**\n     * Gets whether active output sink tracking is enabled.\n     * @returns true if enabled.\n     */\n    getActiveOutputSinkTrackingEnabled(): boolean;\n    /**\n     * Gets local mute state for a user.\n     * @param userId user id.\n     * @returns true if muted.\n     */\n    getLocalMute(userId: string): boolean;\n    /**\n     * Gets local video disabled state for a user.\n     * @param userId user id.\n     * @returns true if disabled.\n     */\n    getLocalVideoDisabled(userId: string): boolean;\n    /**\n     * Gets local video quality want for a ssrc.\n     * @param ssrc optional ssrc.\n     * @returns quality want object.\n     */\n    getLocalWant(ssrc?: number): object;\n    /**\n     * Gets remote video sink pixel count for a user.\n     * @param userId user id.\n     * @returns pixel count.\n     */\n    getRemoteVideoSinkPixelCount(userId: string): number;\n    /**\n     * Gets remote video sink wants for a user.\n     * @param userId user id.\n     * @returns sink wants object.\n     */\n    getRemoteVideoSinkWants(userId: string): object;\n    /**\n     * Gets current stream parameters.\n     * @returns array of stream parameters.\n     */\n    getStreamParameters(): StreamParameter[];\n    /**\n     * Handles desktop source ended event.\n     * @param reason end reason.\n     * @param errorCode error code.\n     */\n    handleDesktopSourceEnded(reason: string, errorCode: number): void;\n    /**\n     * Handles first frame received.\n     * @param userId user id.\n     * @param ssrc ssrc.\n     * @param stats stats object.\n     */\n    handleFirstFrame(userId: string, ssrc: number, stats: object): void;\n    /**\n     * Handles first frame encrypted stats.\n     * @param stats stats object.\n     */\n    handleFirstFrameEncryptedStats(stats: object): void;\n    /**\n     * Handles first frame stats.\n     * @param stats stats object.\n     */\n    handleFirstFrameStats(stats: object): void;\n    /**\n     * Handles MLS failure.\n     * @param error error string.\n     * @param code error code.\n     */\n    handleMLSFailure(error: string, code: number): void;\n    /**\n     * Handles native mute changed.\n     * @param muted new mute state.\n     */\n    handleNativeMuteChanged(muted: boolean): void;\n    /**\n     * Handles native mute toggled from system.\n     */\n    handleNativeMuteToggled(): void;\n    /**\n     * Handles new listener for native events.\n     * @param event event name.\n     */\n    handleNewListenerNative(event: string): void;\n    /**\n     * Handles no input detected.\n     * @param hasInput whether input is detected.\n     */\n    handleNoInput(hasInput: boolean): void;\n    /**\n     * Handles ping response.\n     * @param latency latency in ms.\n     * @param hostname hostname.\n     * @param port port number.\n     */\n    handlePing(latency: number, hostname: string, port: number): void;\n    /**\n     * Handles ping timeout.\n     * @param hostname hostname.\n     * @param port port number.\n     * @param attempts attempt count.\n     * @param timeout timeout in ms.\n     */\n    handlePingTimeout(hostname: string, port: number, attempts: number, timeout: number): void;\n    /**\n     * Handles RTCP message.\n     * @param type message type.\n     * @param data message data.\n     */\n    handleRTCPMessage(type: string, data: string): void;\n    /**\n     * Handles soundshare attached.\n     * @param attached whether attached.\n     */\n    handleSoundshare(attached: boolean): void;\n    /**\n     * Handles soundshare ended.\n     */\n    handleSoundshareEnded(): void;\n    /**\n     * Handles soundshare failed.\n     * @param failureCode failure code.\n     * @param failureReason failure reason.\n     * @param willRetry whether it will retry.\n     */\n    handleSoundshareFailed(failureCode: number, failureReason: string, willRetry: boolean): void;\n    /**\n     * Handles speaking flags change.\n     * @param userId user id.\n     * @param flags speaking flags.\n     * @param ssrc ssrc.\n     */\n    handleSpeakingFlags(userId: string, flags: number, ssrc: number): void;\n    /**\n     * Handles native speaking event.\n     * @param userId user id.\n     * @param speaking speaking state or flags.\n     * @param ssrc ssrc.\n     */\n    handleSpeakingNative(userId: string, speaking: boolean | number, ssrc: number): void;\n    /**\n     * Handles speaking while muted event.\n     */\n    handleSpeakingWhileMuted(): void;\n    /**\n     * Handles stats received.\n     * @param stats stats object.\n     */\n    handleStats(stats: object): void;\n    /**\n     * Handles video stream update.\n     * @param userId user id.\n     * @param ssrc ssrc.\n     * @param active whether active.\n     * @param streams stream array.\n     */\n    handleVideo(userId: string, ssrc: number, active: boolean, streams: object[]): void;\n    /**\n     * Handles video encoder fallback.\n     * @param codecName codec that failed.\n     */\n    handleVideoEncoderFallback(codecName: string): void;\n    /**\n     * Initializes stream parameters.\n     * @param parameters initial parameters.\n     */\n    initializeStreamParameters(parameters: StreamParameter[]): void;\n    /**\n     * Callback when desktop encoding options are set.\n     * @param width width.\n     * @param height height.\n     * @param framerate framerate.\n     */\n    onDesktopEncodingOptionsSet(width: number, height: number, framerate: number): void;\n    /**\n     * Overwrites quality for testing.\n     * @param quality quality value.\n     */\n    overwriteQualityForTesting(quality: number): void;\n    /**\n     * Sets the connection state.\n     * @param state new connection state.\n     */\n    setConnectionState(state: ConnectionState): void;\n    /**\n     * Sets an experiment flag.\n     * @param flag flag name.\n     * @param enabled whether enabled.\n     */\n    setExperimentFlag(flag: string, enabled: boolean): void;\n    /**\n     * Sets whether to use electron video.\n     * @param use whether to use.\n     */\n    setUseElectronVideo(use: boolean): void;\n    /**\n     * Updates video quality settings.\n     * @param ssrc optional ssrc to update.\n     */\n    updateVideoQuality(ssrc?: number): void;\n}\n\n/**\n * Low-level media engine for audio/video processing.\n * Handles device enumeration, encoding/decoding, and connections.\n */\nexport interface MediaEngine {\n    /** camera preview component. */\n    Camera: React.ComponentType<{ disabled?: boolean; deviceId?: string; width?: number; height?: number; }>;\n    /** video display component. */\n    Video: React.ComponentType & { onContainerResized: () => void; };\n    /** set of active voice/video connections. */\n    connections: Set<MediaEngineConnection>;\n\n    /**\n     * Registers a listener for device changes.\n     * @param event event name.\n     * @param listener callback receiving device lists.\n     */\n    on(event: \"DeviceChange\", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this;\n    /**\n     * Registers a listener for volume changes.\n     * @param event event name.\n     * @param listener callback receiving input and output volumes.\n     */\n    on(event: \"VolumeChange\", listener: (inputVolume: number, outputVolume: number) => void): this;\n    /**\n     * Registers a listener for voice activity.\n     * @param event event name.\n     * @param listener callback receiving user id and activity level.\n     */\n    on(event: \"VoiceActivity\", listener: (userId: string, voiceActivity: number) => void): this;\n    /**\n     * Registers a listener for desktop source end.\n     * @param event event name.\n     * @param listener callback receiving reason and error code.\n     */\n    on(event: \"DesktopSourceEnd\", listener: (reason: string, errorCode: number) => void): this;\n    /**\n     * Registers a listener for audio permission changes.\n     * @param event event name.\n     * @param listener callback receiving granted state.\n     */\n    on(event: \"AudioPermission\", listener: (granted: boolean) => void): this;\n    /**\n     * Registers a listener for video permission changes.\n     * @param event event name.\n     * @param listener callback receiving granted state.\n     */\n    on(event: \"VideoPermission\", listener: (granted: boolean) => void): this;\n    /**\n     * Registers a listener for video input initialization.\n     * @param event event name.\n     * @param listener callback receiving initialization info.\n     */\n    on(event: \"VideoInputInitialized\", listener: (info: VideoInputInitializationInfo) => void): this;\n    /**\n     * Registers a listener for audio input initialization.\n     * @param event event name.\n     * @param listener callback receiving initialization info.\n     */\n    on(event: \"AudioInputInitialized\", listener: (info: AudioInputInitializationInfo) => void): this;\n    /**\n     * Registers a listener for clips init failure.\n     * @param event event name.\n     * @param listener callback receiving error message and app name.\n     */\n    on(event: \"ClipsInitFailure\", listener: (errorMessage: string, applicationName: string) => void): this;\n    /**\n     * Registers a listener for clips recording ended.\n     * @param event event name.\n     * @param listener callback receiving source id and soundshare id.\n     */\n    on(event: \"ClipsRecordingEnded\", listener: (sourceId: string, soundshareId: number) => void): this;\n    /**\n     * Registers a listener for native screen share picker update.\n     * @param event event name.\n     * @param listener callback receiving existing state and content.\n     */\n    on(event: \"NativeScreenSharePickerUpdate\", listener: (existing: boolean, content: string) => void): this;\n    /**\n     * Registers a listener for native screen share picker cancel.\n     * @param event event name.\n     * @param listener callback receiving existing state.\n     */\n    on(event: \"NativeScreenSharePickerCancel\", listener: (existing: boolean) => void): this;\n    /**\n     * Registers a listener for native screen share picker error.\n     * @param event event name.\n     * @param listener callback receiving error string.\n     */\n    on(event: \"NativeScreenSharePickerError\", listener: (error: string) => void): this;\n    /**\n     * Registers a listener for audio device module error.\n     * @param event event name.\n     * @param listener callback receiving module, code and device name.\n     */\n    on(event: \"AudioDeviceModuleError\", listener: (module: string, code: number, deviceName: string) => void): this;\n    /**\n     * Registers a listener for video codec error.\n     * @param event event name.\n     * @param listener callback receiving error info.\n     */\n    on(event: \"VideoCodecError\", listener: (info: VideoCodecErrorInfo) => void): this;\n    /**\n     * Registers a listener for system microphone mode change.\n     * @param event event name.\n     * @param listener callback receiving new mode.\n     */\n    on(event: \"SystemMicrophoneModeChange\", listener: (mode: string) => void): this;\n    /**\n     * Registers a listener for events without arguments.\n     * @param event event name.\n     * @param listener callback with no arguments.\n     */\n    on(event: \"Destroy\" | \"Silence\" | \"WatchdogTimeout\" | \"ClipsRecordingRestartNeeded\" | \"VoiceFiltersFailed\", listener: () => void): this;\n    /**\n     * Registers a one-time listener for device changes.\n     * @param event event name.\n     * @param listener callback receiving device lists.\n     */\n    once(event: \"DeviceChange\", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this;\n    /**\n     * Registers a one-time listener for volume changes.\n     * @param event event name.\n     * @param listener callback receiving input and output volumes.\n     */\n    once(event: \"VolumeChange\", listener: (inputVolume: number, outputVolume: number) => void): this;\n    /**\n     * Registers a one-time listener for events without arguments.\n     * @param event event name.\n     * @param listener callback with no arguments.\n     */\n    once(event: \"Destroy\" | \"Silence\" | \"WatchdogTimeout\" | \"ClipsRecordingRestartNeeded\" | \"VoiceFiltersFailed\", listener: () => void): this;\n    /**\n     * Removes a listener for device changes.\n     * @param event event name.\n     * @param listener callback to remove.\n     */\n    off(event: \"DeviceChange\", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this;\n    /**\n     * Removes a listener for volume changes.\n     * @param event event name.\n     * @param listener callback to remove.\n     */\n    off(event: \"VolumeChange\", listener: (inputVolume: number, outputVolume: number) => void): this;\n    /**\n     * Removes a listener for events without arguments.\n     * @param event event name.\n     * @param listener callback to remove.\n     */\n    off(event: \"Destroy\" | \"Silence\" | \"WatchdogTimeout\" | \"ClipsRecordingRestartNeeded\" | \"VoiceFiltersFailed\", listener: () => void): this;\n    /**\n     * Removes all listeners for an event.\n     * @param event event name, or all if omitted.\n     */\n    removeAllListeners(event?: MediaEngineEvent): this;\n    /**\n     * Gets the number of listeners for an event.\n     * @param event event name.\n     * @returns listener count.\n     */\n    listenerCount(event: MediaEngineEvent): number;\n\n    /**\n     * Applies video background filter settings.\n     * @param settings filter settings to apply.\n     */\n    applyMediaFilterSettings(settings: MediaFilterSettings): Promise<void>;\n    /**\n     * Creates a new voice connection.\n     * @param userId user id for the connection.\n     * @param channelId channel to connect to.\n     * @param options connection options.\n     * @returns the created connection.\n     */\n    connect(userId: string, channelId: string, options: ConnectionOptions): MediaEngineConnection;\n    /**\n     * Checks if there are no active connections.\n     * @returns true if no connections exist.\n     */\n    connectionsEmpty(): boolean;\n    /**\n     * Creates a replay connection from a file.\n     * @param userId user id for the connection.\n     * @param options replay options including file path.\n     * @returns the created connection or null if unsupported.\n     */\n    createReplayConnection(userId: string, options: ReplayConnectionOptions): MediaEngineConnection | null;\n    /** destroys the media engine and all connections. */\n    destroy(): void;\n    /**\n     * Iterates over all connections.\n     * @param callback called for each connection.\n     * @param context optional context filter, only iterates connections in this context.\n     */\n    eachConnection(callback: (connection: MediaEngineConnection) => void, context?: MediaEngineContextType): void;\n    /**\n     * Enables the media engine.\n     * @returns promise that resolves when enabled.\n     */\n    enable(): Promise<void>;\n    /**\n     * Exports a clip as a blob.\n     * @param clipId clip identifier.\n     * @param userId user who owns the clip.\n     * @returns promise resolving to the clip blob.\n     */\n    exportClip(clipId: string, userId: string): Promise<Blob>;\n    /**\n     * Fetches async resources like DAVE keys.\n     * @param options fetch options.\n     */\n    fetchAsyncResources(options: { fetchDave?: boolean; }): Promise<void>;\n\n    /**\n     * Gets available audio input devices.\n     * @returns promise resolving to device list.\n     */\n    getAudioInputDevices(): Promise<AudioDevice[]>;\n    /**\n     * Gets the current audio layer name.\n     * @returns audio layer identifier.\n     */\n    getAudioLayer(): string;\n    /**\n     * Gets available audio output devices.\n     * @returns promise resolving to device list.\n     */\n    getAudioOutputDevices(): Promise<AudioDevice[]>;\n    /**\n     * Gets the current audio subsystem.\n     * @returns active audio subsystem.\n     */\n    getAudioSubsystem(): AudioSubsystem;\n    /**\n     * Gets codec capabilities as JSON string.\n     * @param callback called with capabilities string.\n     */\n    getCodecCapabilities(callback: (capabilities: string) => void): void;\n    /**\n     * Gets a survey of supported codecs.\n     * @returns promise resolving to codec info.\n     */\n    getCodecSurvey(): Promise<{ codecs: CodecInfo[]; }>;\n    /**\n     * Gets whether debug logging is enabled.\n     * @returns true if enabled.\n     */\n    getDebugLogging(): boolean;\n    /**\n     * Gets the current desktop source.\n     * @returns promise that rejects with NO_STREAM error if not streaming.\n     */\n    getDesktopSource(): Promise<DesktopSource>;\n    /**\n     * Gets whether loopback is active.\n     * @returns always false for native engine.\n     */\n    getLoopback(): boolean;\n    /**\n     * Gets MLS signing key for e2ee.\n     * @param userId user id.\n     * @param guildId guild id.\n     * @returns promise resolving to key and signature.\n     */\n    getMLSSigningKey(userId: string, guildId: string): Promise<MLSSigningKey>;\n    /**\n     * Gets noise cancellation statistics.\n     * @returns promise resolving to stats or null if disabled.\n     */\n    getNoiseCancellationStats(): Promise<NoiseCancellationStats | null>;\n    /**\n     * Gets screen preview thumbnails.\n     * @param width thumbnail width.\n     * @param height thumbnail height.\n     * @returns promise resolving to preview list.\n     */\n    getScreenPreviews(width: number, height: number): Promise<ScreenPreview[]>;\n    /**\n     * Gets supported bandwidth estimation experiments.\n     * @param callback called with experiment list.\n     */\n    getSupportedBandwidthEstimationExperiments(callback: (experiments: string[]) => void): void;\n    /**\n     * Gets supported secure frames protocol version.\n     * @returns protocol version number.\n     */\n    getSupportedSecureFramesProtocolVersion(): number;\n    /**\n     * Gets supported video codecs.\n     * @param callback called with codec name list.\n     */\n    getSupportedVideoCodecs(callback: (codecs: string[]) => void): void;\n    /**\n     * Gets system microphone mode.\n     * @returns promise resolving to mode string.\n     */\n    getSystemMicrophoneMode(): Promise<string>;\n    /**\n     * Gets current video input device id.\n     * @returns device id or \"disabled\".\n     */\n    getVideoInputDeviceId(): string;\n    /**\n     * Gets available video input devices.\n     * @returns promise resolving to device list.\n     */\n    getVideoInputDevices(): Promise<VideoDevice[]>;\n    /**\n     * Gets window preview thumbnails.\n     * @param width thumbnail width.\n     * @param height thumbnail height.\n     * @returns promise resolving to preview list.\n     */\n    getWindowPreviews(width: number, height: number): Promise<WindowPreview[]>;\n\n    /** signals user interaction to enable autoplay. */\n    interact(): void;\n    /**\n     * Shows native screen share picker.\n     * @param options picker options.\n     */\n    presentNativeScreenSharePicker(options?: string): void;\n    /**\n     * Queues an audio subsystem switch.\n     * @param subsystem subsystem to switch to.\n     */\n    queueAudioSubsystem(subsystem: AudioSubsystem): void;\n    /**\n     * Ranks RTC regions by latency.\n     * @param regions region ids to test.\n     * @returns promise resolving to sorted region ids.\n     */\n    rankRtcRegions(regions: string[]): Promise<string[]>;\n    /** releases native desktop video source picker stream. */\n    releaseNativeDesktopVideoSourcePickerStream(): void;\n\n    /**\n     * Saves a clip.\n     * @param clipId clip identifier.\n     * @param userId user who owns the clip.\n     * @returns promise resolving to saved clip info.\n     */\n    saveClip(clipId: string, userId: string): Promise<SavedClip>;\n    /**\n     * Saves a clip for another user.\n     * @param clipId clip identifier.\n     * @param userId user to save for.\n     * @param options clip metadata.\n     * @returns promise resolving to saved clip info.\n     */\n    saveClipForUser(clipId: string, userId: string, options: ClipMetadata): Promise<SavedClip>;\n    /**\n     * Saves a screenshot.\n     * @param channelId channel context.\n     * @param userId user context.\n     * @param width width or null for auto.\n     * @param height height or null for auto.\n     * @param options screenshot metadata.\n     * @returns promise resolving to screenshot info.\n     */\n    saveScreenshot(channelId: string, userId: string, width: number | null, height: number | null, options: ClipMetadata): Promise<Screenshot>;\n\n    /**\n     * Enables or disables AEC dump.\n     * @param enabled whether to enable.\n     */\n    setAecDump(enabled: boolean): void;\n    /**\n     * Sets callback for async clips source deinit.\n     * @param callback callback function.\n     */\n    setAsyncClipsSourceDeinit(callback: () => void): void;\n    /**\n     * Sets callback for async video input device init.\n     * @param callback callback function.\n     */\n    setAsyncVideoInputDeviceInit(callback: () => void): void;\n    /**\n     * Sets whether to bypass system audio input processing.\n     * @param bypass whether to bypass.\n     */\n    setAudioInputBypassSystemProcessing(bypass: boolean): void;\n    /**\n     * Sets the audio input device.\n     * @param deviceId device identifier.\n     */\n    setAudioInputDevice(deviceId: string): void;\n    /**\n     * Sets the audio output device.\n     * @param deviceId device identifier.\n     */\n    setAudioOutputDevice(deviceId: string): void;\n    /**\n     * Sets the audio subsystem.\n     * @param subsystem subsystem to use.\n     */\n    setAudioSubsystem(subsystem: AudioSubsystem): void;\n    /**\n     * Enables or disables AV1 codec.\n     * @param enabled whether to enable.\n     */\n    setAv1Enabled(enabled: boolean): void;\n    /**\n     * Sets clip buffer length in seconds.\n     * @param seconds buffer duration.\n     */\n    setClipBufferLength(seconds: number): void;\n    /**\n     * Enables or disables clips ML pipeline.\n     * @param enabled whether to enable.\n     */\n    setClipsMLPipelineEnabled(enabled: boolean): void;\n    /**\n     * Enables or disables a clips ML pipeline type.\n     * @param type pipeline type.\n     * @param enabled whether to enable.\n     */\n    setClipsMLPipelineTypeEnabled(type: string, enabled: boolean): void;\n    /**\n     * Sets clips quality settings.\n     * @param resolution resolution height.\n     * @param frameRate frame rate.\n     * @param hdr whether HDR is enabled.\n     * @returns true if settings were applied.\n     */\n    setClipsQualitySettings(resolution: number, frameRate: number, hdr: boolean): boolean;\n    /**\n     * Sets or clears the clips source.\n     * @param source source config or null to clear.\n     */\n    setClipsSource(source: ClipsSource | null): void;\n    /**\n     * Enables or disables debug logging.\n     * @param enabled whether to enable.\n     */\n    setDebugLogging(enabled: boolean): void;\n    /**\n     * Sets or clears the go live source.\n     * @param source source config or null to clear.\n     * @param context context to apply to, defaults to \"default\".\n     */\n    setGoLiveSource(source: GoLiveSource | null, context?: MediaEngineContextType): void;\n    /**\n     * Enables or disables H264 codec.\n     * @param enabled whether to enable.\n     */\n    setH264Enabled(enabled: boolean): void;\n    /**\n     * Enables or disables H265 codec.\n     * @param enabled whether to enable.\n     */\n    setH265Enabled(enabled: boolean): void;\n    /**\n     * Sets whether device has fullband performance.\n     * @param has whether it has fullband performance.\n     */\n    setHasFullbandPerformance(has: boolean): void;\n    /**\n     * Sets input volume.\n     * @param volume volume 0-100.\n     */\n    setInputVolume(volume: number): void;\n    /**\n     * Enables loopback for testing.\n     * @param reason reason for enabling loopback.\n     * @param options loopback audio options.\n     */\n    setLoopback(reason: string, options: LoopbackOptions): void;\n    /**\n     * Sets max sync delay override.\n     * @param delay delay in milliseconds.\n     */\n    setMaxSyncDelayOverride(delay: number): void;\n    /**\n     * Sets maybe preprocess mute state.\n     * @param mute whether to mute.\n     */\n    setMaybePreprocessMute(mute: boolean): void;\n    /**\n     * Sets native desktop video source picker active state.\n     * @param active whether picker is active.\n     */\n    setNativeDesktopVideoSourcePickerActive(active: boolean): void;\n    /**\n     * Enables or disables noise cancellation stats.\n     * @param enabled whether to enable.\n     */\n    setNoiseCancellationEnableStats(enabled: boolean): void;\n    /**\n     * Sets whether to offload ADM controls.\n     * @param offload whether to offload.\n     */\n    setOffloadAdmControls(offload: boolean): void;\n    /**\n     * Sets callback for video container resize.\n     * @param callback callback function.\n     */\n    setOnVideoContainerResized(callback: () => void): void;\n    /**\n     * Sets output volume.\n     * @param volume volume 0-100.\n     */\n    setOutputVolume(volume: number): void;\n    /**\n     * Enables or disables sidechain compression.\n     * @param enabled whether to enable.\n     */\n    setSidechainCompression(enabled: boolean): void;\n    /**\n     * Sets sidechain compression strength.\n     * @param strength strength 0-100.\n     */\n    setSidechainCompressionStrength(strength: number): void;\n    /**\n     * Sets soundshare source.\n     * @param soundshareId soundshare source id.\n     * @param active whether to enable.\n     * @param context context to apply to, defaults to \"default\".\n     */\n    setSoundshareSource(soundshareId: number, active: boolean, context?: MediaEngineContextType): void;\n    /**\n     * Sets the video input device.\n     * @param deviceId device identifier.\n     */\n    setVideoInputDevice(deviceId: string): Promise<void>;\n\n    /**\n     * Checks if a connection should broadcast video.\n     * @param connection connection to check.\n     * @returns true if should broadcast.\n     */\n    shouldConnectionBroadcastVideo(connection: MediaEngineConnection): boolean;\n    /**\n     * Shows system capture configuration UI.\n     * @param options options including display id.\n     */\n    showSystemCaptureConfigurationUI(options: { displayId?: string; }): void;\n\n    /** starts AEC dump recording. */\n    startAecDump(): void;\n    /**\n     * Starts local audio recording.\n     * @param options recording options.\n     */\n    startLocalAudioRecording(options: AudioRecordingOptions): Promise<void>;\n    /**\n     * Starts recording raw audio samples.\n     * @param options sample options.\n     */\n    startRecordingRawSamples(options: RawSamplesOptions): void;\n    /** stops AEC dump recording. */\n    stopAecDump(): void;\n    /**\n     * Stops local audio recording.\n     * @param callback called with success and filepath.\n     */\n    stopLocalAudioRecording(callback: (success: boolean, filepath: string) => void): void;\n    /** stops recording raw audio samples. */\n    stopRecordingRawSamples(): void;\n\n    /**\n     * Checks if media engine is supported.\n     * @returns true if supported.\n     */\n    supported(): boolean;\n    /**\n     * Checks if a feature is supported.\n     * @param feature feature to check.\n     * @returns true if supported.\n     */\n    supports(feature: MediaEngineFeature): boolean;\n    /**\n     * Updates clip metadata.\n     * @param clipId clip identifier.\n     * @param metadata new metadata.\n     */\n    updateClipMetadata(clipId: string, metadata: ClipMetadata): Promise<void>;\n    /** ticks the watchdog timer. */\n    watchdogTick(): void;\n    /**\n     * Writes audio debug state to file.\n     * @returns promise that resolves when written.\n     */\n    writeAudioDebugState(): Promise<void>;\n}\n\n/**\n * Persisted media engine settings for a context.\n */\nexport interface MediaEngineSettings {\n    /** current voice mode (PTT or VAD), default VOICE_ACTIVITY. */\n    mode: VoiceMode;\n    /** voice mode configuration options. */\n    modeOptions: ModeOptions;\n    /** settings version for vadUseKrisp migration. */\n    vadUseKrispSettingVersion: number;\n    /** settings version for ncUseKrisp migration. */\n    ncUseKrispSettingVersion: number;\n    /** settings version for ncUseKrispjs migration. */\n    ncUseKrispjsSettingVersion: number;\n    /** whether self is muted, default false. */\n    mute: boolean;\n    /** whether self is deafened, default false. */\n    deaf: boolean;\n    /** whether echo cancellation is enabled, default false. */\n    echoCancellation: boolean;\n    /** whether noise suppression is enabled, default false. */\n    noiseSuppression: boolean;\n    /** whether automatic gain control is enabled, default true. */\n    automaticGainControl: boolean;\n    /** whether krisp noise cancellation is enabled, default false. */\n    noiseCancellation: boolean;\n    /** whether to bypass system audio input processing, default false. */\n    bypassSystemInputProcessing: boolean;\n    /** most recently requested voice filter id, null if none. */\n    mostRecentlyRequestedVoiceFilter: string | null;\n    /** whether voice filter playback is enabled, default false. */\n    voiceFilterPlaybackEnabled: boolean;\n    /** version for hardware enabled migration. */\n    hardwareEnabledVersion: number;\n    /** whether silence warning is enabled, default true. */\n    silenceWarning: boolean;\n    /** attenuation level 0-100 for other users when speaking, default 0. */\n    attenuation: number;\n    /** whether to attenuate others when self is speaking, default false. */\n    attenuateWhileSpeakingSelf: boolean;\n    /** whether to attenuate others when others are speaking, default true. */\n    attenuateWhileSpeakingOthers: boolean;\n    /** per-user local mute states, keyed by user id. */\n    localMutes: { [userId: string]: boolean; };\n    /** per-user disabled local video states, keyed by user id. */\n    disabledLocalVideos: { [userId: string]: boolean; };\n    /** per-user video toggle states, keyed by user id. */\n    videoToggleStateMap: { [userId: string]: VideoToggleState; };\n    /** per-user local volume levels 0-200, keyed by user id, default 100. */\n    localVolumes: { [userId: string]: number; };\n    /** per-user stereo pan settings, keyed by user id. */\n    localPans: { [userId: string]: LocalPan; };\n    /** microphone input volume 0-100, default 100. */\n    inputVolume: number;\n    /** speaker output volume 0-100, default 100. */\n    outputVolume: number;\n    /** selected audio input device id. */\n    inputDeviceId: string;\n    /** selected audio output device id. */\n    outputDeviceId: string;\n    /** selected video input device id. */\n    videoDeviceId: string;\n    /** whether QoS packet priority is enabled. */\n    qos: boolean;\n    /** whether QoS has been migrated. */\n    qosMigrated: boolean;\n    /** whether video hook is enabled. */\n    videoHook: boolean;\n    /** experimental soundshare setting, null if not set. */\n    experimentalSoundshare2: boolean | null;\n    /** system screenshare picker setting, null if not set. */\n    useSystemScreensharePicker: boolean | null;\n    /** whether H265 codec is enabled. */\n    h265Enabled: boolean;\n    /** whether VAD threshold has been migrated. */\n    vadThrehsoldMigrated: boolean;\n    /** whether AEC dump is enabled. */\n    aecDumpEnabled: boolean;\n    /** whether sidechain compression is enabled. */\n    sidechainCompression: boolean;\n    /** settings version for sidechain compression migration. */\n    sidechainCompressionSettingVersion: number;\n    /** sidechain compression strength 0-100, default 50. */\n    sidechainCompressionStrength: number;\n    /** whether automatic audio subsystem selection is enabled. */\n    automaticAudioSubsystem: boolean;\n    /** active input profile or null. */\n    activeInputProfile: InputProfile | null;\n}\n\n/**\n * Complete serializable state of MediaEngineStore.\n */\nexport interface MediaEngineState {\n    /** settings for each context type, keyed by context. */\n    settingsByContext: { [context in MediaEngineContextType]: MediaEngineSettings; };\n    /** available audio input devices, keyed by device id. */\n    inputDevices: { [deviceId: string]: AudioDevice; };\n    /** available audio output devices, keyed by device id. */\n    outputDevices: { [deviceId: string]: AudioDevice; };\n    /** supported features, keyed by feature name. */\n    appSupported: { [feature in MediaEngineFeature]?: boolean; };\n    /** whether krisp module is loaded. */\n    krispModuleLoaded: boolean;\n    /** krisp module version or undefined. */\n    krispVersion: string | undefined;\n    /** krisp suppression level or undefined. */\n    krispSuppressionLevel: number | undefined;\n    /** current go live source or undefined. */\n    goLiveSource: GoLiveSource | undefined;\n    /** context for go live. */\n    goLiveContext: MediaEngineContextType;\n}\n\n/**\n * Keyboard shortcut binding.\n */\nexport interface Shortcut {\n    /** action the shortcut triggers. */\n    action: string;\n    /** keys in the shortcut combination. */\n    shortcut: string[];\n}\n\n/**\n * Flux store managing audio/video settings, devices, and the media engine.\n * Handles voice activity detection, noise cancellation, device selection,\n * and go live streaming configuration.\n */\nexport class MediaEngineStore extends FluxStore {\n    /** fetches async resources like DAVE keys. */\n    fetchAsyncResources(): void;\n    /**\n     * Gets the active input profile.\n     * @returns current input profile.\n     */\n    getActiveInputProfile(): InputProfile;\n    /**\n     * Gets the active voice filter id.\n     * @returns voice filter id or null if none active.\n     */\n    getActiveVoiceFilter(): string | null;\n    /**\n     * Gets when the active voice filter was applied.\n     * @returns application date or null if none active.\n     */\n    getActiveVoiceFilterAppliedAt(): Date | null;\n    /**\n     * Gets whether AEC dump is enabled.\n     * @returns true if enabled.\n     */\n    getAecDump(): boolean;\n    /**\n     * Gets whether to attenuate while others are speaking.\n     * @returns true if enabled.\n     */\n    getAttenuateWhileSpeakingOthers(): boolean;\n    /**\n     * Gets whether to attenuate while self is speaking.\n     * @returns true if enabled.\n     */\n    getAttenuateWhileSpeakingSelf(): boolean;\n    /**\n     * Gets the attenuation level.\n     * @returns attenuation 0-100, default 0.\n     */\n    getAttenuation(): number;\n    /**\n     * Gets the current audio subsystem.\n     * @returns active audio subsystem.\n     */\n    getAudioSubsystem(): AudioSubsystem;\n    /**\n     * Gets whether automatic gain control is enabled.\n     * @returns true if enabled.\n     */\n    getAutomaticGainControl(): boolean;\n    /**\n     * Gets whether system audio input processing is bypassed.\n     * @returns true if bypassed.\n     */\n    getBypassSystemInputProcessing(): boolean;\n    /**\n     * Gets the camera preview component.\n     * @returns react component for camera preview.\n     */\n    getCameraComponent(): React.ComponentType;\n    /**\n     * Gets whether debug logging is enabled.\n     * @returns true if enabled.\n     */\n    getDebugLogging(): boolean;\n    /**\n     * Gets whether echo cancellation is enabled.\n     * @returns true if enabled.\n     */\n    getEchoCancellation(): boolean;\n    /**\n     * Gets whether silence warning is enabled.\n     * @returns true if enabled.\n     */\n    getEnableSilenceWarning(): boolean;\n    /**\n     * Gets whether user has ever spoken while muted.\n     * @returns true if has spoken while muted.\n     */\n    getEverSpeakingWhileMuted(): boolean;\n    /**\n     * Gets whether experimental soundshare is enabled.\n     * @returns true if enabled.\n     */\n    getExperimentalSoundshare(): boolean;\n    /**\n     * Gets the go live context.\n     * @returns current go live context.\n     */\n    getGoLiveContext(): MediaEngineContextType;\n    /**\n     * Gets the current go live source.\n     * @returns go live source or null if not streaming.\n     */\n    getGoLiveSource(): GoLiveSource | null;\n    /**\n     * Gets the GPU brand name.\n     * @returns GPU brand string.\n     */\n    getGpuBrand(): string;\n    /**\n     * Gets whether H265 is enabled.\n     * @returns true if enabled.\n     */\n    getH265Enabled(): boolean;\n    /**\n     * Gets whether hardware encoding is enabled.\n     * @returns true if enabled.\n     */\n    getHardwareEncoding(): boolean;\n    /**\n     * Gets whether audio input is detected.\n     * @returns true if detected, false if not, null if unknown.\n     */\n    getInputDetected(): boolean | null;\n    /**\n     * Gets the selected audio input device id.\n     * @returns device id.\n     */\n    getInputDeviceId(): string;\n    /**\n     * Gets available audio input devices.\n     * @returns devices keyed by device id.\n     */\n    getInputDevices(): { [deviceId: string]: AudioDevice; };\n    /**\n     * Gets the input volume.\n     * @returns volume 0-100, default 100.\n     */\n    getInputVolume(): number;\n    /**\n     * Gets whether krisp stats are enabled.\n     * @returns true if enabled.\n     */\n    getKrispEnableStats(): boolean;\n    /**\n     * Gets the krisp model override.\n     * @returns model name or undefined if not set.\n     */\n    getKrispModelOverride(): string | undefined;\n    /**\n     * Gets available krisp models.\n     * @returns array of model names.\n     */\n    getKrispModels(): string[];\n    /**\n     * Gets the krisp suppression level.\n     * @returns suppression level 0-100, default 100.\n     */\n    getKrispSuppressionLevel(): number;\n    /**\n     * Gets the krisp VAD activation threshold.\n     * @returns threshold 0-1, default 0.8.\n     */\n    getKrispVadActivationThreshold(): number;\n    /**\n     * Gets the timestamp of the last audio input device change.\n     * @returns timestamp in milliseconds.\n     */\n    getLastAudioInputDeviceChangeTimestamp(): number;\n    /**\n     * Gets the stereo pan for a user.\n     * @param userId user to get pan for.\n     * @param context settings context, defaults to \"default\".\n     * @returns pan with left/right 0-1, default {left: 1, right: 1}.\n     */\n    getLocalPan(userId: string, context?: MediaEngineContextType): LocalPan;\n    /**\n     * Gets the volume for a user.\n     * @param userId user to get volume for.\n     * @param context settings context, defaults to \"default\".\n     * @returns volume 0-200, default 100.\n     */\n    getLocalVolume(userId: string, context?: MediaEngineContextType): number;\n    /**\n     * Gets whether loopback is enabled.\n     * @returns true if enabled.\n     */\n    getLoopback(): boolean;\n    /**\n     * Gets the reasons loopback is enabled.\n     * @returns set of reason strings.\n     */\n    getLoopbackReasons(): Set<string>;\n    /**\n     * Gets the media engine instance.\n     * @returns the media engine.\n     */\n    getMediaEngine(): MediaEngine;\n    /**\n     * Gets MLS signing key for e2ee.\n     * @param userId user id.\n     * @param guildId guild id.\n     * @returns promise resolving to key and signature.\n     */\n    getMLSSigningKey(userId: string, guildId: string): Promise<MLSSigningKey>;\n    /**\n     * Gets the current voice mode.\n     * @param context settings context, defaults to \"default\".\n     * @returns current voice mode.\n     */\n    getMode(context?: MediaEngineContextType): VoiceMode;\n    /**\n     * Gets the mode options.\n     * @param context settings context, defaults to \"default\".\n     * @returns current mode options.\n     */\n    getModeOptions(context?: MediaEngineContextType): ModeOptions;\n    /**\n     * Gets the most recently requested voice filter.\n     * @returns voice filter id or null if none.\n     */\n    getMostRecentlyRequestedVoiceFilter(): string | null;\n    /**\n     * Gets whether no input detected notice is shown.\n     * @returns true if shown.\n     */\n    getNoInputDetectedNotice(): boolean;\n    /**\n     * Gets whether noise cancellation is enabled.\n     * @returns true if enabled.\n     */\n    getNoiseCancellation(): boolean;\n    /**\n     * Gets whether noise suppression is enabled.\n     * @returns true if enabled.\n     */\n    getNoiseSuppression(): boolean;\n    /**\n     * Gets the selected audio output device id.\n     * @returns device id.\n     */\n    getOutputDeviceId(): string;\n    /**\n     * Gets available audio output devices.\n     * @returns devices keyed by device id.\n     */\n    getOutputDevices(): { [deviceId: string]: AudioDevice; };\n    /**\n     * Gets the output volume.\n     * @returns volume 0-100, default 100.\n     */\n    getOutputVolume(): number;\n    /**\n     * Gets the packet delay.\n     * @returns delay in milliseconds.\n     */\n    getPacketDelay(): number;\n    /**\n     * Gets the previous voice filter.\n     * @returns voice filter id or null if none.\n     */\n    getPreviousVoiceFilter(): string | null;\n    /**\n     * Gets when the previous voice filter was applied.\n     * @returns application date or null if none.\n     */\n    getPreviousVoiceFilterAppliedAt(): Date | null;\n    /**\n     * Gets whether QoS is enabled.\n     * @returns true if enabled.\n     */\n    getQoS(): boolean;\n    /**\n     * Gets the settings for a context.\n     * @param context settings context, defaults to \"default\".\n     * @returns current settings.\n     */\n    getSettings(context?: MediaEngineContextType): MediaEngineSettings;\n    /**\n     * Gets registered shortcuts.\n     * @returns shortcuts keyed by action.\n     */\n    getShortcuts(): { [action: string]: Shortcut; };\n    /**\n     * Gets whether sidechain compression is enabled.\n     * @returns true if enabled.\n     */\n    getSidechainCompression(): boolean;\n    /**\n     * Gets the sidechain compression strength.\n     * @returns strength 0-100, default 50.\n     */\n    getSidechainCompressionStrength(): number;\n    /**\n     * Gets whether currently speaking while muted.\n     * @returns true if speaking while muted.\n     */\n    getSpeakingWhileMuted(): boolean;\n    /**\n     * Gets the complete store state.\n     * @returns current state.\n     */\n    getState(): MediaEngineState;\n    /**\n     * Gets supported secure frames protocol version.\n     * @returns protocol version number.\n     */\n    getSupportedSecureFramesProtocolVersion(): number;\n    /**\n     * Gets the system microphone mode.\n     * @returns mode string or undefined if not available.\n     */\n    getSystemMicrophoneMode(): string | undefined;\n    /**\n     * Gets whether gamescope capture is used.\n     * @returns true if used.\n     */\n    getUseGamescopeCapture(): boolean;\n    /**\n     * Gets whether system screenshare picker is used.\n     * @returns true if used.\n     */\n    getUseSystemScreensharePicker(): boolean;\n    /**\n     * Gets whether VA-API encoder is used.\n     * @returns true if used.\n     */\n    getUseVaapiEncoder(): boolean;\n    /**\n     * Gets the video display component.\n     * @returns react component for video display.\n     */\n    getVideoComponent(): React.ComponentType;\n    /**\n     * Gets the selected video device id.\n     * @returns device id.\n     */\n    getVideoDeviceId(): string;\n    /**\n     * Gets available video devices.\n     * @returns devices keyed by device id.\n     */\n    getVideoDevices(): { [deviceId: string]: VideoDevice; };\n    /**\n     * Gets whether video hook is enabled.\n     * @returns true if enabled.\n     */\n    getVideoHook(): boolean;\n    /**\n     * Gets video stream parameters.\n     * @param context settings context, defaults to \"default\".\n     * @returns array of stream parameters.\n     */\n    getVideoStreamParameters(context?: MediaEngineContextType): VideoStreamParameter[];\n    /**\n     * Gets the video toggle state for a user.\n     * @param userId user to check.\n     * @param context settings context, defaults to \"default\".\n     * @returns toggle state, NONE if not in map.\n     */\n    getVideoToggleState(userId: string, context?: MediaEngineContextType): VideoToggleState;\n    /**\n     * Gets whether voice filter playback is enabled.\n     * @returns true if enabled.\n     */\n    getVoiceFilterPlaybackEnabled(): boolean;\n\n    /**\n     * Gets whether go live simulcast is enabled.\n     * @returns true if enabled.\n     */\n    goLiveSimulcastEnabled(): boolean;\n\n    /**\n     * Checks if there is an active CallKit call.\n     * @returns true if active.\n     */\n    hasActiveCallKitCall(): boolean;\n    /**\n     * Checks if there is a clips source.\n     * @returns true if has source.\n     */\n    hasClipsSource(): boolean;\n    /**\n     * Checks if a context exists.\n     * @param context context to check.\n     * @returns true if exists.\n     */\n    hasContext(context: MediaEngineContextType): boolean;\n    /**\n     * Checks if H265 hardware decode is available.\n     * @returns true if available.\n     */\n    hasH265HardwareDecode(): boolean;\n\n    /**\n     * Checks if advanced voice activity is supported.\n     * @returns true if supported.\n     */\n    isAdvancedVoiceActivitySupported(): boolean;\n    /**\n     * Checks if AEC dump is supported.\n     * @returns true if supported.\n     */\n    isAecDumpSupported(): boolean;\n    /**\n     * Checks if any local video is auto disabled.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if any auto disabled.\n     */\n    isAnyLocalVideoAutoDisabled(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if automatic gain control is supported.\n     * @returns true if supported.\n     */\n    isAutomaticGainControlSupported(): boolean;\n    /**\n     * Checks if self is deafened.\n     * @returns true if deafened.\n     */\n    isDeaf(): boolean;\n    /**\n     * Checks if hardware mute notice is enabled.\n     * @returns true if enabled.\n     */\n    isEnableHardwareMuteNotice(): boolean;\n    /**\n     * Checks if media engine is enabled.\n     * @returns true if enabled.\n     */\n    isEnabled(): boolean;\n    /**\n     * Checks if hardware mute is active.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if hardware muted.\n     */\n    isHardwareMute(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if input profile is custom.\n     * @returns true if custom.\n     */\n    isInputProfileCustom(): boolean;\n    /**\n     * Checks if user interaction is required.\n     * @returns true if required.\n     */\n    isInteractionRequired(): boolean;\n    /**\n     * Checks if a user is locally muted.\n     * @param userId user to check.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if muted.\n     */\n    isLocalMute(userId: string, context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if a user's video is auto disabled.\n     * @param userId user to check.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if auto disabled.\n     */\n    isLocalVideoAutoDisabled(userId: string, context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if a user's video is disabled.\n     * @param userId user to check.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if disabled.\n     */\n    isLocalVideoDisabled(userId: string, context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if media filter settings are loading.\n     * @returns true if loading.\n     */\n    isMediaFilterSettingLoading(): boolean;\n    /**\n     * Checks if self is muted.\n     * @returns true if muted.\n     */\n    isMute(): boolean;\n    /**\n     * Checks if native audio permission is ready.\n     * @returns true if ready.\n     */\n    isNativeAudioPermissionReady(): boolean;\n    /**\n     * Checks if there was a noise cancellation error.\n     * @returns true if error occurred.\n     */\n    isNoiseCancellationError(): boolean;\n    /**\n     * Checks if noise cancellation is supported.\n     * @returns true if supported.\n     */\n    isNoiseCancellationSupported(): boolean;\n    /**\n     * Checks if noise suppression is supported.\n     * @returns true if supported.\n     */\n    isNoiseSuppressionSupported(): boolean;\n    /**\n     * Checks if screen sharing is active.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if sharing.\n     */\n    isScreenSharing(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if self is deafened in context.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if deafened.\n     */\n    isSelfDeaf(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if self is muted in context.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if muted.\n     */\n    isSelfMute(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if self is temporarily muted.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if temporarily muted.\n     */\n    isSelfMutedTemporarily(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if simulcast is supported.\n     * @returns true if supported.\n     */\n    isSimulcastSupported(): boolean;\n    /**\n     * Checks if sound sharing is active.\n     * @param context settings context, defaults to \"default\".\n     * @returns true if sharing.\n     */\n    isSoundSharing(context?: MediaEngineContextType): boolean;\n    /**\n     * Checks if media engine is supported.\n     * @returns true if supported.\n     */\n    isSupported(): boolean;\n    /**\n     * Checks if video is available.\n     * @returns true if available.\n     */\n    isVideoAvailable(): boolean;\n    /**\n     * Checks if video is enabled.\n     * @returns true if enabled.\n     */\n    isVideoEnabled(): boolean;\n\n    /** notifies that mute/unmute sound was skipped. */\n    notifyMuteUnmuteSoundWasSkipped(): void;\n    /**\n     * Sets whether a user can have priority speaker.\n     * @param userId user to set.\n     * @param canHavePriority whether can have priority.\n     */\n    setCanHavePriority(userId: string, canHavePriority: boolean): void;\n    /**\n     * Sets whether there is an active CallKit call.\n     * @param active whether active.\n     */\n    setHasActiveCallKitCall(active: boolean): void;\n    /**\n     * Checks if manual subsystem selection should be offered.\n     * @returns true if should offer.\n     */\n    shouldOfferManualSubsystemSelection(): boolean;\n    /**\n     * Checks if mute/unmute sound should be skipped.\n     * @returns true if should skip.\n     */\n    shouldSkipMuteUnmuteSound(): boolean;\n    /**\n     * Checks if bypass system input processing should be shown.\n     * @returns true if should show.\n     */\n    showBypassSystemInputProcessing(): boolean;\n\n    /** starts preloading DAVE encryption. */\n    startDavePreload(): void;\n\n    /**\n     * Checks if a feature is supported.\n     * @param feature feature to check.\n     * @returns true if supported.\n     */\n    supports(feature: MediaEngineFeature): boolean;\n    /**\n     * Checks if disable local video is supported.\n     * @returns true if supported.\n     */\n    supportsDisableLocalVideo(): boolean;\n    /**\n     * Checks if experimental soundshare is supported.\n     * @returns true if supported.\n     */\n    supportsExperimentalSoundshare(): boolean;\n    /**\n     * Checks if hook soundshare is supported.\n     * @returns true if supported.\n     */\n    supportsHookSoundshare(): boolean;\n    /**\n     * Checks if in-app capture is supported for an app.\n     * @param appName application name.\n     * @returns true if supported.\n     */\n    supportsInApp(appName: string): boolean;\n    /**\n     * Checks if screen soundshare is supported.\n     * @returns true if supported.\n     */\n    supportsScreenSoundshare(): boolean;\n    /**\n     * Checks if system screenshare picker is supported.\n     * @returns true if supported.\n     */\n    supportsSystemScreensharePicker(): boolean;\n    /**\n     * Checks if video hook is supported.\n     * @returns true if supported.\n     */\n    supportsVideoHook(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/MessageStore.d.ts",
    "content": "import { FluxStore, Message } from \"..\";\n\nexport type JumpType = \"ANIMATED\" | \"INSTANT\";\n\nexport interface MessageCache {\n    _messages: Message[];\n    _map: Record<string, Message>;\n    _wasAtEdge: boolean;\n    _isCacheBefore: boolean;\n}\n\nexport interface ChannelMessages {\n    channelId: string;\n    ready: boolean;\n    cached: boolean;\n    jumpType: JumpType;\n    jumpTargetId: string | null;\n    jumpTargetOffset: number;\n    jumpSequenceId: number;\n    jumped: boolean;\n    jumpedToPresent: boolean;\n    jumpFlash: boolean;\n    jumpReturnTargetId: string | null;\n    focusTargetId: string | null;\n    focusSequenceId: number;\n    initialScrollSequenceId: number;\n    hasMoreBefore: boolean;\n    hasMoreAfter: boolean;\n    loadingMore: boolean;\n    revealedMessageId: string | null;\n    hasFetched: boolean;\n    error: boolean;\n    _array: Message[];\n    _before: MessageCache;\n    _after: MessageCache;\n    _map: Record<string, Message>;\n}\n\nexport class MessageStore extends FluxStore {\n    focusedMessageId(channelId: string): string | undefined;\n    getLastChatCommandMessage(channelId: string): Message | undefined;\n    getLastEditableMessage(channelId: string): Message | undefined;\n    getLastMessage(channelId: string): Message | undefined;\n    getLastNonCurrentUserMessage(channelId: string): Message | undefined;\n    getMessage(channelId: string, messageId: string): Message;\n    /** @see {@link ChannelMessages} */\n    getMessages(channelId: string): ChannelMessages;\n    hasCurrentUserSentMessage(channelId: string): boolean;\n    hasCurrentUserSentMessageSinceAppStart(channelId: string): boolean;\n    hasPresent(channelId: string): boolean;\n    isLoadingMessages(channelId: string): boolean;\n    isReady(channelId: string): boolean;\n    jumpedMessageId(channelId: string): string | undefined;\n    whenReady(channelId: string, callback: () => void): void;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/NotificationSettingsStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport type DesktopNotificationType = \"ALL\" | \"ONLY_MENTIONS\" | \"NEVER\";\nexport type TTSNotificationType = \"ALL\" | \"ONLY_MENTIONS\" | \"NEVER\";\n\nexport interface NotificationSettingsState {\n    desktopType: DesktopNotificationType;\n    disableAllSounds: boolean;\n    disabledSounds: string[];\n    ttsType: TTSNotificationType;\n    disableUnreadBadge: boolean;\n    taskbarFlash: boolean;\n    notifyMessagesInSelectedChannel: boolean;\n}\n\nexport class NotificationSettingsStore extends FluxStore {\n    get taskbarFlash(): boolean;\n    getUserAgnosticState(): NotificationSettingsState;\n    getDesktopType(): DesktopNotificationType;\n    getTTSType(): TTSNotificationType;\n    getDisabledSounds(): string[];\n    getDisableAllSounds(): boolean;\n    getDisableUnreadBadge(): boolean;\n    getNotifyMessagesInSelectedChannel(): boolean;\n    isSoundDisabled(sound: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface OverridePremiumTypeState {\n    createdAtOverride: Date | undefined;\n    premiumTypeActual: number | null;\n    premiumTypeOverride: number | undefined;\n}\n\nexport class OverridePremiumTypeStore extends FluxStore {\n    getState(): OverridePremiumTypeState;\n    getCreatedAtOverride(): Date | undefined;\n    getPremiumTypeActual(): number | null;\n    getPremiumTypeOverride(): number | undefined;\n    get premiumType(): number | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/PendingReplyStore.d.ts",
    "content": "import { Channel, Message } from \"../common\";\nimport { FluxStore } from \"./FluxStore\";\n\nexport interface PendingReply {\n    channel: Channel;\n    message: Message;\n    shouldMention: boolean;\n    showMentionToggle: boolean;\n}\n\nexport class PendingReplyStore extends FluxStore {\n    getPendingReply(channelId: string): PendingReply | undefined;\n    /** Discord doesn't use this method. Also seems to always return undefined */\n    getPendingReplyActionSource(channelId: string): unknown;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/PermissionStore.d.ts",
    "content": "import { Channel, Guild, Role, FluxStore } from \"..\";\n\nexport interface GuildPermissionProps {\n    canManageGuild: boolean;\n    canManageChannels: boolean;\n    canManageRoles: boolean;\n    canManageBans: boolean;\n    canManageNicknames: boolean;\n    canManageGuildExpressions: boolean;\n    canViewAuditLog: boolean;\n    canViewAuditLogV2: boolean;\n    canManageWebhooks: boolean;\n    canViewGuildAnalytics: boolean;\n    canAccessMembersPage: boolean;\n    isGuildAdmin: boolean;\n    isOwner: boolean;\n    isOwnerWithRequiredMfaLevel: boolean;\n    guild: Guild;\n}\n\nexport interface PartialChannelContext {\n    channelId: string;\n}\n\nexport interface PartialGuildContext {\n    guildId: string;\n}\n\nexport type PartialContext = PartialChannelContext | PartialGuildContext;\n\ntype PartialChannel = Channel | { id: string; };\ntype PartialGuild = Guild | { id: string; };\n\nexport class PermissionStore extends FluxStore {\n    // TODO: finish typing these\n    can(permission: bigint, channelOrGuild: PartialChannel | PartialGuild, guildId?: string, overwrites?: Record<string, any>, userId?: string): boolean;\n    canBasicChannel(permission: bigint, channel: PartialChannel, guildId?: string, overwrites?: Record<string, any>, userId?: string): boolean;\n    canWithPartialContext(permission: bigint, context: PartialContext): boolean;\n    canManageUser(permission: bigint, userOrUserId: string, guild: PartialGuild): boolean;\n    canAccessGuildSettings(guild: PartialGuild): boolean;\n    canAccessMemberSafetyPage(guild: PartialGuild): boolean;\n    canImpersonateRole(guild: PartialGuild, role: Role): boolean;\n\n    // TODO: finish typing\n    computePermissions(channel: PartialChannel, guildId?: string, overwrites?: Record<string, any>, userId?: string): bigint;\n    computeBasicPermissions(channel: PartialChannel): number;\n\n    getChannelPermissions(channel: PartialChannel): bigint;\n    getGuildPermissions(guild: PartialGuild): bigint;\n    getGuildPermissionProps(guild: PartialGuild): GuildPermissionProps;\n\n    getHighestRole(guild: PartialGuild): Role | null;\n    isRoleHigher(guild: PartialGuild, firstRole: Role | null, secondRole: Role | null): boolean;\n\n    getGuildVersion(guildId: string): number;\n    getChannelsVersion(): number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/PopoutWindowStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\n/**\n * Known popout window key constants.\n * Used as the key parameter for PopoutWindowStore and PopoutActions methods.\n */\nexport type PopoutWindowKey =\n    | \"DISCORD_CHANNEL_CALL_POPOUT\"\n    | \"DISCORD_CALL_TILE_POPOUT\"\n    | \"DISCORD_SOUNDBOARD\"\n    | \"DISCORD_RTC_DEBUG_POPOUT\"\n    | \"DISCORD_CHANNEL_POPOUT\"\n    | \"DISCORD_ACTIVITY_POPOUT\"\n    | \"DISCORD_OVERLAY_POPOUT\"\n    | \"DISCORD_DEVTOOLS_POPOUT\";\n\n/**\n * Popout window lifecycle event types.\n * Sent via postMessage from popout to parent window.\n */\nexport type PopoutWindowEventType = \"loaded\" | \"unloaded\";\n\n/**\n * Persisted window position and size state.\n * Saved to localStorage and restored when reopening popouts.\n */\nexport interface PopoutWindowState {\n    /** window x position on screen in pixels. */\n    x: number;\n    /** window y position on screen in pixels. */\n    y: number;\n    /** window inner width in pixels. */\n    width: number;\n    /** window inner height in pixels. */\n    height: number;\n    /** whether window stays above other windows, only on desktop app. */\n    alwaysOnTop?: boolean;\n}\n\n/**\n * Features passed to window.open() for popout configuration.\n * Merged with default features (menubar, toolbar, location, directories = false).\n */\nexport interface BrowserWindowFeatures {\n    /** whether to show browser toolbar. */\n    toolbar?: boolean;\n    /** whether to show menu bar. */\n    menubar?: boolean;\n    /** whether to show location/address bar. */\n    location?: boolean;\n    /** whether to show directory buttons. */\n    directories?: boolean;\n    /** window width in pixels. */\n    width?: number;\n    /** window height in pixels. */\n    height?: number;\n    /** default width if no persisted state exists. */\n    defaultWidth?: number;\n    /** default height if no persisted state exists. */\n    defaultHeight?: number;\n    /** window left position in pixels. */\n    left?: number;\n    /** window top position in pixels. */\n    top?: number;\n    /** default always-on-top state, defaults to false. */\n    defaultAlwaysOnTop?: boolean;\n    /** whether window can be moved by user. */\n    movable?: boolean;\n    /** whether window can be resized by user. */\n    resizable?: boolean;\n    /** whether window has a frame/border. */\n    frame?: boolean;\n    /** whether window stays above other windows. */\n    alwaysOnTop?: boolean;\n    /** whether window has a shadow (macOS). */\n    hasShadow?: boolean;\n    /** whether window background is transparent. */\n    transparent?: boolean;\n    /** whether to hide window from taskbar. */\n    skipTaskbar?: boolean;\n    /** title bar style, null for default. */\n    titleBarStyle?: string | null;\n    /** window background color as hex string. */\n    backgroundColor?: string;\n    /** whether this is an out-of-process overlay window. */\n    outOfProcessOverlay?: boolean;\n}\n\n/**\n * Manages Discord's popout windows (voice calls, activities, etc.).\n * Extends PersistedStore to save window positions across sessions.\n *\n * Handles Flux actions:\n * - POPOUT_WINDOW_OPEN: opens a new popout window\n * - POPOUT_WINDOW_CLOSE: closes a popout window\n * - POPOUT_WINDOW_SET_ALWAYS_ON_TOP: toggles always-on-top (desktop only)\n * - POPOUT_WINDOW_ADD_STYLESHEET: injects stylesheet into all open popouts\n * - LOGOUT: closes all popout windows\n */\nexport class PopoutWindowStore extends FluxStore {\n    /**\n     * Gets the Window object for a popout.\n     * @param key unique identifier for the popout window\n     * @returns Window reference or undefined if not open\n     */\n    getWindow(key: string): Window | undefined;\n\n    /**\n     * Gets persisted position/size state for a window.\n     * State is saved when window closes and restored when reopened.\n     * @param key unique identifier for the popout window\n     * @returns saved state or undefined if never opened\n     */\n    getWindowState(key: string): PopoutWindowState | undefined;\n\n    /**\n     * Gets all currently open popout window keys.\n     * @returns array of window key identifiers\n     */\n    getWindowKeys(): string[];\n\n    /**\n     * Checks if a popout window is currently open.\n     * @param key unique identifier for the popout window\n     * @returns true if window exists and is not closed\n     */\n    getWindowOpen(key: string): boolean;\n\n    /**\n     * Checks if a popout window has always-on-top enabled.\n     * Only functional on desktop app (isPlatformEmbedded).\n     * @param key unique identifier for the popout window\n     * @returns true if always-on-top is enabled\n     */\n    getIsAlwaysOnTop(key: string): boolean;\n\n    /**\n     * Checks if a popout window's document has focus.\n     * @param key unique identifier for the popout window\n     * @returns true if window document has focus\n     */\n    getWindowFocused(key: string): boolean;\n\n    /**\n     * Checks if a popout window is visible (not minimized/hidden).\n     * Uses document.visibilityState === \"visible\".\n     * @param key unique identifier for the popout window\n     * @returns true if window is visible\n     */\n    getWindowVisible(key: string): boolean;\n\n    /**\n     * Gets all persisted window states.\n     * Keyed by window identifier, contains position/size data.\n     * @returns record of window key to persisted state\n     */\n    getState(): Record<string, PopoutWindowState>;\n\n    /**\n     * Checks if a window is fully initialized and ready for rendering.\n     * A window is fully initialized when it has:\n     * - Window object created\n     * - React root mounted\n     * - Render function stored\n     * @param key unique identifier for the popout window\n     * @returns true if window is fully initialized\n     */\n    isWindowFullyInitialized(key: string): boolean;\n\n    /**\n     * Checks if a popout window is in fullscreen mode.\n     * Checks if document.fullscreenElement.id === \"app-mount\".\n     * @param key unique identifier for the popout window\n     * @returns true if window is fullscreen\n     */\n    isWindowFullScreen(key: string): boolean;\n\n    /**\n     * Unmounts and closes a popout window.\n     * Saves current position/size before closing.\n     * Logs warning if window was not fully initialized.\n     * @param key unique identifier for the popout window\n     */\n    unmountWindow(key: string): void;\n}\n\n/**\n * Actions for managing popout windows.\n * Dispatches Flux actions to PopoutWindowStore.\n */\nexport interface PopoutActions {\n    /**\n     * Opens a new popout window.\n     * If window with key already exists and is not out-of-process:\n     * - On desktop: focuses the existing window via native module\n     * - On web: calls window.focus()\n     * @param key unique identifier for the popout window\n     * @param render function that returns React element to render, receives key as arg\n     * @param features window features (size, position, etc.)\n     */\n    open(key: string, render: (key: string) => React.ReactNode, features?: BrowserWindowFeatures): void;\n\n    /**\n     * Closes a popout window.\n     * Saves position/size state before closing unless preventPopoutClose setting is true.\n     * @param key unique identifier for the popout window\n     */\n    close(key: string): void;\n\n    /**\n     * Sets always-on-top state for a popout window.\n     * Only functional on desktop app (isPlatformEmbedded).\n     * @param key unique identifier for the popout window\n     * @param alwaysOnTop whether window should stay above others\n     */\n    setAlwaysOnTop(key: string, alwaysOnTop: boolean): void;\n\n    /**\n     * Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it\n     *\n     * Injects a stylesheet into all open popout windows.\n     * Validates origin matches current host or webpack public path.\n     * @param url stylesheet URL to inject\n     * @param integrity optional SRI integrity hash\n     */\n    addStylesheet?(url: string, integrity?: string): void;\n\n    /**\n     * Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it\n     *\n     * Opens a channel call popout for voice/video calls.\n     * Dispatches CHANNEL_CALL_POPOUT_WINDOW_OPEN action.\n     * @param channel channel object to open call popout for\n     */\n    openChannelCallPopout?(channel: { id: string; }): void;\n\n    /**\n     * Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it\n     *\n     * Opens a call tile popout for a specific participant.\n     * Dispatches CALL_TILE_POPOUT_WINDOW_OPEN action.\n     * @param channelId channel ID of the call\n     * @param participantId user ID of the participant\n     */\n    openCallTilePopout?(channelId: string, participantId: string): void;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/PresenceStore.d.ts",
    "content": "import { Activity, OnlineStatus } from \"../common\";\nimport { FluxStore } from \"./FluxStore\";\n\nexport interface UserAndActivity {\n    userId: string;\n    activity: Activity;\n}\n\nexport type DiscordPlatform = \"desktop\" | \"mobile\" | \"web\" | \"embedded\" | \"vr\";\n\nexport interface PresenceStoreState {\n    presencesForGuilds: Record<string, Record<string, { status: OnlineStatus; activities: Activity[]; clientStatus: Partial<Record<DiscordPlatform, OnlineStatus>>; }>>;\n    statuses: Record<string, OnlineStatus>;\n    activities: Record<string, Activity[]>;\n    filteredActivities: Record<string, Activity[]>;\n    hiddenActivities: Record<string, Activity[]>;\n    // TODO: finish typing\n    activityMetadata: Record<string, any>;\n    clientStatuses: Record<string, Partial<Record<DiscordPlatform, OnlineStatus>>>;\n}\n\nexport class PresenceStore extends FluxStore {\n    findActivity(userId: string, predicate: (activity: Activity) => boolean, guildId?: string): Activity | undefined;\n    getActivities(userId: string, guildId?: string): Activity[];\n    // TODO: finish typing\n    getActivityMetadata(userId: string): any;\n    getAllApplicationActivities(applicationId: string): UserAndActivity[];\n    getApplicationActivity(userId: string, applicationId: string, guildId?: string): Activity | null;\n    getClientStatus(userId: string): Record<DiscordPlatform, OnlineStatus>;\n    getHiddenActivities(): Activity[];\n    /** literally just getActivities(...)[0] */\n    getPrimaryActivity(userId: string, guildId?: string): Activity | null;\n    getState(): PresenceStoreState;\n    getStatus(userId: string, guildId?: string | null, defaultStatus?: OnlineStatus): OnlineStatus;\n    getUnfilteredActivities(userId: string, guildId?: string): Activity[];\n    getUserIds(): string[];\n    isMobileOnline(userId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/RTCConnectionStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport type RTCConnectionState =\n    | \"DISCONNECTED\"\n    | \"AWAITING_ENDPOINT\"\n    | \"AUTHENTICATING\"\n    | \"CONNECTING\"\n    | \"RTC_DISCONNECTED\"\n    | \"RTC_CONNECTING\"\n    | \"RTC_CONNECTED\"\n    | \"NO_ROUTE\"\n    | \"ICE_CHECKING\"\n    | \"DTLS_CONNECTING\";\n\nexport type RTCConnectionQuality = \"unknown\" | \"bad\" | \"average\" | \"fine\";\n\nexport interface LastRTCConnectionState {\n    duration: number | null;\n    mediaSessionId: string | null;\n    rtcConnectionId: string | null;\n    wasEverMultiParticipant: boolean;\n    wasEverRtcConnected: boolean;\n    // TODO: type\n    voiceStateAnalytics: any;\n    channelId: string;\n}\n\nexport interface RTCConnectionPacketStats {\n    inbound: number;\n    outbound: number;\n    lost: number;\n}\n\nexport interface VoiceStateStats {\n    max_voice_state_count: number;\n}\n\nexport interface SecureFramesState {\n    state: string;\n}\n\nexport interface SecureFramesRosterMapEntry {\n    pendingVerifyState: number;\n    verifiedState: number;\n}\n\nexport class RTCConnectionStore extends FluxStore {\n    // TODO: type\n    getRTCConnection(): any | null;\n    getState(): RTCConnectionState;\n    isConnected(): boolean;\n    isDisconnected(): boolean;\n    getRemoteDisconnectVoiceChannelId(): string | null;\n    getLastSessionVoiceChannelId(): string | null;\n    setLastSessionVoiceChannelId(channelId: string | null): void;\n    getGuildId(): string | undefined;\n    getChannelId(): string | undefined;\n    getHostname(): string;\n    getQuality(): RTCConnectionQuality;\n    getPings(): number[];\n    getAveragePing(): number;\n    getLastPing(): number | undefined;\n    getOutboundLossRate(): number | undefined;\n    getMediaSessionId(): string | undefined;\n    getRTCConnectionId(): string | undefined;\n    getDuration(): number | undefined;\n    getLastRTCConnectionState(): LastRTCConnectionState | null;\n    getVoiceFilterSpeakingDurationMs(): number | undefined;\n    getPacketStats(): RTCConnectionPacketStats | undefined;\n    getVoiceStateStats(): VoiceStateStats | undefined;\n    // TODO: finish typing\n    getUserVoiceSettingsStats(userId: string): any | undefined;\n    getWasEverMultiParticipant(): boolean;\n    getWasEverRtcConnected(): boolean;\n    getUserIds(): string[] | undefined;\n    getJoinVoiceId(): string | null;\n    isUserConnected(userId: string): boolean | undefined;\n    getSecureFramesState(): SecureFramesState | undefined;\n    getSecureFramesRosterMapEntry(oderId: string): SecureFramesRosterMapEntry | undefined;\n    getLastNonZeroRemoteVideoSinkWantsTime(): number | null;\n    getWasMoved(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ReadStateStore.d.ts",
    "content": "import { Channel, FluxStore } from \"..\";\nimport { ReadStateType } from \"../../enums\";\n\nexport interface GuildChannelUnreadState {\n    mentionCount: number;\n    unread: boolean;\n    isMentionLowImportance: boolean;\n}\n\nexport interface ReadStateSnapshot {\n    unread: boolean;\n    mentionCount: number;\n    guildUnread: boolean | null;\n    guildMentionCount: number | null;\n    takenAt: number;\n}\n\nexport interface SerializedReadState {\n    channelId: string;\n    type: ReadStateType;\n    _guildId: string;\n    _persisted: boolean;\n    _lastMessageId: string;\n    _lastMessageTimestamp: number;\n    _ackMessageId: string;\n    _ackMessageTimestamp: number;\n    ackPinTimestamp: number;\n    lastPinTimestamp: number;\n    _mentionCount: number;\n    flags: number;\n    lastViewed: number;\n}\n\nexport class ReadStateStore extends FluxStore {\n    ackMessageId(channelId: string, type?: ReadStateType): string | null;\n    getAllReadStates(includePrivate?: boolean): SerializedReadState[];\n    getChannelIdsForWindowId(windowId: string): string[];\n    getForDebugging(channelId: string): object | undefined;\n    getGuildChannelUnreadState(\n        channel: Channel,\n        isOptInEnabled: boolean,\n        guildHasActiveThreads: boolean,\n        isChannelMuted: boolean,\n        isGuildHome: boolean\n    ): GuildChannelUnreadState;\n    getGuildUnreadsSentinel(guildId: string): number;\n    getIsMentionLowImportance(channelId: string, type?: ReadStateType): boolean;\n    getMentionChannelIds(): string[];\n    getMentionCount(channelId: string, type?: ReadStateType): number;\n    getNonChannelAckId(type: ReadStateType): string | null;\n    getNotifCenterReadState(channelId: string): object | undefined;\n    getOldestUnreadMessageId(channelId: string, type?: ReadStateType): string | null;\n    getOldestUnreadTimestamp(channelId: string, type?: ReadStateType): number;\n    getReadStatesByChannel(): Record<string, object>;\n    getSnapshot(channelId: string, maxAge: number): ReadStateSnapshot;\n    getTrackedAckMessageId(channelId: string, type?: ReadStateType): string | null;\n    getUnreadCount(channelId: string, type?: ReadStateType): number;\n    hasOpenedThread(channelId: string): boolean;\n    hasRecentlyVisitedAndRead(channelId: string): boolean;\n    hasTrackedUnread(channelId: string): boolean;\n    hasUnread(channelId: string, type?: ReadStateType): boolean;\n    hasUnreadOrMentions(channelId: string, type?: ReadStateType): boolean;\n    hasUnreadPins(channelId: string): boolean;\n    isEstimated(channelId: string, type?: ReadStateType): boolean;\n    isForumPostUnread(channelId: string): boolean;\n    isNewForumThread(threadId: string, parentChannelId: string, guildId: string): boolean;\n    lastMessageId(channelId: string, type?: ReadStateType): string | null;\n    lastMessageTimestamp(channelId: string, type?: ReadStateType): number;\n    lastPinTimestamp(channelId: string): number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/RelationshipStore.d.ts",
    "content": "import { FluxStore } from \"..\";\nimport { RelationshipType } from \"../../enums\";\n\nexport class RelationshipStore extends FluxStore {\n    getBlockedIDs(): string[];\n    getBlockedOrIgnoredIDs(): string[];\n    getFriendCount(): number;\n    getFriendIDs(): string[];\n    getIgnoredIDs(): string[];\n\n    getMutableRelationships(): Map<string, RelationshipType>;\n    getNickname(userId: string): string;\n    getOriginApplicationId(userId: string): string | undefined;\n    getOutgoingCount(): number;\n    getPendingCount(): number;\n    getPendingIgnoredCount(): number;\n    getRelationshipCount(): number;\n\n    /** @returns Enum value from constants.RelationshipTypes */\n    getRelationshipType(userId: string): RelationshipType;\n    getSince(userId: string): string;\n    getSinces(): Record<string, string>;\n    getSpamCount(): number;\n    getVersion(): number;\n\n    isBlocked(userId: string): boolean;\n    isBlockedForMessage(userId: string): boolean;\n\n    /**\n     * @see {@link isBlocked}\n     * @see {@link isIgnored}\n     */\n    isBlockedOrIgnored(userId: string): boolean;\n    isBlockedOrIgnoredForMessage(userId: string): boolean;\n\n    isFriend(userId: string): boolean;\n    isIgnored(userId: string): boolean;\n    isIgnoredForMessage(userId: string): boolean;\n    isSpam(userId: string): boolean;\n    isStranger(userId: string): boolean;\n    isUnfilteredPendingIncoming(userId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/RunningGameStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface RunningGame {\n    id?: string;\n    name: string;\n    exePath: string;\n    cmdLine: string;\n    distributor: string;\n    lastFocused: number;\n    lastLaunched: number;\n    nativeProcessObserverId: number;\n    pid?: number;\n    hidden?: boolean;\n    isLauncher?: boolean;\n    elevated?: boolean;\n    sandboxed?: boolean;\n}\n\nexport interface GameOverlayStatus {\n    enabledLegacy: boolean;\n    enabledOOP: boolean;\n}\n\nexport interface SystemServiceStatus {\n    state: string;\n}\n\nexport class RunningGameStore extends FluxStore {\n    canShowAdminWarning: boolean;\n\n    addExecutableTrackedByAnalytics(exe: string): void;\n    getCandidateGames(): RunningGame[];\n    getCurrentGameForAnalytics(): RunningGame | null;\n    getCurrentNonGameForAnalytics(): RunningGame | null;\n    getGameForName(name: string): RunningGame | null;\n    getGameForPID(pid: number): RunningGame | null;\n    getGameOrTransformedSubgameForPID(pid: number): RunningGame | null;\n    getGameOverlayStatus(game: RunningGame): GameOverlayStatus | null;\n    getGamesSeen(includeHidden?: boolean): RunningGame[];\n    getLauncherForPID(pid: number): RunningGame | null;\n    getObservedAppNameForWindow(windowHandle: number): string | null;\n    getOverlayEnabledForGame(game: RunningGame): boolean;\n    getOverlayOptionsForPID(pid: number): object | null;\n    getOverrideForGame(game: RunningGame): object | null;\n    getOverrides(): object[];\n    getRunningDiscordApplicationIds(): string[];\n    getRunningGames(): RunningGame[];\n    getRunningNonGames(): RunningGame[];\n    getRunningVerifiedApplicationIds(): string[];\n    getSeenGameByName(name: string): RunningGame | null;\n    getSystemServiceStatus(service: string): SystemServiceStatus;\n    getVisibleGame(): RunningGame | null;\n    getVisibleRunningGames(): RunningGame[];\n    isDetectionEnabled(type?: string): boolean;\n    isGamesSeenLoaded(): boolean;\n    isObservedAppRunning(app: string): boolean;\n    isSystemServiceInitialized(service: string): boolean;\n    shouldContinueWithoutElevatedProcessForPID(pid: number): boolean;\n    shouldElevateProcessForPID(pid: number): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/SelectedChannelStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface ChannelFollowingDestination {\n    guildId?: string;\n    channelId?: string;\n}\n\nexport class SelectedChannelStore extends FluxStore {\n    getChannelId(guildId?: string | null): string;\n    getVoiceChannelId(): string | undefined;\n    getCurrentlySelectedChannelId(guildId?: string): string | undefined;\n    getMostRecentSelectedTextChannelId(guildId: string): string | undefined;\n    getLastSelectedChannelId(guildId?: string): string;\n    getLastSelectedChannels(guildId?: string): string;\n    getLastChannelFollowingDestination(): ChannelFollowingDestination | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/SelectedGuildStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface SelectedGuildState {\n    selectedGuildTimestampMillis: Record<string | number, number>;\n    selectedGuildId: string | null;\n    lastSelectedGuildId: string | null;\n}\n\nexport class SelectedGuildStore extends FluxStore {\n    getGuildId(): string | null;\n    getLastSelectedGuildId(): string | null;\n    getLastSelectedTimestamp(guildId: string): number | null;\n    getState(): SelectedGuildState | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/SoundboardStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface SoundboardSound {\n    soundId: string;\n    name: string;\n    volume: number;\n    emojiId: string | null;\n    emojiName: string | null;\n    available: boolean;\n    guildId: string;\n    userId?: string;\n}\n\nexport interface TopSoundForGuild {\n    soundId: string;\n    rank: number;\n}\n\nexport interface SoundboardOverlayState {\n    soundboardSounds: Record<string, SoundboardSound[]>;\n    favoritedSoundIds: string[];\n    localSoundboardMutes: string[];\n}\n\nexport class SoundboardStore extends FluxStore {\n    getOverlaySerializedState(): SoundboardOverlayState;\n    getSounds(): Map<string, SoundboardSound[]>;\n    getSoundsForGuild(guildId: string): SoundboardSound[] | null;\n    getSound(guildId: string, soundId: string): SoundboardSound;\n    getSoundById(soundId: string): SoundboardSound;\n    isFetchingSounds(): boolean;\n    isFetchingDefaultSounds(): boolean;\n    isFetching(): boolean;\n    shouldFetchDefaultSounds(): boolean;\n    hasFetchedDefaultSounds(): boolean;\n    isUserPlayingSounds(userId: string): boolean;\n    isPlayingSound(soundId: string): boolean;\n    isFavoriteSound(soundId: string): boolean;\n    getFavorites(): Set<string>;\n    getAllTopSoundsForGuilds(): Map<string, TopSoundForGuild[]>;\n    isLocalSoundboardMuted(userId: string): boolean;\n    hasHadOtherUserPlaySoundInSession(): boolean;\n    shouldFetchTopSoundsForGuilds(): boolean;\n    hasFetchedTopSoundsForGuilds(): boolean;\n    hasFetchedAllSounds(): boolean;\n    isFetchingAnySounds(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/SpellCheckStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport class SpellCheckStore extends FluxStore {\n    hasLearnedWord(word: string): boolean;\n    isEnabled(): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/SpotifyStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface SpotifyDevice {\n    id: string;\n    is_active: boolean;\n    is_private_session: boolean;\n    is_restricted: boolean;\n    name: string;\n    supports_volume: boolean;\n    type: string;\n    volume_percent: number;\n}\n\nexport interface SpotifySocket {\n    accessToken: string;\n    accountId: string;\n    connectionId: string;\n    isPremium: boolean;\n    socket: WebSocket;\n}\n\nexport interface SpotifySocketAndDevice {\n    socket: SpotifySocket;\n    device: SpotifyDevice;\n}\n\nexport interface SpotifyArtist {\n    id: string;\n    name: string;\n}\n\nexport interface SpotifyImage {\n    url: string;\n    height: number;\n    width: number;\n}\n\nexport interface SpotifyAlbum {\n    id: string;\n    name: string;\n    type: string;\n    image: SpotifyImage | null;\n}\n\nexport interface SpotifyTrack {\n    id: string;\n    name: string;\n    duration: number;\n    isLocal: boolean;\n    type: string;\n    album: SpotifyAlbum;\n    artists: SpotifyArtist[];\n}\n\nexport interface SpotifyPlayerState {\n    track: SpotifyTrack;\n    startTime: number;\n    context: { uri: string } | null;\n}\n\nexport interface SpotifyActivity {\n    name: string;\n    assets: {\n        large_image?: string;\n        large_text?: string;\n    };\n    details: string;\n    state: string | undefined;\n    timestamps: {\n        start: number;\n        end: number;\n    };\n    party: {\n        id: string;\n    };\n    sync_id?: string;\n    flags?: number;\n    metadata?: {\n        context_uri: string | undefined;\n        album_id: string;\n        artist_ids: string[];\n        type: string;\n        button_urls: string[];\n    };\n}\n\nexport interface SpotifySyncingWith {\n    oderId: string;\n    partyId: string;\n    sessionId: string;\n    userId: string;\n}\n\nexport class SpotifyStore extends FluxStore {\n    hasConnectedAccount(): boolean;\n    getActiveSocketAndDevice(): SpotifySocketAndDevice | null;\n    getPlayableComputerDevices(): SpotifySocketAndDevice[];\n    canPlay(deviceId: string): boolean;\n    getSyncingWith(): SpotifySyncingWith | undefined;\n    wasAutoPaused(): boolean;\n    getLastPlayedTrackId(): string | undefined;\n    getTrack(): SpotifyTrack | null;\n    getPlayerState(accountId: string): SpotifyPlayerState | null;\n    shouldShowActivity(): boolean;\n    getActivity(): SpotifyActivity | null;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/StickersStore.d.ts",
    "content": "import { FluxStore, GuildSticker, PremiumStickerPack, Sticker } from \"..\";\n\nexport type StickerGuildMap = Map<string, GuildSticker[]>;\nexport type StickerPackMap = Map<string, Sticker[]>;\n\nexport class StickersStore extends FluxStore {\n    hasLoadedStickerPacks: boolean;\n    isFetchingStickerPacks: boolean;\n    isLoaded: boolean;\n    loadState: number;\n\n    getAllGuildStickers(): StickerGuildMap;\n    getAllPackStickers(): StickerPackMap;\n    getPremiumPacks(): PremiumStickerPack[];\n    getRawStickersByGuild(): StickerGuildMap;\n    getStickerById(id: string): Sticker | undefined;\n    // TODO: type\n    getStickerMetadataArrays(): any[];\n    getStickerPack(id: string): PremiumStickerPack | undefined;\n    getStickersByGuildId(guildId: string): Sticker[] | undefined;\n    isPremiumPack(id: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/StreamerModeStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface StreamerModeSettings {\n    enabled: boolean;\n    autoToggle: boolean;\n    hideInstantInvites: boolean;\n    hidePersonalInformation: boolean;\n    disableSounds: boolean;\n    disableNotifications: boolean;\n    enableContentProtection: boolean;\n}\n\nexport class StreamerModeStore extends FluxStore {\n    get autoToggle(): boolean;\n    get disableNotifications(): boolean;\n    get disableSounds(): boolean;\n    get enableContentProtection(): boolean;\n    get enabled(): boolean;\n    get hideInstantInvites(): boolean;\n    get hidePersonalInformation(): boolean;\n\n    getSettings(): StreamerModeSettings;\n    getState(): Record<string, StreamerModeSettings>;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/ThemeStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport type ThemePreference = \"dark\" | \"light\" | \"unknown\";\nexport type SystemTheme = \"dark\" | \"light\";\nexport type Theme = \"light\" | \"dark\" | \"darker\" | \"midnight\";\n\nexport interface ThemeState {\n    theme: Theme;\n    /** 0 = not loaded, 1 = loaded */\n    status: 0 | 1;\n    preferences: Record<ThemePreference, Theme>;\n}\nexport class ThemeStore extends FluxStore {\n    get systemTheme(): SystemTheme;\n    get theme(): Theme;\n\n    getState(): ThemeState;\n    themePreferenceForSystemTheme(preference: ThemePreference): Theme;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/TypingStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport class TypingStore extends FluxStore {\n    /**\n     * returns a map of user ids to timeout ids\n     */\n    getTypingUsers(channelId: string): Record<string, number>;\n    isTyping(channelId: string, userId: string): boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/UploadAttachmentStore.d.ts",
    "content": "import { CloudUpload, FluxStore } from \"..\";\nimport { DraftType } from \"../../enums\";\n\nexport class UploadAttachmentStore extends FluxStore {\n    getFirstUpload(channelId: string, draftType: DraftType): CloudUpload | null;\n    hasAdditionalUploads(channelId: string, draftType: DraftType): boolean;\n    getUploads(channelId: string, draftType: DraftType): CloudUpload[];\n    getUploadCount(channelId: string, draftType: DraftType): number;\n    getUpload(channelId: string, uploadId: string, draftType: DraftType): CloudUpload;\n    findUpload(channelId: string, draftType: DraftType, predicate: (upload: CloudUpload) => boolean): CloudUpload | undefined;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/UserGuildSettingsStore.d.ts",
    "content": "import { Channel, FluxStore } from \"..\";\n\nexport interface MuteConfig {\n    selected_time_window: number;\n    end_time: string | null;\n}\n\nexport interface ChannelOverride {\n    muted: boolean;\n    mute_config: MuteConfig | null;\n    message_notifications: number;\n    flags: number;\n    collapsed: boolean;\n    channel_id: string;\n}\n\nexport interface GuildSettings {\n    suppress_everyone: boolean;\n    suppress_roles: boolean;\n    mute_scheduled_events: boolean;\n    mobile_push: boolean;\n    muted: boolean;\n    message_notifications: number;\n    flags: number;\n    channel_overrides: Record<string, ChannelOverride>;\n    notify_highlights: number;\n    hide_muted_channels: boolean;\n    version: number;\n    mute_config: MuteConfig | null;\n    guild_id: string;\n}\n\nexport interface AccountNotificationSettings {\n    flags: number;\n}\n\nexport interface UserGuildSettingsState {\n    useNewNotifications: boolean;\n}\n\nexport class UserGuildSettingsStore extends FluxStore {\n    get accountNotificationSettings(): AccountNotificationSettings;\n    get mentionOnAllMessages(): boolean;\n    get useNewNotifications(): boolean;\n\n    allowAllMessages(guildId: string): boolean;\n    allowNoMessages(guildId: string): boolean;\n    getAddedToMessages(): string[];\n    // TODO: finish typing\n    getAllSettings(): { userGuildSettings: Record<string, GuildSettings>; };\n    getChannelFlags(channel: Channel): number;\n    getChannelIdFlags(guildId: string, channelId: string): number;\n    getChannelMessageNotifications(guildId: string, channelId: string): number | null;\n    getChannelMuteConfig(guildId: string, channelId: string): MuteConfig | null;\n    getChannelOverrides(guildId: string): Record<string, ChannelOverride>;\n    getChannelRecordUnreadSetting(channel: Channel): number;\n    getChannelUnreadSetting(guildId: string, channelId: string): number;\n    getGuildFavorites(guildId: string): string[];\n    getGuildFlags(guildId: string): number;\n    getGuildUnreadSetting(guildId: string): number;\n    getMessageNotifications(guildId: string): number;\n    getMuteConfig(guildId: string): MuteConfig | null;\n    getMutedChannels(guildId: string): string[];\n    getNewForumThreadsCreated(guildId: string): boolean;\n    getNotifyHighlights(guildId: string): number;\n    getOptedInChannels(guildId: string): string[];\n    // TODO: finish typing these\n    getOptedInChannelsWithPendingUpdates(guildId: string): Record<string, any>;\n    getPendingChannelUpdates(guildId: string): Record<string, any>;\n    getState(): UserGuildSettingsState;\n    isAddedToMessages(channelId: string): boolean;\n    isCategoryMuted(guildId: string, channelId: string): boolean;\n    isChannelMuted(guildId: string, channelId: string): boolean;\n    isChannelOptedIn(guildId: string, channelId: string, usePending?: boolean): boolean;\n    isChannelOrParentOptedIn(guildId: string, channelId: string, usePending?: boolean): boolean;\n    isChannelRecordOrParentOptedIn(channel: Channel, usePending?: boolean): boolean;\n    isFavorite(guildId: string, channelId: string): boolean;\n    isGuildCollapsed(guildId: string): boolean;\n    isGuildOrCategoryOrChannelMuted(guildId: string, channelId: string): boolean;\n    isMessagesFavorite(guildId: string): boolean;\n    isMobilePushEnabled(guildId: string): boolean;\n    isMuteScheduledEventsEnabled(guildId: string): boolean;\n    isMuted(guildId: string): boolean;\n    isOptInEnabled(guildId: string): boolean;\n    isSuppressEveryoneEnabled(guildId: string): boolean;\n    isSuppressRolesEnabled(guildId: string): boolean;\n    isTemporarilyMuted(guildId: string): boolean;\n    resolveGuildUnreadSetting(guildId: string): number;\n    resolveUnreadSetting(channel: Channel): number;\n    resolvedMessageNotifications(guildId: string): number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/UserProfileStore.d.ts",
    "content": "import { FluxStore, Guild, User, Application, ApplicationInstallParams } from \"..\";\nimport { ApplicationIntegrationType } from \"../../enums\";\n\nexport interface MutualFriend {\n    /**\n     * the userid of the mutual friend\n     */\n    key: string;\n    /**\n     * the status of the mutual friend\n     */\n    status: \"online\" | \"offline\" | \"idle\" | \"dnd\";\n    /**\n     * the user object of the mutual friend\n     */\n    user: User;\n}\n\nexport interface MutualGuild {\n    /**\n     * the guild object of the mutual guild\n     */\n    guild: Guild;\n    /**\n     * the user's nickname in the guild, if any\n     */\n    nick: string | null;\n\n}\n\nexport interface ProfileBadge {\n    id: string;\n    description: string;\n    icon: string;\n    link?: string;\n}\n\nexport interface ConnectedAccount {\n    type: \"twitch\" | \"youtube\" | \"skype\" | \"steam\" | \"leagueoflegends\" | \"battlenet\" | \"bluesky\" | \"bungie\" | \"reddit\" | \"twitter\" | \"twitter_legacy\" | \"spotify\" | \"facebook\" | \"xbox\" | \"samsung\" | \"contacts\" | \"instagram\" | \"mastodon\" | \"soundcloud\" | \"github\" | \"playstation\" | \"playstation-stg\" | \"epicgames\" | \"riotgames\" | \"roblox\" | \"paypal\" | \"ebay\" | \"tiktok\" | \"crunchyroll\" | \"domain\" | \"amazon-music\";\n    /**\n     * underlying id of connected account\n     * eg. account uuid\n     */\n    id: string;\n    /**\n     * display name of connected account\n     */\n    name: string;\n    verified: boolean;\n    metadata?: Record<string, string>;\n}\n\nexport interface ProfileApplication {\n    id: string;\n    customInstallUrl: string | undefined;\n    installParams: ApplicationInstallParams | undefined;\n    flags: number;\n    popularApplicationCommandIds?: string[];\n    integrationTypesConfig: Record<ApplicationIntegrationType, Partial<{\n        oauth2_install_params: ApplicationInstallParams;\n    }>>;\n    primarySkuId: string | undefined;\n    storefront_available: boolean;\n}\n\nexport interface UserProfileBase extends Pick<User, \"banner\"> {\n    accentColor: number | null;\n    /**\n     * often empty for guild profiles, get the user profile for badges\n     */\n    badges: ProfileBadge[];\n    bio: string | undefined;\n    popoutAnimationParticleType: string | null;\n    profileEffectExpiresAt: number | Date | undefined;\n    profileEffectId: undefined | string;\n    /**\n     * often an empty string when not set\n     */\n    pronouns: string | \"\" | undefined;\n    themeColors: [number, number] | undefined;\n    userId: string;\n}\n\nexport interface ApplicationRoleConnection {\n    application: Application;\n    application_metadata: Record<string, any>;\n    metadata: Record<string, any>;\n    platform_name: string;\n    platform_username: string;\n}\n\nexport interface UserProfile extends UserProfileBase, Pick<User, \"premiumType\"> {\n    /** If this is a bot user profile, this will be its application */\n    application: ProfileApplication | null;\n    applicationRoleConnections: ApplicationRoleConnection[] | undefined;\n    connectedAccounts: ConnectedAccount[] | undefined;\n    fetchStartedAt: number;\n    fetchEndedAt: number;\n    legacyUsername: string | undefined;\n    premiumGuildSince: Date | null;\n    premiumSince: Date | null;\n}\n\nexport interface ApplicationWidgetConfig {\n    applicationId: string;\n    widgetType: number;\n}\n\nexport interface WishlistSettings {\n    privacy: number;\n}\n\nexport class UserProfileStore extends FluxStore {\n    get applicationWidgetApplicationConfigs(): Record<string, ApplicationWidgetConfig>;\n    get isSubmitting(): boolean;\n\n    getApplicationWidgetApplicationConfig(applicationId: string): ApplicationWidgetConfig | undefined;\n    getFirstWishlistId(userId: string): string | null;\n    getGuildMemberProfile(userId: string, guildId: string | undefined): UserProfileBase | null;\n    /**\n     * Get the mutual friends of a user.\n     *\n     * @param userId the user ID of the user to get the mutual friends of.\n     *\n     * @returns an array of mutual friends, or undefined if the user has no mutual friends\n     */\n    getMutualFriends(userId: string): MutualFriend[] | undefined;\n    /**\n     * Get the count of mutual friends for a user.\n     *\n     * @param userId the user ID of the user to get the mutual friends count of.\n     *\n     * @returns the count of mutual friends, or undefined if the user has no mutual friends\n     */\n    getMutualFriendsCount(userId: string): number | undefined;\n    /**\n     * Get the mutual guilds of a user.\n     *\n     * @param userId the user ID of the user to get the mutual guilds of.\n     *\n     * @returns an array of mutual guilds, or undefined if the user has no mutual guilds\n     */\n    getMutualGuilds(userId: string): MutualGuild[] | undefined;\n    getUserProfile(userId: string): UserProfile | undefined;\n    // TODO: finish typing\n    getWidgets(userId: string): any[] | undefined;\n    getWishlistIds(userId: string): string[];\n    getWishlistSettings(userId: string): WishlistSettings | null;\n    /**\n     * Check if mutual friends for {@link userId} are currently being fetched.\n     *\n     * @param userId the user ID of the mutual friends being fetched.\n     *\n     * @returns true if mutual friends are being fetched, false otherwise.\n     */\n    isFetchingFriends(userId: string): boolean;\n    /**\n     * @param userId the user ID of the profile being fetched.\n     * @param guildId the guild ID to of the profile being fetched.\n     * defaults to the internal symbol `NO GUILD ID` if nullish\n     *\n     * @returns true if the profile is being fetched, false otherwise.\n     */\n    isFetchingProfile(userId: string, guildId?: string): boolean;\n    takeSnapshot(): Record<string, UserProfile>;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/UserSettingsProtoStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface GuildFolder {\n    guildIds: string[];\n    folderId?: number;\n    folderName?: string;\n    folderColor?: number;\n}\n\nexport interface GuildProto {\n    // TODO: finish typing\n    channels: Record<string, any>;\n    hubProgress: number;\n    guildOnboardingProgress: number;\n    dismissedGuildContent: Record<string, number>;\n    disableRaidAlertPush: boolean;\n    disableRaidAlertNag: boolean;\n    leaderboardsDisabled: boolean;\n    // TODO: finish typing\n    guildDismissibleContentStates: Record<string, any>;\n}\n\nexport interface UserSettingsVersions {\n    clientVersion: number;\n    serverVersion: number;\n    dataVersion: number;\n}\n\nexport interface InboxSettings {\n    currentTab: number;\n    viewedTutorial: boolean;\n}\n\nexport interface GuildsSettings {\n    guilds: Record<string, GuildProto>;\n}\n\nexport interface UserContentSettings {\n    dismissedContents: string;\n    lastReceivedChangelogId: string;\n    // TODO: finish typing\n    recurringDismissibleContentStates: Record<string, any>;\n    // TODO: type\n    lastDismissedOutboundPromotionStartDate: any;\n    premiumTier0ModalDismissedAt: any;\n}\n\nexport interface VoiceAndVideoSettings {\n    // TODO: type\n    videoBackgroundFilterDesktop: any;\n    alwaysPreviewVideo: boolean;\n    afkTimeout: number;\n    streamNotificationsEnabled: boolean;\n    nativePhoneIntegrationEnabled: boolean;\n    disableStreamPreviews: boolean;\n    soundmojiVolume: number;\n}\n\nexport interface TextAndImagesSettings {\n    emojiPickerCollapsedSections: string[];\n    stickerPickerCollapsedSections: string[];\n    soundboardPickerCollapsedSections: string[];\n    dmSpamFilterV2: number;\n    viewImageDescriptions: boolean;\n    inlineAttachmentMedia: boolean;\n    inlineEmbedMedia: boolean;\n    gifAutoPlay: boolean;\n    renderEmbeds: boolean;\n    renderReactions: boolean;\n    animateEmoji: boolean;\n    animateStickers: number;\n    enableTtsCommand: boolean;\n    messageDisplayCompact: boolean;\n    explicitContentFilter: number;\n    viewNsfwGuilds: boolean;\n    convertEmoticons: boolean;\n    viewNsfwCommands: boolean;\n    includeStickersInAutocomplete: boolean;\n    // TODO: type these\n    explicitContentSettings: any;\n    goreContentSettings: any;\n    showMentionSuggestions: boolean;\n}\n\nexport interface NotificationsSettings {\n    notificationCenterAckedBeforeId: string;\n    focusModeExpiresAtMs: string;\n    reactionNotifications: number;\n    gameActivityNotifications: boolean;\n    customStatusPushNotifications: boolean;\n    showInAppNotifications: boolean;\n    notifyFriendsOnGoLive: boolean;\n    enableVoiceActivityNotifications: boolean;\n    enableUserResurrectionNotifications: boolean;\n}\n\nexport interface PrivacySettings {\n    restrictedGuildIds: string[];\n    defaultGuildsRestricted: boolean;\n    allowAccessibilityDetection: boolean;\n    activityRestrictedGuildIds: string[];\n    defaultGuildsActivityRestricted: boolean;\n    activityJoiningRestrictedGuildIds: string[];\n    messageRequestRestrictedGuildIds: string[];\n    guildsLeaderboardOptOutDefault: boolean;\n    slayerSdkReceiveDmsInGame: boolean;\n    defaultGuildsActivityRestrictedV2: boolean;\n    detectPlatformAccounts: boolean;\n    passwordless: boolean;\n    contactSyncEnabled: boolean;\n    friendSourceFlags: number;\n    friendDiscoveryFlags: number;\n    dropsOptedOut: boolean;\n    hideLegacyUsername: boolean;\n    defaultGuildsRestrictedV2: boolean;\n    quests3PDataOptedOut: boolean;\n}\n\nexport interface GameLibrarySettings {\n    disableGamesTab: boolean;\n}\n\nexport interface StatusSettings {\n    statusExpiresAtMs: string;\n    status: { status: string; } | null;\n    showCurrentGame: boolean;\n    statusCreatedAtMs: string;\n}\n\nexport interface LocalizationSettings {\n    locale: { localeCode: string; } | null;\n    timezoneOffset: { offset: number; } | null;\n}\n\nexport interface AppearanceSettings {\n    theme: number;\n    developerMode: boolean;\n    mobileRedesignDisabled: boolean;\n    timestampHourCycle: number;\n    launchPadMode: number;\n    uiDensity: number;\n    swipeRightToLeftMode: number;\n    // TODO: type\n    clientThemeSettings: any;\n}\n\nexport interface GuildFoldersSettings {\n    folders: GuildFolder[];\n    guildPositions: string[];\n}\n\nexport interface AudioContextSettings {\n    // TODO: finish these\n    user: Record<string, any>;\n    stream: Record<string, any>;\n}\n\nexport interface ClipsSettings {\n    allowVoiceRecording: boolean;\n}\n\nexport interface InAppFeedbackSettings {\n    // TODO: finish typing\n    inAppFeedbackStates: Record<string, any>;\n}\n\nexport interface UserSettings {\n    versions: UserSettingsVersions;\n    inbox: InboxSettings;\n    guilds: GuildsSettings;\n    userContent: UserContentSettings;\n    voiceAndVideo: VoiceAndVideoSettings;\n    textAndImages: TextAndImagesSettings;\n    notifications: NotificationsSettings;\n    privacy: PrivacySettings;\n    // TODO: finish typing\n    debug: Record<string, any>;\n    gameLibrary: GameLibrarySettings;\n    status: StatusSettings;\n    localization: LocalizationSettings;\n    appearance: AppearanceSettings;\n    guildFolders: GuildFoldersSettings;\n    audioContextSettings: AudioContextSettings;\n    clips: ClipsSettings;\n    inAppFeedbackSettings: InAppFeedbackSettings;\n}\n\nexport interface FrecencySettings {\n    // TODO: type all of these\n    versions: any;\n    favoriteGifs: any;\n    favoriteStickers: any;\n    stickerFrecency: any;\n    favoriteEmojis: any;\n    emojiFrecency: any;\n    applicationCommandFrecency: any;\n    favoriteSoundboardSounds: any;\n    applicationFrecency: any;\n    playedSoundFrecency: any;\n    guildAndChannelFrecency: any;\n    emojiReactionFrecency: any;\n}\n\nexport interface ProtoState {\n    // TODO: type\n    proto: any;\n}\n\nexport class UserSettingsProtoStore extends FluxStore {\n    settings: UserSettings;\n    frecencyWithoutFetchingLatest: FrecencySettings;\n    wasMostRecentUpdateFromServer: boolean;\n    getState(): Record<string, ProtoState>;\n    computeState(): Record<string, ProtoState>;\n    getFullState(): Record<string, ProtoState>;\n    hasLoaded(settingsType: number): boolean;\n    getGuildFolders(): GuildFolder[];\n    getGuildRecentsDismissedAt(guildId: string): number;\n    getDismissedGuildContent(guildId: string): Record<string, number> | null;\n    // TODO: finish typing\n    getGuildDismissedContentState(guildId: string): any;\n    getGuildsProto(): Record<string, GuildProto>;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/UserStore.d.ts",
    "content": "import { FluxStore, User } from \"..\";\n\n/** returned by takeSnapshot for persistence */\nexport interface UserStoreSnapshot {\n    /** snapshot format version, currently 1 */\n    version: number;\n    data: {\n        /** contains only the current user */\n        users: User[];\n    };\n}\n\nexport class UserStore extends FluxStore {\n    /**\n     * filters users and optionally sorts results.\n     * @param sort if true (default false), sorts alphabetically by username\n     */\n    filter(filter: (user: User) => boolean, sort?: boolean): User[];\n    /**\n     * finds user by username and discriminator.\n     * for new username system (unique usernames), pass null/undefined as discriminator.\n     */\n    findByTag(username: string, discriminator?: string | null): User | undefined;\n    /** @param action return false to break iteration early */\n    forEach(action: (user: User) => boolean | void): void;\n    getCurrentUser(): User;\n    getUser(userId: string): User;\n    /** keyed by user ID */\n    getUsers(): Record<string, User>;\n    /** increments when users are added/updated/removed */\n    getUserStoreVersion(): number;\n    /** only includes current user, used for persistence */\n    takeSnapshot(): UserStoreSnapshot;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/VoiceStateStore.d.ts",
    "content": "import { DiscordRecord } from \"../common\";\nimport { FluxStore } from \"./FluxStore\";\n\nexport type UserVoiceStateRecords = Record<string, VoiceState>;\nexport type VoiceStates = Record<string, UserVoiceStateRecords>;\n\nexport interface VoiceState extends DiscordRecord {\n    userId: string;\n    channelId: string | null | undefined;\n    sessionId: string | null | undefined;\n    mute: boolean;\n    deaf: boolean;\n    selfMute: boolean;\n    selfDeaf: boolean;\n    selfVideo: boolean;\n    selfStream: boolean | undefined;\n    suppress: boolean;\n    requestToSpeakTimestamp: string | null | undefined;\n    discoverable: boolean;\n\n    isVoiceMuted(): boolean;\n    isVoiceDeafened(): boolean;\n}\n\nexport class VoiceStateStore extends FluxStore {\n    getAllVoiceStates(): VoiceStates;\n    getVoiceStateVersion(): number;\n\n    getVoiceStates(guildId?: string | null): UserVoiceStateRecords;\n    getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;\n    getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;\n\n    getVoiceState(guildId: string | null, userId: string): VoiceState | undefined;\n    getDiscoverableVoiceState(guildId: string | null, userId: string): VoiceState | null;\n    getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined;\n    getVoiceStateForUser(userId: string): VoiceState | undefined;\n    getDiscoverableVoiceStateForUser(userId: string): VoiceState | undefined;\n    getVoiceStateForSession(userId: string, sessionId?: string | null): VoiceState | null | undefined;\n\n    getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined;\n    getCurrentClientVoiceChannelId(guildId: string | null): string | undefined;\n\n    getUsersWithVideo(channelId: string): Set<string>;\n    getVoicePlatformForChannel(channelId: string, guildId: string): string | undefined;\n\n    isCurrentClientInVoiceChannel(): boolean;\n    isInChannel(channelId: string, userId?: string): boolean;\n    hasVideo(channelId: string): boolean;\n\n    get userHasBeenMovedVersion(): number;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/WindowStore.d.ts",
    "content": "import { FluxStore } from \"..\";\n\nexport interface WindowSize {\n    width: number;\n    height: number;\n}\n\nexport class WindowStore extends FluxStore {\n    /** returns focused window ID, or null if no window is focused */\n    getFocusedWindowId(): string | null;\n    getLastFocusedWindowId(): string;\n    /** true if any window is focused (getFocusedWindowId() !== null) */\n    isAppFocused(): boolean;\n    /** @param windowId defaults to current window */\n    isElementFullScreen(windowId?: string): boolean;\n    /** @param windowId defaults to current window */\n    isFocused(windowId?: string): boolean;\n    /** @param windowId defaults to current window */\n    isVisible(windowId?: string): boolean;\n    /** @param windowId defaults to current window, returns {width: 0, height: 0} for invalid ID */\n    windowSize(windowId?: string): WindowSize;\n}\n"
  },
  {
    "path": "packages/discord-types/src/stores/index.d.ts",
    "content": "// please keep in alphabetical order\nexport * from \"./AccessibilityStore\";\nexport * from \"./ActiveJoinedThreadsStore\";\nexport * from \"./ApplicationStore\";\nexport * from \"./AuthenticationStore\";\nexport * from \"./CallStore\";\nexport * from \"./ChannelRTCStore\";\nexport * from \"./ChannelStore\";\nexport * from \"./DraftStore\";\nexport * from \"./EmojiStore\";\nexport * from \"./FluxStore\";\nexport * from \"./FriendsStore\";\nexport * from \"./GuildChannelStore\";\nexport * from \"./GuildMemberCountStore\";\nexport * from \"./GuildMemberStore\";\nexport * from \"./GuildRoleStore\";\nexport * from \"./GuildScheduledEventStore\";\nexport * from \"./GuildStore\";\nexport * from \"./InstantInviteStore\";\nexport * from \"./InviteStore\";\nexport * from \"./LocaleStore\";\nexport * from \"./MediaEngineStore\";\nexport * from \"./MessageStore\";\nexport * from \"./NotificationSettingsStore\";\nexport * from \"./OverridePremiumTypeStore\";\nexport * from \"./PendingReplyStore\";\nexport * from \"./PermissionStore\";\nexport * from \"./PopoutWindowStore\";\nexport * from \"./PresenceStore\";\nexport * from \"./ReadStateStore\";\nexport * from \"./RelationshipStore\";\nexport * from \"./RTCConnectionStore\";\nexport * from \"./RunningGameStore\";\nexport * from \"./SelectedChannelStore\";\nexport * from \"./SelectedGuildStore\";\nexport * from \"./SoundboardStore\";\nexport * from \"./SpellCheckStore\";\nexport * from \"./SpotifyStore\";\nexport * from \"./StickersStore\";\nexport * from \"./StreamerModeStore\";\nexport * from \"./ThemeStore\";\nexport * from \"./TypingStore\";\nexport * from \"./UploadAttachmentStore\";\nexport * from \"./UserGuildSettingsStore\";\nexport * from \"./UserProfileStore\";\nexport * from \"./UserSettingsProtoStore\";\nexport * from \"./UserStore\";\nexport * from \"./VoiceStateStore\";\nexport * from \"./WindowStore\";\n\n/**\n * React hook that returns stateful data for one or more stores\n * You might need a custom comparator (4th argument) if your store data is an object\n * @param stores The stores to listen to\n * @param mapper A function that returns the data you need\n * @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook\n * @param isEqual A custom comparator for the data returned by mapper\n *\n * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);\n */\nexport type useStateFromStores = <T>(\n    stores: any[],\n    mapper: () => T,\n    dependencies?: any,\n    isEqual?: (old: T, newer: T) => boolean\n) => T;\n"
  },
  {
    "path": "packages/discord-types/src/utils.d.ts",
    "content": "import { Channel, Guild, GuildMember, Message, User } from \".\";\nimport type { ReactNode } from \"react\";\nimport { LiteralUnion } from \"type-fest\";\n\nimport type { FluxEvents } from \"./fluxEvents\";\n\nexport { FluxEvents };\n\ntype FluxEventsAutoComplete = LiteralUnion<FluxEvents, string>;\n\nexport interface FluxDispatcher {\n    _actionHandlers: any;\n    _subscriptions: any;\n    dispatch(event: { [key: string]: unknown; type: FluxEventsAutoComplete; }): Promise<void>;\n    isDispatching(): boolean;\n    subscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;\n    unsubscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;\n    wait(callback: () => void): void;\n}\n\nexport type Parser = Record<\n    | \"parse\"\n    | \"parseTopic\"\n    | \"parseEmbedTitle\"\n    | \"parseInlineReply\"\n    | \"parseGuildVerificationFormRule\"\n    | \"parseGuildEventDescription\"\n    | \"parseAutoModerationSystemMessage\"\n    | \"parseForumPostGuidelines\"\n    | \"parseForumPostMostRecentMessage\",\n    (content: string, inline?: boolean, state?: Record<string, any>) => ReactNode[]\n> & Record<\"defaultRules\" | \"guildEventRules\", Record<string, Record<\"react\" | \"html\" | \"parse\" | \"match\" | \"order\", any>>>;\n\nexport interface Alerts {\n    show(alert: {\n        title: any;\n        body: React.ReactNode;\n        className?: string;\n        confirmColor?: string;\n        cancelText?: string;\n        confirmText?: string;\n        secondaryConfirmText?: string;\n        onCancel?(): void;\n        onConfirm?(): void;\n        onConfirmSecondary?(): void;\n        onCloseCallback?(): void;\n    }): void;\n    /** This is a noop, it does nothing. */\n    close(): void;\n}\n\nexport interface SnowflakeUtils {\n    fromTimestamp(timestamp: number): string;\n    extractTimestamp(snowflake: string): number;\n    age(snowflake: string): number;\n    atPreviousMillisecond(snowflake: string): string;\n    compare(snowflake1?: string, snowflake2?: string): number;\n}\n\ninterface RestRequestData {\n    url: string;\n    query?: Record<string, any>;\n    body?: Record<string, any>;\n    oldFormErrors?: boolean;\n    retries?: number;\n}\n\nexport type RestAPI = Record<\"del\" | \"get\" | \"patch\" | \"post\" | \"put\", (data: RestRequestData) => Promise<any>>;\n\nexport type Permissions = \"CREATE_INSTANT_INVITE\"\n    | \"KICK_MEMBERS\"\n    | \"BAN_MEMBERS\"\n    | \"ADMINISTRATOR\"\n    | \"MANAGE_CHANNELS\"\n    | \"MANAGE_GUILD\"\n    | \"CHANGE_NICKNAME\"\n    | \"MANAGE_NICKNAMES\"\n    | \"MANAGE_ROLES\"\n    | \"MANAGE_WEBHOOKS\"\n    | \"MANAGE_GUILD_EXPRESSIONS\"\n    | \"CREATE_GUILD_EXPRESSIONS\"\n    | \"VIEW_AUDIT_LOG\"\n    | \"VIEW_CHANNEL\"\n    | \"VIEW_GUILD_ANALYTICS\"\n    | \"VIEW_CREATOR_MONETIZATION_ANALYTICS\"\n    | \"MODERATE_MEMBERS\"\n    | \"SEND_MESSAGES\"\n    | \"SEND_TTS_MESSAGES\"\n    | \"MANAGE_MESSAGES\"\n    | \"EMBED_LINKS\"\n    | \"ATTACH_FILES\"\n    | \"READ_MESSAGE_HISTORY\"\n    | \"MENTION_EVERYONE\"\n    | \"USE_EXTERNAL_EMOJIS\"\n    | \"ADD_REACTIONS\"\n    | \"USE_APPLICATION_COMMANDS\"\n    | \"MANAGE_THREADS\"\n    | \"CREATE_PUBLIC_THREADS\"\n    | \"CREATE_PRIVATE_THREADS\"\n    | \"USE_EXTERNAL_STICKERS\"\n    | \"SEND_MESSAGES_IN_THREADS\"\n    | \"SEND_VOICE_MESSAGES\"\n    | \"CONNECT\"\n    | \"SPEAK\"\n    | \"MUTE_MEMBERS\"\n    | \"DEAFEN_MEMBERS\"\n    | \"MOVE_MEMBERS\"\n    | \"USE_VAD\"\n    | \"PRIORITY_SPEAKER\"\n    | \"STREAM\"\n    | \"USE_EMBEDDED_ACTIVITIES\"\n    | \"USE_SOUNDBOARD\"\n    | \"USE_EXTERNAL_SOUNDS\"\n    | \"REQUEST_TO_SPEAK\"\n    | \"MANAGE_EVENTS\"\n    | \"CREATE_EVENTS\";\n\nexport type PermissionsBits = Record<Permissions, bigint>;\n\nexport interface MessageSnapshot {\n    message: Message;\n}\n\nexport interface Locale {\n    name: string;\n    value: string;\n    localizedName: string;\n}\n\nexport interface LocaleInfo {\n    code: string;\n    enabled: boolean;\n    name: string;\n    englishName: string;\n    postgresLang: string;\n}\n\nexport interface Clipboard {\n    copy(text: string): void;\n    SUPPORTS_COPY: boolean;\n}\n\nexport interface NavigationRouter {\n    back(): void;\n    forward(): void;\n    transitionTo(path: string, ...args: unknown[]): void;\n    transitionToGuild(guildId: string, ...args: unknown[]): void;\n}\n\nexport interface ChannelRouter {\n    transitionToChannel: (channelId: string) => void;\n    transitionToThread: (channel: Channel) => void;\n}\n\nexport interface IconUtils {\n    getUserAvatarURL(user: User, canAnimate?: boolean, size?: number, format?: string): string;\n    getDefaultAvatarURL(id: string, discriminator?: string): string;\n    getUserBannerURL(data: { id: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined;\n    getAvatarDecorationURL(dara: { avatarDecoration: string, size: number; canCanimate?: boolean; }): string | undefined;\n\n    getGuildMemberAvatarURL(member: GuildMember, canAnimate?: string): string | null;\n    getGuildMemberAvatarURLSimple(data: { guildId: string, userId: string, avatar: string, canAnimate?: boolean; size?: number; }): string;\n    getGuildMemberBannerURL(data: { id: string, guildId: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined;\n\n    getGuildIconURL(data: { id: string, icon?: string, size?: number, canAnimate?: boolean; }): string | undefined;\n    getGuildBannerURL(guild: Guild, canAnimate?: boolean): string | null;\n\n    getChannelIconURL(data: { id: string; icon?: string; applicationId?: string; size?: number; }): string | undefined;\n    getEmojiURL(data: { id: string, animated: boolean, size: number, forcePNG?: boolean; }): string;\n\n    hasAnimatedGuildIcon(guild: Guild): boolean;\n    isAnimatedIconHash(hash: string): boolean;\n\n    getGuildSplashURL: any;\n    getGuildDiscoverySplashURL: any;\n    getGuildHomeHeaderURL: any;\n    getResourceChannelIconURL: any;\n    getNewMemberActionIconURL: any;\n    getGuildTemplateIconURL: any;\n    getApplicationIconURL: any;\n    getGameAssetURL: any;\n    getVideoFilterAssetURL: any;\n\n    getGuildMemberAvatarSource: any;\n    getUserAvatarSource: any;\n    getGuildSplashSource: any;\n    getGuildDiscoverySplashSource: any;\n    makeSource: any;\n    getGameAssetSource: any;\n    getGuildIconSource: any;\n    getGuildTemplateIconSource: any;\n    getGuildBannerSource: any;\n    getGuildHomeHeaderSource: any;\n    getChannelIconSource: any;\n    getApplicationIconSource: any;\n    getAnimatableSourceWithFallback: any;\n}\n\nexport interface Constants {\n    Endpoints: Record<string, any>;\n    UserFlags: Record<string, number>;\n    FriendsSections: Record<string, string>;\n}\n\nexport type ActiveView = LiteralUnion<\"emoji\" | \"gif\" | \"sticker\" | \"soundboard\", string>;\n\nexport interface ExpressionPickerStoreState extends Record<PropertyKey, any> {\n    activeView: ActiveView | null;\n    lastActiveView: ActiveView | null;\n    activeViewType: any | null;\n    searchQuery: string;\n    isSearchSuggestion: boolean,\n    pickerId: string;\n}\n\nexport interface ExpressionPickerStore {\n    openExpressionPicker(activeView: ActiveView, activeViewType?: any): void;\n    closeExpressionPicker(activeViewType?: any): void;\n    toggleMultiExpressionPicker(activeViewType?: any): void;\n    toggleExpressionPicker(activeView: ActiveView, activeViewType?: any): void;\n    setExpressionPickerView(activeView: ActiveView): void;\n    setSearchQuery(searchQuery: string, isSearchSuggestion?: boolean): void;\n    useExpressionPickerStore(): ExpressionPickerStoreState;\n    useExpressionPickerStore<T>(selector: (state: ExpressionPickerStoreState) => T): T;\n}\n\nexport { BrowserWindowFeatures, PopoutActions } from \"./stores/PopoutWindowStore\";\n\nexport type UserNameUtilsTagInclude = LiteralUnion<\"auto\" | \"always\" | \"never\", string>;\nexport interface UserNameUtilsTagOptions {\n    forcePomelo?: boolean;\n    identifiable?: UserNameUtilsTagInclude;\n    decoration?: UserNameUtilsTagInclude;\n    mode?: \"full\" | \"username\";\n}\n\nexport interface UsernameUtils {\n    getGlobalName(user: User): string;\n    getFormattedName(user: User, useTagInsteadOfUsername?: boolean): string;\n    getName(user: User): string;\n    useName(user: User): string;\n    getUserTag(user: User, options?: UserNameUtilsTagOptions): string;\n    useUserTag(user: User, options?: UserNameUtilsTagOptions): string;\n\n\n    useDirectMessageRecipient: any;\n    humanizeStatus: any;\n}\n\n// TODO: fix type\nexport class DisplayProfile {\n    userId: string;\n    banner?: string;\n    bio?: string;\n    pronouns?: string;\n    accentColor?: number;\n    themeColors?: number[];\n    popoutAnimationParticleType?: any;\n    profileEffectId?: string;\n    _userProfile?: any;\n    _guildMemberProfile?: any;\n    canUsePremiumProfileCustomization: boolean;\n    canEditThemes: boolean;\n    premiumGuildSince: Date | null;\n    premiumSince: Date | null;\n    premiumType?: number;\n    primaryColor?: number;\n\n    getBadges(): Array<{\n        id: string;\n        description: string;\n        icon: string;\n        link?: string;\n    }>;\n    getBannerURL(options: { canAnimate: boolean; size: number; }): string;\n    getLegacyUsername(): string | null;\n    hasFullProfile(): boolean;\n    hasPremiumCustomization(): boolean;\n    hasThemeColors(): boolean;\n    isUsingGuildMemberBanner(): boolean;\n    isUsingGuildMemberBio(): boolean;\n    isUsingGuildMemberPronouns(): boolean;\n}\n\nexport interface DisplayProfileUtils {\n    getDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;\n    useDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null;\n}\n\nexport interface DateUtils {\n    isSameDay(date1: Date, date2: Date): boolean;\n    calendarFormat(date: Date): string;\n    dateFormat(date: Date, format: string): string;\n    diffAsUnits(start: Date, end: Date, stopAtOneSecond?: boolean): Record<\"days\" | \"hours\" | \"minutes\" | \"seconds\", number>;\n}\n\nexport interface CommandOptions {\n    type: number;\n    name: string;\n    description: string;\n    required?: boolean;\n    choices?: {\n        name: string;\n        values: string | number;\n    }[];\n    options?: CommandOptions[];\n    channel_types?: number[];\n    min_value?: number;\n    max_value?: number;\n    autocomplete?: boolean;\n}\n"
  },
  {
    "path": "packages/discord-types/webpack/index.d.ts",
    "content": "/*\n * @vencord/discord-types\n * Copyright (c) 2024 Vendicated, Nuckyz and contributors\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nexport type ModuleExports = any;\n\nexport type Module = {\n    id: PropertyKey;\n    loaded: boolean;\n    exports: ModuleExports;\n};\n\n/** exports can be anything, however initially it is always an empty object */\nexport type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void;\n\n/** Keys here can be symbols too, but we can't properly type them */\nexport type AsyncModulePromise = Promise<ModuleExports> & {\n    \"__webpack_queues__\": (fnQueue: ((queue: any[]) => any)) => any;\n    \"__webpack_exports__\": ModuleExports;\n    \"__webpack_error__\"?: any;\n};\n\nexport type AsyncModuleBody = (\n    handleAsyncDependencies: (deps: AsyncModulePromise[]) =>\n        Promise<() => ModuleExports[]> | (() => ModuleExports[]),\n    asyncResult: (error?: any) => void\n) => Promise<void>;\n\nexport type EnsureChunkHandlers = {\n    /**\n     * Ensures the js file for this chunk is loaded, or starts to load if it's not.\n     * @param chunkId The chunk id\n     * @param promises The promises array to add the loading promise to\n     */\n    j: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;\n    /**\n     * Ensures the css file for this chunk is loaded, or starts to load if it's not.\n     * @param chunkId The chunk id\n     * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too\n     */\n    css: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;\n    /**\n     * Trigger for prefetching next chunks. This is called after ensuring a chunk is loaded and internally looks up\n     * a map to see if the chunk that just loaded has next chunks to prefetch.\n     *\n     * Note that this does not add an extra promise to the promises array, and instead only executes the prefetching after\n     * calling Promise.all on the promises array.\n     * @param chunkId The chunk id\n     * @param promises The promises array of ensuring the chunk is loaded\n     */\n    prefetch: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;\n};\n\nexport type PrefetchChunkHandlers = {\n    /**\n     * Prefetches the js file for this chunk.\n     * @param chunkId The chunk id\n     */\n    j: (this: PrefetchChunkHandlers, chunkId: PropertyKey) => void;\n};\n\nexport type ScriptLoadDone = (event: Event) => void;\n\nexport type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & {\n    /** Check if a chunk has been loaded */\n    j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean;\n};\n\nexport type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {\n    /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */\n    m: Record<PropertyKey, ModuleFactory>;\n    /** The module cache, where all modules which have been WebpackRequire'd are stored */\n    c: Record<PropertyKey, Module>;\n    // /**\n    //  * Export star. Sets properties of \"fromObject\" to \"toObject\" as getters that return the value from \"fromObject\", like this:\n    //  * @example\n    //  * const fromObject = { a: 1 };\n    //  * Object.keys(fromObject).forEach(key => {\n    //  *     if (key !== \"default\" && !Object.hasOwn(toObject, key)) {\n    //  *         Object.defineProperty(toObject, key, {\n    //  *             get: () => fromObject[key],\n    //  *             enumerable: true\n    //  *         });\n    //  *     }\n    //  * });\n    //  * @returns fromObject\n    //  */\n    // es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord;\n    /**\n     * Creates an async module. A module that which has top level await, or requires an export from an async module.\n     *\n     * The body function must be an async function. \"module.exports\" will become an {@link AsyncModulePromise}.\n     *\n     * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies:\n     * @example\n     * const factory = (module, exports, wreq) => {\n     *     wreq.a(module, async (handleAsyncDependencies, asyncResult) => {\n     *         try {\n     *             const asyncRequireA = wreq(...);\n     *\n     *             const asyncDependencies = handleAsyncDependencies([asyncRequire]);\n     *             const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies;\n     *\n     *             // Use the required module\n     *             console.log(requireAResult);\n     *\n     *             // Mark this async module as resolved\n     *             asyncResult();\n     *         } catch(error) {\n     *             // Mark this async module as rejected with an error\n     *             asyncResult(error);\n     *         }\n     *     }, false); // false because our module does not have an await after dealing with the async requires\n     * }\n     */\n    a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void;\n    /** getDefaultExport function for compatibility with non-harmony modules */\n    n: (this: WebpackRequire, exports: any) => () => ModuleExports;\n    /**\n     * Create a fake namespace object, useful for faking an __esModule with a default export.\n     *\n     * mode & 1: Value is a module id, require it\n     *\n     * mode & 2: Merge all properties of value into the namespace\n     *\n     * mode & 4: Return value when already namespace object\n     *\n     * mode & 16: Return value when it's Promise-like\n     *\n     * mode & (8|1): Behave like require\n     */\n    t: (this: WebpackRequire, value: any, mode: number) => any;\n    /**\n     * Define getter functions for harmony exports. For every prop in \"definiton\" (the module exports), set a getter in \"exports\" for the getter function in the \"definition\", like this:\n     * @example\n     * const exports = {};\n     * const definition = { exportName: () => someExportedValue };\n     * for (const key in definition) {\n     *     if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {\n     *         Object.defineProperty(exports, key, {\n     *             get: definition[key],\n     *             enumerable: true\n     *         });\n     *     }\n     * }\n     * // exports is now { exportName: someExportedValue } (but each value is actually a getter)\n     */\n    d: (this: WebpackRequire, exports: Record<PropertyKey, any>, definiton: Record<PropertyKey, () => ModuleExports>) => void;\n    /** The ensure chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */\n    f: EnsureChunkHandlers;\n    /**\n     * The ensure chunk function, it ensures a chunk is loaded, or loads if needed.\n     * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded.\n     */\n    e: (this: WebpackRequire, chunkId: PropertyKey) => Promise<void[]>;\n    /** The prefetch chunk handlers, which are used to prefetch the files of the chunks */\n    F: PrefetchChunkHandlers;\n    /**\n     * The prefetch chunk function.\n     * Internally it uses the handlers in {@link WebpackRequire.F} to prefetch a chunk.\n     */\n    E: (this: WebpackRequire, chunkId: PropertyKey) => void;\n    /** Get the filename for the css part of a chunk */\n    k: (this: WebpackRequire, chunkId: PropertyKey) => string;\n    /** Get the filename for the js part of a chunk */\n    u: (this: WebpackRequire, chunkId: PropertyKey) => string;\n    /** The global object, will likely always be the window */\n    g: typeof globalThis;\n    /** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js \"module.exports\" from being set */\n    hmd: (this: WebpackRequire, module: Module) => any;\n    /** Shorthand for Object.prototype.hasOwnProperty */\n    o: typeof Object.prototype.hasOwnProperty;\n    /**\n     * Function to load a script tag. \"done\" is called when the loading has finished or a timeout has occurred.\n     * \"done\" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`,\n     * so it will be called when that existing script finishes loading.\n     */\n    l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void;\n    /** Defines __esModule on the exports, marking ES Modules compatibility as true */\n    r: (this: WebpackRequire, exports: ModuleExports) => void;\n    /** Node.js module decorator. Decorates a module as a Node.js module */\n    nmd: (this: WebpackRequire, module: Module) => any;\n    /**\n     * Register deferred code which will be executed when the passed chunks are loaded.\n     *\n     * If chunkIds is defined, it defers the execution of the callback and returns undefined.\n     *\n     * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument.\n     *\n     * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code.\n     *\n     * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed.\n     */\n    O: OnChunksLoaded;\n    /**\n     * Instantiate a wasm instance with source using \"wasmModuleHash\", and importObject \"importsObj\", and then assign the exports of its instance to \"exports\".\n     * @returns The exports argument, but now assigned with the exports of the wasm instance\n     */\n    v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise<any>;\n    /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */\n    p: string;\n    /** The runtime id of the current runtime */\n    j: string;\n    /** Document baseURI or WebWorker location.href */\n    b: string;\n\n    /* rspack only */\n\n    /** rspack version */\n    rv: (this: WebpackRequire) => string;\n    /** rspack unique id */\n    ruid: string;\n};\n"
  },
  {
    "path": "packages/vencord-types/.gitignore",
    "content": "*\n!.*ignore\n!package.json\n!*.md\n!prepare.ts\n!index.d.ts\n!globals.d.ts\n"
  },
  {
    "path": "packages/vencord-types/.npmignore",
    "content": "node_modules\nprepare.ts\n.gitignore\nHOW2PUB.md\n"
  },
  {
    "path": "packages/vencord-types/HOW2PUB.md",
    "content": "# How to publish\n\n1. run `pnpm generateTypes` in the project root\n2. bump package.json version\n3. npm publish\n"
  },
  {
    "path": "packages/vencord-types/README.md",
    "content": "# Vencord Types\n\nTypings for Vencord's api, published to npm\n\n```sh\nnpm i @vencord/types\n\nyarn add @vencord/types\n\npnpm add @vencord/types\n```\n"
  },
  {
    "path": "packages/vencord-types/globals.d.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\ndeclare global {\n    export var VencordNative: typeof import(\"./VencordNative\").default;\n    export var Vencord: typeof import(\"./Vencord\");\n}\n\nexport { };\n"
  },
  {
    "path": "packages/vencord-types/index.d.ts",
    "content": "/* eslint-disable */\n\n/// <reference path=\"Vencord.d.ts\" />\n/// <reference path=\"globals.d.ts\" />\n/// <reference path=\"modules.d.ts\" />\n"
  },
  {
    "path": "packages/vencord-types/package.json",
    "content": "{\n    \"name\": \"@vencord/types\",\n    \"private\": false,\n    \"version\": \"1.14.1\",\n    \"description\": \"\",\n    \"types\": \"index.d.ts\",\n    \"scripts\": {\n        \"prepublishOnly\": \"tsx ./prepare.ts\",\n        \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n    },\n    \"keywords\": [],\n    \"author\": \"Vencord\",\n    \"license\": \"GPL-3.0\",\n    \"devDependencies\": {\n        \"@types/fs-extra\": \"^11.0.4\",\n        \"fs-extra\": \"^11.3.0\",\n        \"tsx\": \"^4.19.2\"\n    },\n    \"dependencies\": {\n        \"@types/lodash\": \"4.17.15\",\n        \"@types/node\": \"^22.13.4\",\n        \"@vencord/discord-types\": \"^1.0.0\",\n        \"highlight.js\": \"11.11.1\",\n        \"moment\": \"^2.22.2\",\n        \"ts-pattern\": \"^5.6.0\",\n        \"type-fest\": \"^4.35.0\"\n    },\n    \"peerDependencies\": {\n        \"@types/react\": \"18.3.1\",\n        \"@types/react-dom\": \"18.3.1\"\n    }\n}\n"
  },
  {
    "path": "packages/vencord-types/prepare.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { cpSync, moveSync, readdirSync, rmSync } from \"fs-extra\";\nimport { join } from \"path\";\n\nreaddirSync(join(__dirname, \"src\"))\n    .forEach(child => moveSync(join(__dirname, \"src\", child), join(__dirname, child), { overwrite: true }));\n\nconst VencordSrc = join(__dirname, \"..\", \"..\", \"src\");\n\nfor (const file of [\"preload.d.ts\", \"userplugins\", \"main\", \"debug\", \"src\", \"browser\", \"scripts\"]) {\n    rmSync(join(__dirname, file), { recursive: true, force: true });\n}\n\nfunction copyDtsFiles(from: string, to: string) {\n    for (const file of readdirSync(from, { withFileTypes: true })) {\n        // bad\n        if (from === VencordSrc && file.name === \"globals.d.ts\") continue;\n\n        const fullFrom = join(from, file.name);\n        const fullTo = join(to, file.name);\n\n        if (file.isDirectory()) {\n            copyDtsFiles(fullFrom, fullTo);\n        } else if (file.name.endsWith(\".d.ts\")) {\n            cpSync(fullFrom, fullTo);\n        }\n    }\n}\n\ncopyDtsFiles(VencordSrc, __dirname);\n"
  },
  {
    "path": "patches/eslint-plugin-path-alias@2.1.0.patch",
    "content": "diff --git a/dist/index.js b/dist/index.js\nindex 67de6fb139070fd0e49beca65e3b63c531202e16..aa2883c8126e4952a42872ee920f59547a066430 100644\n--- a/dist/index.js\n+++ b/dist/index.js\n@@ -1 +1 @@\n-var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,\"default\",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},\"__esModule\",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h=\"eslint-plugin-path-alias\",v=\"2.0.0\";var l=require(\"path\"),M=b(require(\"nanomatch\"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require(\"get-tsconfig\"),a=require(\"path\"),w=b(require(\"find-pkg\")),O=require(\"fs\");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!=\"string\")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\\/\\*$/,\"\"),o=o.map(n=>(0,a.resolve)(i,n.replace(/\\/\\*$/,\"\"))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!=\"string\")return;if(s.startsWith(\"/\")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:\"suggestion\",docs:{description:\"Ensure imports use path aliases whenever possible vs. relative paths\",url:j(\"no-relative\")},fixable:\"code\",schema:[{type:\"object\",properties:{exceptions:{type:\"array\",items:{type:\"string\"}},paths:{type:\"object\"}},additionalProperties:!1}],messages:{shouldUseAlias:\"Import should use path alias instead of relative path\"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!==\"Literal\"||typeof s.source.value!=\"string\")return;let o=s.source.raw,n=s.source.value;if(!/^(\\.?\\.\\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:\"shouldUseAlias\",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!=\"string\")return;let o=s.source.value;if(!/^(\\.?\\.\\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:\"shouldUseAlias\",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{\"no-relative\":T}};\n+var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,\"default\",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},\"__esModule\",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h=\"eslint-plugin-path-alias\",v=\"2.0.0\";var l=require(\"path\"),M=b(require(\"nanomatch\"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require(\"get-tsconfig\"),a=require(\"path\"),w=b(require(\"find-pkg\")),O=require(\"fs\");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!=\"string\")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\\/\\*$/,\"\"),o=o.map(n=>(0,a.resolve)(i,n.replace(/\\/\\*$/,\"\"))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!=\"string\")return;if(s.startsWith(\"/\")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:\"suggestion\",docs:{description:\"Ensure imports use path aliases whenever possible vs. relative paths\",url:j(\"no-relative\")},fixable:\"code\",schema:[{type:\"object\",properties:{exceptions:{type:\"array\",items:{type:\"string\"}},paths:{type:\"object\"}},additionalProperties:!1}],messages:{shouldUseAlias:\"Import should use path alias instead of relative path\"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!==\"Literal\"||typeof s.source.value!=\"string\")return;let o=s.source.raw,n=s.source.value;if(!/^(\\.\\.\\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:\"shouldUseAlias\",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!=\"string\")return;let o=s.source.value;if(!/^(\\.\\.\\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:\"shouldUseAlias\",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{\"no-relative\":T}};\ndiff --git a/dist/index.mjs b/dist/index.mjs\nindex 96de18e06d4cc413e11af038cd760e4804c32e59..27e8c4e3e2c942400cc3982e52159904ca6eedfa 100644\n--- a/dist/index.mjs\n+++ b/dist/index.mjs\n@@ -1 +1 @@\n-var d=\"eslint-plugin-path-alias\",h=\"2.0.0\";import{dirname as x,resolve as j,basename as I}from\"path\";import U from\"nanomatch\";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from\"get-tsconfig\";import{resolve as c,dirname as u}from\"path\";import A from\"find-pkg\";import{readFileSync as E}from\"fs\";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!=\"string\")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\\/\\*$/,\"\"),o=o.map(n=>c(r,n.replace(/\\/\\*$/,\"\"))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!=\"string\")return;if(t.startsWith(\"/\")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:\"suggestion\",docs:{description:\"Ensure imports use path aliases whenever possible vs. relative paths\",url:y(\"no-relative\")},fixable:\"code\",schema:[{type:\"object\",properties:{exceptions:{type:\"array\",items:{type:\"string\"}},paths:{type:\"object\"}},additionalProperties:!1}],messages:{shouldUseAlias:\"Import should use path alias instead of relative path\"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!==\"Literal\"||typeof t.source.value!=\"string\")return;let o=t.source.raw,n=t.source.value;if(!/^(\\.?\\.\\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:\"shouldUseAlias\",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!=\"string\")return;let o=t.source.value;if(!/^(\\.?\\.\\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:\"shouldUseAlias\",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{\"no-relative\":P}};export{Q as default};\n+var d=\"eslint-plugin-path-alias\",h=\"2.0.0\";import{dirname as x,resolve as j,basename as I}from\"path\";import U from\"nanomatch\";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from\"get-tsconfig\";import{resolve as c,dirname as u}from\"path\";import A from\"find-pkg\";import{readFileSync as E}from\"fs\";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!=\"string\")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\\/\\*$/,\"\"),o=o.map(n=>c(r,n.replace(/\\/\\*$/,\"\"))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!=\"string\")return;if(t.startsWith(\"/\")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:\"suggestion\",docs:{description:\"Ensure imports use path aliases whenever possible vs. relative paths\",url:y(\"no-relative\")},fixable:\"code\",schema:[{type:\"object\",properties:{exceptions:{type:\"array\",items:{type:\"string\"}},paths:{type:\"object\"}},additionalProperties:!1}],messages:{shouldUseAlias:\"Import should use path alias instead of relative path\"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!==\"Literal\"||typeof t.source.value!=\"string\")return;let o=t.source.raw,n=t.source.value;if(!/^(\\.\\.\\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:\"shouldUseAlias\",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!=\"string\")return;let o=t.source.value;if(!/^(\\.\\.\\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:\"shouldUseAlias\",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{\"no-relative\":P}};export{Q as default};\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n    - packages/*\n"
  },
  {
    "path": "scripts/build/build.mjs",
    "content": "#!/usr/bin/node\n/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// @ts-check\n\nimport { readdir } from \"fs/promises\";\nimport { join, resolve } from \"path\";\n\nimport { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from \"./common.mjs\";\n\nconst defines = stringifyValues({\n    IS_STANDALONE,\n    IS_DEV,\n    IS_REPORTER,\n    IS_ANTI_CRASH_TEST,\n    IS_UPDATER_DISABLED,\n    IS_WEB: false,\n    IS_EXTENSION: false,\n    IS_USERSCRIPT: false,\n    VERSION,\n    BUILD_TIMESTAMP\n});\n\nif (defines.IS_STANDALONE === \"false\") {\n    // If this is a local build (not standalone), optimize\n    // for the specific platform we're on\n    defines[\"process.platform\"] = JSON.stringify(process.platform);\n}\n\n/**\n * @type {import(\"esbuild\").BuildOptions}\n */\nconst nodeCommonOpts = {\n    ...commonOpts,\n    define: defines,\n    format: \"cjs\",\n    platform: \"node\",\n    target: [\"esnext\"],\n    // @ts-expect-error this is never undefined\n    external: [\"electron\", \"original-fs\", \"~pluginNatives\", ...commonOpts.external]\n};\n\nconst sourceMapFooter = s => watch ? \"\" : `//# sourceMappingURL=vencord://${s}.js.map`;\nconst sourcemap = watch ? \"inline\" : \"external\";\n\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nconst globNativesPlugin = {\n    name: \"glob-natives-plugin\",\n    setup: build => {\n        const filter = /^~pluginNatives$/;\n        build.onResolve({ filter }, args => {\n            return {\n                namespace: \"import-natives\",\n                path: args.path\n            };\n        });\n\n        build.onLoad({ filter, namespace: \"import-natives\" }, async () => {\n            const pluginDirs = [\"plugins\", \"userplugins\"];\n            let code = \"\";\n            let natives = \"\\n\";\n            let i = 0;\n            /**\n             * @type {string[]}\n             */\n            const watchFiles = [];\n            for (const dir of pluginDirs) {\n                const dirPath = join(\"src\", dir);\n                if (!await exists(dirPath)) continue;\n                const plugins = await readdir(dirPath, { withFileTypes: true });\n                for (const file of plugins) {\n                    const fileName = file.name;\n                    const nativePath = join(dirPath, fileName, \"native.ts\");\n                    const indexNativePath = join(dirPath, fileName, \"native/index.ts\");\n\n                    watchFiles.push(resolve(nativePath), resolve(indexNativePath));\n\n                    if (!(await exists(nativePath)) && !(await exists(indexNativePath)))\n                        continue;\n\n                    const pluginName = await resolvePluginName(dirPath, file);\n\n                    const mod = `p${i}`;\n                    code += `import * as ${mod} from \"./${dir}/${fileName}/native\";\\n`;\n                    natives += `${JSON.stringify(pluginName)}:${mod},\\n`;\n                    i++;\n                }\n            }\n            code += `export default {${natives}};`;\n            return {\n                contents: code,\n                resolveDir: \"./src\",\n                watchDirs: pluginDirs.map(d => resolve(\"src\", d)),\n                watchFiles,\n            };\n        });\n    }\n};\n\n/** @type {import(\"esbuild\").BuildOptions[]} */\nconst buildConfigs = ([\n    // Discord Desktop main & renderer & preload\n    {\n        ...nodeCommonOpts,\n        entryPoints: [\"src/main/index.ts\"],\n        outfile: \"dist/patcher.js\",\n        footer: { js: \"//# sourceURL=file:///VencordPatcher\\n\" + sourceMapFooter(\"patcher\") },\n        sourcemap,\n        plugins: [\n            // @ts-ignore this is never undefined\n            ...nodeCommonOpts.plugins,\n            globNativesPlugin\n        ],\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"true\",\n            IS_VESKTOP: \"false\"\n        }\n    },\n    {\n        ...commonOpts,\n        entryPoints: [\"src/Vencord.ts\"],\n        outfile: \"dist/renderer.js\",\n        format: \"iife\",\n        target: [\"esnext\"],\n        footer: { js: \"//# sourceURL=file:///VencordRenderer\\n\" + sourceMapFooter(\"renderer\") },\n        globalName: \"Vencord\",\n        sourcemap,\n        plugins: [\n            globPlugins(\"discordDesktop\"),\n            ...commonRendererPlugins\n        ],\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"true\",\n            IS_VESKTOP: \"false\"\n        }\n    },\n    {\n        ...nodeCommonOpts,\n        entryPoints: [\"src/preload.ts\"],\n        outfile: \"dist/preload.js\",\n        footer: { js: \"//# sourceURL=file:///VencordPreload\\n\" + sourceMapFooter(\"preload\") },\n        sourcemap,\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"true\",\n            IS_VESKTOP: \"false\"\n        }\n    },\n\n    // Vencord Desktop main & renderer & preload\n    {\n        ...nodeCommonOpts,\n        entryPoints: [\"src/main/index.ts\"],\n        outfile: \"dist/vencordDesktopMain.js\",\n        footer: { js: \"//# sourceURL=file:///VencordDesktopMain\\n\" + sourceMapFooter(\"vencordDesktopMain\") },\n        sourcemap,\n        plugins: [\n            ...nodeCommonOpts.plugins,\n            globNativesPlugin\n        ],\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"false\",\n            IS_VESKTOP: \"true\"\n        }\n    },\n    {\n        ...commonOpts,\n        entryPoints: [\"src/Vencord.ts\"],\n        outfile: \"dist/vencordDesktopRenderer.js\",\n        format: \"iife\",\n        target: [\"esnext\"],\n        footer: { js: \"//# sourceURL=file:///VencordDesktopRenderer\\n\" + sourceMapFooter(\"vencordDesktopRenderer\") },\n        globalName: \"Vencord\",\n        sourcemap,\n        plugins: [\n            globPlugins(\"vesktop\"),\n            ...commonRendererPlugins\n        ],\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"false\",\n            IS_VESKTOP: \"true\"\n        }\n    },\n    {\n        ...nodeCommonOpts,\n        entryPoints: [\"src/preload.ts\"],\n        outfile: \"dist/vencordDesktopPreload.js\",\n        footer: { js: \"//# sourceURL=file:///VencordPreload\\n\" + sourceMapFooter(\"vencordDesktopPreload\") },\n        sourcemap,\n        define: {\n            ...defines,\n            IS_DISCORD_DESKTOP: \"false\",\n            IS_VESKTOP: \"true\"\n        }\n    }\n]);\n\nawait buildOrWatchAll(buildConfigs);\n"
  },
  {
    "path": "scripts/build/buildWeb.mjs",
    "content": "#!/usr/bin/node\n/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// @ts-check\n\nimport { readFileSync } from \"fs\";\nimport { appendFile, mkdir, readdir, readFile, rm, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport Zip from \"zip-local\";\n\nimport { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from \"./common.mjs\";\n\n/**\n * @type {import(\"esbuild\").BuildOptions}\n */\nconst commonOptions = {\n    ...commonOpts,\n    entryPoints: [\"browser/Vencord.ts\"],\n    format: \"iife\",\n    globalName: \"Vencord\",\n    external: [\"~plugins\", \"~git-hash\", \"/assets/*\"],\n    target: [\"esnext\"],\n    plugins: [\n        globPlugins(\"web\"),\n        ...commonRendererPlugins\n    ],\n    define: stringifyValues({\n        IS_WEB: true,\n        IS_EXTENSION: false,\n        IS_USERSCRIPT: false,\n        IS_STANDALONE: true,\n        IS_DEV,\n        IS_REPORTER,\n        IS_ANTI_CRASH_TEST,\n        IS_DISCORD_DESKTOP: false,\n        IS_VESKTOP: false,\n        IS_UPDATER_DISABLED: true,\n        VERSION,\n        BUILD_TIMESTAMP\n    })\n};\n\nconst MonacoWorkerEntryPoints = [\n    \"vs/language/css/css.worker.js\",\n    \"vs/editor/editor.worker.js\"\n];\n\n/** @type {import(\"esbuild\").BuildOptions[]} */\nconst buildConfigs = [\n    {\n        entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),\n        bundle: true,\n        minify: true,\n        format: \"iife\",\n        outbase: \"node_modules/monaco-editor/esm/\",\n        outdir: \"dist/vendor/monaco\"\n    },\n    {\n        entryPoints: [\"browser/monaco.ts\"],\n        bundle: true,\n        minify: true,\n        format: \"iife\",\n        outfile: \"dist/vendor/monaco/index.js\",\n        loader: {\n            \".ttf\": \"file\"\n        }\n    },\n    {\n        ...commonOptions,\n        outfile: \"dist/browser.js\",\n        footer: { js: \"//# sourceURL=file:///VencordWeb\" }\n    },\n    {\n        ...commonOptions,\n        outfile: \"dist/extension.js\",\n        define: {\n            ...commonOptions.define,\n            IS_EXTENSION: \"true\"\n        },\n        footer: { js: \"//# sourceURL=file:///VencordWeb\" }\n    },\n    {\n        ...commonOptions,\n        inject: [\"browser/GMPolyfill.js\", ...(commonOptions?.inject || [])],\n        define: {\n            ...commonOptions.define,\n            IS_USERSCRIPT: \"true\",\n            window: \"unsafeWindow\",\n        },\n        outfile: \"dist/Vencord.user.js\",\n        banner: {\n            js: readFileSync(\"browser/userscript.meta.js\", \"utf-8\").replace(\"%version%\", `${VERSION}.${new Date().getTime()}`)\n        },\n        footer: {\n            // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local\n            js: \"Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});\"\n        }\n    }\n];\n\nawait buildOrWatchAll(buildConfigs);\n\n/**\n * @type {(dir: string) => Promise<string[]>}\n */\nasync function globDir(dir) {\n    const files = [];\n\n    for (const child of await readdir(dir, { withFileTypes: true })) {\n        const p = join(dir, child.name);\n        if (child.isDirectory())\n            files.push(...await globDir(p));\n        else\n            files.push(p);\n    }\n\n    return files;\n}\n\n/**\n * @type {(dir: string, basePath?: string) => Promise<Record<string, string>>}\n */\nasync function loadDir(dir, basePath = \"\") {\n    const files = await globDir(dir);\n    return Object.fromEntries(await Promise.all(files.map(async f => [f.slice(basePath.length), await readFile(f)])));\n}\n\n/**\n  * @type {(target: string, files: string[]) => Promise<void>}\n */\nasync function buildExtension(target, files) {\n    const entries = {\n        \"dist/Vencord.js\": await readFile(\"dist/extension.js\"),\n        \"dist/Vencord.css\": await readFile(\"dist/extension.css\"),\n        ...await loadDir(\"dist/vendor/monaco\", \"dist/\"),\n        ...Object.fromEntries(await Promise.all(files.map(async f => {\n            let content = await readFile(join(\"browser\", f));\n            if (f.startsWith(\"manifest\")) {\n                const json = JSON.parse(content.toString(\"utf-8\"));\n                json.version = VERSION;\n                content = Buffer.from(new TextEncoder().encode(JSON.stringify(json)));\n            }\n\n            return [\n                f.startsWith(\"manifest\") ? \"manifest.json\" : f,\n                content\n            ];\n        })))\n    };\n\n    await rm(target, { recursive: true, force: true });\n    await Promise.all(Object.entries(entries).map(async ([file, content]) => {\n        const dest = join(\"dist\", target, file);\n        const parentDirectory = join(dest, \"..\");\n        await mkdir(parentDirectory, { recursive: true });\n        await writeFile(dest, content);\n    }));\n\n    console.info(\"Unpacked Extension written to dist/\" + target);\n}\n\nconst appendCssRuntime = readFile(\"dist/Vencord.user.css\", \"utf-8\").then(content => {\n    const cssRuntime = `unsafeWindow._vcUserScriptRendererCss=\\`${content.replaceAll(\"`\", \"\\\\`\")}\\``;\n\n    return appendFile(\"dist/Vencord.user.js\", cssRuntime);\n});\n\nif (!process.argv.includes(\"--skip-extension\")) {\n    await Promise.all([\n        appendCssRuntime,\n        buildExtension(\"chromium-unpacked\", [\"modifyResponseHeaders.json\", \"content.js\", \"manifest.json\", \"icon.png\"]),\n        buildExtension(\"firefox-unpacked\", [\"background.js\", \"content.js\", \"manifestv2.json\", \"icon.png\"]),\n    ]);\n\n    Zip.sync.zip(\"dist/chromium-unpacked\").compress().save(\"dist/extension-chrome.zip\");\n    console.info(\"Packed Chromium Extension written to dist/extension-chrome.zip\");\n\n    Zip.sync.zip(\"dist/firefox-unpacked\").compress().save(\"dist/extension-firefox.zip\");\n    console.info(\"Packed Firefox Extension written to dist/extension-firefox.zip\");\n} else {\n    await appendCssRuntime;\n}\n"
  },
  {
    "path": "scripts/build/common.mjs",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// @ts-check\n\nimport \"../suppressExperimentalWarnings.js\";\nimport \"../checkNodeVersion.js\";\n\nimport { exec, execSync } from \"child_process\";\nimport esbuild, { build, context } from \"esbuild\";\nimport { constants as FsConstants, readFileSync } from \"fs\";\nimport { access, readdir, readFile } from \"fs/promises\";\nimport { minify as minifyHtml } from \"html-minifier-terser\";\nimport { optimize as optimizeSvg } from 'svgo';\nimport { join, relative, resolve } from \"path\";\nimport { promisify } from \"util\";\n\nimport { getPluginTarget } from \"../utils.mjs\";\nimport { builtinModules } from \"module\";\n\n/** @type {import(\"../../package.json\")} */\nconst PackageJSON = JSON.parse(readFileSync(\"package.json\", \"utf-8\"));\n\nexport const VERSION = PackageJSON.version;\n// https://reproducible-builds.org/docs/source-date-epoch/\nexport const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();\n\nexport const watch = process.argv.includes(\"--watch\");\nexport const IS_DEV = watch || process.argv.includes(\"--dev\");\nexport const IS_REPORTER = process.argv.includes(\"--reporter\");\nexport const IS_ANTI_CRASH_TEST = process.argv.includes(\"--anti-crash-test\");\nexport const IS_STANDALONE = process.argv.includes(\"--standalone\");\n\nexport const IS_UPDATER_DISABLED = process.argv.includes(\"--disable-updater\");\nexport const gitHash = process.env.VENCORD_HASH || execSync(\"git rev-parse --short HEAD\", { encoding: \"utf-8\" }).trim();\n\nexport const banner = {\n    js: `\n// Vencord ${gitHash}\n// Standalone: ${IS_STANDALONE}\n// Platform: ${IS_STANDALONE === false ? process.platform : \"Universal\"}\n// Updater Disabled: ${IS_UPDATER_DISABLED}\n`.trim()\n};\n\n/**\n * JSON.stringify all values in an object\n * @type {(obj: Record<string, any>) => Record<string, string>}\n */\nexport function stringifyValues(obj) {\n    for (const key in obj) {\n        obj[key] = JSON.stringify(obj[key]);\n    }\n    return obj;\n}\n\n/**\n * @param {import(\"esbuild\").BuildOptions[]} buildConfigs\n */\nexport async function buildOrWatchAll(buildConfigs) {\n    if (watch) {\n        await Promise.all(buildConfigs.map(cfg =>\n            context(cfg).then(ctx => ctx.watch())\n        ));\n    } else {\n        await Promise.all(buildConfigs.map(cfg => build(cfg)))\n            .catch(error => {\n                console.error(error.message);\n                process.exit(1); // exit immediately to skip the rest of the builds\n            });\n    }\n}\n\nconst PluginDefinitionNameMatcher = /definePlugin\\(\\{\\s*([\"'])?name\\1:\\s*([\"'`])(.+?)\\2/;\n/**\n * @param {string} base\n * @param {import(\"fs\").Dirent} dirent\n */\nexport async function resolvePluginName(base, dirent) {\n    const fullPath = join(base, dirent.name);\n    const content = dirent.isFile()\n        ? await readFile(fullPath, \"utf-8\")\n        : await (async () => {\n            for (const file of [\"index.ts\", \"index.tsx\"]) {\n                try {\n                    return await readFile(join(fullPath, file), \"utf-8\");\n                } catch {\n                    continue;\n                }\n            }\n            throw new Error(`Invalid plugin ${fullPath}: could not resolve entry point`);\n        })();\n\n    return PluginDefinitionNameMatcher.exec(content)?.[3]\n        ?? (() => {\n            throw new Error(`Invalid plugin ${fullPath}: must contain definePlugin call with simple string name property as first property`);\n        })();\n}\n\nexport async function exists(path) {\n    return await access(path, FsConstants.F_OK)\n        .then(() => true)\n        .catch(() => false);\n}\n\n// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nexport const makeAllPackagesExternalPlugin = {\n    name: \"make-all-packages-external\",\n    setup(build) {\n        const filter = /^[^./]|^\\.[^./]|^\\.\\.[^/]/; // Must not start with \"/\" or \"./\" or \"../\"\n        build.onResolve({ filter }, args => ({ path: args.path, external: true }));\n    }\n};\n\n/**\n * @type {(kind: \"web\" | \"discordDesktop\" | \"vesktop\") => import(\"esbuild\").Plugin}\n */\nexport const globPlugins = kind => ({\n    name: \"glob-plugins\",\n    setup: build => {\n        const filter = /^~plugins$/;\n        build.onResolve({ filter }, args => {\n            return {\n                namespace: \"import-plugins\",\n                path: args.path\n            };\n        });\n\n        build.onLoad({ filter, namespace: \"import-plugins\" }, async () => {\n            const pluginDirs = [\"plugins/_api\", \"plugins/_core\", \"plugins\", \"userplugins\"];\n            let code = \"\";\n            let pluginsCode = \"\\n\";\n            let metaCode = \"\\n\";\n            let excludedCode = \"\\n\";\n            let i = 0;\n            for (const dir of pluginDirs) {\n                const userPlugin = dir === \"userplugins\";\n\n                const fullDir = `./src/${dir}`;\n                if (!await exists(fullDir)) continue;\n                const files = await readdir(fullDir, { withFileTypes: true });\n                for (const file of files) {\n                    const fileName = file.name;\n                    if (fileName.startsWith(\"_\") || fileName.startsWith(\".\")) continue;\n                    if (fileName === \"index.ts\") continue;\n\n                    const target = getPluginTarget(fileName);\n\n                    if (target && !IS_REPORTER) {\n                        const excluded =\n                            (target === \"dev\" && !IS_DEV) ||\n                            (target === \"web\" && kind === \"discordDesktop\") ||\n                            (target === \"desktop\" && kind === \"web\") ||\n                            (target === \"discordDesktop\" && kind !== \"discordDesktop\") ||\n                            (target === \"vesktop\" && kind !== \"vesktop\");\n\n                        if (excluded) {\n                            const name = await resolvePluginName(fullDir, file);\n                            excludedCode += `${JSON.stringify(name)}:${JSON.stringify(target)},\\n`;\n                            continue;\n                        }\n                    }\n\n                    const folderName = `src/${dir}/${fileName}`.replace(/^src\\/plugins\\//, \"\");\n\n                    const mod = `p${i}`;\n                    code += `import ${mod} from \"./${dir}/${fileName.replace(/\\.tsx?$/, \"\")}\";\\n`;\n                    pluginsCode += `[${mod}.name]:${mod},\\n`;\n                    metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\\n`;\n                    i++;\n                }\n            }\n            code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;\n            return {\n                contents: code,\n                resolveDir: \"./src\",\n                watchDirs: pluginDirs.map(d => resolve(\"src\", d)),\n            };\n        });\n    }\n});\n\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nexport const gitHashPlugin = {\n    name: \"git-hash-plugin\",\n    setup: build => {\n        const filter = /^~git-hash$/;\n        build.onResolve({ filter }, args => ({\n            namespace: \"git-hash\", path: args.path\n        }));\n        build.onLoad({ filter, namespace: \"git-hash\" }, () => ({\n            contents: `export default \"${gitHash}\"`\n        }));\n    }\n};\n\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nexport const gitRemotePlugin = {\n    name: \"git-remote-plugin\",\n    setup: build => {\n        const filter = /^~git-remote$/;\n        build.onResolve({ filter }, args => ({\n            namespace: \"git-remote\", path: args.path\n        }));\n        build.onLoad({ filter, namespace: \"git-remote\" }, async () => {\n            let remote = process.env.VENCORD_REMOTE;\n            if (!remote) {\n                const res = await promisify(exec)(\"git remote get-url origin\", { encoding: \"utf-8\" });\n                remote = res.stdout.trim()\n                    .replace(\"https://github.com/\", \"\")\n                    .replace(\"git@github.com:\", \"\")\n                    .replace(/.git$/, \"\");\n            }\n\n            return { contents: `export default \"${remote}\"` };\n        });\n    }\n};\n\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nexport const fileUrlPlugin = {\n    name: \"file-uri-plugin\",\n    setup: build => {\n        const filter = /^file:\\/\\/.+$/;\n        build.onResolve({ filter }, args => ({\n            namespace: \"file-uri\",\n            path: args.path,\n            pluginData: {\n                uri: args.path,\n                path: join(args.resolveDir, args.path.slice(\"file://\".length).split(\"?\")[0])\n            }\n        }));\n        build.onLoad({ filter, namespace: \"file-uri\" }, async ({ pluginData: { path, uri } }) => {\n            const { searchParams } = new URL(uri);\n            const base64 = searchParams.has(\"base64\");\n            const minify = searchParams.has(\"minify\");\n            const noTrim = searchParams.get(\"trim\") === \"false\";\n\n            const encoding = base64 ? \"base64\" : \"utf-8\";\n\n            let content;\n            if (!minify) {\n                content = await readFile(path, encoding);\n                if (!noTrim) content = content.trimEnd();\n            } else {\n                if (path.endsWith(\".html\")) {\n                    content = await minifyHtml(await readFile(path, \"utf-8\"), {\n                        collapseWhitespace: true,\n                        removeComments: true,\n                        minifyCSS: true,\n                        minifyJS: true,\n                        removeEmptyAttributes: true,\n                        removeRedundantAttributes: true,\n                        removeScriptTypeAttributes: true,\n                        removeStyleLinkTypeAttributes: true,\n                        useShortDoctype: true\n                    });\n                } else if (path.endsWith(\".svg\")) {\n                    content = optimizeSvg(await readFile(path, \"utf-8\"), {\n                        datauri: base64 ? \"base64\" : void 0,\n                        multipass: true,\n                        floatPrecision: 2,\n                    }).data;\n                } else if (/[mc]?[jt]sx?$/.test(path)) {\n                    const res = await esbuild.build({\n                        entryPoints: [path],\n                        write: false,\n                        minify: true\n                    });\n                    content = res.outputFiles[0].text;\n                } else {\n                    throw new Error(`Don't know how to minify file type: ${path}`);\n                }\n\n                if (base64 && !content.startsWith(\"data:\"))\n                    content = Buffer.from(content).toString(\"base64\");\n            }\n\n            return {\n                contents: `export default ${JSON.stringify(content)}`\n            };\n        });\n    }\n};\n\nconst styleModule = readFileSync(\"./scripts/build/module/style.js\", \"utf-8\");\n/**\n * @type {import(\"esbuild\").Plugin}\n */\nexport const stylePlugin = {\n    name: \"style-plugin\",\n    setup: ({ onResolve, onLoad }) => {\n        onResolve({ filter: /\\.css\\?managed$/, namespace: \"file\" }, ({ path, resolveDir }) => ({\n            path: relative(process.cwd(), join(resolveDir, path.replace(\"?managed\", \"\"))),\n            namespace: \"managed-style\",\n        }));\n        onLoad({ filter: /\\.css$/, namespace: \"managed-style\" }, async ({ path }) => {\n            const css = await readFile(path, \"utf-8\");\n            const name = relative(process.cwd(), path).replaceAll(\"\\\\\", \"/\");\n\n            return {\n                loader: \"js\",\n                contents: styleModule\n                    .replaceAll(\"STYLE_SOURCE\", JSON.stringify(css))\n                    .replaceAll(\"STYLE_NAME\", JSON.stringify(name))\n            };\n        });\n    }\n};\n\n/**\n * @type {(filter: RegExp, message: string) => import(\"esbuild\").Plugin}\n */\nexport const banImportPlugin = (filter, message) => ({\n    name: \"ban-imports\",\n    setup: build => {\n        build.onResolve({ filter }, () => {\n            return { errors: [{ text: message }] };\n        });\n    }\n});\n\n/**\n * @type {import(\"esbuild\").BuildOptions}\n */\nexport const commonOpts = {\n    logLevel: \"info\",\n    bundle: true,\n    minify: !watch && !IS_REPORTER,\n    sourcemap: watch ? \"inline\" : \"external\",\n    legalComments: \"linked\",\n    banner,\n    plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],\n    external: [\"~plugins\", \"~git-hash\", \"~git-remote\", \"/assets/*\"],\n    inject: [\"./scripts/build/inject/react.mjs\"],\n    jsx: \"transform\",\n    jsxFactory: \"VencordCreateElement\",\n    jsxFragment: \"VencordFragment\"\n};\n\nconst escapedBuiltinModules = builtinModules\n    .map(m => m.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\"))\n    .join(\"|\");\nconst builtinModuleRegex = new RegExp(`^(node:)?(${escapedBuiltinModules})$`);\n\nexport const commonRendererPlugins = [\n    banImportPlugin(builtinModuleRegex, \"Cannot import node inbuilt modules in browser code. You need to use a native.ts file\"),\n    banImportPlugin(/^react$/, \"Cannot import from react. React and hooks should be imported from @webpack/common\"),\n    banImportPlugin(/^electron(\\/.*)?$/, \"Cannot import electron in browser code. You need to use a native.ts file\"),\n    banImportPlugin(/^ts-pattern$/, \"Cannot import from ts-pattern. match and P should be imported from @webpack/common\"),\n    // @ts-expect-error this is never undefined\n    ...commonOpts.plugins\n];\n"
  },
  {
    "path": "scripts/build/inject/react.mjs",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const VencordFragment = /* #__PURE__*/ Symbol.for(\"react.fragment\");\nexport let VencordCreateElement =\n    (...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);\n"
  },
  {
    "path": "scripts/build/module/style.js",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n(window.VencordStyles ??= new Map()).set(STYLE_NAME, {\n    name: STYLE_NAME,\n    source: STYLE_SOURCE,\n    classNames: {},\n    dom: null,\n});\n\nexport default STYLE_NAME;\n"
  },
  {
    "path": "scripts/checkNodeVersion.js",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nif (Number(process.versions.node.split(\".\")[0]) < 18)\n    throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`;\n"
  },
  {
    "path": "scripts/generatePluginList.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Dirent, readdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { join, sep } from \"path\";\nimport { normalize as posixNormalize, sep as posixSep } from \"path/posix\";\nimport { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from \"typescript\";\n\nimport { getPluginTarget } from \"./utils.mjs\";\n\ninterface Dev {\n    name: string;\n    id: string;\n}\n\ninterface PluginData {\n    name: string;\n    description: string;\n    tags: string[];\n    authors: Dev[];\n    dependencies: string[];\n    hasPatches: boolean;\n    hasCommands: boolean;\n    required: boolean;\n    enabledByDefault: boolean;\n    target: \"discordDesktop\" | \"vesktop\" | \"desktop\" | \"web\" | \"dev\";\n    filePath: string;\n}\n\nconst devs = {} as Record<string, Dev>;\n\nfunction getName(node: NamedDeclaration) {\n    return node.name && isIdentifier(node.name) ? node.name.text : undefined;\n}\n\nfunction hasName(node: NamedDeclaration, name: string) {\n    return getName(node) === name;\n}\n\nfunction getObjectProp(node: ObjectLiteralExpression, name: string) {\n    const prop = node.properties.find(p => hasName(p, name));\n    if (prop && isPropertyAssignment(prop)) return prop.initializer;\n    return prop;\n}\n\nfunction parseDevs() {\n    const file = createSourceFile(\"constants.ts\", readFileSync(\"src/utils/constants.ts\", \"utf8\"), ScriptTarget.Latest);\n\n    for (const child of file.getChildAt(0).getChildren()) {\n        if (!isVariableStatement(child)) continue;\n\n        const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, \"Devs\"));\n        if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;\n\n        const value = devsDeclaration.initializer.arguments[0];\n\n        if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error(\"Failed to parse devs: not an object literal\");\n\n        for (const prop of value.expression.properties) {\n            const name = (prop.name as Identifier).text;\n            const value = isPropertyAssignment(prop) ? prop.initializer : prop;\n\n            if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);\n\n            devs[name] = {\n                name: (getObjectProp(value, \"name\") as StringLiteral).text,\n                id: (getObjectProp(value, \"id\") as BigIntLiteral).text.slice(0, -1)\n            };\n        }\n\n        return;\n    }\n\n    throw new Error(\"Could not find Devs constant\");\n}\n\nasync function parseFile(fileName: string) {\n    const file = createSourceFile(fileName, await readFile(fileName, \"utf8\"), ScriptTarget.Latest);\n\n    const fail = (reason: string) => {\n        return new Error(`Invalid plugin ${fileName}, because ${reason}`);\n    };\n\n    for (const node of file.getChildAt(0).getChildren()) {\n        if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;\n\n        const call = node.expression;\n        if (!isIdentifier(call.expression) || call.expression.text !== \"definePlugin\") continue;\n\n        const pluginObj = node.expression.arguments[0];\n        if (!isObjectLiteralExpression(pluginObj)) throw fail(\"no object literal passed to definePlugin\");\n\n        const data = {\n            hasPatches: false,\n            hasCommands: false,\n            enabledByDefault: false,\n            required: false,\n            tags: [] as string[]\n        } as PluginData;\n\n        for (const prop of pluginObj.properties) {\n            const key = getName(prop);\n            const value = isPropertyAssignment(prop) ? prop.initializer : prop;\n\n            switch (key) {\n                case \"name\":\n                case \"description\":\n                    if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);\n                    data[key] = value.text;\n                    break;\n                case \"patches\":\n                    data.hasPatches = true;\n                    break;\n                case \"commands\":\n                    data.hasCommands = true;\n                    break;\n                case \"authors\":\n                    if (!isArrayLiteralExpression(value)) throw fail(\"authors is not an array literal\");\n                    data.authors = value.elements.map(e => {\n                        if (!isPropertyAccessExpression(e)) throw fail(\"authors array contains non-property access expressions\");\n                        const d = devs[getName(e)!];\n                        if (!d) throw fail(`couldn't look up author ${getName(e)}`);\n                        return d;\n                    });\n                    break;\n                case \"tags\":\n                    if (!isArrayLiteralExpression(value)) throw fail(\"tags is not an array literal\");\n                    data.tags = value.elements.map(e => {\n                        if (!isStringLiteral(e)) throw fail(\"tags array contains non-string literals\");\n                        return e.text;\n                    });\n                    break;\n                case \"dependencies\":\n                    if (!isArrayLiteralExpression(value)) throw fail(\"dependencies is not an array literal\");\n                    const { elements } = value;\n                    if (elements.some(e => !isStringLiteral(e))) throw fail(\"dependencies array contains non-string elements\");\n                    data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);\n                    break;\n                case \"required\":\n                case \"enabledByDefault\":\n                    data[key] = value.kind === SyntaxKind.TrueKeyword;\n                    break;\n            }\n        }\n\n        if (!data.name || !data.description || !data.authors) throw fail(\"name, description or authors are missing\");\n\n        const target = getPluginTarget(fileName);\n        if (target) {\n            if (![\"web\", \"discordDesktop\", \"vesktop\", \"desktop\", \"dev\"].includes(target)) throw fail(`invalid target ${target}`);\n            data.target = target as any;\n        }\n\n        data.filePath = posixNormalize(fileName)\n            .split(sep)\n            .join(posixSep)\n            .replace(/\\/index\\.([jt]sx?)$/, \"\")\n            .replace(/^src\\/plugins\\//, \"\");\n\n        let readme = \"\";\n        try {\n            readme = readFileSync(join(fileName, \"..\", \"README.md\"), \"utf-8\");\n        } catch { }\n        return [data, readme] as const;\n    }\n\n    throw fail(\"no default export called 'definePlugin' found\");\n}\n\nasync function getEntryPoint(dir: string, dirent: Dirent) {\n    const base = join(dir, dirent.name);\n    if (!dirent.isDirectory()) return base;\n\n    for (const name of [\"index.ts\", \"index.tsx\"]) {\n        const full = join(base, name);\n        try {\n            await access(full);\n            return full;\n        } catch { }\n    }\n\n    throw new Error(`${dirent.name}: Couldn't find entry point`);\n}\n\nfunction isPluginFile({ name }: { name: string; }) {\n    if (name === \"index.ts\") return false;\n    return !name.startsWith(\"_\") && !name.startsWith(\".\");\n}\n\n(async () => {\n    parseDevs();\n\n    const plugins = [] as PluginData[];\n    const readmes = {} as Record<string, string>;\n\n    await Promise.all([\"src/plugins\", \"src/plugins/_core\"].flatMap(dir =>\n        readdirSync(dir, { withFileTypes: true })\n            .filter(isPluginFile)\n            .map(async dirent => {\n                const [data, readme] = await parseFile(await getEntryPoint(dir, dirent));\n                plugins.push(data);\n                if (readme) readmes[data.name] = readme;\n            })\n    ));\n\n    const data = JSON.stringify(plugins);\n\n    if (process.argv.length > 3) {\n        writeFileSync(process.argv[2], data);\n        writeFileSync(process.argv[3], JSON.stringify(readmes));\n    } else {\n        console.log(data);\n    }\n})();\n"
  },
  {
    "path": "scripts/generateReport.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/// <reference types=\"../src/globals\" />\n/// <reference types=\"../src/modules\" />\n\nimport { createHmac } from \"crypto\";\nimport { readFileSync } from \"fs\";\nimport pup, { JSHandle } from \"puppeteer-core\";\n\nconst logStderr = (...data: any[]) => console.error(`${CANARY ? \"CANARY\" : \"STABLE\"} ---`, ...data);\n\nfor (const variable of [\"CHROMIUM_BIN\"]) {\n    if (!process.env[variable]) {\n        logStderr(`Missing environment variable ${variable}`);\n        process.exit(1);\n    }\n}\n\nconst CANARY = process.env.USE_CANARY === \"true\";\nlet metaData = {\n    buildNumber: \"Unknown Build Number\",\n    buildHash: \"Unknown Build Hash\"\n};\n\nconst browser = await pup.launch({\n    headless: true,\n    executablePath: process.env.CHROMIUM_BIN,\n    args: [\"--no-sandbox\"]\n});\n\nconst page = await browser.newPage();\nawait page.setUserAgent(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36\");\nawait page.setBypassCSP(true);\n\nasync function maybeGetError(handle: JSHandle): Promise<string | undefined> {\n    return await (handle as JSHandle<Error>)?.getProperty(\"message\")\n        .then(m => m?.jsonValue())\n        .catch(() => undefined);\n}\n\ninterface PatchInfo {\n    plugin: string;\n    type: string;\n    id: string;\n    match: string;\n    error?: string;\n};\n\nconst report = {\n    badPatches: [] as PatchInfo[],\n    slowPatches: [] as PatchInfo[],\n    badStarts: [] as {\n        plugin: string;\n        error: string;\n    }[],\n    otherErrors: [] as string[],\n    ignoredErrors: [] as string[],\n    badWebpackFinds: [] as string[]\n};\n\nconst IGNORED_DISCORD_ERRORS = [\n    \"KeybindStore: Looking for callback action\",\n    \"Unable to process domain list delta: Client revision number is null\",\n    \"Downloading the full bad domains file\",\n    /\\[GatewaySocket\\].{0,110}Cannot access '/,\n    \"search for 'name' in undefined\",\n    \"Attempting to set fast connect zstd when unsupported\"\n] as Array<string | RegExp>;\n\nfunction toCodeBlock(s: string, indentation = 0, isDiscord = false) {\n    s = s.replace(/```/g, \"`\\u200B`\\u200B`\");\n\n    const indentationStr = Array(!isDiscord ? indentation : 0).fill(\" \").join(\"\");\n    return `\\`\\`\\`\\n${s.split(\"\\n\").map(s => indentationStr + s).join(\"\\n\")}\\n${indentationStr}\\`\\`\\``;\n}\n\nasync function printReport() {\n    console.log();\n\n    console.log(\"# Vencord Report\" + (CANARY ? \" (Canary)\" : \"\"));\n\n    console.log();\n\n    console.log(\"## Bad Patches\");\n    report.badPatches.forEach(p => {\n        console.log(`- ${p.plugin} (${p.type})`);\n        console.log(`  - ID: \\`${p.id}\\``);\n        console.log(`  - Match: ${toCodeBlock(p.match, \"  - Match: \".length)}`);\n        if (p.error) console.log(`  - Error: ${toCodeBlock(p.error, \"  - Error: \".length)}`);\n    });\n\n    console.log();\n\n    console.log(\"## Bad Webpack Finds\");\n    report.badWebpackFinds.forEach(p => console.log(\"- \" + toCodeBlock(p, \"- \".length)));\n\n    console.log();\n\n    console.log(\"## Bad Starts\");\n    report.badStarts.forEach(p => {\n        console.log(`- ${p.plugin}`);\n        console.log(`  - Error: ${toCodeBlock(p.error, \"  - Error: \".length)}`);\n    });\n\n    console.log();\n\n    console.log(\"## Discord Errors\");\n    report.otherErrors.forEach(e => {\n        console.log(`- ${toCodeBlock(e, \"- \".length)}`);\n    });\n\n    console.log();\n\n    console.log(\"## Ignored Discord Errors\");\n    report.ignoredErrors.forEach(e => {\n        console.log(`- ${toCodeBlock(e, \"- \".length)}`);\n    });\n\n    console.log();\n\n    if (process.env.WEBHOOK_URL) {\n        const patchesToEmbed = (title: string, patches: PatchInfo[], color: number) => ({\n            title,\n            color,\n            description: patches.map(p => {\n                const lines = [\n                    `**__${p.plugin} (${p.type}):__**`,\n                    `ID: \\`${p.id}\\``,\n                    `Match: ${toCodeBlock(p.match, \"Match: \".length, true)}`\n                ];\n                if (p.error) lines.push(`Error: ${toCodeBlock(p.error, \"Error: \".length, true)}`);\n\n                return lines.join(\"\\n\");\n            }).join(\"\\n\\n\"),\n        });\n\n        const embeds = [\n            {\n                author: {\n                    name: `Discord ${CANARY ? \"Canary\" : \"Stable\"} (${metaData.buildNumber})`,\n                    url: `https://nelly.tools/builds/app/${metaData.buildHash}`,\n                    icon_url: CANARY ? \"https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128\" : \"https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128\"\n                },\n                color: CANARY ? 0xfbb642 : 0x5865f2\n            },\n            report.badPatches.length > 0 && patchesToEmbed(\"Bad Patches\", report.badPatches, 0xff0000),\n            report.slowPatches.length > 0 && patchesToEmbed(\"Slow Patches\", report.slowPatches, 0xf0b232),\n            report.badWebpackFinds.length > 0 && {\n                title: \"Bad Webpack Finds\",\n                description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join(\"\\n\") || \"None\",\n                color: 0xff0000\n            },\n            report.badStarts.length > 0 && {\n                title: \"Bad Starts\",\n                description: report.badStarts.map(p => {\n                    const lines = [\n                        `**__${p.plugin}:__**`,\n                        toCodeBlock(p.error, 0, true)\n                    ];\n                    return lines.join(\"\\n\");\n                }\n                ).join(\"\\n\\n\") || \"None\",\n                color: 0xff0000\n            },\n            report.otherErrors.length > 0 && {\n                title: \"Discord Errors\",\n                description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join(\"\\n\"), 0, true) : \"None\",\n                color: 0xff0000\n            }\n        ].filter(Boolean);\n\n        if (embeds.length === 1) {\n            embeds.push({\n                title: \"No issues found\",\n                description: \"Seems like everything is working fine (for now) <:shipit:1330992641466433556>\",\n                color: 0x00ff00\n            });\n        }\n\n        const body = JSON.stringify({\n            username: \"Vencord Reporter\" + (CANARY ? \" (Canary)\" : \"\"),\n            embeds\n        });\n\n        const headers = {\n            \"Content-Type\": \"application/json\"\n        };\n\n        // functions similar to https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries\n        // used by venbot to ensure webhook invocations are genuine (since we will pass the webhook url as a workflow input which is publicly visible)\n        // generate a secret with something like `openssl rand -hex 128`\n        if (process.env.WEBHOOK_SECRET) {\n            headers[\"X-Signature\"] = \"sha256=\" + createHmac(\"sha256\", process.env.WEBHOOK_SECRET).update(body).digest(\"hex\");\n        }\n\n        await fetch(process.env.WEBHOOK_URL, {\n            method: \"POST\",\n            headers,\n            body\n        }).then(res => {\n            if (!res.ok) logStderr(`Webhook failed with status ${res.status}`);\n            else logStderr(\"Posted to Webhook successfully\");\n        });\n    }\n}\n\npage.on(\"console\", async e => {\n    const level = e.type();\n    const rawArgs = e.args();\n\n    async function getText(skipFirst = true) {\n        let args = e.args();\n        if (skipFirst) args = args.slice(1);\n\n        try {\n            return await Promise.all(\n                args.map(async a => {\n                    return await maybeGetError(a) || await a.jsonValue();\n                })\n            ).then(a => a.join(\" \").trim());\n        } catch {\n            return e.text();\n        }\n    }\n\n    const firstArg = await rawArgs[0]?.jsonValue();\n\n    const isVencord = firstArg === \"[Vencord]\";\n    const isDebug = firstArg === \"[PUP_DEBUG]\";\n    const isReporterMeta = firstArg === \"[REPORTER_META]\";\n\n    if (isReporterMeta) {\n        metaData = await rawArgs[1].jsonValue() as any;\n        return;\n    }\n\n    outer:\n    if (isVencord) {\n        try {\n            var args = await Promise.all(e.args().map(a => a.jsonValue()));\n        } catch {\n            break outer;\n        }\n\n        const [, tag, message, otherMessage] = args as Array<string>;\n\n        switch (tag) {\n            case \"WebpackPatcher:\":\n                const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \\(Module id is (.+?)\\): (.+)/);\n                const patchSlowMatch = message.match(/Patch by (.+?) (took [\\d.]+?ms) \\(Module id is (.+?)\\): (.+)/);\n                const match = patchFailMatch ?? patchSlowMatch;\n                if (!match) break;\n\n                logStderr(await getText());\n                process.exitCode = 1;\n\n                const [, plugin, type, id, regex] = match;\n                const list = patchFailMatch ? report.badPatches : report.slowPatches;\n                list.push({\n                    plugin,\n                    type,\n                    id,\n                    match: regex,\n                    error: await maybeGetError(e.args()[3])\n                });\n\n                break;\n            case \"PluginManager:\":\n                const failedToStartMatch = message.match(/Failed to start (.+)/);\n                if (!failedToStartMatch) break;\n\n                logStderr(await getText());\n                process.exitCode = 1;\n\n                const [, name] = failedToStartMatch;\n                report.badStarts.push({\n                    plugin: name,\n                    error: await maybeGetError(e.args()[3]) ?? \"Unknown error\"\n                });\n\n                break;\n            case \"LazyChunkLoader:\":\n                logStderr(await getText());\n\n                switch (message) {\n                    case \"A fatal error occurred:\":\n                        process.exit(1);\n                }\n\n                break;\n            case \"Reporter:\":\n                logStderr(await getText());\n\n                switch (message) {\n                    case \"A fatal error occurred:\":\n                        process.exit(1);\n                    case \"Webpack Find Fail:\":\n                        process.exitCode = 1;\n                        report.badWebpackFinds.push(otherMessage);\n                        break;\n                    case \"Finished test\":\n                        await browser.close();\n                        await printReport();\n                        process.exit();\n                }\n        }\n    }\n\n    if (isDebug) {\n        logStderr(await getText());\n    } else if (level === \"error\") {\n        const text = await getText(false);\n\n        if (text.length && !text.startsWith(\"Failed to load resource: the server responded with a status of\") && !text.includes(\"Webpack\")) {\n            if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {\n                report.ignoredErrors.push(text);\n            } else {\n                logStderr(\"[Unexpected Error]\", text);\n                report.otherErrors.push(text);\n            }\n        }\n    }\n});\n\npage.on(\"error\", e => logStderr(\"[Error]\", e.message));\npage.on(\"pageerror\", (e: any) => {\n    if (e.message.includes(\"Sentry successfully disabled\")) return;\n\n    if (!e.message.startsWith(\"Object\") && !e.message.includes(\"Cannot find module\") && !/^.{1,2}$/.test(e.message)) {\n        logStderr(\"[Page Error]\", e.message);\n        report.otherErrors.push(e.message);\n    } else {\n        report.ignoredErrors.push(e.message);\n    }\n});\n\nawait page.evaluateOnNewDocument(`\n    if (location.host.endsWith(\"discord.com\")) {\n        ${readFileSync(\"./dist/browser.js\", \"utf-8\")};\n    }\n`);\n\nawait page.goto(CANARY ? \"https://canary.discord.com/login\" : \"https://discord.com/login\");\n"
  },
  {
    "path": "scripts/header-new.txt",
    "content": "Vencord, a Discord client mod\nCopyright (c) {year} {author}\nSPDX-License-Identifier: GPL-3.0-or-later\n"
  },
  {
    "path": "scripts/header-old.txt",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) {year} {author}\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n"
  },
  {
    "path": "scripts/runInstaller.mjs",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./checkNodeVersion.js\";\n\nimport { execFileSync, execSync } from \"child_process\";\nimport { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport { fileURLToPath } from \"url\";\n\nconst BASE_URL = \"https://github.com/Vencord/Installer/releases/latest/download/\";\nconst INSTALLER_PATH_DARWIN = \"VencordInstaller.app/Contents/MacOS/VencordInstaller\";\n\nconst BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst FILE_DIR = join(BASE_DIR, \"dist\", \"Installer\");\nconst ETAG_FILE = join(FILE_DIR, \"etag.txt\");\n\nfunction getFilename() {\n    switch (process.platform) {\n        case \"win32\":\n            return \"VencordInstallerCli.exe\";\n        case \"darwin\":\n            return \"VencordInstaller.MacOS.zip\";\n        case \"linux\":\n            return \"VencordInstallerCli-linux\";\n        default:\n            throw new Error(\"Unsupported platform: \" + process.platform);\n    }\n}\n\nasync function ensureBinary() {\n    const filename = getFilename();\n    console.log(\"Downloading \" + filename);\n\n    mkdirSync(FILE_DIR, { recursive: true });\n\n    const downloadName = join(FILE_DIR, filename);\n    const outputFile = process.platform === \"darwin\"\n        ? join(FILE_DIR, \"VencordInstaller\")\n        : downloadName;\n\n    const etag = existsSync(outputFile) && existsSync(ETAG_FILE)\n        ? readFileSync(ETAG_FILE, \"utf-8\")\n        : null;\n\n    const res = await fetch(BASE_URL + filename, {\n        headers: {\n            \"User-Agent\": \"Vencord (https://github.com/Vendicated/Vencord)\",\n            \"If-None-Match\": etag\n        }\n    });\n\n    if (res.status === 304) {\n        console.log(\"Up to date, not redownloading!\");\n        return outputFile;\n    }\n    if (!res.ok)\n        throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`);\n\n    writeFileSync(ETAG_FILE, res.headers.get(\"etag\"));\n\n    if (process.platform === \"darwin\") {\n        console.log(\"Unzipping...\");\n        const zip = new Uint8Array(await res.arrayBuffer());\n\n        const ff = await import(\"fflate\");\n        const bytes = ff.unzipSync(zip, {\n            filter: f => f.name === INSTALLER_PATH_DARWIN\n        })[INSTALLER_PATH_DARWIN];\n\n        writeFileSync(outputFile, bytes, { mode: 0o755 });\n\n        console.log(\"Overriding security policy for installer binary (this is required to run it)\");\n        console.log(\"xattr might error, that's okay\");\n\n        const logAndRun = cmd => {\n            console.log(\"Running\", cmd);\n            try {\n                execSync(cmd);\n            } catch { }\n        };\n        logAndRun(`sudo spctl --add '${outputFile}' --label \"Vencord Installer\"`);\n        logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);\n    } else {\n        // WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD\n        const body = Readable.fromWeb(res.body);\n        await finished(body.pipe(createWriteStream(outputFile, {\n            mode: 0o755,\n            autoClose: true\n        })));\n    }\n\n    console.log(\"Finished downloading!\");\n\n    return outputFile;\n}\n\n\n\nconst installerBin = await ensureBinary();\n\nconsole.log(\"Now running Installer...\");\n\nconst argStart = process.argv.indexOf(\"--\");\nconst args = argStart === -1 ? [] : process.argv.slice(argStart + 1);\n\ntry {\n    execFileSync(installerBin, args, {\n        stdio: \"inherit\",\n        env: {\n            ...process.env,\n            VENCORD_USER_DATA_DIR: BASE_DIR,\n            VENCORD_DEV_INSTALL: \"1\"\n        }\n    });\n} catch {\n    console.error(\"Something went wrong. Please check the logs above.\");\n}\n"
  },
  {
    "path": "scripts/suppressExperimentalWarnings.js",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nprocess.emit = (originalEmit => function (name, data) {\n    if (name === \"warning\" && data?.name === \"ExperimentalWarning\")\n        return false;\n\n    return originalEmit.apply(process, arguments);\n})(process.emit);\n"
  },
  {
    "path": "scripts/utils.mjs",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/**\n * @param {string} filePath\n * @returns {string | null}\n */\nexport function getPluginTarget(filePath) {\n    const pathParts = filePath.split(/[/\\\\]/);\n    if (/^index\\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();\n\n    const identifier = pathParts.at(-1).replace(/\\.tsx?$/, \"\");\n    const identiferBits = identifier.split(\".\");\n    return identiferBits.length === 1 ? null : identiferBits.at(-1);\n}\n"
  },
  {
    "path": "src/Vencord.ts",
    "content": "/*!\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!!\nimport \"~plugins\";\n\nexport * as Api from \"./api\";\nexport * as Plugins from \"./api/PluginManager\";\nexport * as Components from \"./components\";\nexport * as Util from \"./utils\";\nexport * as Updater from \"./utils/updater\";\nexport * as Webpack from \"./webpack\";\nexport * as WebpackPatcher from \"./webpack/patchWebpack\";\nexport { PlainSettings, Settings };\n\nimport { coreStyleRootNode, initStyles } from \"@api/Styles\";\nimport { openSettingsTabModal, UpdaterTab } from \"@components/settings\";\nimport { debounce } from \"@shared/debounce\";\nimport { IS_WINDOWS } from \"@utils/constants\";\nimport { createAndAppendStyle } from \"@utils/css\";\nimport { StartAt } from \"@utils/types\";\nimport { SettingsRouter } from \"@webpack/common\";\n\nimport { get as dsGet } from \"./api/DataStore\";\nimport { NotificationData, showNotification } from \"./api/Notifications\";\nimport { initPluginManager, PMLogger, startAllPlugins } from \"./api/PluginManager\";\nimport { PlainSettings, Settings, SettingsStore } from \"./api/Settings\";\nimport { getCloudSettings, putCloudSettings, shouldCloudSync } from \"./api/SettingsSync/cloudSync\";\nimport { localStorage } from \"./utils/localStorage\";\nimport { relaunch } from \"./utils/native\";\nimport { checkForUpdates, update, UpdateLogger } from \"./utils/updater\";\nimport { onceReady } from \"./webpack\";\nimport { patches } from \"./webpack/patchWebpack\";\n\nif (IS_REPORTER) {\n    require(\"./debug/runReporter\");\n}\n\nasync function syncSettings() {\n    if (localStorage.Vencord_cloudSyncDirection === undefined) {\n        // by default, sync bi-directionally\n        localStorage.Vencord_cloudSyncDirection = \"both\";\n    }\n\n    // pre-check for local shared settings\n    if (\n        Settings.cloud.authenticated &&\n        !await dsGet(\"Vencord_cloudSecret\") // this has been enabled due to local settings share or some other bug\n    ) {\n        // show a notification letting them know and tell them how to fix it\n        showNotification({\n            title: \"Cloud Integrations\",\n            body: \"We've noticed you have cloud integrations enabled in another client! Due to limitations, you will \" +\n                \"need to re-authenticate to continue using them. Click here to go to the settings page to do so!\",\n            color: \"var(--yellow-360)\",\n            onClick: () => SettingsRouter.openUserSettings(\"vencord_cloud_panel\")\n        });\n        return;\n    }\n\n    if (\n        Settings.cloud.settingsSync && // if it's enabled\n        Settings.cloud.authenticated && // if cloud integrations are enabled\n        localStorage.Vencord_cloudSyncDirection !== \"manual\" // if we're not in manual mode\n    ) {\n        if (localStorage.Vencord_settingsDirty && shouldCloudSync(\"push\")) {\n            await putCloudSettings();\n        } else if (shouldCloudSync(\"pull\") && await getCloudSettings(false)) { // if we synchronized something (false means no sync)\n            // we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of\n            // potential notifications that might occur. getCloudSettings() will always send a notification regardless if\n            // there was an error to notify the user, but besides that we only want to show one notification instead of all\n            // of the possible ones it has (such as when your settings are newer).\n            showNotification({\n                title: \"Cloud Settings\",\n                body: \"Your settings have been updated! Click here to restart to fully apply changes!\",\n                color: \"var(--green-360)\",\n                onClick: relaunch\n            });\n        }\n    }\n\n    const saveSettingsOnFrequentAction = debounce(async () => {\n        if (Settings.cloud.settingsSync && Settings.cloud.authenticated && shouldCloudSync(\"push\")) {\n            await putCloudSettings();\n        }\n    }, 60_000);\n\n    SettingsStore.addGlobalChangeListener(() => {\n        localStorage.Vencord_settingsDirty = true;\n        saveSettingsOnFrequentAction();\n    });\n}\n\nlet notifiedForUpdatesThisSession = false;\n\nasync function runUpdateCheck() {\n    if (IS_UPDATER_DISABLED) return;\n\n    const notify = (data: NotificationData) => {\n        if (notifiedForUpdatesThisSession) return;\n        notifiedForUpdatesThisSession = true;\n\n        setTimeout(() => showNotification({\n            permanent: true,\n            noPersist: true,\n            ...data\n        }), 10_000);\n    };\n\n    try {\n        const isOutdated = await checkForUpdates();\n        if (!isOutdated) return;\n\n        if (Settings.autoUpdate) {\n            await update();\n            if (Settings.autoUpdateNotification) {\n                notify({\n                    title: \"Vencord has been updated!\",\n                    body: \"Click here to restart\",\n                    onClick: relaunch\n                });\n            }\n            return;\n        }\n\n        notify({\n            title: \"A Vencord update is available!\",\n            body: \"Click here to view the update\",\n            onClick: () => openSettingsTabModal(UpdaterTab!)\n        });\n    } catch (err) {\n        UpdateLogger.error(\"Failed to check for updates\", err);\n    }\n}\n\nasync function init() {\n    await onceReady;\n    startAllPlugins(StartAt.WebpackReady);\n\n    syncSettings();\n\n    if (!IS_WEB && !IS_UPDATER_DISABLED) {\n        runUpdateCheck();\n\n        // this tends to get really annoying, so only do this if the user has auto-update without notification enabled\n        if (Settings.autoUpdate && !Settings.autoUpdateNotification) {\n            setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes\n        }\n    }\n\n    if (IS_DEV) {\n        const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);\n        if (pendingPatches.length)\n            PMLogger.warn(\n                \"Webpack has finished initialising, but some patches haven't been applied yet.\",\n                \"This might be expected since some Modules are lazy loaded, but please verify\",\n                \"that all plugins are working as intended.\",\n                \"You are seeing this warning because this is a Development build of Vencord.\",\n                \"\\nThe following patches have not been applied:\",\n                \"\\n\\n\" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join(\"\\n\")\n            );\n    }\n}\n\ninitPluginManager();\ninitStyles();\nstartAllPlugins(StartAt.Init);\ninit();\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    startAllPlugins(StartAt.DOMContentLoaded);\n\n    // FIXME\n    if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) {\n        createAndAppendStyle(\"vencord-native-titlebar-style\", coreStyleRootNode).textContent = \"[class*=titleBar]{display: none!important}\";\n    }\n}, { once: true });\n"
  },
  {
    "path": "src/VencordNative.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport type { Settings } from \"@api/Settings\";\nimport type { CspRequestResult } from \"@main/csp/manager\";\nimport type { PluginIpcMappings } from \"@main/ipcPlugins\";\nimport type { UserThemeHeader } from \"@main/themes\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport type { IpcRes } from \"@utils/types\";\nimport { ipcRenderer } from \"electron/renderer\";\n\nexport function invoke<T = any>(event: IpcEvents, ...args: any[]) {\n    return ipcRenderer.invoke(event, ...args) as Promise<T>;\n}\n\nexport function sendSync<T = any>(event: IpcEvents, ...args: any[]) {\n    return ipcRenderer.sendSync(event, ...args) as T;\n}\n\nconst PluginHelpers = {} as Record<string, Record<string, (...args: any[]) => Promise<any>>>;\nconst pluginIpcMap = sendSync<PluginIpcMappings>(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP);\n\nfor (const [plugin, methods] of Object.entries(pluginIpcMap)) {\n    const map = PluginHelpers[plugin] = {};\n    for (const [methodName, method] of Object.entries(methods)) {\n        map[methodName] = (...args: any[]) => invoke(method as IpcEvents, ...args);\n    }\n}\n\nexport default {\n    themes: {\n        uploadTheme: async (fileName: string, fileData: string): Promise<void> => {\n            throw new Error(\"uploadTheme is WEB only\");\n        },\n        deleteTheme: async (fileName: string): Promise<void> => {\n            throw new Error(\"deleteTheme is WEB only\");\n        },\n        getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),\n        getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),\n        getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),\n\n        openFolder: () => invoke<void>(IpcEvents.OPEN_THEMES_FOLDER),\n    },\n\n    updater: {\n        getUpdates: () => invoke<IpcRes<Record<\"hash\" | \"author\" | \"message\", string>[]>>(IpcEvents.GET_UPDATES),\n        update: () => invoke<IpcRes<boolean>>(IpcEvents.UPDATE),\n        rebuild: () => invoke<IpcRes<boolean>>(IpcEvents.BUILD),\n        getRepo: () => invoke<IpcRes<string>>(IpcEvents.GET_REPO),\n    },\n\n    settings: {\n        get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),\n        set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),\n\n        openFolder: () => invoke<void>(IpcEvents.OPEN_SETTINGS_FOLDER),\n    },\n\n    quickCss: {\n        get: () => invoke<string>(IpcEvents.GET_QUICK_CSS),\n        set: (css: string) => invoke<void>(IpcEvents.SET_QUICK_CSS, css),\n\n        addChangeListener(cb: (newCss: string) => void) {\n            ipcRenderer.on(IpcEvents.QUICK_CSS_UPDATE, (_, css) => cb(css));\n        },\n\n        addThemeChangeListener(cb: () => void) {\n            ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());\n        },\n\n        openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),\n        openEditor: () => invoke<void>(IpcEvents.OPEN_MONACO_EDITOR),\n        getEditorTheme: () => sendSync<string>(IpcEvents.GET_MONACO_THEME),\n    },\n\n    native: {\n        getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,\n        openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url),\n        getRendererCss: () => invoke<string>(IpcEvents.GET_RENDERER_CSS),\n        onRendererCssUpdate: (cb: (newCss: string) => void) => {\n            if (!IS_DEV) return;\n\n            ipcRenderer.on(IpcEvents.RENDERER_CSS_UPDATE, (_e, newCss: string) => cb(newCss));\n        }\n    },\n\n    csp: {\n        /**\n         * Note: Only supports full explicit matches, not wildcards.\n         *\n         * If `*.example.com` is allowed, `isDomainAllowed(\"https://sub.example.com\")` will return false.\n         */\n        isDomainAllowed: (url: string, directives: string[]) => invoke<boolean>(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives),\n        removeOverride: (url: string) => invoke<boolean>(IpcEvents.CSP_REMOVE_OVERRIDE, url),\n        requestAddOverride: (url: string, directives: string[], callerName: string) =>\n            invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),\n    },\n\n    pluginHelpers: PluginHelpers\n};\n"
  },
  {
    "path": "src/api/Badges.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport BadgeAPIPlugin from \"@plugins/_api/badges\";\nimport { ComponentType, HTMLProps } from \"react\";\n\nexport const enum BadgePosition {\n    START,\n    END\n}\n\nexport interface ProfileBadge {\n    /** The tooltip to show on hover. Required for image badges */\n    description?: string;\n    /** Custom component for the badge (tooltip not included) */\n    component?: ComponentType<ProfileBadge & BadgeUserArgs>;\n    /** The custom image to use */\n    iconSrc?: string;\n    link?: string;\n    /** Action to perform when you click the badge */\n    onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;\n    /** Action to perform when you right click the badge */\n    onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void;\n    /** Should the user display this badge? */\n    shouldShow?(userInfo: BadgeUserArgs): boolean;\n    /** Optional props (e.g. style) for the badge, ignored for component badges */\n    props?: HTMLProps<HTMLImageElement>;\n    /** Insert at start or end? */\n    position?: BadgePosition;\n    /** The badge name to display, Discord uses this. Required for component badges */\n    key?: string;\n\n    /**\n     * Allows dynamically returning multiple badges.\n     * Must not call hooks\n     */\n    getBadges?(userInfo: BadgeUserArgs): ProfileBadge[];\n}\n\nconst Badges = new Set<ProfileBadge>();\n\n/**\n * Register a new badge with the Badges API\n * @param badge The badge to register\n */\nexport function addProfileBadge(badge: ProfileBadge) {\n    badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });\n    Badges.add(badge);\n}\n\n/**\n * Unregister a badge from the Badges API\n * @param badge The badge to remove\n */\nexport function removeProfileBadge(badge: ProfileBadge) {\n    return Badges.delete(badge);\n}\n\n/**\n * Inject badges into the profile badges array.\n * You probably don't need to use this.\n */\nexport function _getBadges(args: BadgeUserArgs) {\n    const badges = [] as ProfileBadge[];\n    for (const badge of Badges) {\n        if (badge.shouldShow && !badge.shouldShow(args)) {\n            continue;\n        }\n\n        const b = badge.getBadges\n            ? badge.getBadges(args).map(badge => ({\n                ...args,\n                ...badge,\n                component: badge.component && ErrorBoundary.wrap(badge.component, { noop: true })\n            }))\n            : [{ ...args, ...badge }];\n\n        if (badge.position === BadgePosition.START) {\n            badges.unshift(...b);\n        } else {\n            badges.push(...b);\n        }\n    }\n\n    const donorBadges = BadgeAPIPlugin.getDonorBadges(args.userId);\n    if (donorBadges) {\n        badges.unshift(\n            ...donorBadges.map(badge => ({\n                ...args,\n                ...badge,\n            }))\n        );\n    }\n\n    return badges;\n}\n\nexport interface BadgeUserArgs {\n    userId: string;\n    guildId: string;\n}\n"
  },
  {
    "path": "src/api/ChatButton.css",
    "content": ".vc-chatbar-button {\n    display: flex;\n    align-items: center;\n}\n"
  },
  {
    "path": "src/api/ChatButtons.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./ChatButton.css\";\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Logger } from \"@utils/Logger\";\nimport { classes } from \"@utils/misc\";\nimport { IconComponent } from \"@utils/types\";\nimport { Channel } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Clickable, Menu, Tooltip } from \"@webpack/common\";\nimport { HTMLProps, JSX, MouseEventHandler, ReactNode } from \"react\";\n\nimport { addContextMenuPatch, findGroupChildrenByChildId } from \"./ContextMenu\";\nimport { useSettings } from \"./Settings\";\n\nconst ButtonWrapperClasses = findCssClassesLazy(\"button\", \"buttonWrapper\", \"notificationDot\");\nconst ChannelTextAreaClasses = findCssClassesLazy(\"buttonContainer\", \"channelTextArea\", \"button\");\n\nexport interface ChatBarProps {\n    channel: Channel;\n    disabled: boolean;\n    isEmpty: boolean;\n    type: {\n        analyticsName: string;\n        attachments: boolean;\n        autocomplete: {\n            addReactionShortcut: boolean,\n            forceChatLayer: boolean,\n            reactions: boolean;\n        },\n        commands: {\n            enabled: boolean;\n        },\n        drafts: {\n            type: number,\n            commandType: number,\n            autoSave: boolean;\n        },\n        emojis: {\n            button: boolean;\n        },\n        gifs: {\n            button: boolean,\n            allowSending: boolean;\n        },\n        gifts: {\n            button: boolean;\n        },\n        permissions: {\n            requireSendMessages: boolean;\n        },\n        showThreadPromptOnReply: boolean,\n        stickers: {\n            button: boolean,\n            allowSending: boolean,\n            autoSuggest: boolean;\n        },\n        users: {\n            allowMentioning: boolean;\n        },\n        submit: {\n            button: boolean,\n            ignorePreference: boolean,\n            disableEnterToSubmit: boolean,\n            clearOnSubmit: boolean,\n            useDisabledStylesOnSubmit: boolean;\n        },\n        uploadLongMessages: boolean,\n        upsellLongMessages: {\n            iconOnly: boolean;\n        },\n        showCharacterCount: boolean,\n        sedReplace: boolean;\n    };\n}\n\nexport type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; isAnyChat: boolean; }) => JSX.Element | null;\nexport type ChatBarButtonData = {\n    render: ChatBarButtonFactory;\n    /**\n     * This icon is used only for Settings UI. Your render function must still render an icon,\n     * and it can be different from this one.\n     */\n    icon: IconComponent;\n};\n\n/**\n * Don't use this directly, use {@link addChatBarButton} and {@link removeChatBarButton} instead.\n */\nexport const ChatBarButtonMap = new Map<string, ChatBarButtonData>();\nconst logger = new Logger(\"ChatButtons\");\n\nfunction VencordChatBarButtons(props: ChatBarProps) {\n    const { chatBarButtons } = useSettings([\"uiElements.chatBarButtons.*\"]).uiElements;\n\n    const { analyticsName } = props.type;\n    return (\n        <>\n            {Array.from(ChatBarButtonMap)\n                .filter(([key]) => chatBarButtons[key]?.enabled !== false)\n                .map(([key, { render: Button }]) => (\n                    <ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>\n                        <Button {...props} isMainChat={analyticsName === \"normal\"} isAnyChat={[\"normal\", \"sidebar\"].includes(analyticsName)} />\n                    </ErrorBoundary>\n                ))}\n        </>\n    );\n}\n\nexport function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {\n    if (props.disabled || buttons.length === 0) return;\n\n    buttons.unshift(<VencordChatBarButtons key=\"vencord-chat-buttons\" {...props} />);\n}\n\n/**\n * The icon argument is used only for Settings UI. Your render function must still render an icon,\n * and it can be different from this one.\n */\nexport const addChatBarButton = (id: string, render: ChatBarButtonFactory, icon: IconComponent) => ChatBarButtonMap.set(id, { render, icon });\nexport const removeChatBarButton = (id: string) => ChatBarButtonMap.delete(id);\n\nexport interface ChatBarButtonProps {\n    children: ReactNode;\n    tooltip: string;\n    onClick: MouseEventHandler;\n    onContextMenu?: MouseEventHandler;\n    onAuxClick?: MouseEventHandler;\n    buttonProps?: Omit<HTMLProps<HTMLDivElement>, \"size\" | \"onClick\" | \"onContextMenu\" | \"onAuxClick\">;\n}\n\nexport const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {\n    return (\n        <Tooltip text={props.tooltip}>\n            {({ onMouseEnter, onMouseLeave }) => (\n                <div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? \"\"} vc-chatbar-button`}>\n                    <Clickable\n                        aria-label={props.tooltip}\n                        onMouseEnter={onMouseEnter}\n                        onMouseLeave={onMouseLeave}\n                        className={classes(ButtonWrapperClasses.button, ChannelTextAreaClasses?.button)}\n                        onClick={props.onClick}\n                        onContextMenu={props.onContextMenu}\n                        onAuxClick={props.onAuxClick}\n                        {...props.buttonProps}\n                    >\n                        <div className={ButtonWrapperClasses.buttonWrapper}>\n                            {props.children}\n                        </div>\n                    </Clickable>\n                </div>\n            )}\n        </Tooltip>\n    );\n}, { noop: true });\n\naddContextMenuPatch(\"textarea-context\", (children, args) => {\n    const { chatBarButtons } = useSettings([\"uiElements.chatBarButtons.*\"]).uiElements;\n\n    const buttons = Array.from(ChatBarButtonMap.entries());\n    if (!buttons.length) return;\n\n    const group = findGroupChildrenByChildId(\"submit-button\", children);\n    if (!group) return;\n\n    const idx = group.findIndex(c => c?.props?.id === \"submit-button\");\n    if (idx === -1) return;\n\n    group.splice(idx, 0,\n        <Menu.MenuItem id=\"vc-chat-buttons\" key=\"vencord-chat-buttons\" label=\"Vencord Buttons\">\n            {buttons.map(([id]) => (\n                <Menu.MenuCheckboxItem\n                    label={id}\n                    key={id}\n                    id={`vc-chat-button-${id}`}\n                    checked={chatBarButtons[id]?.enabled !== false}\n                    action={() => {\n                        const wasEnabled = chatBarButtons[id]?.enabled !== false;\n\n                        chatBarButtons[id] ??= {} as any;\n                        chatBarButtons[id].enabled = !wasEnabled;\n                    }}\n                />\n            ))}\n        </Menu.MenuItem>\n    );\n});\n"
  },
  {
    "path": "src/api/Commands/commandHelpers.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { mergeDefaults } from \"@utils/mergeDefaults\";\nimport { CommandArgument, Message } from \"@vencord/discord-types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { MessageActions, SnowflakeUtils } from \"@webpack/common\";\nimport type { PartialDeep } from \"type-fest\";\n\nconst createBotMessage = findByCodeLazy('username:\"Clyde\"');\n\nexport function generateId() {\n    return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;\n}\n\n/**\n * Send a message as Clyde\n * @param {string} channelId ID of channel to send message to\n * @param {Message} message Message to send\n * @returns {Message}\n */\nexport function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {\n    const botMessage = createBotMessage({ channelId, content: \"\", embeds: [] });\n\n    MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));\n\n    return message as Message;\n}\n\n/**\n * Get the value of an option by name\n * @param args Arguments array (first argument passed to execute)\n * @param name Name of the argument\n * @param fallbackValue Fallback value in case this option wasn't passed\n * @returns Value\n */\nexport function findOption<T>(args: CommandArgument[], name: string): T & {} | undefined;\nexport function findOption<T>(args: CommandArgument[], name: string, fallbackValue: T): T & {};\nexport function findOption(args: CommandArgument[], name: string, fallbackValue?: any) {\n    return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;\n}\n"
  },
  {
    "path": "src/api/Commands/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Logger } from \"@utils/Logger\";\nimport { makeCodeblock } from \"@utils/text\";\nimport { CommandArgument, CommandContext, CommandOption } from \"@vencord/discord-types\";\n\nimport { sendBotMessage } from \"./commandHelpers\";\nimport { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, VencordCommand } from \"./types\";\n\nexport * from \"./commandHelpers\";\nexport * from \"./types\";\n\nexport let BUILT_IN: VencordCommand[];\nexport const commands = {} as Record<string, VencordCommand>;\n\n// hack for plugins being evaluated before we can grab these from webpack\nconst OptPlaceholder = Symbol(\"OptionalMessageOption\") as any as CommandOption;\nconst ReqPlaceholder = Symbol(\"RequiredMessageOption\") as any as CommandOption;\n\n/**\n * Optional message option named \"message\" you can use in commands.\n * Used in \"tableflip\" or \"shrug\"\n * @see {@link RequiredMessageOption}\n */\nexport let OptionalMessageOption: CommandOption = OptPlaceholder;\n/**\n * Required message option named \"message\" you can use in commands.\n * Used in \"me\"\n * @see {@link OptionalMessageOption}\n */\nexport let RequiredMessageOption: CommandOption = ReqPlaceholder;\n\n// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands\n// Add this offset to every added command to keep them unique\nlet commandIdOffset: number;\n\nexport const _init = function (cmds: VencordCommand[]) {\n    try {\n        BUILT_IN = cmds;\n        OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === \"shrug\")!.options![0];\n        RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === \"me\")!.options![0];\n        commandIdOffset = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) - BUILT_IN.length;\n    } catch (e) {\n        new Logger(\"CommandsAPI\").error(\"Failed to load CommandsApi\", e, \" - cmds is\", cmds);\n    }\n    return cmds;\n} as never;\n\nexport const _handleCommand = function (cmd: VencordCommand, args: CommandArgument[], ctx: CommandContext) {\n    if (!cmd.isVencordCommand)\n        return cmd.execute(args, ctx);\n\n    const handleError = (err: any) => {\n        // TODO: cancel send if cmd.inputType === BUILT_IN_TEXT\n        const msg = `An Error occurred while executing command \"${cmd.name}\"`;\n        const reason = err instanceof Error ? err.stack || err.message : String(err);\n\n        console.error(msg, err);\n        sendBotMessage(ctx.channel.id, {\n            content: `${msg}:\\n${makeCodeblock(reason)}`,\n            author: {\n                username: \"Vencord\"\n            }\n        });\n    };\n\n    try {\n        const res = cmd.execute(args, ctx);\n        return res instanceof Promise ? res.catch(handleError) : res;\n    } catch (err) {\n        return handleError(err);\n    }\n} as never;\n\n\n/**\n * Prepare a Command Option for Discord by filling missing fields\n * @param opt\n */\nexport function prepareOption<O extends CommandOption | VencordCommand>(opt: O): O {\n    opt.displayName ||= opt.name;\n    opt.displayDescription ||= opt.description;\n    opt.options?.forEach((opt, i, opts) => {\n        // See comment above Placeholders\n        if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;\n        else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;\n        opt.choices?.forEach(x => x.displayName ||= x.name);\n\n        prepareOption(opts[i]);\n    });\n    return opt;\n}\n\n// Yes, Discord registers individual commands for each subcommand\n// TODO: This probably doesn't support nested subcommands. If that is ever needed,\n// investigate\nfunction registerSubCommands(cmd: VencordCommand, plugin: string) {\n    cmd.options?.forEach(o => {\n        if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)\n            throw new Error(\"When specifying sub-command options, all options must be sub-commands.\");\n        const subCmd = {\n            ...cmd,\n            ...o,\n            options: o.options !== undefined ? o.options : undefined,\n            type: ApplicationCommandType.CHAT_INPUT,\n            name: `${cmd.name} ${o.name}`,\n            id: `${o.name}-${cmd.id}`,\n            displayName: `${cmd.name} ${o.name}`,\n            subCommandPath: [{\n                name: o.name,\n                type: o.type,\n                displayName: o.name\n            }],\n            rootCommand: cmd\n        };\n        registerCommand(subCmd as any, plugin);\n    });\n}\n\nexport function registerCommand<C extends VencordCommand>(command: C, plugin: string) {\n    if (!BUILT_IN) {\n        console.warn(\n            \"[CommandsAPI]\",\n            `Not registering ${command.name} as the CommandsAPI hasn't been initialised.`,\n            \"Please restart to use commands\"\n        );\n        return;\n    }\n\n    if (BUILT_IN.some(c => c.name === command.name))\n        throw new Error(`Command '${command.name}' already exists.`);\n\n    command.isVencordCommand = true;\n    command.untranslatedName ??= command.name;\n    command.untranslatedDescription ??= command.description;\n    command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;\n    command.applicationId ??= \"-1\"; // BUILT_IN;\n    command.type ??= ApplicationCommandType.CHAT_INPUT;\n    command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;\n    command.plugin ||= plugin;\n\n    prepareOption(command);\n\n    if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {\n        registerSubCommands(command, plugin);\n        return;\n    }\n\n    commands[command.name] = command;\n    BUILT_IN.push(command);\n}\n\nexport function unregisterCommand(name: string) {\n    const idx = BUILT_IN.findIndex(c => c.name === name);\n    if (idx === -1)\n        return false;\n\n    BUILT_IN.splice(idx, 1);\n    delete commands[name];\n\n    return true;\n}\n"
  },
  {
    "path": "src/api/Commands/types.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Command } from \"@vencord/discord-types\";\nexport { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from \"@vencord/discord-types/enums\";\n\nexport interface VencordCommand extends Command {\n    isVencordCommand?: boolean;\n}\n"
  },
  {
    "path": "src/api/ContextMenu.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Logger } from \"@utils/Logger\";\nimport { Menu, React } from \"@webpack/common\";\nimport type { ReactElement } from \"react\";\n\n/**\n * @param children The rendered context menu elements\n * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example\n */\nexport type NavContextMenuPatchCallback = (children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;\n/**\n * @param navId The navId of the context menu being patched\n * @param children The rendered context menu elements\n * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example\n */\nexport type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;\n\nconst ContextMenuLogger = new Logger(\"ContextMenu\");\n\nexport const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();\nexport const globalPatches = new Set<GlobalContextMenuPatchCallback>();\n\n/**\n * Add a context menu patch\n * @param navId The navId(s) for the context menu(s) to patch\n * @param patch The patch to be applied\n */\nexport function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {\n    if (!Array.isArray(navId)) navId = [navId];\n    for (const id of navId) {\n        let contextMenuPatches = navPatches.get(id);\n        if (!contextMenuPatches) {\n            contextMenuPatches = new Set();\n            navPatches.set(id, contextMenuPatches);\n        }\n\n        contextMenuPatches.add(patch);\n    }\n}\n\n/**\n * Add a global context menu patch that fires the patch for all context menus\n * @param patch The patch to be applied\n */\nexport function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {\n    globalPatches.add(patch);\n}\n\n/**\n * Remove a context menu patch\n * @param navId The navId(s) for the context menu(s) to remove the patch\n * @param patch The patch to be removed\n * @returns Whether the patch was successfully removed from the context menu(s)\n */\nexport function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {\n    const navIds: string[] = Array.isArray(navId) ? navId : [navId];\n\n    const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);\n\n    return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;\n}\n\n/**\n * Remove a global context menu patch\n * @param patch The patch to be removed\n * @returns Whether the patch was successfully removed\n */\nexport function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {\n    return globalPatches.delete(patch);\n}\n\n/**\n * A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children\n * @param id The id of the child. If an array is specified, all ids will be tried\n * @param children The context menu children\n * @param matchSubstring Whether to check if the id is a substring of the child id\n */\nexport function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement<any> | null | undefined>, matchSubstring = false): Array<ReactElement<any> | null | undefined> | null {\n    for (const child of children) {\n        if (child == null) continue;\n\n        if (Array.isArray(child)) {\n            const found = findGroupChildrenByChildId(id, child, matchSubstring);\n            if (found !== null) return found;\n        }\n\n        if (\n            (Array.isArray(id) && id.some(id => matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id))\n            || (matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id)\n        ) return children;\n\n        let nextChildren = child.props?.children;\n        if (nextChildren) {\n            if (!Array.isArray(nextChildren)) {\n                nextChildren = [nextChildren];\n                child.props.children = nextChildren;\n            }\n\n            const found = findGroupChildrenByChildId(id, nextChildren, matchSubstring);\n            if (found !== null) return found;\n        }\n    }\n\n    return null;\n}\n\ninterface ContextMenuProps {\n    contextMenuAPIArguments?: Array<any>;\n    navId: string;\n    children: Array<ReactElement<any> | null>;\n    \"aria-label\": string;\n    onSelect: (() => void) | undefined;\n    onClose: (callback: (...args: Array<any>) => any) => void;\n}\n\nexport function _usePatchContextMenu(props: ContextMenuProps) {\n    props = {\n        ...props,\n        children: cloneMenuChildren(props.children),\n    };\n\n    props.contextMenuAPIArguments ??= [];\n    const contextMenuPatches = navPatches.get(props.navId);\n\n    if (!Array.isArray(props.children)) props.children = [props.children];\n\n    if (contextMenuPatches) {\n        for (const patch of contextMenuPatches) {\n            try {\n                patch(props.children, ...props.contextMenuAPIArguments);\n            } catch (err) {\n                ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);\n            }\n        }\n    }\n\n    for (const patch of globalPatches) {\n        try {\n            patch(props.navId, props.children, ...props.contextMenuAPIArguments);\n        } catch (err) {\n            ContextMenuLogger.error(\"Global patch errored,\", err);\n        }\n    }\n\n    return props;\n}\n\nfunction cloneMenuChildren(obj: ReactElement<any> | Array<ReactElement<any> | null> | null) {\n    if (Array.isArray(obj)) {\n        return obj.map(cloneMenuChildren);\n    }\n\n    if (React.isValidElement(obj)) {\n        obj = React.cloneElement(obj);\n\n        if (\n            obj?.props?.children &&\n            (obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)\n        ) {\n            obj.props.children = cloneMenuChildren(obj.props.children);\n        }\n    }\n\n    return obj;\n}\n"
  },
  {
    "path": "src/api/DataStore/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "src/api/DataStore/index.ts",
    "content": "/* eslint-disable simple-header/header */\n\n/*!\n * idb-keyval v6.2.0\n * Copyright 2016, Jake Archibald\n * Copyright 2022, Vendicated\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport function promisifyRequest<T = undefined>(\n    request: IDBRequest<T> | IDBTransaction,\n): Promise<T> {\n    return new Promise<T>((resolve, reject) => {\n        // @ts-expect-error - file size hacks\n        request.oncomplete = request.onsuccess = () => resolve(request.result);\n        // @ts-expect-error - file size hacks\n        request.onabort = request.onerror = () => reject(request.error);\n    });\n}\n\nexport function createStore(dbName: string, storeName: string): UseStore {\n    const request = indexedDB.open(dbName);\n    request.onupgradeneeded = () => request.result.createObjectStore(storeName);\n    const dbp = promisifyRequest(request);\n\n    return (txMode, callback) =>\n        dbp.then(db =>\n            callback(db.transaction(storeName, txMode).objectStore(storeName)),\n        );\n}\n\nexport type UseStore = <T>(\n    txMode: IDBTransactionMode,\n    callback: (store: IDBObjectStore) => T | PromiseLike<T>,\n) => Promise<T>;\n\nlet defaultGetStoreFunc: UseStore | undefined;\n\nfunction defaultGetStore() {\n    if (!defaultGetStoreFunc) {\n        defaultGetStoreFunc = createStore(!IS_REPORTER ? \"VencordData\" : \"VencordDataReporter\", \"VencordStore\");\n    }\n    return defaultGetStoreFunc;\n}\n\n/**\n * Get a value by its key.\n *\n * @param key\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function get<T = any>(\n    key: IDBValidKey,\n    customStore = defaultGetStore(),\n): Promise<T | undefined> {\n    return customStore(\"readonly\", store => promisifyRequest(store.get(key)));\n}\n\n/**\n * Set a value with a key.\n *\n * @param key\n * @param value\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function set(\n    key: IDBValidKey,\n    value: any,\n    customStore = defaultGetStore(),\n): Promise<void> {\n    return customStore(\"readwrite\", store => {\n        store.put(value, key);\n        return promisifyRequest(store.transaction);\n    });\n}\n\n/**\n * Set multiple values at once. This is faster than calling set() multiple times.\n * It's also atomic – if one of the pairs can't be added, none will be added.\n *\n * @param entries Array of entries, where each entry is an array of `[key, value]`.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function setMany(\n    entries: [IDBValidKey, any][],\n    customStore = defaultGetStore(),\n): Promise<void> {\n    return customStore(\"readwrite\", store => {\n        entries.forEach(entry => store.put(entry[1], entry[0]));\n        return promisifyRequest(store.transaction);\n    });\n}\n\n/**\n * Get multiple values by their keys\n *\n * @param keys\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function getMany<T = any>(\n    keys: IDBValidKey[],\n    customStore = defaultGetStore(),\n): Promise<T[]> {\n    return customStore(\"readonly\", store =>\n        Promise.all(keys.map(key => promisifyRequest(store.get(key)))),\n    );\n}\n\n/**\n * Update a value. This lets you see the old value and update it as an atomic operation.\n *\n * @param key\n * @param updater A callback that takes the old value and returns a new value.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function update<T = any>(\n    key: IDBValidKey,\n    updater: (oldValue: T | undefined) => T,\n    customStore = defaultGetStore(),\n): Promise<void> {\n    return customStore(\n        \"readwrite\",\n        store =>\n            // Need to create the promise manually.\n            // If I try to chain promises, the transaction closes in browsers\n            // that use a promise polyfill (IE10/11).\n            new Promise((resolve, reject) => {\n                store.get(key).onsuccess = function () {\n                    try {\n                        store.put(updater(this.result), key);\n                        resolve(promisifyRequest(store.transaction));\n                    } catch (err) {\n                        reject(err);\n                    }\n                };\n            }),\n    );\n}\n\n/**\n * Delete a particular key from the store.\n *\n * @param key\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function del(\n    key: IDBValidKey,\n    customStore = defaultGetStore(),\n): Promise<void> {\n    return customStore(\"readwrite\", store => {\n        store.delete(key);\n        return promisifyRequest(store.transaction);\n    });\n}\n\n/**\n * Delete multiple keys at once.\n *\n * @param keys List of keys to delete.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function delMany(\n    keys: IDBValidKey[],\n    customStore = defaultGetStore(),\n): Promise<void> {\n    return customStore(\"readwrite\", (store: IDBObjectStore) => {\n        keys.forEach((key: IDBValidKey) => store.delete(key));\n        return promisifyRequest(store.transaction);\n    });\n}\n\n/**\n * Clear all values in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function clear(customStore = defaultGetStore()): Promise<void> {\n    return customStore(\"readwrite\", store => {\n        store.clear();\n        return promisifyRequest(store.transaction);\n    });\n}\n\nfunction eachCursor(\n    store: IDBObjectStore,\n    callback: (cursor: IDBCursorWithValue) => void,\n): Promise<void> {\n    store.openCursor().onsuccess = function () {\n        if (!this.result) return;\n        callback(this.result);\n        this.result.continue();\n    };\n    return promisifyRequest(store.transaction);\n}\n\n/**\n * Get all keys in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function keys<KeyType extends IDBValidKey>(\n    customStore = defaultGetStore(),\n): Promise<KeyType[]> {\n    return customStore(\"readonly\", store => {\n        // Fast path for modern browsers\n        if (store.getAllKeys) {\n            return promisifyRequest(\n                store.getAllKeys() as unknown as IDBRequest<KeyType[]>,\n            );\n        }\n\n        const items: KeyType[] = [];\n\n        return eachCursor(store, cursor =>\n            items.push(cursor.key as KeyType),\n        ).then(() => items);\n    });\n}\n\n/**\n * Get all values in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function values<T = any>(customStore = defaultGetStore()): Promise<T[]> {\n    return customStore(\"readonly\", store => {\n        // Fast path for modern browsers\n        if (store.getAll) {\n            return promisifyRequest(store.getAll() as IDBRequest<T[]>);\n        }\n\n        const items: T[] = [];\n\n        return eachCursor(store, cursor => items.push(cursor.value as T)).then(\n            () => items,\n        );\n    });\n}\n\n/**\n * Get all entries in the store. Each entry is an array of `[key, value]`.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nexport function entries<KeyType extends IDBValidKey, ValueType = any>(\n    customStore = defaultGetStore(),\n): Promise<[KeyType, ValueType][]> {\n    return customStore(\"readonly\", store => {\n        // Fast path for modern browsers\n        // (although, hopefully we'll get a simpler path some day)\n        if (store.getAll && store.getAllKeys) {\n            return Promise.all([\n                promisifyRequest(\n                    store.getAllKeys() as unknown as IDBRequest<KeyType[]>,\n                ),\n                promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),\n            ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));\n        }\n\n        const items: [KeyType, ValueType][] = [];\n\n        return customStore(\"readonly\", store =>\n            eachCursor(store, cursor =>\n                items.push([cursor.key as KeyType, cursor.value]),\n            ).then(() => items),\n        );\n    });\n}\n"
  },
  {
    "path": "src/api/MemberListDecorators.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Channel, User } from \"@vencord/discord-types\";\nimport { JSX } from \"react\";\n\ninterface DecoratorProps {\n    type: \"guild\" | \"dm\";\n    user: User;\n    /** only present when this is a DM list item */\n    channel: Channel;\n    /** only present when this is a guild list item */\n    isOwner: boolean;\n}\n\nexport type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;\ntype OnlyIn = \"guilds\" | \"dms\";\n\nexport const decoratorsFactories = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();\n\nexport function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {\n    decoratorsFactories.set(identifier, { render, onlyIn });\n}\n\nexport function removeMemberListDecorator(identifier: string) {\n    decoratorsFactories.delete(identifier);\n}\n\nexport function __getDecorators(props: DecoratorProps, type: \"guild\" | \"dm\"): JSX.Element {\n    const decorators = Array.from(\n        decoratorsFactories.entries(),\n        ([key, { render: Decorator, onlyIn }]) => {\n            if ((onlyIn === \"guilds\" && type !== \"guild\") || (onlyIn === \"dms\" && type !== \"dm\"))\n                return null;\n\n            return (\n                <ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>\n                    <Decorator {...props} type={type} />\n                </ErrorBoundary>\n            );\n        }\n    );\n\n    return (\n        <div className=\"vc-member-list-decorators-wrapper\">\n            {decorators}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/api/MessageAccessories.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { JSX, ReactNode } from \"react\";\n\nexport type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;\nexport type MessageAccessory = {\n    render: MessageAccessoryFactory;\n    position?: number;\n};\n\nexport const accessories = new Map<string, MessageAccessory>();\n\nexport function addMessageAccessory(\n    identifier: string,\n    render: MessageAccessoryFactory,\n    position?: number\n) {\n    accessories.set(identifier, {\n        render,\n        position,\n    });\n}\n\nexport function removeMessageAccessory(identifier: string) {\n    accessories.delete(identifier);\n}\n\nexport function _modifyAccessories(\n    elements: JSX.Element[],\n    props: Record<string, any>\n) {\n    for (const [key, accessory] of accessories.entries()) {\n        const res = (\n            <ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>\n                <accessory.render {...props} />\n            </ErrorBoundary>\n        );\n\n        elements.splice(\n            accessory.position != null\n                ? accessory.position < 0\n                    ? elements.length + accessory.position\n                    : accessory.position\n                : elements.length,\n            0,\n            res\n        );\n    }\n\n    return elements;\n}\n"
  },
  {
    "path": "src/api/MessageDecorations.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Channel, Message } from \"@vencord/discord-types\";\nimport { JSX } from \"react\";\n\nexport interface MessageDecorationProps {\n    author: {\n        /**\n         * Will be username if the user has no nickname\n         */\n        nick: string;\n        iconRoleId: string;\n        guildMemberAvatar: string;\n        colorRoleName: string;\n        colorString: string;\n    };\n    channel: Channel;\n    compact: boolean;\n    decorations: {\n        /**\n         * Element for the [BOT] tag if there is one\n         */\n        0: JSX.Element | null;\n        /**\n         * Other decorations (including ones added with this api)\n         */\n        1: JSX.Element[];\n    };\n    message: Message;\n    [key: string]: any;\n}\nexport type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;\n\nexport const decorationsFactories = new Map<string, MessageDecorationFactory>();\n\nexport function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {\n    decorationsFactories.set(identifier, decoration);\n}\n\nexport function removeMessageDecoration(identifier: string) {\n    decorationsFactories.delete(identifier);\n}\n\nexport function __addDecorationsToMessage(props: MessageDecorationProps): JSX.Element {\n    const decorations = Array.from(\n        decorationsFactories.entries(),\n        ([key, Decoration]) => (\n            <ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>\n                <Decoration {...props} />\n            </ErrorBoundary>\n        )\n    );\n\n    return (\n        <div className=\"vc-message-decorations-wrapper\">\n            {decorations}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/api/MessageEvents.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Logger } from \"@utils/Logger\";\nimport type { Channel, CloudUpload, CustomEmoji, Message } from \"@vencord/discord-types\";\nimport { MessageStore } from \"@webpack/common\";\nimport type { Promisable } from \"type-fest\";\n\nconst MessageEventsLogger = new Logger(\"MessageEvents\", \"#e5c890\");\n\nexport interface MessageObject {\n    content: string,\n    validNonShortcutEmojis: CustomEmoji[];\n    invalidEmojis: any[];\n    tts: boolean;\n}\n\nexport interface MessageReplyOptions {\n    messageReference: Message[\"messageReference\"];\n    allowedMentions?: {\n        parse: Array<string>;\n        repliedUser: boolean;\n    };\n}\n\nexport interface MessageOptions {\n    stickers?: string[];\n    uploads?: CloudUpload[];\n    replyOptions: MessageReplyOptions;\n    content: string;\n    channel: Channel;\n    type?: any;\n    openWarningPopout: (props: any) => any;\n}\n\nexport type MessageSendListener = (channelId: string, messageObj: MessageObject, options: MessageOptions) => Promisable<void | { cancel: boolean; }>;\nexport type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;\n\nconst sendListeners = new Set<MessageSendListener>();\nconst editListeners = new Set<MessageEditListener>();\n\nexport async function _handlePreSend(channelId: string, messageObj: MessageObject, options: MessageOptions, replyOptions: MessageReplyOptions) {\n    options.replyOptions = replyOptions;\n    for (const listener of sendListeners) {\n        try {\n            const result = await listener(channelId, messageObj, options);\n            if (result?.cancel) {\n                return true;\n            }\n        } catch (e) {\n            MessageEventsLogger.error(\"MessageSendHandler: Listener encountered an unknown error\\n\", e);\n        }\n    }\n    return false;\n}\n\nexport async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {\n    for (const listener of editListeners) {\n        try {\n            const result = await listener(channelId, messageId, messageObj);\n            if (result?.cancel) {\n                return true;\n            }\n        } catch (e) {\n            MessageEventsLogger.error(\"MessageEditHandler: Listener encountered an unknown error\\n\", e);\n        }\n    }\n    return false;\n}\n\n/**\n * Note: This event fires off before a message is sent, allowing you to edit the message.\n */\nexport function addMessagePreSendListener(listener: MessageSendListener) {\n    sendListeners.add(listener);\n    return listener;\n}\n/**\n * Note: This event fires off before a message's edit is applied, allowing you to further edit the message.\n */\nexport function addMessagePreEditListener(listener: MessageEditListener) {\n    editListeners.add(listener);\n    return listener;\n}\nexport function removeMessagePreSendListener(listener: MessageSendListener) {\n    return sendListeners.delete(listener);\n}\nexport function removeMessagePreEditListener(listener: MessageEditListener) {\n    return editListeners.delete(listener);\n}\n\n\n// Message clicks\nexport type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;\n\nconst listeners = new Set<MessageClickListener>();\n\nexport function _handleClick(message: Message, channel: Channel, event: MouseEvent) {\n    // message object may be outdated, so (try to) fetch latest one\n    message = MessageStore.getMessage(channel.id, message.id) ?? message;\n    for (const listener of listeners) {\n        try {\n            listener(message, channel, event);\n        } catch (e) {\n            MessageEventsLogger.error(\"MessageClickHandler: Listener encountered an unknown error\\n\", e);\n        }\n    }\n}\n\nexport function addMessageClickListener(listener: MessageClickListener) {\n    listeners.add(listener);\n    return listener;\n}\n\nexport function removeMessageClickListener(listener: MessageClickListener) {\n    return listeners.delete(listener);\n}\n"
  },
  {
    "path": "src/api/MessagePopover.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Logger } from \"@utils/Logger\";\nimport { IconComponent } from \"@utils/types\";\nimport { Channel, Message } from \"@vencord/discord-types\";\nimport type { ComponentType, MouseEventHandler } from \"react\";\n\nimport { useSettings } from \"./Settings\";\n\nconst logger = new Logger(\"MessagePopover\");\n\nexport interface MessagePopoverButtonItem {\n    key?: string,\n    label: string,\n    icon: ComponentType<any>,\n    message: Message,\n    channel: Channel,\n    onClick?: MouseEventHandler<HTMLButtonElement>,\n    onContextMenu?: MouseEventHandler<HTMLButtonElement>;\n}\n\nexport type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null;\nexport type MessagePopoverButtonData = {\n    render: MessagePopoverButtonFactory;\n    /**\n     * This icon is used only for Settings UI. Your render function must still return an icon,\n     * and it can be different from this one.\n     */\n    icon: IconComponent;\n};\n\nexport const MessagePopoverButtonMap = new Map<string, MessagePopoverButtonData>();\n\n/**\n * The icon argument is used only for Settings UI. Your render function must still return an icon,\n * and it can be different from this one.\n */\nexport function addMessagePopoverButton(\n    identifier: string,\n    render: MessagePopoverButtonFactory,\n    icon: IconComponent\n) {\n    MessagePopoverButtonMap.set(identifier, { render, icon });\n}\n\nexport function removeMessagePopoverButton(identifier: string) {\n    MessagePopoverButtonMap.delete(identifier);\n}\n\nfunction VencordPopoverButtons(props: { Component: React.ComponentType<MessagePopoverButtonItem>, message: Message; }) {\n    const { Component, message } = props;\n\n    const { messagePopoverButtons } = useSettings([\"uiElements.messagePopoverButtons.*\"]).uiElements;\n\n    const elements = Array.from(MessagePopoverButtonMap.entries())\n        .filter(([key]) => messagePopoverButtons[key]?.enabled !== false)\n        .map(([key, { render }]) => {\n            try {\n                // FIXME: this should use proper React to ensure hooks work\n                const item = render(message);\n                if (!item) return null;\n\n                return (\n                    <ErrorBoundary noop>\n                        <Component key={key} {...item} />\n                    </ErrorBoundary>\n                );\n            } catch (err) {\n                logger.error(`[${key}]`, err);\n                return null;\n            }\n        });\n\n    return <>{elements}</>;\n}\n\nexport function _buildPopoverElements(\n    Component: React.ComponentType<MessagePopoverButtonItem>,\n    message: Message\n) {\n    return <VencordPopoverButtons Component={Component} message={message} />;\n}\n"
  },
  {
    "path": "src/api/MessageUpdater.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Message } from \"@vencord/discord-types\";\nimport { MessageCache, MessageStore } from \"@webpack/common\";\n\n/**\n * Update and re-render a message\n * @param channelId The channel id of the message\n * @param messageId The message id\n * @param fields The fields of the message to change. Leave empty if you just want to re-render\n */\nexport function updateMessage(channelId: string, messageId: string, fields?: Partial<Message & Record<string, any>>) {\n    const channelMessageCache = MessageCache.getOrCreate(channelId);\n    if (!channelMessageCache.has(messageId)) return;\n\n    // To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference\n    // If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields\n    const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {\n        return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);\n    });\n\n    MessageCache.commit(newChannelMessageCache);\n    MessageStore.emitChange();\n}\n"
  },
  {
    "path": "src/api/Notices.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { isPrimitiveReactNode } from \"@utils/react\";\nimport { waitFor } from \"@webpack\";\nimport { ReactNode } from \"react\";\n\nlet NoticesModule: any;\nwaitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);\n\nexport const noticesQueue = [] as any[];\nexport let currentNotice: any = null;\n\nexport function popNotice() {\n    NoticesModule.dismiss();\n}\n\nexport function nextNotice() {\n    currentNotice = noticesQueue.shift();\n\n    if (currentNotice) {\n        NoticesModule.show(...currentNotice, \"VencordNotice\");\n    }\n}\n\nexport function showNotice(message: ReactNode, buttonText: string, onOkClick: () => void) {\n    const notice = isPrimitiveReactNode(message)\n        ? message\n        : <ErrorBoundary fallback={() => \"Error Showing Notice\"}>{message}</ErrorBoundary>;\n\n    noticesQueue.push([\"GENERIC\", notice, buttonText, onOkClick]);\n    if (!currentNotice) nextNotice();\n}\n"
  },
  {
    "path": "src/api/Notifications/NotificationComponent.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { useSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { classes } from \"@utils/misc\";\nimport { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from \"@webpack/common\";\n\nimport { NotificationData } from \"./Notifications\";\n\nexport default ErrorBoundary.wrap(function NotificationComponent({\n    title,\n    body,\n    richBody,\n    color,\n    icon,\n    onClick,\n    onClose,\n    image,\n    permanent,\n    className,\n    dismissOnClick\n}: NotificationData & { className?: string; }) {\n    const { timeout, position } = useSettings([\"notifications.timeout\", \"notifications.position\"]).notifications;\n    const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());\n\n    const [isHover, setIsHover] = useState(false);\n    const [elapsed, setElapsed] = useState(0);\n\n    const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);\n\n    useEffect(() => {\n        if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);\n\n        const intervalId = setInterval(() => {\n            const elapsed = Date.now() - start;\n            if (elapsed >= timeout)\n                onClose!();\n            else\n                setElapsed(elapsed);\n        }, 10);\n\n        return () => clearInterval(intervalId);\n    }, [timeout, isHover, hasFocus]);\n\n    const timeoutProgress = elapsed / timeout;\n\n    return (\n        <button\n            className={classes(\"vc-notification-root\", className)}\n            style={position === \"bottom-right\" ? { bottom: \"1rem\" } : { top: \"3rem\" }}\n            onClick={() => {\n                onClick?.();\n                if (dismissOnClick !== false)\n                    onClose!();\n            }}\n            onContextMenu={e => {\n                e.preventDefault();\n                e.stopPropagation();\n                onClose!();\n            }}\n            onMouseEnter={() => setIsHover(true)}\n            onMouseLeave={() => setIsHover(false)}\n        >\n            <div className=\"vc-notification\">\n                {icon && <img className=\"vc-notification-icon\" src={icon} alt=\"\" />}\n                <div className=\"vc-notification-content\">\n                    <div className=\"vc-notification-header\">\n                        <h2 className=\"vc-notification-title\">{title}</h2>\n                        <button\n                            className=\"vc-notification-close-btn\"\n                            onClick={e => {\n                                e.preventDefault();\n                                e.stopPropagation();\n                                onClose!();\n                            }}\n                        >\n                            <svg\n                                width=\"24\"\n                                height=\"24\"\n                                viewBox=\"0 0 24 24\"\n                                role=\"img\"\n                                aria-labelledby=\"vc-notification-dismiss-title\"\n                            >\n                                <title id=\"vc-notification-dismiss-title\">Dismiss Notification</title>\n                                <path fill=\"currentColor\" d=\"M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z\" />\n                            </svg>\n                        </button>\n                    </div>\n                    {richBody ?? <p className=\"vc-notification-p\">{body}</p>}\n                </div>\n            </div>\n            {image && <img className=\"vc-notification-img\" src={image} alt=\"\" />}\n            {timeout !== 0 && !permanent && (\n                <div\n                    className=\"vc-notification-progressbar\"\n                    style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || \"var(--brand-500)\" }}\n                />\n            )}\n        </button>\n    );\n}, {\n    onError: ({ props }) => props.onClose!()\n});\n"
  },
  {
    "path": "src/api/Notifications/Notifications.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings } from \"@api/Settings\";\nimport { Queue } from \"@utils/Queue\";\nimport { createRoot } from \"@webpack/common\";\nimport type { ReactNode } from \"react\";\nimport type { Root } from \"react-dom/client\";\n\nimport NotificationComponent from \"./NotificationComponent\";\nimport { persistNotification } from \"./notificationLog\";\n\nconst NotificationQueue = new Queue();\n\nlet reactRoot: Root;\nlet id = 42;\n\nfunction getRoot() {\n    if (!reactRoot) {\n        const container = document.createElement(\"div\");\n        container.id = \"vc-notification-container\";\n        document.body.append(container);\n        reactRoot = createRoot(container);\n    }\n    return reactRoot;\n}\n\nexport interface NotificationData {\n    title: string;\n    body: string;\n    /**\n     * Same as body but can be a custom component.\n     * Will be used over body if present.\n     * Not supported on desktop notifications, those will fall back to body */\n    richBody?: ReactNode;\n    /** Small icon. This is for things like profile pictures and should be square */\n    icon?: string;\n    /** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */\n    image?: string;\n    onClick?(): void;\n    onClose?(): void;\n    color?: string;\n    /** Whether this notification should not have a timeout */\n    permanent?: boolean;\n    /** Whether this notification should not be persisted in the Notification Log */\n    noPersist?: boolean;\n    /** Whether this notification should be dismissed when clicked (defaults to true) */\n    dismissOnClick?: boolean;\n}\n\nfunction _showNotification(notification: NotificationData, id: number) {\n    const root = getRoot();\n    return new Promise<void>(resolve => {\n        root.render(\n            <NotificationComponent key={id} {...notification} onClose={() => {\n                notification.onClose?.();\n                root.render(null);\n                resolve();\n            }} />,\n        );\n    });\n}\n\nfunction shouldBeNative() {\n    if (typeof Notification === \"undefined\") return false;\n\n    const { useNative } = Settings.notifications;\n    if (useNative === \"always\") return true;\n    if (useNative === \"not-focused\") return !document.hasFocus();\n    return false;\n}\n\nexport async function requestPermission() {\n    return (\n        Notification.permission === \"granted\" ||\n        (Notification.permission !== \"denied\" && (await Notification.requestPermission()) === \"granted\")\n    );\n}\n\nexport async function showNotification(data: NotificationData) {\n    persistNotification(data);\n\n    if (shouldBeNative() && await requestPermission()) {\n        const { title, body, icon, image, onClick = null, onClose = null } = data;\n        const n = new Notification(title, {\n            body,\n            icon,\n            // @ts-expect-error ts is drunk\n            image\n        });\n        n.onclick = onClick;\n        n.onclose = onClose;\n    } else {\n        NotificationQueue.push(() => _showNotification(data, id++));\n    }\n}\n"
  },
  {
    "path": "src/api/Notifications/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport * from \"./Notifications\";\n"
  },
  {
    "path": "src/api/Notifications/notificationLog.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as DataStore from \"@api/DataStore\";\nimport { Settings } from \"@api/Settings\";\nimport { Flex } from \"@components/Flex\";\nimport { openNotificationSettingsModal } from \"@components/settings/tabs/vencord/NotificationSettings\";\nimport { classNameFactory } from \"@utils/css\";\nimport { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { useAwaiter } from \"@utils/react\";\nimport { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from \"@webpack/common\";\nimport { nanoid } from \"nanoid\";\nimport type { DispatchWithoutAction } from \"react\";\n\nimport NotificationComponent from \"./NotificationComponent\";\nimport type { NotificationData } from \"./Notifications\";\n\ninterface PersistentNotificationData extends Pick<NotificationData, \"title\" | \"body\" | \"image\" | \"icon\" | \"color\"> {\n    timestamp: number;\n    id: string;\n}\n\nconst KEY = \"notification-log\";\n\nconst getLog = async () => {\n    const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;\n    return log ?? [];\n};\n\nconst cl = classNameFactory(\"vc-notification-log-\");\nconst signals = new Set<DispatchWithoutAction>();\n\nexport async function persistNotification(notification: NotificationData) {\n    if (notification.noPersist) return;\n\n    const limit = Settings.notifications.logLimit;\n    if (limit === 0) return;\n\n    await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {\n        const log = old ?? [];\n\n        // Omit stuff we don't need\n        const {\n            onClick, onClose, richBody, permanent, noPersist, dismissOnClick,\n            ...pureNotification\n        } = notification;\n\n        log.unshift({\n            ...pureNotification,\n            timestamp: Date.now(),\n            id: nanoid()\n        });\n\n        if (log.length > limit && limit !== 200)\n            log.length = limit;\n\n        return log;\n    });\n\n    signals.forEach(x => x());\n}\n\nexport async function deleteNotification(timestamp: number) {\n    const log = await getLog();\n    const index = log.findIndex(x => x.timestamp === timestamp);\n    if (index === -1) return;\n\n    log.splice(index, 1);\n    await DataStore.set(KEY, log);\n    signals.forEach(x => x());\n}\n\nexport function useLogs() {\n    const [signal, setSignal] = useReducer(x => x + 1, 0);\n\n    useEffect(() => {\n        signals.add(setSignal);\n        return () => void signals.delete(setSignal);\n    }, []);\n\n    const [log, _, pending] = useAwaiter(getLog, {\n        fallbackValue: [],\n        deps: [signal]\n    });\n\n    return [log, pending] as const;\n}\n\nfunction NotificationEntry({ data }: { data: PersistentNotificationData; }) {\n    const [removing, setRemoving] = useState(false);\n\n    return (\n        <div className={cl(\"wrapper\", { removing })}>\n            <NotificationComponent\n                {...data}\n                permanent={true}\n                dismissOnClick={false}\n                onClose={() => {\n                    if (removing) return;\n                    setRemoving(true);\n\n                    setTimeout(() => deleteNotification(data.timestamp), 200);\n                }}\n                richBody={\n                    <div className={cl(\"body-wrapper\")}>\n                        <div className={cl(\"body\")}>{data.body}</div>\n                        <Timestamp timestamp={new Date(data.timestamp)} className={cl(\"timestamp\")} />\n                    </div>\n                }\n            />\n        </div >\n    );\n}\n\nexport function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {\n    if (!log.length && !pending)\n        return (\n            <div className={cl(\"container\")}>\n                <div className={cl(\"empty\")} />\n                <Forms.FormText style={{ textAlign: \"center\" }}>\n                    No notifications yet\n                </Forms.FormText>\n            </div>\n        );\n\n    return (\n        <ListScrollerThin\n            className={cl(\"container\")}\n            sections={[log.length]}\n            sectionHeight={0}\n            rowHeight={120}\n            renderSection={() => null}\n            renderRow={item => <NotificationEntry data={log[item.row]} key={log[item.row].id} />}\n        />\n    );\n}\n\nfunction LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {\n    const [log, pending] = useLogs();\n\n    return (\n        <ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl(\"modal\")}>\n            <ModalHeader>\n                <Text variant=\"heading-lg/semibold\" style={{ flexGrow: 1 }}>Notification Log</Text>\n                <ModalCloseButton onClick={close} />\n            </ModalHeader>\n\n            <div style={{ width: \"100%\" }}>\n                <NotificationLog log={log} pending={pending} />\n            </div>\n\n            <ModalFooter>\n                <Flex>\n                    <Button onClick={openNotificationSettingsModal}>\n                        Notification Settings\n                    </Button>\n\n                    <Button\n                        disabled={log.length === 0}\n                        color={Button.Colors.RED}\n                        onClick={() => {\n                            Alerts.show({\n                                title: \"Are you sure?\",\n                                body: `This will permanently remove ${log.length} notification${log.length === 1 ? \"\" : \"s\"}. This action cannot be undone.`,\n                                async onConfirm() {\n                                    await DataStore.set(KEY, []);\n                                    signals.forEach(x => x());\n                                },\n                                confirmText: \"Do it!\",\n                                confirmColor: \"vc-notification-log-danger-btn\",\n                                cancelText: \"Nevermind\"\n                            });\n                        }}\n                    >\n                        Clear Notification Log\n                    </Button>\n                </Flex>\n            </ModalFooter>\n        </ModalRoot>\n    );\n}\n\nexport function openNotificationLogModal() {\n    const key = openModal(modalProps => (\n        <LogModal\n            modalProps={modalProps}\n            close={() => closeModal(key)}\n        />\n    ));\n}\n"
  },
  {
    "path": "src/api/Notifications/styles.css",
    "content": ".vc-notification-root {\n    /* clear default button styles */\n    all: unset;\n    display: flex;\n    flex-direction: column;\n    color: var(--text-default);\n    background-color: var(--background-base-low);\n    border-radius: 6px;\n    overflow: hidden;\n    cursor: pointer;\n    width: 100%;\n}\n\n.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {\n    position: absolute;\n    z-index: 2147483647;\n    right: 1rem;\n    width: 25vw;\n    min-height: 10vh;\n}\n\n.vc-notification {\n    display: flex;\n    flex-direction: row;\n    padding: 1.25rem;\n    gap: 1.25rem;\n}\n\n.vc-notification-content {\n    width: 100%;\n    overflow: hidden;\n}\n\n.vc-notification-header {\n    display: flex;\n    justify-content: space-between;\n}\n\n.vc-notification-title {\n    color: var(--text-strong);\n    font-size: 1rem;\n    font-weight: 600;\n    line-height: 1.25rem;\n    text-transform: uppercase;\n}\n\n.vc-notification-close-btn {\n    all: unset;\n    cursor: pointer;\n    color: var(--interactive-icon-default);\n    opacity: 0.5;\n    transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;\n}\n\n.vc-notification-close-btn:hover {\n    color: var(--interactive-icon-hover);\n    opacity: 1;\n}\n\n.vc-notification-icon {\n    height: 4rem;\n    width: 4rem;\n    border-radius: 6px;\n}\n\n.vc-notification-progressbar {\n    height: 0.25rem;\n    border-radius: 5px;\n    margin-top: auto;\n}\n\n.vc-notification-p {\n    margin: 0.5rem 0 0;\n    line-height: 140%;\n}\n\n.vc-notification-img {\n    width: 100%;\n}\n\n.vc-notification-log-modal {\n    max-width: 962px;\n    width: clamp(var(--modal-width-large, 800px), 962px, 85vw);\n}\n\n.vc-notification-log-empty {\n    height: 218px;\n    background: url(\"/assets/b36de980b174d7b798c89f35c116e5c6.svg\") center no-repeat;\n    margin-bottom: 40px;\n}\n\n.vc-notification-log-container {\n    padding: 1em;\n    max-height: min(750px, 75vh);\n    width: 100%;\n}\n\n.vc-notification-log-wrapper {\n    height: 120px;\n    width: 100%;\n    padding-bottom: 16px;\n    box-sizing: border-box;\n    transition: 200ms ease;\n    transition-property: height, opacity;\n\n    /* stylelint-disable-next-line no-descending-specificity */\n    .vc-notification-root {\n        height: 104px;\n    }\n}\n\n.vc-notification-log-removing {\n    height: 0 !important;\n    opacity: 0;\n    margin-bottom: 1em;\n}\n\n.vc-notification-log-body-wrapper {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    box-sizing: border-box;\n}\n\n.vc-notification-log-body {\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    line-height: 1.2em;\n}\n\n.vc-notification-log-timestamp {\n    margin-left: auto;\n    font-size: 0.8em;\n    font-weight: lighter;\n}\n\n.vc-notification-log-danger-btn {\n    color: var(--control-critical-primary-text-default);\n    background-color: var(--control-critical-primary-background-default);\n}"
  },
  {
    "path": "src/api/PluginManager.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { addProfileBadge, removeProfileBadge } from \"@api/Badges\";\nimport { addChatBarButton, removeChatBarButton } from \"@api/ChatButtons\";\nimport { registerCommand, unregisterCommand } from \"@api/Commands\";\nimport { addContextMenuPatch, removeContextMenuPatch } from \"@api/ContextMenu\";\nimport { addMemberListDecorator, removeMemberListDecorator } from \"@api/MemberListDecorators\";\nimport { addMessageAccessory, removeMessageAccessory } from \"@api/MessageAccessories\";\nimport { addMessageDecoration, removeMessageDecoration } from \"@api/MessageDecorations\";\nimport { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from \"@api/MessageEvents\";\nimport { addMessagePopoverButton, removeMessagePopoverButton } from \"@api/MessagePopover\";\nimport { Settings, SettingsStore } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport { Logger } from \"@utils/Logger\";\nimport { onlyOnce } from \"@utils/onlyOnce\";\nimport { canonicalizeFind, canonicalizeReplacement } from \"@utils/patches\";\nimport { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from \"@utils/types\";\nimport { FluxEvents } from \"@vencord/discord-types\";\nimport { FluxDispatcher } from \"@webpack/common\";\nimport { patches } from \"@webpack/patcher\";\n\nimport Plugins from \"~plugins\";\nexport { Plugins as plugins };\n\nimport { traceFunction } from \"../debug/Tracer\";\n\nconst logger = new Logger(\"PluginManager\", \"#a6d189\");\n\nexport const PMLogger = logger;\n\n/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */\nlet enabledPluginsSubscribedFlux = false;\nconst subscribedFluxEventsPlugins = new Set<string>();\n\nexport function isPluginEnabled(p: string) {\n    return (\n        Plugins[p]?.required ||\n        Plugins[p]?.isDependency ||\n        Settings.plugins[p]?.enabled\n    ) ?? false;\n}\n\nexport function addPatch(newPatch: Omit<Patch, \"plugin\">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {\n    const patch = newPatch as Patch;\n    patch.plugin = pluginName;\n\n    if (IS_REPORTER) {\n        delete patch.predicate;\n        delete patch.group;\n    }\n\n    if (patch.predicate && !patch.predicate()) return;\n\n    canonicalizeFind(patch);\n    if (!Array.isArray(patch.replacement)) {\n        patch.replacement = [patch.replacement];\n    }\n\n    for (const replacement of patch.replacement) {\n        canonicalizeReplacement(replacement, pluginPath);\n\n        if (IS_REPORTER) {\n            delete replacement.predicate;\n        }\n    }\n\n    patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());\n\n    patches.push(patch);\n}\n\nfunction isReporterTestable(p: Plugin, part: ReporterTestable) {\n    return p.reporterTestable == null\n        ? true\n        : (p.reporterTestable & part) === part;\n}\n\nexport function pluginRequiresRestart(p: Plugin) {\n    return p.requiresRestart !== false && (p.requiresRestart || !!p.patches?.length);\n}\n\nexport const startAllPlugins = traceFunction(\"startAllPlugins\", function startAllPlugins(target: StartAt) {\n    logger.info(`Starting plugins (stage ${target})`);\n    for (const name in Plugins) {\n        if (isPluginEnabled(name) && (!IS_REPORTER || isReporterTestable(Plugins[name], ReporterTestable.Start))) {\n            const p = Plugins[name];\n\n            const startAt = p.startAt ?? StartAt.WebpackReady;\n            if (startAt !== target) continue;\n\n            startPlugin(Plugins[name]);\n        }\n    }\n});\n\nexport function startDependenciesRecursive(p: Plugin) {\n    const settings = Settings.plugins;\n    let restartNeeded = false;\n    const failures: string[] = [];\n\n    p.dependencies?.forEach(d => {\n        if (!settings[d].enabled) {\n            const dep = Plugins[d];\n            startDependenciesRecursive(dep);\n\n            // If the plugin has patches, don't start the plugin, just enable it.\n            settings[d].enabled = true;\n            dep.isDependency = true;\n\n            if (pluginRequiresRestart(dep)) {\n                logger.warn(`Enabling dependency ${d} requires restart.`);\n                restartNeeded = true;\n                return;\n            }\n\n            const result = startPlugin(dep);\n            if (!result) failures.push(d);\n        }\n    });\n\n    return { restartNeeded, failures };\n}\n\nexport function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {\n    if (p.flux && !subscribedFluxEventsPlugins.has(p.name) && (!IS_REPORTER || isReporterTestable(p, ReporterTestable.FluxEvents))) {\n        subscribedFluxEventsPlugins.add(p.name);\n\n        logger.debug(\"Subscribing to flux events of plugin\", p.name);\n        for (const [event, handler] of Object.entries(p.flux)) {\n            const wrappedHandler = p.flux[event] = function () {\n                try {\n                    const res = handler!.apply(p, arguments as any);\n                    return res instanceof Promise\n                        ? res.catch(e => logger.error(`${p.name}: Error while handling ${event}\\n`, e))\n                        : res;\n                } catch (e) {\n                    logger.error(`${p.name}: Error while handling ${event}\\n`, e);\n                }\n            };\n\n            fluxDispatcher.subscribe(event as FluxEvents, wrappedHandler);\n        }\n    }\n}\n\nexport function unsubscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {\n    if (p.flux) {\n        subscribedFluxEventsPlugins.delete(p.name);\n\n        logger.debug(\"Unsubscribing from flux events of plugin\", p.name);\n        for (const [event, handler] of Object.entries(p.flux)) {\n            fluxDispatcher.unsubscribe(event as FluxEvents, handler!);\n        }\n    }\n}\n\nexport function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatcher) {\n    enabledPluginsSubscribedFlux = true;\n\n    for (const name in Plugins) {\n        if (!isPluginEnabled(name)) continue;\n        subscribePluginFluxEvents(Plugins[name], fluxDispatcher);\n    }\n}\n\nexport const startPlugin = traceFunction(\"startPlugin\", function startPlugin(p: Plugin) {\n    const {\n        name, commands, contextMenus, managedStyle, userProfileBadge,\n        onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,\n        renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton\n    } = p;\n\n    if (p.start) {\n        logger.info(\"Starting plugin\", name);\n        if (p.started) {\n            logger.warn(`${name} already started`);\n            return false;\n        }\n        try {\n            p.start();\n        } catch (e) {\n            logger.error(`Failed to start ${name}\\n`, e);\n            return false;\n        }\n    }\n\n    p.started = true;\n\n    if (commands?.length) {\n        logger.debug(\"Registering commands of plugin\", name);\n        for (const cmd of commands) {\n            try {\n                registerCommand(cmd, name);\n            } catch (e) {\n                logger.error(`Failed to register command ${cmd.name}\\n`, e);\n                return false;\n            }\n        }\n    }\n\n    if (enabledPluginsSubscribedFlux) {\n        subscribePluginFluxEvents(p, FluxDispatcher);\n    }\n\n    if (contextMenus) {\n        logger.debug(\"Adding context menus patches of plugin\", name);\n        for (const navId in contextMenus) {\n            addContextMenuPatch(navId, contextMenus[navId]);\n        }\n    }\n\n    if (managedStyle) enableStyle(managedStyle);\n\n    if (userProfileBadge) addProfileBadge(userProfileBadge);\n\n    if (onBeforeMessageEdit) addMessagePreEditListener(onBeforeMessageEdit);\n    if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend);\n    if (onMessageClick) addMessageClickListener(onMessageClick);\n\n    if (chatBarButton) addChatBarButton(name, chatBarButton.render, chatBarButton.icon);\n    // @ts-expect-error: legacy code doesn't have icon\n    else if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);\n    if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator);\n    if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration);\n    if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory);\n    if (messagePopoverButton) addMessagePopoverButton(name, messagePopoverButton.render, messagePopoverButton.icon);\n    // @ts-expect-error: legacy code doesn't have icon\n    else if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);\n\n    return true;\n}, p => `startPlugin ${p.name}`);\n\nexport const stopPlugin = traceFunction(\"stopPlugin\", function stopPlugin(p: Plugin) {\n    const {\n        name, commands, contextMenus, managedStyle, userProfileBadge,\n        onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,\n        renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton\n    } = p;\n\n    if (p.stop) {\n        logger.info(\"Stopping plugin\", name);\n        if (!p.started) {\n            logger.warn(`${name} already stopped`);\n            return false;\n        }\n        try {\n            p.stop();\n        } catch (e) {\n            logger.error(`Failed to stop ${name}\\n`, e);\n            return false;\n        }\n    }\n\n    p.started = false;\n\n    if (commands?.length) {\n        logger.debug(\"Unregistering commands of plugin\", name);\n        for (const cmd of commands) {\n            try {\n                unregisterCommand(cmd.name);\n            } catch (e) {\n                logger.error(`Failed to unregister command ${cmd.name}\\n`, e);\n                return false;\n            }\n        }\n    }\n\n    unsubscribePluginFluxEvents(p, FluxDispatcher);\n\n    if (contextMenus) {\n        logger.debug(\"Removing context menus patches of plugin\", name);\n        for (const navId in contextMenus) {\n            removeContextMenuPatch(navId, contextMenus[navId]);\n        }\n    }\n\n    if (managedStyle) disableStyle(managedStyle);\n\n    if (userProfileBadge) removeProfileBadge(userProfileBadge);\n\n    if (onBeforeMessageEdit) removeMessagePreEditListener(onBeforeMessageEdit);\n    if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);\n    if (onMessageClick) removeMessageClickListener(onMessageClick);\n\n    if (chatBarButton || renderChatBarButton) removeChatBarButton(name);\n    if (renderMemberListDecorator) removeMemberListDecorator(name);\n    if (renderMessageDecoration) removeMessageDecoration(name);\n    if (renderMessageAccessory) removeMessageAccessory(name);\n    if (messagePopoverButton || renderMessagePopoverButton) removeMessagePopoverButton(name);\n\n    return true;\n}, p => `stopPlugin ${p.name}`);\n\nexport const initPluginManager = onlyOnce(function init() {\n    const pluginsValues = Object.values(Plugins);\n    const settings = Settings.plugins;\n\n    const pluginKeysToBind: Array<keyof PluginDef & `${\"on\" | \"render\"}${string}`> = [\n        \"onBeforeMessageEdit\", \"onBeforeMessageSend\", \"onMessageClick\",\n        \"renderChatBarButton\", \"renderMemberListDecorator\", \"renderMessageAccessory\", \"renderMessageDecoration\", \"renderMessagePopoverButton\"\n    ];\n\n    const neededApiPlugins = new Set<string>();\n\n    // First round-trip to mark and force enable dependencies\n    //\n    // FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only\n    // goes for the top level and their children, but for now this works okay with the current API plugins\n    for (const p of pluginsValues) if (isPluginEnabled(p.name)) {\n        p.dependencies?.forEach(d => {\n            const dep = Plugins[d];\n\n            if (!dep) {\n                const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`);\n\n                if (IS_DEV) {\n                    throw error;\n                }\n\n                logger.warn(error);\n                return;\n            }\n\n            settings[d].enabled = true;\n            dep.isDependency = true;\n        });\n\n        if (p.commands?.length) neededApiPlugins.add(\"CommandsAPI\");\n        if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add(\"MessageEventsAPI\");\n        if (p.chatBarButton || p.renderChatBarButton) neededApiPlugins.add(\"ChatInputButtonAPI\");\n        if (p.renderMemberListDecorator) neededApiPlugins.add(\"MemberListDecoratorsAPI\");\n        if (p.renderMessageAccessory) neededApiPlugins.add(\"MessageAccessoriesAPI\");\n        if (p.renderMessageDecoration) neededApiPlugins.add(\"MessageDecorationsAPI\");\n        if (p.messagePopoverButton || p.renderMessagePopoverButton) neededApiPlugins.add(\"MessagePopoverAPI\");\n        if (p.userProfileBadge) neededApiPlugins.add(\"BadgeAPI\");\n\n        for (const key of pluginKeysToBind) {\n            p[key] &&= p[key].bind(p) as any;\n        }\n    }\n\n    for (const p of neededApiPlugins) {\n        Plugins[p].isDependency = true;\n        settings[p].enabled = true;\n    }\n\n    for (const p of pluginsValues) {\n        if (p.settings) {\n            p.options ??= {};\n\n            p.settings.pluginName = p.name;\n            for (const name in p.settings.def) {\n                const def = p.settings.def[name];\n                const checks = p.settings.checks?.[name];\n                p.options[name] = { ...def, ...checks };\n            }\n        }\n\n        if (p.options) {\n            for (const name in p.options) {\n                const opt = p.options[name];\n                if (opt.onChange != null) {\n                    SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);\n                }\n            }\n        }\n\n        if (p.patches && isPluginEnabled(p.name)) {\n            if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {\n                for (const patch of p.patches) {\n                    addPatch(patch, p.name);\n                }\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "src/api/ServerList.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { ComponentType } from \"react\";\n\nexport const enum ServerListRenderPosition {\n    Above,\n    In,\n}\n\nconst componentsAbove = new Set<ComponentType>();\nconst componentsBelow = new Set<ComponentType>();\n\nfunction getRenderFunctions(position: ServerListRenderPosition) {\n    return position === ServerListRenderPosition.Above ? componentsAbove : componentsBelow;\n}\n\nexport function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {\n    getRenderFunctions(position).add(renderFunction);\n}\n\nexport function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {\n    getRenderFunctions(position).delete(renderFunction);\n}\n\nexport const renderAll = (position: ServerListRenderPosition) => {\n    return Array.from(\n        getRenderFunctions(position),\n        (Component, i) => (\n            <ErrorBoundary noop key={i}>\n                <Component />\n            </ErrorBoundary>\n        )\n    );\n};\n"
  },
  {
    "path": "src/api/Settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { SettingsStore as SettingsStoreClass } from \"@shared/SettingsStore\";\nimport { Logger } from \"@utils/Logger\";\nimport { mergeDefaults } from \"@utils/mergeDefaults\";\nimport { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from \"@utils/types\";\nimport { React, useEffect } from \"@webpack/common\";\n\nimport plugins from \"~plugins\";\n\nconst logger = new Logger(\"Settings\");\n\nexport interface SettingsPluginUiElement {\n    enabled: boolean;\n    // TODO\n    /** not implemented for now */\n    order?: number;\n}\nexport type SettingsPluginUiElements = {\n    /** id will be whatever id the element was registered with. Usually, but not always, the plugin name */\n    [id: string]: SettingsPluginUiElement;\n};\n\nexport interface Settings {\n    autoUpdate: boolean;\n    autoUpdateNotification: boolean,\n    useQuickCss: boolean;\n    eagerPatches: boolean;\n    enabledThemes: string[];\n    enableReactDevtools: boolean;\n    themeLinks: string[];\n    frameless: boolean;\n    transparent: boolean;\n    winCtrlQ: boolean;\n    macosVibrancyStyle:\n    | \"content\"\n    | \"fullscreen-ui\"\n    | \"header\"\n    | \"hud\"\n    | \"menu\"\n    | \"popover\"\n    | \"selection\"\n    | \"sidebar\"\n    | \"titlebar\"\n    | \"tooltip\"\n    | \"under-page\"\n    | \"window\"\n    | undefined;\n    disableMinSize: boolean;\n    winNativeTitleBar: boolean;\n    plugins: {\n        [plugin: string]: {\n            enabled: boolean;\n            [setting: string]: any;\n        };\n    };\n\n    uiElements: {\n        messagePopoverButtons: SettingsPluginUiElements;\n        chatBarButtons: SettingsPluginUiElements;\n    },\n\n    notifications: {\n        timeout: number;\n        position: \"top-right\" | \"bottom-right\";\n        useNative: \"always\" | \"never\" | \"not-focused\";\n        logLimit: number;\n    };\n\n    cloud: {\n        authenticated: boolean;\n        url: string;\n        settingsSync: boolean;\n        settingsSyncVersion: number;\n    };\n}\n\nconst DefaultSettings: Settings = {\n    autoUpdate: true,\n    autoUpdateNotification: true,\n    useQuickCss: true,\n    themeLinks: [],\n    eagerPatches: IS_REPORTER,\n    enabledThemes: [],\n    enableReactDevtools: false,\n    frameless: false,\n    transparent: false,\n    winCtrlQ: false,\n    macosVibrancyStyle: undefined,\n    disableMinSize: false,\n    winNativeTitleBar: false,\n    plugins: {},\n\n    uiElements: {\n        chatBarButtons: {},\n        messagePopoverButtons: {}\n    },\n\n    notifications: {\n        timeout: 5000,\n        position: \"bottom-right\",\n        useNative: \"not-focused\",\n        logLimit: 50\n    },\n\n    cloud: {\n        authenticated: false,\n        url: \"https://api.vencord.dev/\",\n        settingsSync: false,\n        settingsSyncVersion: 0\n    }\n};\n\nconst settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;\nmergeDefaults(settings, DefaultSettings);\n\nexport const SettingsStore = new SettingsStoreClass(settings, {\n    readOnly: true,\n    getDefaultValue({\n        target,\n        key,\n        path\n    }) {\n        const v = target[key];\n        if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level\n\n        if (path === \"plugins\" && key in plugins)\n            return target[key] = {\n                enabled: IS_REPORTER || plugins[key].required || plugins[key].enabledByDefault || false\n            };\n\n        // Since the property is not set, check if this is a plugin's setting and if so, try to resolve\n        // the default value.\n        if (path.startsWith(\"plugins.\")) {\n            const plugin = path.slice(\"plugins.\".length);\n            if (plugin in plugins) {\n                const setting = plugins[plugin].options?.[key];\n                if (!setting) return v;\n\n                if (\"default\" in setting)\n                    // normal setting with a default value\n                    return (target[key] = setting.default);\n\n                if (setting.type === OptionType.SELECT) {\n                    const def = setting.options.find(o => o.default);\n                    if (def)\n                        target[key] = def.value;\n                    return def?.value;\n                }\n            }\n        }\n        return v;\n    }\n});\n\nif (!IS_REPORTER) {\n    SettingsStore.addGlobalChangeListener((_, path) => {\n        SettingsStore.plain.cloud.settingsSyncVersion = Date.now();\n        VencordNative.settings.set(SettingsStore.plain, path);\n    });\n}\n\n/**\n * Same as {@link Settings} but unproxied. You should treat this as readonly,\n * as modifying properties on this will not save to disk or call settings\n * listeners.\n * WARNING: default values specified in plugin.options will not be ensured here. In other words,\n * settings for which you specified a default value may be uninitialised. If you need proper\n * handling for default values, use {@link Settings}\n */\nexport const PlainSettings = settings;\n/**\n * A smart settings object. Altering props automagically saves\n * the updated settings to disk.\n * This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}\n */\nexport const Settings = SettingsStore.store;\n\n/**\n * Settings hook for React components. Returns a smart settings\n * object that automagically triggers a rerender if any properties\n * are altered\n * @param paths An optional list of paths to whitelist for rerenders\n * @returns Settings\n */\n// TODO: Representing paths as essentially \"string[].join('.')\" wont allow dots in paths, change to \"paths?: string[][]\" later\nexport function useSettings(paths?: UseSettings<Settings>[]) {\n    const [, forceUpdate] = React.useReducer(() => ({}), {});\n\n    useEffect(() => {\n        if (paths) {\n            paths.forEach(p => {\n                if (p.endsWith(\".*\")) {\n                    SettingsStore.addPrefixChangeListener(p.slice(0, -2), forceUpdate);\n                } else {\n                    SettingsStore.addChangeListener(p, forceUpdate);\n                }\n            });\n\n            return () => paths.forEach(p => {\n                if (p.endsWith(\".*\")) {\n                    SettingsStore.removePrefixChangeListener(p.slice(0, -2), forceUpdate);\n                } else {\n                    SettingsStore.removeChangeListener(p, forceUpdate);\n                }\n            });\n        } else {\n            SettingsStore.addGlobalChangeListener(forceUpdate);\n            return () => SettingsStore.removeGlobalChangeListener(forceUpdate);\n        }\n    }, [paths]);\n\n    return SettingsStore.store;\n}\n\nexport function migratePluginSettings(name: string, ...oldNames: string[]) {\n    const { plugins } = SettingsStore.plain;\n    if (name in plugins) return;\n\n    for (const oldName of oldNames) {\n        if (oldName in plugins) {\n            logger.info(`Migrating settings from old name ${oldName} to ${name}`);\n            plugins[name] = plugins[oldName];\n            delete plugins[oldName];\n            SettingsStore.markAsChanged();\n            break;\n        }\n    }\n}\n\nexport function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {\n    const settings = SettingsStore.plain.plugins[pluginName];\n    if (!settings) return;\n\n    if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;\n\n    settings[newSetting] = settings[oldSetting];\n    delete settings[oldSetting];\n    SettingsStore.markAsChanged();\n}\n\nexport function definePluginSettings<\n    Def extends SettingsDefinition,\n    Checks extends SettingsChecks<Def>,\n    PrivateSettings extends object = {}\n>(def: Def, checks?: Checks) {\n    const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {\n        get store() {\n            if (!definedSettings.pluginName) throw new Error(\"Cannot access settings before plugin is initialized\");\n            return Settings.plugins[definedSettings.pluginName] as any;\n        },\n        get plain() {\n            if (!definedSettings.pluginName) throw new Error(\"Cannot access settings before plugin is initialized\");\n            return PlainSettings.plugins[definedSettings.pluginName] as any;\n        },\n        use: settings => useSettings((\n            settings\n                ? settings.map(name => `plugins.${definedSettings.pluginName}.${name}`)\n                : [`plugins.${definedSettings.pluginName}.*`]\n        ) as UseSettings<Settings>[]).plugins[definedSettings.pluginName] as any,\n        def,\n        checks: checks ?? {} as any,\n        pluginName: \"\",\n\n        withPrivateSettings<T extends object>() {\n            return this as DefinedSettings<Def, Checks, T>;\n        }\n    };\n\n    return definedSettings;\n}\n\ntype UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];\n\ntype ResolveUseSettings<T extends object> = {\n    [Key in keyof T]:\n    Key extends string\n    ? T[Key] extends Record<string, unknown>\n    // @ts-expect-error \"Type instantiation is excessively deep and possibly infinite\"\n    ? `${Key}.*` | (ResolveUseSettings<T[Key]> extends Record<string, string> ? `${Key}.${ResolveUseSettings<T[Key]>[keyof T[Key]]}` : never)\n    : Key\n    : never;\n};\n"
  },
  {
    "path": "src/api/SettingsSync/cloudSetup.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport * as DataStore from \"@api/DataStore\";\nimport { showNotification } from \"@api/Notifications\";\nimport { Settings } from \"@api/Settings\";\nimport { Logger } from \"@utils/Logger\";\nimport { openModal } from \"@utils/modal\";\nimport { relaunch } from \"@utils/native\";\nimport { Alerts, OAuth2AuthorizeModal, UserStore } from \"@webpack/common\";\n\nexport const logger = new Logger(\"SettingsSync:CloudSetup\", \"#39b7e0\");\n\nexport const getCloudUrl = () => new URL(Settings.cloud.url);\nconst getCloudUrlOrigin = () => getCloudUrl().origin;\n\nexport async function checkCloudUrlCsp() {\n    if (IS_WEB) return true;\n\n    const { host } = getCloudUrl();\n    if (host === \"api.vencord.dev\") return true;\n\n    if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, [\"connect-src\"])) {\n        return true;\n    }\n\n    const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, [\"connect-src\"], \"Cloud Sync\");\n    if (res === \"ok\") {\n        Alerts.show({\n            title: \"Cloud Integration enabled\",\n            body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`,\n            confirmText: \"Restart now\",\n            cancelText: \"Later!\",\n            onConfirm: relaunch\n        });\n    }\n    return false;\n}\n\nconst getUserId = () => {\n    const id = UserStore.getCurrentUser()?.id;\n    if (!id) throw new Error(\"User not yet logged in\");\n    return id;\n};\n\nexport async function getAuthorization() {\n    const secrets = await DataStore.get<Record<string, string>>(\"Vencord_cloudSecret\") ?? {};\n\n    const origin = getCloudUrlOrigin();\n\n    // we need to migrate from the old format here\n    if (secrets[origin]) {\n        await DataStore.update<Record<string, string>>(\"Vencord_cloudSecret\", secrets => {\n            secrets ??= {};\n            // use the current user ID\n            secrets[`${origin}:${getUserId()}`] = secrets[origin];\n            delete secrets[origin];\n            return secrets;\n        });\n\n        // since this doesn't update the original object, we'll early return the existing authorization\n        return secrets[origin];\n    }\n\n    return secrets[`${origin}:${getUserId()}`];\n}\n\nasync function setAuthorization(secret: string) {\n    await DataStore.update<Record<string, string>>(\"Vencord_cloudSecret\", secrets => {\n        secrets ??= {};\n        secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret;\n        return secrets;\n    });\n}\n\nexport async function deauthorizeCloud() {\n    await DataStore.update<Record<string, string>>(\"Vencord_cloudSecret\", secrets => {\n        secrets ??= {};\n        delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`];\n        return secrets;\n    });\n}\n\nexport async function authorizeCloud() {\n    if (await getAuthorization() !== undefined) {\n        Settings.cloud.authenticated = true;\n        return;\n    }\n\n    if (!await checkCloudUrlCsp()) return;\n\n    try {\n        const oauthConfiguration = await fetch(new URL(\"/v1/oauth/settings\", getCloudUrl()));\n        var { clientId, redirectUri } = await oauthConfiguration.json();\n    } catch {\n        showNotification({\n            title: \"Cloud Integration\",\n            body: \"Setup failed (couldn't retrieve OAuth configuration).\"\n        });\n        Settings.cloud.authenticated = false;\n        return;\n    }\n\n    openModal((props: any) => <OAuth2AuthorizeModal\n        {...props}\n        scopes={[\"identify\"]}\n        responseType=\"code\"\n        redirectUri={redirectUri}\n        permissions={0n}\n        clientId={clientId}\n        cancelCompletesFlow={false}\n        callback={async ({ location }: any) => {\n            if (!location) {\n                Settings.cloud.authenticated = false;\n                return;\n            }\n\n            try {\n                const res = await fetch(location, {\n                    headers: { Accept: \"application/json\" }\n                });\n                const { secret } = await res.json();\n                if (secret) {\n                    logger.info(\"Authorized with secret\");\n                    await setAuthorization(secret);\n                    showNotification({\n                        title: \"Cloud Integration\",\n                        body: \"Cloud integrations enabled!\"\n                    });\n                    Settings.cloud.authenticated = true;\n                } else {\n                    showNotification({\n                        title: \"Cloud Integration\",\n                        body: \"Setup failed (no secret returned?).\"\n                    });\n                    Settings.cloud.authenticated = false;\n                }\n            } catch (e: any) {\n                logger.error(\"Failed to authorize\", e);\n                showNotification({\n                    title: \"Cloud Integration\",\n                    body: `Setup failed (${e.toString()}).`\n                });\n                Settings.cloud.authenticated = false;\n            }\n        }\n        }\n    />);\n}\n\nexport async function getCloudAuth() {\n    const secret = await getAuthorization();\n\n    return window.btoa(`${secret}:${getUserId()}`);\n}\n"
  },
  {
    "path": "src/api/SettingsSync/cloudSync.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { showNotification } from \"@api/Notifications\";\nimport { PlainSettings, Settings } from \"@api/Settings\";\nimport { localStorage } from \"@utils/localStorage\";\nimport { Logger } from \"@utils/Logger\";\nimport { relaunch } from \"@utils/native\";\nimport { deflateSync, inflateSync } from \"fflate\";\n\nimport { checkCloudUrlCsp, deauthorizeCloud, getCloudAuth, getCloudUrl } from \"./cloudSetup\";\nimport { exportSettings, importSettings } from \"./offline\";\n\nconst logger = new Logger(\"SettingsSync:Cloud\", \"#39b7e0\");\n\nexport function shouldCloudSync(direction: \"push\" | \"pull\") {\n    const localDirection = localStorage.Vencord_cloudSyncDirection;\n\n    return localDirection === direction || localDirection === \"both\";\n}\n\nexport async function putCloudSettings(manual?: boolean) {\n    const settings = await exportSettings({ minify: true });\n\n    if (!await checkCloudUrlCsp()) return;\n\n    try {\n        const res = await fetch(new URL(\"/v1/settings\", getCloudUrl()), {\n            method: \"PUT\",\n            headers: {\n                Authorization: await getCloudAuth(),\n                \"Content-Type\": \"application/octet-stream\"\n            },\n            body: deflateSync(new TextEncoder().encode(settings)) as Uint8Array<ArrayBuffer>\n        });\n\n        if (!res.ok) {\n            logger.error(`Failed to sync up, API returned ${res.status}`);\n            showNotification({\n                title: \"Cloud Settings\",\n                body: `Could not synchronize settings to cloud (API returned ${res.status}).`,\n                color: \"var(--red-360)\"\n            });\n            return;\n        }\n\n        const { written } = await res.json();\n        PlainSettings.cloud.settingsSyncVersion = written;\n        VencordNative.settings.set(PlainSettings);\n\n        logger.info(\"Settings uploaded to cloud successfully\");\n\n        if (manual) {\n            showNotification({\n                title: \"Cloud Settings\",\n                body: \"Synchronized settings to the cloud!\",\n                noPersist: true,\n            });\n        }\n\n        delete localStorage.Vencord_settingsDirty;\n    } catch (e: any) {\n        logger.error(\"Failed to sync up\", e);\n        showNotification({\n            title: \"Cloud Settings\",\n            body: `Could not synchronize settings to the cloud (${e.toString()}).`,\n            color: \"var(--red-360)\"\n        });\n    }\n}\n\nexport async function getCloudSettings(shouldNotify = true, force = false) {\n    if (!await checkCloudUrlCsp()) return;\n\n    try {\n        const res = await fetch(new URL(\"/v1/settings\", getCloudUrl()), {\n            method: \"GET\",\n            headers: {\n                Authorization: await getCloudAuth(),\n                Accept: \"application/octet-stream\",\n                \"If-None-Match\": Settings.cloud.settingsSyncVersion.toString()\n            },\n        });\n\n        if (res.status === 404) {\n            logger.info(\"No settings on the cloud\");\n            if (shouldNotify)\n                showNotification({\n                    title: \"Cloud Settings\",\n                    body: \"There are no settings in the cloud.\",\n                    noPersist: true\n                });\n            return false;\n        }\n\n        if (res.status === 304) {\n            logger.info(\"Settings up to date\");\n            if (shouldNotify)\n                showNotification({\n                    title: \"Cloud Settings\",\n                    body: \"Your settings are up to date.\",\n                    noPersist: true\n                });\n            return false;\n        }\n\n        if (!res.ok) {\n            logger.error(`Failed to sync down, API returned ${res.status}`);\n            showNotification({\n                title: \"Cloud Settings\",\n                body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,\n                color: \"var(--red-360)\"\n            });\n            return false;\n        }\n\n        const written = Number(res.headers.get(\"etag\")!);\n        const localWritten = Settings.cloud.settingsSyncVersion;\n\n        // don't need to check for written > localWritten because the server will return 304 due to if-none-match\n        if (!force && written < localWritten) {\n            if (shouldNotify)\n                showNotification({\n                    title: \"Cloud Settings\",\n                    body: \"Your local settings are newer than the cloud ones.\",\n                    noPersist: true,\n                });\n            return;\n        }\n\n        const data = await res.arrayBuffer();\n\n        const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));\n        await importSettings(settings);\n\n        // sync with server timestamp instead of local one\n        PlainSettings.cloud.settingsSyncVersion = written;\n        VencordNative.settings.set(PlainSettings);\n\n        logger.info(\"Settings loaded from cloud successfully\");\n        if (shouldNotify)\n            showNotification({\n                title: \"Cloud Settings\",\n                body: \"Your settings have been updated! Click here to restart to fully apply changes!\",\n                color: \"var(--green-360)\",\n                onClick: IS_WEB ? () => location.reload() : relaunch,\n                noPersist: true\n            });\n\n        delete localStorage.Vencord_settingsDirty;\n\n        return true;\n    } catch (e: any) {\n        logger.error(\"Failed to sync down\", e);\n        showNotification({\n            title: \"Cloud Settings\",\n            body: `Could not synchronize settings from the cloud (${e.toString()}).`,\n            color: \"var(--red-360)\"\n        });\n\n        return false;\n    }\n}\n\nexport async function deleteCloudSettings() {\n    if (!await checkCloudUrlCsp()) return;\n\n    try {\n        const res = await fetch(new URL(\"/v1/settings\", getCloudUrl()), {\n            method: \"DELETE\",\n            headers: { Authorization: await getCloudAuth() },\n        });\n\n        if (!res.ok) {\n            logger.error(`Failed to delete, API returned ${res.status}`);\n            showNotification({\n                title: \"Cloud Settings\",\n                body: `Could not delete settings (API returned ${res.status}).`,\n                color: \"var(--red-360)\"\n            });\n            return;\n        }\n\n        logger.info(\"Settings deleted from cloud successfully\");\n        showNotification({\n            title: \"Cloud Settings\",\n            body: \"Settings deleted from cloud!\",\n            color: \"var(--green-360)\"\n        });\n    } catch (e: any) {\n        logger.error(\"Failed to delete\", e);\n        showNotification({\n            title: \"Cloud Settings\",\n            body: `Could not delete settings (${e.toString()}).`,\n            color: \"var(--red-360)\"\n        });\n    }\n}\n\nexport async function eraseAllCloudData() {\n    if (!await checkCloudUrlCsp()) return;\n\n    const res = await fetch(new URL(\"/v1/\", getCloudUrl()), {\n        method: \"DELETE\",\n        headers: { Authorization: await getCloudAuth() }\n    });\n\n    if (!res.ok) {\n        logger.error(`Failed to erase data, API returned ${res.status}`);\n        showNotification({\n            title: \"Cloud Integrations\",\n            body: `Could not erase all data (API returned ${res.status}), please contact support.`,\n            color: \"var(--red-360)\"\n        });\n        return;\n    }\n\n    Settings.cloud.authenticated = false;\n    await deauthorizeCloud();\n\n    showNotification({\n        title: \"Cloud Integrations\",\n        body: \"Successfully erased all data.\",\n        color: \"var(--green-360)\"\n    });\n}\n"
  },
  {
    "path": "src/api/SettingsSync/offline.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { PlainSettings } from \"@api/Settings\";\nimport { Logger } from \"@utils/Logger\";\nimport { chooseFile, saveFile } from \"@utils/web\";\nimport { moment, Toasts } from \"@webpack/common\";\n\nconst toast = (type: string, message: string) =>\n    Toasts.show({\n        type,\n        message,\n        id: Toasts.genId()\n    });\n\nconst toastSuccess = () =>\n    toast(Toasts.Type.SUCCESS, \"Settings successfully imported. Restart to apply changes!\");\n\nconst toastFailure = (err: any) =>\n    toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);\n\nconst logger = new Logger(\"SettingsSync:Offline\", \"#39b7e0\");\n\nfunction isSafeObject(obj: any) {\n    if (obj == null || typeof obj !== \"object\") return true;\n\n    for (const key in obj) {\n        if ([\"__proto__\", \"constructor\", \"prototype\"].includes(key)) {\n            return false;\n        }\n        if (!isSafeObject(obj[key])) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\nexport async function importSettings(data: string) {\n    try {\n        var parsed = JSON.parse(data);\n    } catch (err) {\n        console.log(data);\n        throw new Error(\"Failed to parse JSON: \" + String(err));\n    }\n\n    if (!isSafeObject(parsed))\n        throw new Error(\"Unsafe Settings\");\n\n    if (\"settings\" in parsed && \"quickCss\" in parsed) {\n        Object.assign(PlainSettings, parsed.settings);\n        await VencordNative.settings.set(parsed.settings);\n        await VencordNative.quickCss.set(parsed.quickCss);\n    } else\n        throw new Error(\"Invalid Settings. Is this even a Vencord Settings file?\");\n}\n\nexport async function exportSettings({ minify }: { minify?: boolean; } = {}) {\n    const settings = VencordNative.settings.get();\n    const quickCss = await VencordNative.quickCss.get();\n    return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4);\n}\n\nexport async function downloadSettingsBackup() {\n    const filename = `vencord-settings-backup-${moment().format(\"YYYY-MM-DD\")}.json`;\n    const backup = await exportSettings();\n    const data = new TextEncoder().encode(backup);\n\n    if (IS_DISCORD_DESKTOP) {\n        DiscordNative.fileManager.saveWithDialog(data, filename);\n    } else {\n        saveFile(new File([data], filename, { type: \"application/json\" }));\n    }\n}\n\nexport async function uploadSettingsBackup(showToast = true): Promise<void> {\n    if (IS_DISCORD_DESKTOP) {\n        const [file] = await DiscordNative.fileManager.openFiles({\n            filters: [\n                { name: \"Vencord Settings Backup\", extensions: [\"json\"] },\n                { name: \"all\", extensions: [\"*\"] }\n            ]\n        });\n\n        if (file) {\n            try {\n                await importSettings(new TextDecoder().decode(file.data));\n                if (showToast) toastSuccess();\n            } catch (err) {\n                logger.error(err);\n                if (showToast) toastFailure(err);\n            }\n        }\n    } else {\n        const file = await chooseFile(\"application/json\");\n        if (!file) return;\n\n        const reader = new FileReader();\n        reader.onload = async () => {\n            try {\n                await importSettings(reader.result as string);\n                if (showToast) toastSuccess();\n            } catch (err) {\n                logger.error(err);\n                if (showToast) toastFailure(err);\n            }\n        };\n        reader.readAsText(file);\n    }\n}\n"
  },
  {
    "path": "src/api/Styles.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { generateTextCss } from \"@components/BaseText\";\nimport { generateMarginCss } from \"@components/margins\";\nimport { classNameFactory as _classNameFactory, classNameToSelector, createAndAppendStyle } from \"@utils/css\";\n\n// Backwards compat for Vesktop\n/** @deprecated Import this from `@utils/css` instead */\nexport const classNameFactory = _classNameFactory;\n\nexport interface Style {\n    name: string;\n    source: string;\n    classNames: Record<string, string>;\n    dom: HTMLStyleElement | null;\n}\n\nexport const styleMap = window.VencordStyles ??= new Map();\n\nexport const vencordRootNode = document.createElement(\"vencord-root\");\n/**\n * Houses all Vencord core styles. This includes all imported css files\n */\nexport const coreStyleRootNode = document.createElement(\"vencord-styles\");\n/**\n * Houses all plugin specific managed styles\n */\nexport const managedStyleRootNode = document.createElement(\"vencord-managed-styles\");\n/**\n * Houses the user's themes and quick css\n */\nexport const userStyleRootNode = document.createElement(\"vencord-user-styles\");\n\nvencordRootNode.style.display = \"none\";\nvencordRootNode.append(coreStyleRootNode, managedStyleRootNode, userStyleRootNode);\n\nexport function initStyles() {\n    const osValuesNode = createAndAppendStyle(\"vencord-os-theme-values\", coreStyleRootNode);\n    createAndAppendStyle(\"vencord-text\", coreStyleRootNode).textContent = generateTextCss();\n    const rendererCssNode = createAndAppendStyle(\"vencord-css-core\", coreStyleRootNode);\n    const vesktopCssNode = IS_VESKTOP ? createAndAppendStyle(\"vesktop-css-core\", coreStyleRootNode) : null;\n    createAndAppendStyle(\"vencord-margins\", coreStyleRootNode).textContent = generateMarginCss();\n\n    VencordNative.native.getRendererCss().then(css => rendererCssNode.textContent = css);\n    if (IS_DEV) {\n        VencordNative.native.onRendererCssUpdate(newCss => {\n            rendererCssNode.textContent = newCss;\n        });\n    }\n\n    if (IS_VESKTOP && VesktopNative.app.getRendererCss) {\n        VesktopNative.app.getRendererCss().then(css => vesktopCssNode!.textContent = css);\n        VesktopNative.app.onRendererCssUpdate(newCss => {\n            vesktopCssNode!.textContent = newCss;\n        });\n    }\n\n    VencordNative.themes.getSystemValues().then(values => {\n        const variables = Object.entries(values)\n            .filter(([, v]) => !!v)\n            .map(([k, v]) => `--${k}: ${v};`)\n            .join(\"\");\n        osValuesNode.textContent = `:root{${variables}}`;\n    });\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    document.documentElement.append(vencordRootNode);\n}, { once: true });\n\nexport function requireStyle(name: string) {\n    const style = styleMap.get(name);\n    if (!style) throw new Error(`Style \"${name}\" does not exist`);\n    return style;\n}\n\n/**\n * A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import\n * @param name The name of the style\n * @returns `false` if the style was already enabled, `true` otherwise\n * @example\n * import pluginStyle from \"./plugin.css?managed\";\n *\n * // Inside some plugin method like \"start()\" or \"[option].onChange()\"\n * enableStyle(pluginStyle);\n */\nexport function enableStyle(name: string) {\n    const style = requireStyle(name);\n\n    if (style.dom?.isConnected)\n        return false;\n\n    if (!style.dom) {\n        style.dom = document.createElement(\"style\");\n        style.dom.dataset.vencordName = style.name;\n    }\n    compileStyle(style);\n\n    managedStyleRootNode.appendChild(style.dom);\n    return true;\n}\n\n/**\n * @param name The name of the style\n * @returns `false` if the style was already disabled, `true` otherwise\n * @see {@link enableStyle} for info on getting the name of an imported style\n */\nexport function disableStyle(name: string) {\n    const style = requireStyle(name);\n    if (!style.dom?.isConnected)\n        return false;\n\n    style.dom.remove();\n    style.dom = null;\n    return true;\n}\n\n/**\n * @param name The name of the style\n * @returns `true` in most cases, may return `false` in some edge cases\n * @see {@link enableStyle} for info on getting the name of an imported style\n */\nexport const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);\n\n/**\n * @param name The name of the style\n * @returns Whether the style is enabled\n * @see {@link enableStyle} for info on getting the name of an imported style\n */\nexport const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;\n\n/**\n * Sets the variables of a style\n * ```ts\n * // -- plugin.ts --\n * import pluginStyle from \"./plugin.css?managed\";\n * import { setStyleVars } from \"@api/Styles\";\n * import { findByPropsLazy } from \"@webpack\";\n * const classNames = findByPropsLazy(\"thin\", \"scrollerBase\"); // { thin: \"thin-31rlnD scrollerBase-_bVAAt\", ... }\n *\n * // Inside some plugin method like \"start()\"\n * setStyleClassNames(pluginStyle, classNames);\n * enableStyle(pluginStyle);\n * ```\n * ```scss\n * // -- plugin.css --\n * .plugin-root [--thin]::-webkit-scrollbar { ... }\n * ```\n * ```scss\n * // -- final stylesheet --\n * .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }\n * ```\n * @param name The name of the style\n * @param classNames An object where the keys are the variable names and the values are the variable values\n * @param recompile Whether to recompile the style after setting the variables, defaults to `true`\n * @see {@link enableStyle} for info on getting the name of an imported style\n */\nexport const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {\n    const style = requireStyle(name);\n    style.classNames = classNames;\n    if (recompile && isStyleEnabled(style.name))\n        compileStyle(style);\n};\n\n/**\n * Updates the stylesheet after doing the following to the sourcecode:\n *   - Interpolate style classnames\n * @param style **_Must_ be a style with a DOM element**\n * @see {@link setStyleClassNames} for more info on style classnames\n */\nexport const compileStyle = (style: Style) => {\n    if (!style.dom) throw new Error(\"Style has no DOM element\");\n\n    style.dom.textContent = style.source\n        .replace(/\\[--(\\w+)\\]/g, (match, name) => {\n            const className = style.classNames[name];\n            return className ? classNameToSelector(className) : match;\n        });\n};\n\n\n"
  },
  {
    "path": "src/api/Themes.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings, SettingsStore } from \"@api/Settings\";\nimport { createAndAppendStyle } from \"@utils/css\";\nimport { ThemeStore } from \"@vencord/discord-types\";\nimport { PopoutWindowStore } from \"@webpack/common\";\n\nimport { userStyleRootNode, vencordRootNode } from \"./Styles\";\n\nlet style: HTMLStyleElement;\nlet themesStyle: HTMLStyleElement;\n\nasync function toggle(isEnabled: boolean) {\n    if (!style) {\n        if (isEnabled) {\n            style = createAndAppendStyle(\"vencord-custom-css\", userStyleRootNode);\n            VencordNative.quickCss.addChangeListener(css => {\n                style.textContent = css;\n                // At the time of writing this, changing textContent resets the disabled state\n                style.disabled = !Settings.useQuickCss;\n                updatePopoutWindows();\n            });\n            style.textContent = await VencordNative.quickCss.get();\n        }\n    } else\n        style.disabled = !isEnabled;\n}\n\nasync function initThemes() {\n    themesStyle ??= createAndAppendStyle(\"vencord-themes\", userStyleRootNode);\n\n    const { themeLinks, enabledThemes } = Settings;\n\n    const { ThemeStore } = require(\"@webpack/common/stores\") as typeof import(\"@webpack/common/stores\");\n\n    // \"darker\" and \"midnight\" both count as dark\n    // This function is first called on DOMContentLoaded, so ThemeStore may not have been loaded yet\n    const activeTheme = ThemeStore == null\n        ? undefined\n        : ThemeStore.theme === \"light\" ? \"light\" : \"dark\";\n\n    const links = themeLinks\n        .map(rawLink => {\n            const match = /^@(light|dark) (.*)/.exec(rawLink);\n            if (!match) return rawLink;\n\n            const [, mode, link] = match;\n            return mode === activeTheme ? link : null;\n        })\n        .filter(link => link !== null);\n\n    if (IS_WEB) {\n        for (const theme of enabledThemes) {\n            const themeData = await VencordNative.themes.getThemeData(theme);\n            if (!themeData) continue;\n            const blob = new Blob([themeData], { type: \"text/css\" });\n            links.push(URL.createObjectURL(blob));\n        }\n    } else {\n        const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`);\n        links.push(...localThemes);\n    }\n\n    themesStyle.textContent = links.map(link => `@import url(\"${link.trim()}\");`).join(\"\\n\");\n    updatePopoutWindows();\n}\n\nfunction applyToPopout(popoutWindow: Window | undefined, key: string) {\n    if (!popoutWindow?.document) return;\n    // skip game overlay cuz it needs to stay transparent, themes broke it\n    if (key === \"DISCORD_OutOfProcessOverlay\") return;\n\n    const doc = popoutWindow.document;\n\n    doc.querySelector(\"vencord-root\")?.remove();\n\n    doc.documentElement.appendChild(vencordRootNode.cloneNode(true));\n}\n\nfunction updatePopoutWindows() {\n    if (!PopoutWindowStore) return;\n\n    for (const key of PopoutWindowStore.getWindowKeys()) {\n        applyToPopout(PopoutWindowStore.getWindow(key), key);\n    }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    if (IS_USERSCRIPT) return;\n\n    initThemes();\n\n    toggle(Settings.useQuickCss);\n    SettingsStore.addChangeListener(\"useQuickCss\", toggle);\n\n    SettingsStore.addChangeListener(\"themeLinks\", initThemes);\n    SettingsStore.addChangeListener(\"enabledThemes\", initThemes);\n\n    window.addEventListener(\"message\", event => {\n        const { discordPopoutEvent } = event.data || {};\n        if (discordPopoutEvent?.type !== \"loaded\") return;\n\n        applyToPopout(PopoutWindowStore.getWindow(discordPopoutEvent.key), discordPopoutEvent.key);\n    });\n\n    if (!IS_WEB) {\n        VencordNative.quickCss.addThemeChangeListener(initThemes);\n    }\n}, { once: true });\n\nexport function initQuickCssThemeStore(themeStore: ThemeStore) {\n    if (IS_USERSCRIPT) return;\n\n    initThemes();\n\n    let currentTheme = themeStore.theme;\n    themeStore.addChangeListener(() => {\n        if (currentTheme === themeStore.theme) return;\n\n        currentTheme = themeStore.theme;\n        initThemes();\n    });\n}\n"
  },
  {
    "path": "src/api/UserSettings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { proxyLazy } from \"@utils/lazy\";\nimport { Logger } from \"@utils/Logger\";\nimport { findModuleId, proxyLazyWebpack, wreq } from \"@webpack\";\n\nimport { isPluginEnabled } from \"./PluginManager\";\n\ninterface UserSettingDefinition<T> {\n    /**\n     * Get the setting value\n     */\n    getSetting(): T;\n    /**\n     * Update the setting value\n     * @param value The new value\n     */\n    updateSetting(value: T): Promise<void>;\n    /**\n     * Update the setting value\n     * @param value A callback that accepts the old value as the first argument, and returns the new value\n     */\n    updateSetting(value: (old: T) => T): Promise<void>;\n    /**\n     * Stateful React hook for this setting value\n     */\n    useSetting(): T;\n    userSettingsAPIGroup: string;\n    userSettingsAPIName: string;\n}\n\nexport const UserSettings: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {\n    const modId = findModuleId('\"textAndImages\",\"renderSpoilers\"');\n    if (modId == null) return new Logger(\"UserSettingsAPI\").error(\"Didn't find settings module.\");\n\n    return wreq(modId as any);\n});\n\n/**\n * Get the setting with the given setting group and name.\n *\n * @param group The setting group\n * @param name The name of the setting\n */\nexport function getUserSetting<T = any>(group: string, name: string): UserSettingDefinition<T> | undefined {\n    if (!isPluginEnabled(\"UserSettingsAPI\")) throw new Error(\"Cannot use UserSettingsAPI without setting it as a dependency.\");\n\n    for (const key in UserSettings) {\n        const userSetting = UserSettings[key];\n\n        if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) {\n            return userSetting;\n        }\n    }\n}\n\n/**\n * {@link getUserSettingDefinition}, lazy.\n *\n * Get the setting with the given setting group and name.\n *\n * @param group The setting group\n * @param name The name of the setting\n */\nexport function getUserSettingLazy<T = any>(group: string, name: string) {\n    return proxyLazy(() => getUserSetting<T>(group, name));\n}\n"
  },
  {
    "path": "src/api/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as $Badges from \"./Badges\";\nimport * as $ChatButtons from \"./ChatButtons\";\nimport * as $Commands from \"./Commands\";\nimport * as $ContextMenu from \"./ContextMenu\";\nimport * as $DataStore from \"./DataStore\";\nimport * as $MemberListDecorators from \"./MemberListDecorators\";\nimport * as $MessageAccessories from \"./MessageAccessories\";\nimport * as $MessageDecorations from \"./MessageDecorations\";\nimport * as $MessageEventsAPI from \"./MessageEvents\";\nimport * as $MessagePopover from \"./MessagePopover\";\nimport * as $MessageUpdater from \"./MessageUpdater\";\nimport * as $Notices from \"./Notices\";\nimport * as $Notifications from \"./Notifications\";\nexport * as PluginManager from \"./PluginManager\";\nimport * as $ServerList from \"./ServerList\";\nimport * as $Settings from \"./Settings\";\nimport * as $Styles from \"./Styles\";\nimport * as $Themes from \"./Themes\";\nimport * as $UserSettings from \"./UserSettings\";\n\n/**\n * An API allowing you to listen to Message Clicks or run your own logic\n * before a message is sent\n *\n * If your plugin uses this, you must add MessageEventsAPI to its dependencies\n */\nexport const MessageEvents = $MessageEventsAPI;\n/**\n * An API allowing you to create custom notices\n * (snackbars on the top, like the Update prompt)\n */\nexport const Notices = $Notices;\n/**\n * An API allowing you to register custom commands\n */\nexport const Commands = $Commands;\n/**\n * A wrapper around IndexedDB. This can store arbitrarily\n * large data and supports a lot of datatypes (Blob, Map, ...).\n * For a full list, see the mdn link below\n *\n * This should always be preferred over the Settings API if possible, as\n * localstorage has very strict size restrictions and blocks the event loop\n *\n * Make sure your keys are unique (tip: prefix them with ur plugin name)\n * and please clean up no longer needed entries.\n *\n * This is actually just idb-keyval, so if you're familiar with that, you're golden!\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}\n */\nexport const DataStore = $DataStore;\n/**\n * An API allowing you to add custom components as message accessories\n */\nexport const MessageAccessories = $MessageAccessories;\n/**\n * An API allowing you to add custom buttons in the message popover\n */\nexport const MessagePopover = $MessagePopover;\n/**\n * An API allowing you to add badges to user profiles\n */\nexport const Badges = $Badges;\n/**\n * An API allowing you to add custom elements to the server list\n */\nexport const ServerList = $ServerList;\n/**\n * An API allowing you to add components as message accessories\n */\nexport const MessageDecorations = $MessageDecorations;\n/**\n * An API allowing you to add components to member list users, in both DM's and servers\n */\nexport const MemberListDecorators = $MemberListDecorators;\n/**\n * An API allowing you to persist data\n */\nexport const Settings = $Settings;\n/**\n * An API allowing you to dynamically load styles\n * a\n */\nexport const Styles = $Styles;\n/**\n * An API allowing you to display notifications\n */\nexport const Notifications = $Notifications;\n\n/**\n * An api allowing you to patch and add/remove items to/from context menus\n */\nexport const ContextMenu = $ContextMenu;\n\n/**\n * An API allowing you to add buttons to the chat input\n */\nexport const ChatButtons = $ChatButtons;\n\n/**\n * An API allowing you to update and re-render messages\n */\nexport const MessageUpdater = $MessageUpdater;\n\n/**\n * An API allowing you to get an user setting\n */\nexport const UserSettings = $UserSettings;\n\n/**\n * Don't use this\n */\nexport const Themes = $Themes;\n"
  },
  {
    "path": "src/components/BaseText.css",
    "content": ".vc-text-base {\n    font-family: var(--font-primary);\n    line-height: normal;\n\n    /* Discord puts an insane default margin on p tags, so reset that here */\n    margin: 0;\n}\n\n.vc-text-defaultColor {\n    color: var(--text-default);\n}"
  },
  {
    "path": "src/components/BaseText.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./BaseText.css\";\n\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport type { Text as DiscordText } from \"@vencord/discord-types\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\n\nconst textCls = classNameFactory(\"vc-text-\");\n\nconst Sizes = {\n    xxs: \"0.625rem\",\n    xs: \"0.75rem\",\n    sm: \"0.875rem\",\n    md: \"1rem\",\n    lg: \"1.25rem\",\n    xl: \"1.5rem\",\n    xxl: \"2rem\"\n} as const;\n\nconst Weights = {\n    thin: \"100\",\n    extralight: \"200\",\n    light: \"300\",\n    normal: \"400\",\n    medium: \"500\",\n    semibold: \"600\",\n    bold: \"700\",\n    extrabold: \"800\",\n} as const;\n\nexport function generateTextCss() {\n    let css = \"\";\n\n    for (const [size, value] of Object.entries(Sizes)) {\n        css += `.${textCls(size)}{font-size:${value};}`;\n    }\n\n    for (const [weight, value] of Object.entries(Weights)) {\n        css += `.${textCls(weight)}{font-weight:${value};}`;\n    }\n\n    return css;\n}\n\nexport type TextSize = keyof typeof Sizes;\nexport type TextWeight = keyof typeof Weights;\nexport type TextTag = \"div\" | \"span\" | \"p\" | `h${1 | 2 | 3 | 4 | 5 | 6}`;\n\nexport type BaseTextProps<Tag extends TextTag = \"div\"> = ComponentPropsWithoutRef<Tag> & {\n    size?: TextSize;\n    weight?: TextWeight;\n    tag?: Tag;\n    defaultColor?: boolean;\n};\n\nexport function BaseText<T extends TextTag = \"div\">(props: BaseTextProps<T>): ReactNode {\n    const {\n        size = \"md\",\n        weight = \"normal\",\n        tag: Tag = \"div\",\n        defaultColor = true,\n        children,\n        className,\n        ...restProps\n    } = props;\n\n    return (\n        <Tag className={classes(textCls(\"base\", size, weight, defaultColor && \"defaultColor\"), className)} {...restProps}>\n            {children}\n        </Tag>\n    );\n}\n\n// #region Old compability\n\nexport const TextCompat: DiscordText = function TextCompat({ color, variant, ...restProps }) {\n    const newBaseTextProps = restProps as BaseTextProps;\n\n    if (variant) {\n        const [left, right] = variant.split(\"/\");\n        if (left && right) {\n            const size = left.split(\"-\").pop();\n            newBaseTextProps.size = size as TextSize;\n            newBaseTextProps.weight = right as TextWeight;\n        }\n    }\n\n    if (color) {\n        newBaseTextProps.style ??= {};\n        newBaseTextProps.style.color = `var(--${color}, var(--text-default))`;\n    }\n\n    return <BaseText {...newBaseTextProps} />;\n};\n\n// #endregion\n"
  },
  {
    "path": "src/components/Button.css",
    "content": ".vc-btn-base {\n    position: relative;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    max-width: 100%;\n    border: 1px solid transparent;\n    border-radius: var(--radius-sm, 8px);\n    font-family: var(--font-primary);\n    text-align: start;\n    transition: 50ms ease-in;\n    transition-property: background-color, color, border-color, opacity;\n    background: var(--control-secondary-background-default);\n    color: var(--text-default);\n    white-space: nowrap;\n\n    &:hover {\n        transition: .15s ease-out;\n    }\n\n    &:disabled {\n        opacity: .5;\n        pointer-events: none;\n        cursor: not-allowed;\n    }\n\n    &:focus-visible {\n        /* stylelint-disable-next-line custom-property-pattern */\n        box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--border-focus, #00b0f4));\n    }\n}\n\n.vc-btn-min,\n.vc-btn-xs {\n    padding: 3px 7px;\n    min-height: 22px;\n    min-width: unset;\n    font-size: 12px;\n    font-weight: 400;\n    line-height: 1.3333;\n}\n\n.vc-btn-xs {\n    min-width: 60px;\n}\n\n.vc-btn-small {\n    padding: 3px 11px;\n    min-height: 30px;\n    min-width: 60px;\n    font-size: 14px;\n    font-weight: 500;\n    line-height: 1.2857;\n}\n\n.vc-btn-medium {\n    padding: 7px 15px;\n    min-height: 38px;\n    min-width: 100px;\n    font-size: 16px;\n    font-weight: 500;\n    line-height: 1.25;\n}\n\n.vc-btn-iconOnly {\n    width: 32px;\n    height: 32px;\n    min-width: unset;\n    min-height: unset;\n    padding: 0;\n    background-color: transparent;\n    border-color: transparent;\n\n    &:hover {\n        background-color: var(--control-icon-only-background-hover);\n        border-color: var(--control-icon-only-border-hover);\n    }\n\n    &:active {\n        background-color: var(--control-icon-only-background-active);\n        border-color: var(--control-icon-only-border-active);\n    }\n}\n\n.vc-btn-primary {\n    background-color: var(--control-primary-background-default);\n    border-color: var(--control-primary-border-default);\n    color: var(--control-primary-text-default);\n\n    &:hover {\n        background-color: var(--control-primary-background-hover);\n        border-color: var(--control-primary-border-hover);\n        color: var(--control-primary-text-hover);\n    }\n}\n\n.vc-btn-secondary,\n.vc-btn-link {\n    background-color: var(--control-secondary-background-default);\n    border-color: var(--control-secondary-border-default);\n    color: var(--control-secondary-text-default);\n\n    &:hover {\n        background-color: var(--control-secondary-background-hover);\n        border-color: var(--control-secondary-border-hover);\n        color: var(--control-secondary-text-hover);\n    }\n}\n\n.vc-btn-dangerPrimary {\n    background-color: var(--control-critical-primary-background-default);\n    border-color: var(--control-critical-primary-border-default);\n    color: var(--control-critical-primary-text-default);\n\n    &:hover {\n        background-color: var(--control-critical-primary-background-hover);\n        border-color: var(--control-critical-primary-border-hover);\n        color: var(--control-critical-primary-text-hover);\n    }\n}\n\n.vc-btn-dangerSecondary {\n    background-color: var(--control-critical-secondary-background-default);\n    border-color: var(--control-critical-secondary-border-default);\n    color: var(--control-critical-secondary-text-default);\n\n    &:hover {\n        background-color: var(--control-critical-secondary-background-hover);\n        border-color: var(--control-critical-secondary-border-hover);\n        color: var(--control-critical-secondary-text-hover);\n    }\n}\n\n.vc-btn-overlayPrimary {\n    background-color: var(--control-overlay-primary-background-default);\n    border-color: var(--control-overlay-primary-border-default);\n    color: var(--control-overlay-primary-text-default);\n\n    &:hover {\n        background-color: var(--control-overlay-primary-background-hover);\n        border-color: var(--control-overlay-primary-border-hover);\n        color: var(--control-overlay-primary-text-hover);\n    }\n}\n\n.vc-btn-positive {\n    background-color: var(--control-connected-background-default, var(--green-430));\n    color: var(--white);\n\n    &:hover {\n        background-color: var(--control-connected-background-hover, var(--green-460));\n    }\n}\n\n.vc-btn-none {\n    background-color: transparent;\n    border-color: transparent;\n    color: var(--control-icon-only-icon-default);\n\n    &:hover {\n        background-color: var(--control-icon-only-background-hover);\n        border-color: var(--control-icon-only-border-hover);\n        color: var(--control-icon-only-icon-hover);\n    }\n}\n\n.vc-btn-link-icon {\n    width: 0.875em;\n    height: 0.875em;\n    margin-left: 8px;\n    flex-shrink: 0;\n}\n\n.vc-text-btn-base {\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    gap: var(--space-4, 4px);\n    background: initial;\n    color: var(--text-default);\n    font-size: medium;\n    font-weight: 400;\n    margin: 0;\n    padding: 0;\n    text-align: start;\n    text-decoration: none;\n    max-width: 100%;\n    white-space: nowrap;\n\n    &:disabled {\n        opacity: 0.5;\n    }\n\n    &:hover {\n        text-decoration: underline;\n    }\n\n    &:focus-visible {\n        /* stylelint-disable-next-line custom-property-pattern */\n        box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--border-focus, #00b0f4));\n    }\n}\n\n.vc-text-btn-primary {\n    color: var(--text-brand);\n}\n\n.vc-text-btn-secondary {\n    color: var(--text-strong, var(--text-default));\n}\n\n.vc-text-btn-danger {\n    color: var(--text-feedback-critical);\n}\n\n.vc-text-btn-link {\n    color: var(--text-link);\n}"
  },
  {
    "path": "src/components/Button.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./Button.css\";\n\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport type { Button as DiscordButton } from \"@vencord/discord-types\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { OpenExternalIcon } from \"./Icons\";\nimport { Link } from \"./Link\";\n\nconst btnCls = classNameFactory(\"vc-btn-\");\nconst textBtnCls = classNameFactory(\"vc-text-btn-\");\n\nexport type ButtonVariant =\n    \"primary\" | \"secondary\" | \"dangerPrimary\" | \"dangerSecondary\" | \"overlayPrimary\" | \"positive\" | \"link\" | \"none\";\nexport type ButtonSize = \"min\" | \"xs\" | \"small\" | \"medium\" | \"iconOnly\";\n\nexport type ButtonProps = ComponentPropsWithRef<\"button\"> & {\n    variant?: ButtonVariant;\n    size?: ButtonSize;\n};\n\nexport type LinkButtonProps = ComponentPropsWithRef<\"a\"> & {\n    size?: ButtonSize;\n    variant?: ButtonVariant;\n};\n\nexport function Button({ variant = \"primary\", size = \"medium\", children, className, ...restProps }: ButtonProps) {\n    return (\n        <button data-mana-component=\"button\" className={classes(btnCls(\"base\", variant, size), className)} {...restProps}>\n            {children}\n            {variant === \"link\" && <OpenExternalIcon className={btnCls(\"link-icon\")} />}\n        </button>\n    );\n}\n\nexport function LinkButton({ variant = \"link\", size = \"medium\", className, children, ...restProps }: LinkButtonProps) {\n    return (\n        <Link data-mana-component=\"button\" className={classes(btnCls(\"base\", variant, size), className)} {...restProps}>\n            {children}\n            <OpenExternalIcon className={btnCls(\"link-icon\")} />\n        </Link>\n    );\n}\n\nexport type TextButtonVariant = \"primary\" | \"secondary\" | \"danger\" | \"link\";\n\nexport type TextButtonProps = ComponentPropsWithRef<\"button\"> & {\n    variant?: TextButtonVariant;\n};\n\nexport function TextButton({ variant = \"primary\", className, ...restProps }: TextButtonProps) {\n    return (\n        <button className={classes(textBtnCls(\"base\", variant), className)} {...restProps} />\n    );\n}\n\n// #region Old compability\n\nexport const ButtonCompat: DiscordButton = function ButtonCompat({ look, color = \"BRAND\", size = \"medium\", ...restProps }) {\n    return look === \"LINK\"\n        ? <TextButton variant={TextButtonPropsColorMapping[color]} {...restProps as TextButtonProps} />\n        : <Button variant={ButtonColorMapping[color]} size={size as ButtonSize} {...restProps as ButtonProps} />;\n};\n\n/** @deprecated */\nButtonCompat.Looks = {\n    FILLED: \"\",\n    LINK: \"LINK\"\n} as const;\n\n/** @deprecated */\nButtonCompat.Colors = {\n    BRAND: \"BRAND\",\n    PRIMARY: \"PRIMARY\",\n    RED: \"RED\",\n    TRANSPARENT: \"TRANSPARENT\",\n    CUSTOM: \"CUSTOM\",\n    GREEN: \"GREEN\",\n    LINK: \"LINK\",\n    WHITE: \"WHITE\",\n} as const;\n\nconst ButtonColorMapping: Record<keyof typeof ButtonCompat[\"Colors\"], ButtonProps[\"variant\"]> = {\n    BRAND: \"primary\",\n    PRIMARY: \"secondary\",\n    RED: \"dangerPrimary\",\n    TRANSPARENT: \"secondary\",\n    CUSTOM: \"none\",\n    GREEN: \"positive\",\n    LINK: \"link\",\n    WHITE: \"overlayPrimary\"\n};\n\nconst TextButtonPropsColorMapping: Record<keyof typeof ButtonCompat[\"Colors\"], TextButtonProps[\"variant\"]> = {\n    BRAND: \"primary\",\n    PRIMARY: \"primary\",\n    RED: \"danger\",\n    TRANSPARENT: \"secondary\",\n    CUSTOM: \"secondary\",\n    GREEN: \"primary\",\n    LINK: \"link\",\n    WHITE: \"secondary\"\n};\n\n/** @deprecated */\nButtonCompat.Sizes = {\n    SMALL: \"small\",\n    MEDIUM: \"medium\",\n    LARGE: \"medium\",\n    XLARGE: \"medium\",\n    NONE: \"min\",\n    MIN: \"min\"\n} as const;\n\n// #endregion\n"
  },
  {
    "path": "src/components/Card.css",
    "content": ".vc-card-base {\n    background: var(--card-background-default);\n    border-radius: var(--radius-sm, 8px);\n}\n\n.vc-card-normal {\n    border: 1px var(--border-subtle);\n}\n\n.vc-card-warning {\n    background-color: var(--background-feedback-warning);\n    border: 1px solid var(--icon-feedback-warning);\n}\n\n.vc-card-danger {\n    background-color: var(--background-feedback-critical);\n    border: 1px solid var(--icon-feedback-critical);\n}\n\n.vc-card-success {\n    background-color: var(--background-feedback-positive);\n    border: 1px solid var(--icon-feedback-positive);\n}\n\n.vc-card-info {\n    background: var(--background-feedback-info);\n    border: 1px solid var(--icon-feedback-info);\n}\n\n.vc-card-defaultPadding {\n    padding: 1em;\n}"
  },
  {
    "path": "src/components/Card.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./Card.css\";\n\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport { ComponentPropsWithRef } from \"react\";\n\nconst cl = classNameFactory(\"vc-card-\");\n\nexport interface CardProps extends ComponentPropsWithRef<\"div\"> {\n    variant?: \"normal\" | \"warning\" | \"danger\" | \"info\" | \"success\";\n    /** Add a default padding of 1em to the card. This is implied if no className prop is passed */\n    defaultPadding?: boolean;\n}\n\nexport function Card({ variant = \"normal\", defaultPadding, children, className, ...restProps }: CardProps) {\n    const addDefaultPadding = defaultPadding != null\n        ? defaultPadding\n        : !className;\n\n    return (\n        <div className={classes(cl(\"base\", variant, addDefaultPadding && \"defaultPadding\"), className)} {...restProps}>\n            {children}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/CheckedTextInput.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { React, TextInput } from \"@webpack/common\";\n\ninterface TextInputProps {\n    /**\n     * WARNING: Changing this between renders will have no effect!\n     */\n    value: string;\n    /**\n     * This will only be called if the new value passed validate()\n     */\n    onChange(newValue: string): void;\n    /**\n     * Optionally validate the user input\n     * Return true if the input is valid\n     * Otherwise, return a string containing the reason for this input being invalid\n     */\n    validate(v: string): true | string;\n}\n\n/**\n * A very simple wrapper around Discord's TextInput that validates input and shows\n * the user an error message and only calls your onChange when the input is valid\n */\nexport function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {\n    const [value, setValue] = React.useState(initialValue);\n    const [error, setError] = React.useState<string>();\n\n    function handleChange(v: string) {\n        setValue(v);\n        const res = validate(v);\n        if (res === true) {\n            setError(void 0);\n            onChange(v);\n        } else {\n            setError(res);\n        }\n    }\n\n    return (\n        <>\n            <TextInput\n                type=\"text\"\n                value={value}\n                onChange={handleChange}\n                error={error}\n            />\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/CodeBlock.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Parser } from \"@webpack/common\";\n\nconst CodeContainerClasses = findCssClassesLazy(\"markup\", \"codeContainer\");\n\n/**\n * Renders code in a Discord codeblock\n */\nexport function CodeBlock(props: { content?: string, lang: string; }) {\n    return (\n        <div className={CodeContainerClasses.markup}>\n            {Parser.defaultRules.codeBlock.react(props, null, {})}\n        </div>\n    );\n}\n\n/**\n * Renders inline code like `this`\n */\nexport function InlineCode({ children }: { children: React.ReactNode; }) {\n    return (\n        <span className={CodeContainerClasses.markup}>\n            <code className=\"inline\">{children}</code>\n        </span>\n    );\n}\n"
  },
  {
    "path": "src/components/Divider.css",
    "content": ".vc-divider {\n    height: 1px;\n    width: 100%;\n    border: none;\n    border-top: 1px solid var(--border-subtle);\n}"
  },
  {
    "path": "src/components/Divider.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./Divider.css\";\n\nimport { classes } from \"@utils/misc\";\nimport type { ComponentPropsWithoutRef } from \"react\";\n\nexport type DividerProps = ComponentPropsWithoutRef<\"hr\">;\n\nexport function Divider({ className, ...restProps }: DividerProps) {\n    return (\n        <hr\n            className={classes(\"vc-divider\", className)}\n            {...restProps}\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/ErrorBoundary.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { LazyComponent, LazyComponentWrapper } from \"@utils/lazyReact\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport type { React } from \"@webpack/common\";\nimport { ComponentType } from \"react\";\n\nimport { ErrorCard } from \"./ErrorCard\";\n\ninterface Props<T = any> {\n    /** Render nothing if an error occurs */\n    noop?: boolean;\n    /** Fallback component to render if an error occurs */\n    fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; wrappedProps: T; }>>;\n    /** called when an error occurs. The props property is only available if using .wrap */\n    onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;\n    /** Custom error message */\n    message?: string;\n\n    /** The props passed to the wrapped component. Only used by wrap */\n    wrappedProps?: T;\n}\n\nconst color = \"#e78284\";\n\nconst logger = new Logger(\"React ErrorBoundary\", color);\n\nconst NO_ERROR = {};\n\n// We might want to import this in a place where React isn't ready yet.\n// Thus, wrap in a LazyComponent\nconst ErrorBoundary = LazyComponent(() => {\n    // This component is used in a lot of files which end up importing other Webpack commons and causing circular imports.\n    // For this reason, use a non import access here.\n    return class ErrorBoundary extends Vencord.Webpack.Common.React.PureComponent<React.PropsWithChildren<Props>> {\n        state = {\n            error: NO_ERROR as any,\n            stack: \"\",\n            message: \"\"\n        };\n\n        static getDerivedStateFromError(error: any) {\n            let stack = error?.stack ?? \"\";\n            let message = error?.message || String(error);\n\n            if (error instanceof Error && stack) {\n                const eolIdx = stack.indexOf(\"\\n\");\n                if (eolIdx !== -1) {\n                    message = stack.slice(0, eolIdx);\n                    stack = stack.slice(eolIdx + 1).replace(/https:\\/\\/\\S+\\/assets\\//g, \"\");\n                }\n            }\n\n            return { error, stack, message };\n        }\n\n        componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n            this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });\n            logger.error(`${this.props.message || \"A component threw an Error\"}\\n`, error, errorInfo.componentStack);\n        }\n\n        get isNoop() {\n            if (IS_DEV) return false;\n            return this.props.noop;\n        }\n\n        render() {\n            if (this.state.error === NO_ERROR) return this.props.children;\n\n            if (this.isNoop) return null;\n\n            if (this.props.fallback)\n                return (\n                    <this.props.fallback\n                        wrappedProps={this.props.wrappedProps}\n                        {...this.state}\n                    >\n                        {this.props.children}\n                    </this.props.fallback>\n                );\n\n            const msg = this.props.message || \"An error occurred while rendering this Component. More info can be found below and in your console.\";\n\n            return (\n                <ErrorCard style={{ overflow: \"hidden\" }}>\n                    <h1>Oh no!</h1>\n                    <p>{msg}</p>\n                    <code>\n                        {this.state.message}\n                        {!!this.state.stack && (\n                            <pre className={Margins.top8}>\n                                {this.state.stack}\n                            </pre>\n                        )}\n                    </code>\n                </ErrorCard>\n            );\n        }\n    };\n}) as\n    LazyComponentWrapper<React.ComponentType<React.PropsWithChildren<Props>> & {\n        wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, \"wrappedProps\"> & { displayName?: string; }): React.FunctionComponent<T>;\n    }>;\n\nErrorBoundary.wrap = (Component, errorBoundaryProps) => {\n    const wrapper: ComponentType<any> = props => (\n        <ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>\n            <Component {...props} />\n        </ErrorBoundary>\n    );\n\n    if (errorBoundaryProps?.displayName) {\n        wrapper.displayName = errorBoundaryProps.displayName;\n    }\n\n    return wrapper;\n};\n\nexport default ErrorBoundary;\n"
  },
  {
    "path": "src/components/ErrorCard.css",
    "content": ".vc-error-card {\n    padding: 2em;\n    background-color: #e7828430;\n    border: 1px solid #e78284;\n    border-radius: 5px;\n    color: var(--text-default, white);\n\n    & a:hover {\n        text-decoration: underline;\n    }\n}\n"
  },
  {
    "path": "src/components/ErrorCard.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./ErrorCard.css\";\n\nimport { classes } from \"@utils/misc\";\nimport type { HTMLProps } from \"react\";\n\nexport function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {\n    return (\n        <div {...props} className={classes(props.className, \"vc-error-card\")}>\n            {props.children}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/ExpandableCard.css",
    "content": ".vc-expandable-card {\n    color: var(--text-default);\n}\n\n.vc-expandable-card-header {\n    display: flex;\n    cursor: pointer;\n    padding: .75em 1em;\n    margin: 4px;\n    border-radius: var(--radius-xs);\n\n    .vc-expandable-card[data-expanded=\"true\"] & {\n        background: var(--card-secondary-bg);\n    }\n}\n\n.vc-expandable-card-icon {\n    margin-left: auto;\n}\n\n.vc-expandable-card-content {\n    padding: 0.5em 1em;\n}"
  },
  {
    "path": "src/components/ExpandableCard.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2026 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./ExpandableCard.css\";\n\nimport { classes } from \"@utils/misc\";\nimport { Clickable, useState } from \"@webpack/common\";\nimport { PropsWithChildren } from \"react\";\n\nimport { Card } from \"./Card\";\nimport { DownArrow, RightArrow } from \"./Icons\";\n\nexport type ExpandableSectionProps = PropsWithChildren<{\n    renderContent: () => React.ReactNode;\n    className?: string;\n    initialExpanded?: boolean;\n}>;\n\n/**\n * A card component that can expand and collapse to show/hide content. The header (props.children) is always visible, and the content (props.renderContent) is only visible when expanded.\n */\nexport function ExpandableSection({ children, renderContent: Content, className, initialExpanded = false }: ExpandableSectionProps) {\n    const [expanded, setExpanded] = useState(initialExpanded);\n\n    const Icon = expanded ? DownArrow : RightArrow;\n\n    return (\n        <Card data-expanded={expanded} className={classes(\"vc-expandable-card\", className)}>\n            <Clickable className=\"vc-expandable-card-header\" onClick={() => setExpanded(c => !c)} >\n                {children}\n                <Icon className=\"vc-expandable-card-icon\" />\n            </Clickable>\n\n            {expanded\n                ? <div className=\"vc-expandable-card-content\">\n                    <Content />\n                </div>\n                : null\n            }\n        </Card>\n    );\n}\n"
  },
  {
    "path": "src/components/Flex.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type { CSSProperties, HTMLAttributes } from \"react\";\n\nexport interface FlexProps extends HTMLAttributes<HTMLDivElement> {\n    flexDirection?: CSSProperties[\"flexDirection\"];\n    gap?: CSSProperties[\"gap\"];\n    justifyContent?: CSSProperties[\"justifyContent\"];\n    alignItems?: CSSProperties[\"alignItems\"];\n    flexWrap?: CSSProperties[\"flexWrap\"];\n}\n\nexport function Flex({ flexDirection, gap = \"1em\", justifyContent, alignItems, flexWrap, children, style, ...restProps }: FlexProps) {\n    style = {\n        display: \"flex\",\n        flexDirection,\n        gap,\n        justifyContent,\n        alignItems,\n        flexWrap,\n        ...style\n    };\n\n    return (\n        <div style={style} {...restProps}>\n            {children}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/FormSwitch.css",
    "content": ".vc-form-switch-wrapper {\n    margin-bottom: 20px;\n    cursor: pointer;\n}\n\n.vc-form-switch {\n    display: flex;\n    width: 100%;\n\n    > :last-child {\n        margin-left: auto;\n    }\n}\n\n.vc-form-switch-disabled {\n    opacity: 0.5;\n    pointer-events: none;\n    cursor: not-allowed;\n}\n\n.vc-form-switch-text {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    gap: 8px;\n}\n\n.vc-form-switch-border {\n    margin-top: 20px;\n}"
  },
  {
    "path": "src/components/FormSwitch.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./FormSwitch.css\";\n\nimport { classes } from \"@utils/misc\";\nimport type { PropsWithChildren, ReactNode } from \"react\";\n\nimport { Divider } from \"./Divider\";\nimport { Span } from \"./Span\";\nimport { Switch } from \"./Switch\";\n\nexport interface FormSwitchProps {\n    title: ReactNode;\n    description?: ReactNode;\n    value: boolean;\n    onChange(value: boolean): void;\n\n    className?: string;\n    disabled?: boolean;\n    hideBorder?: boolean;\n}\n\nexport function FormSwitch({ onChange, title, value, description, disabled, className, hideBorder }: FormSwitchProps) {\n    return (\n        <label className=\"vc-form-switch-wrapper\">\n            <div className={classes(\"vc-form-switch\", className, disabled && \"vc-form-switch-disabled\")}>\n                <div className={\"vc-form-switch-text\"}>\n                    <Span size=\"md\" weight=\"medium\">{title}</Span>\n                    {description && <Span size=\"sm\" weight=\"normal\">{description}</Span>}\n                </div>\n\n                <Switch checked={value} onChange={onChange} disabled={disabled} />\n            </div>\n            {!hideBorder && <Divider className=\"vc-form-switch-border\" />}\n        </label>\n    );\n}\n\n// #region Old compatibility\n\nexport function FormSwitchCompat({ note, children, ...restProps }: PropsWithChildren<any>) {\n    return <FormSwitch title={children ?? \"\"} description={note} {...restProps} />;\n}\n\n// #endregion\n"
  },
  {
    "path": "src/components/Grid.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { CSSProperties, JSX } from \"react\";\n\ninterface Props {\n    columns: number;\n    gap?: string;\n    inline?: boolean;\n}\n\nexport function Grid(props: Props & JSX.IntrinsicElements[\"div\"]) {\n    const style: CSSProperties = {\n        display: props.inline ? \"inline-grid\" : \"grid\",\n        gridTemplateColumns: `repeat(${props.columns}, 1fr)`,\n        gap: props.gap,\n        ...props.style\n    };\n\n    return (\n        <div {...props} style={style}>\n            {props.children}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/Heading.css",
    "content": ".vc-h1,\n.vc-h2 {\n    color: var(--text-strong);\n    font-weight: 600;\n}\n\n.vc-h3,\n.vc-h4,\n.vc-h5 {\n    color: var(--text-strong);\n}\n\n.vc-h1 {\n    font-size: 20px;\n    line-height: 24px\n}\n\n.vc-h2 {\n    font-size: 16px;\n    line-height: 20px\n}\n\n.vc-h3 {\n    font-weight: 500;\n    line-height: 24px\n}\n\n.vc-h3,\n.vc-h4 {\n    font-size: 16px\n}\n\n.vc-h4 {\n    font-weight: 600;\n    letter-spacing: .3px\n}\n\n.vc-h4,\n.vc-h5 {\n    line-height: 20px\n}\n\n.vc-h5 {\n    color: var(--text-strong);\n    font-size: 16px;\n    font-weight: 500;\n    margin-bottom: 8px;\n    text-transform: unset\n}\n\n.vc-h1-defaultMargin,\n.vc-h2-defaultMargin {\n    margin-bottom: 20px\n}\n\n/* This is copied from Discord, don't ask me why 0 margin */\n.vc-h4-defaultMargin {\n    margin-bottom: 0;\n    margin-top: 0;\n}\n\n.vc-h3-defaultMargin,\n.vc-h5-defaultMargin {\n    margin-bottom: 8px\n}"
  },
  {
    "path": "src/components/Heading.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./Heading.css\";\n\nimport { classes } from \"@utils/misc\";\nimport type { ComponentPropsWithoutRef } from \"react\";\n\nexport type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;\n\nexport type HeadingProps<Tag extends HeadingTag> = ComponentPropsWithoutRef<Tag> & {\n    tag?: Tag;\n};\n\n/**\n * A simple heading component that automatically sizes according to the tag used.\n *\n * If you need more control, use the BaseText component instead.\n */\nexport function Heading<T extends HeadingTag>(props: HeadingProps<T>) {\n    const {\n        tag: Tag = \"h5\",\n        children,\n        className,\n        ...restProps\n    } = props;\n\n    return (\n        <Tag className={classes(`vc-${Tag}`, !className && `vc-${Tag}-defaultMargin`, className)} {...restProps}>\n            {children}\n        </Tag>\n    );\n}\n\nexport function HeadingPrimary({ children, ...restProps }: HeadingProps<\"h2\">) {\n    return (\n        <Heading tag=\"h2\" {...restProps}>\n            {children}\n        </Heading>\n    );\n}\n\nexport function HeadingSecondary({ children, ...restProps }: HeadingProps<\"h3\">) {\n    return (\n        <Heading tag=\"h3\" {...restProps}>\n            {children}\n        </Heading>\n    );\n}\n\nexport function HeadingTertiary({ children, ...restProps }: HeadingProps<\"h4\">) {\n    return (\n        <Heading tag=\"h4\" {...restProps}>\n            {children}\n        </Heading>\n    );\n}\n"
  },
  {
    "path": "src/components/Heart.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { classes } from \"@utils/misc\";\nimport { SVGProps } from \"react\";\n\nexport function Heart(props: SVGProps<SVGSVGElement>) {\n    return (\n        <svg\n            aria-hidden=\"true\"\n            viewBox=\"0 0 16 16\"\n            height=\"16\"\n            width=\"16\"\n            {...props}\n            className={classes(\"vc-heart-icon\", props.className)}\n        >\n            <path\n                fill=\"#db61a2\"\n                fillRule=\"evenodd\"\n                d=\"M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z\"\n            />\n        </svg>\n    );\n}\n"
  },
  {
    "path": "src/components/Icons.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./iconStyles.css\";\n\nimport { getIntlMessage } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport type { JSX, PropsWithChildren } from \"react\";\n\ninterface BaseIconProps extends IconProps {\n    viewBox: string;\n}\n\ntype IconProps = JSX.IntrinsicElements[\"svg\"];\n\nfunction Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {\n    return (\n        <svg\n            className={classes(className, \"vc-icon\")}\n            role=\"img\"\n            width={width}\n            height={height}\n            viewBox={viewBox}\n            {...svgProps}\n        >\n            {children}\n        </svg>\n    );\n}\n\n/**\n * Discord's link icon, as seen in the Message context menu \"Copy Message Link\" option\n */\nexport function LinkIcon({ height = 24, width = 24, className }: IconProps) {\n    return (\n        <Icon\n            height={height}\n            width={width}\n            className={classes(className, \"vc-link-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <g fill=\"none\" fillRule=\"evenodd\">\n                <path fill=\"currentColor\" d=\"M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z\" />\n                <rect width={width} height={height} />\n            </g>\n        </Icon>\n    );\n}\n\n/**\n * Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks\n */\nexport function CopyIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-copy-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <g fill=\"currentColor\">\n                <path d=\"M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z\" />\n                <path d=\"M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z\" />\n                <path d=\"M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z\" />\n            </g>\n        </Icon>\n    );\n}\n\n/**\n * Discord's open external icon, as seen in the user profile connections\n */\nexport function OpenExternalIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-open-external-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill=\"currentColor\" d=\"M15 2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V4.41l-4.3 4.3a1 1 0 1 1-1.4-1.42L19.58 3H16a1 1 0 0 1-1-1Z\" />\n            <path fill=\"currentColor\" d=\"M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a1 1 0 1 0-2 0v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 1 0 0-2H5Z\" />\n        </Icon>\n    );\n}\n\nexport function ImageIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-image-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill=\"currentColor\" d=\"M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z\" />\n        </Icon>\n    );\n}\n\nexport function InfoIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-info-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                d=\"M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z\" clipRule=\"evenodd\"\n            />\n        </Icon>\n    );\n}\n\nexport function OwnerCrownIcon(props: IconProps) {\n    return (\n        <Icon\n            aria-label={getIntlMessage(\"GUILD_OWNER\")}\n            {...props}\n            className={classes(props.className, \"vc-owner-crown-icon\")}\n            role=\"img\"\n            viewBox=\"0 0 16 16\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M13.6572 5.42868C13.8879 5.29002 14.1806 5.30402 14.3973 5.46468C14.6133 5.62602 14.7119 5.90068 14.6473 6.16202L13.3139 11.4954C13.2393 11.7927 12.9726 12.0007 12.6666 12.0007H3.33325C3.02725 12.0007 2.76058 11.792 2.68592 11.4954L1.35258 6.16202C1.28792 5.90068 1.38658 5.62602 1.60258 5.46468C1.81992 5.30468 2.11192 5.29068 2.34325 5.42868L5.13192 7.10202L7.44592 3.63068C7.46173 3.60697 7.48377 3.5913 7.50588 3.57559C7.5192 3.56612 7.53255 3.55663 7.54458 3.54535L6.90258 2.90268C6.77325 2.77335 6.77325 2.56068 6.90258 2.43135L7.76458 1.56935C7.89392 1.44002 8.10658 1.44002 8.23592 1.56935L9.09792 2.43135C9.22725 2.56068 9.22725 2.77335 9.09792 2.90268L8.45592 3.54535C8.46794 3.55686 8.48154 3.56651 8.49516 3.57618C8.51703 3.5917 8.53897 3.60727 8.55458 3.63068L10.8686 7.10202L13.6572 5.42868ZM2.66667 12.6673H13.3333V14.0007H2.66667V12.6673Z\"\n            />\n        </Icon>\n    );\n}\n\n/**\n * Discord's screenshare icon, as seen in the connection panel\n */\nexport function ScreenshareIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-screenshare-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M2 4.5C2 3.397 2.897 2.5 4 2.5H20C21.103 2.5 22 3.397 22 4.5V15.5C22 16.604 21.103 17.5 20 17.5H13V19.5H17V21.5H7V19.5H11V17.5H4C2.897 17.5 2 16.604 2 15.5V4.5ZM13.2 14.3375V11.6C9.864 11.6 7.668 12.6625 6 15C6.672 11.6625 8.532 8.3375 13.2 7.6625V5L18 9.6625L13.2 14.3375Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function ImageVisible(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-image-visible\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill=\"currentColor\" d=\"M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Zm1-2h12l-3.75-5-3 4L9 13Zm-1 2V5v14Z\" />\n        </Icon>\n    );\n}\n\nexport function ImageInvisible(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-image-invisible\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill=\"currentColor\" d=\"m21 18.15-2-2V5H7.85l-2-2H19q.825 0 1.413.587Q21 4.175 21 5Zm-1.2 4.45L18.2 21H5q-.825 0-1.413-.587Q3 19.825 3 19V5.8L1.4 4.2l1.4-1.4 18.4 18.4ZM6 17l3-4 2.25 3 .825-1.1L5 7.825V19h11.175l-2-2Zm7.425-6.425ZM10.6 13.4Z\" />\n        </Icon>\n    );\n}\n\nexport function Microphone(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-microphone\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fillRule=\"evenodd\" clipRule=\"evenodd\" d=\"M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V21H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1ZM12 4C11.2 4 11 4.66667 11 5V11C11 11.3333 11.2 12 12 12C12.8 12 13 11.3333 13 11V5C13 4.66667 12.8 4 12 4Z\" fill=\"currentColor\" />\n            <path fillRule=\"evenodd\" clipRule=\"evenodd\" d=\"M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V22H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1Z\" fill=\"currentColor\" />\n        </Icon >\n    );\n}\n\nexport function CogWheel(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-cog-wheel\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                d=\"M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z\"\n                clipRule=\"evenodd\"\n            />\n        </Icon>\n    );\n}\n\nexport function ReplyIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-reply-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M10 8.26667V4L3 11.4667L10 18.9333V14.56C15 14.56 18.5 16.2667 21 20C20 14.6667 17 9.33333 10 8.26667Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function DeleteIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-delete-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z\"\n            />\n            <path\n                fill=\"currentColor\"\n                d=\"M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function PlusIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-plus-icon\")}\n            viewBox=\"0 0 18 18\"\n        >\n            <polygon\n                fillRule=\"nonzero\"\n                fill=\"currentColor\"\n                points=\"15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8\"\n            />\n        </Icon>\n    );\n}\n\nexport function NoEntrySignIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-no-entry-sign-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                d=\"M0 0h24v24H0z\"\n                fill=\"none\"\n            />\n            <path\n                fill=\"currentColor\"\n                d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z\"\n            />\n        </Icon>\n    );\n}\n\nexport function SafetyIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-safety-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M4.27 5.22A2.66 2.66 0 0 0 3 7.5v2.3c0 5.6 3.3 10.68 8.42 12.95.37.17.79.17 1.16 0A14.18 14.18 0 0 0 21 9.78V7.5c0-.93-.48-1.78-1.27-2.27l-6.17-3.76a3 3 0 0 0-3.12 0L4.27 5.22ZM6 7.68l6-3.66V12H6.22C6.08 11.28 6 10.54 6 9.78v-2.1Zm6 12.01V12h5.78A11.19 11.19 0 0 1 12 19.7Z\"\n            />\n        </Icon>\n\n    );\n}\n\nexport function NotesIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-notes-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M8 3C7.44771 3 7 3.44772 7 4V5C7 5.55228 7.44772 6 8 6H16C16.5523 6 17 5.55228 17 5V4C17 3.44772 16.5523 3 16 3H15.1245C14.7288 3 14.3535 2.82424 14.1002 2.52025L13.3668 1.64018C13.0288 1.23454 12.528 1 12 1C11.472 1 10.9712 1.23454 10.6332 1.64018L9.8998 2.52025C9.64647 2.82424 9.27121 3 8.8755 3H8Z\"\n            />\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                fill=\"currentColor\"\n                d=\"M19 4.49996V4.99996C19 6.65681 17.6569 7.99996 16 7.99996H8C6.34315 7.99996 5 6.65681 5 4.99996V4.49996C5 4.22382 4.77446 3.99559 4.50209 4.04109C3.08221 4.27826 2 5.51273 2 6.99996V19C2 20.6568 3.34315 22 5 22H19C20.6569 22 22 20.6568 22 19V6.99996C22 5.51273 20.9178 4.27826 19.4979 4.04109C19.2255 3.99559 19 4.22382 19 4.49996ZM8 12C7.44772 12 7 12.4477 7 13C7 13.5522 7.44772 14 8 14H16C16.5523 14 17 13.5522 17 13C17 12.4477 16.5523 12 16 12H8ZM7 17C7 16.4477 7.44772 16 8 16H13C13.5523 16 14 16.4477 14 17C14 17.5522 13.5523 18 13 18H8C7.44772 18 7 17.5522 7 17Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function FolderIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-folder-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function LogIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-log-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function RestartIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-restart-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function PaintbrushIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-paintbrush-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function PencilIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            className={classes(props.className, \"vc-pencil-icon\")}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill=\"currentColor\"\n                d=\"m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function GithubIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"-3 -3 30 30\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.998.108-.775.42-1.305.763-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.625-5.475 5.92.43.37.823 1.102.823 2.222v3.293c0 .32.218.694.825.577C20.565 21.797 24 17.298 24 12c0-6.63-5.37-12-12-12z\"\n            />\n        </Icon>\n    );\n}\n\nexport function WebsiteIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zM4 12c0-.899.156-1.762.431-2.569L6 11l2 2v2l2 2 1 1v1.931C7.061 19.436 4 16.072 4 12zm14.33 4.873C17.677 16.347 16.687 16 16 16v-1a2 2 0 0 0-2-2h-4v-3a2 2 0 0 0 2-2V7h1a2 2 0 0 0 2-2v-.411C17.928 5.778 20 8.65 20 12a7.947 7.947 0 0 1-1.67 4.873z\"\n            />\n        </Icon>\n    );\n}\n\n/**\n * A question mark inside a square, used as a placeholder icon when no other icon is available\n */\nexport function PlaceholderIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill={props.fill || \"currentColor\"} fillRule=\"evenodd\" d=\"M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z\" clipRule=\"evenodd\" />\n        </Icon>\n    );\n}\n\nexport function MainSettingsIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function PluginsIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M18.559 12.8227C17.7884 13.4957 16.6663 13.3616 15.9404 12.641C14.7975 11.5063 11.4931 8.21104 11.4931 8.21104C10.897 7.63087 10.897 6.44662 11.4931 5.85464C12.319 5.03435 13.6053 3.75146 13.6053 3.75146C13.9641 3.39195 14.456 3.18972 14.9653 3.18886L18.3363 3.18425L19.5255 2L22.5 4.96048L21.3108 6.14473L21.3021 9.50878C21.2992 10.0164 21.0967 10.5026 20.735 10.8613C20.735 10.8613 19.5718 11.9384 18.559 12.8227ZM15.2315 13.9548L13.4954 15.8273C14.0972 16.4265 14.0972 16.9113 13.64 17.6997L11.3976 20.2485C11.0359 20.6081 10.5469 20.8103 10.0347 20.8111L6.66378 20.8158L5.47455 22L2.5 19.0395L3.68927 17.8553L3.70082 14.4912C3.70082 13.9836 3.90338 13.4974 4.26507 13.1387L6.37153 11.0404C6.96759 10.4485 8.15685 10.4485 8.73844 11.0404L8.74424 11.0465L10.5295 9.26998L11.7188 10.4542L9.93347 12.2305L12.3119 14.599L14.0972 12.8227L15.2315 13.9548Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function CloudIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M16.8333 19H5.16667C3.16667 19 1.5 17.3333 1.5 15.3333C1.5 13.4 2.96667 11.8667 4.83333 11.6667V11.3333C4.83333 7.86667 7.7 5 11.1667 5C14.0333 5 16.5667 6.93333 17.3 9.66667C19.7 9.86667 21.5 11.8667 21.5 14.3333C21.5 16.9333 19.4333 19 16.8333 19Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function BackupRestoreIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M21 2.01232C21.2652 2.01232 21.5196 2.11757 21.7071 2.30492C21.8946 2.49226 22 2.74636 22 3.0113V9.00521C22 9.27015 21.8946 9.52425 21.7071 9.7116C21.5196 9.89894 21.2652 10.0042 21 10.0042H15C14.7348 10.0042 14.4804 9.89894 14.2929 9.7116C14.1054 9.52425 14 9.27015 14 9.00521C14 8.74026 14.1054 8.48617 14.2929 8.29882C14.4804 8.11147 14.7348 8.00622 15 8.00622H18.93C18.352 7.00597 17.5638 6.14275 16.6198 5.47602C15.6758 4.80929 14.5983 4.35488 13.4616 4.1441C12.3249 3.93332 11.1559 3.97117 10.0353 4.25505C8.91459 4.53892 7.86883 5.06208 6.97 5.78848C6.76313 5.9554 6.49836 6.03338 6.23393 6.00528C5.96951 5.97718 5.72709 5.84529 5.56 5.63863C5.39291 5.43197 5.31485 5.16747 5.34298 4.90331C5.37111 4.63916 5.50313 4.39698 5.71 4.23006C6.7542 3.38308 7.959 2.7557 9.25204 2.38561C10.5451 2.01552 11.8996 1.91037 13.2344 2.07646C14.5691 2.24255 15.8565 2.67646 17.0191 3.35212C18.1818 4.02778 19.1957 4.93125 20 6.00826V3.0113C20 2.74636 20.1054 2.49226 20.2929 2.30492C20.4804 2.11757 20.7348 2.01232 21 2.01232ZM3 21.992C2.73478 21.992 2.48043 21.8867 2.29289 21.6994C2.10536 21.5121 2 21.258 2 20.993V14.9991C2 14.7342 2.10536 14.4801 2.29289 14.2927C2.48043 14.1054 2.73478 14.0001 3 14.0001H9C9.26522 14.0001 9.51957 14.1054 9.70711 14.2927C9.89464 14.4801 10 14.7342 10 14.9991C10 15.2641 9.89464 15.5182 9.70711 15.7055C9.51957 15.8928 9.26522 15.9981 9 15.9981H5.07C5.64801 16.9983 6.43617 17.8616 7.3802 18.5283C8.32424 19.195 9.40171 19.6494 10.5384 19.8602C11.6751 20.071 12.8441 20.0331 13.9647 19.7493C15.0854 19.4654 16.1312 18.9422 17.03 18.2158C17.1324 18.1332 17.2502 18.0715 17.3764 18.0343C17.5027 17.9971 17.6351 17.9851 17.7661 17.999C17.897 18.013 18.0239 18.0525 18.1395 18.1154C18.2552 18.1783 18.3573 18.2634 18.44 18.3657C18.5227 18.468 18.5845 18.5856 18.6217 18.7118C18.659 18.8379 18.6709 18.9702 18.657 19.101C18.6431 19.2318 18.6035 19.3586 18.5405 19.4741C18.4776 19.5896 18.3924 19.6916 18.29 19.7743C17.2452 20.6199 16.0403 21.2461 14.7475 21.6154C13.4547 21.9847 12.1005 22.0895 10.7662 21.9235C9.43181 21.7574 8.14476 21.324 6.98212 20.6491C5.81947 19.9743 4.80518 19.0719 4 17.9961V20.993C4 21.258 3.89464 21.5121 3.70711 21.6994C3.51957 21.8867 3.26522 21.992 3 21.992Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function UpdaterIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M12 2C12.2652 2 12.5196 2.10536 12.7071 2.29289C12.8946 2.48043 13 2.73478 13 3V13.59L16.3 10.29C16.3904 10.186 16.5013 10.1018 16.6258 10.0427C16.7503 9.98362 16.8856 9.95088 17.0234 9.94656C17.1611 9.94224 17.2982 9.96644 17.4261 10.0176C17.5541 10.0688 17.6701 10.1459 17.7668 10.244C17.8635 10.3421 17.939 10.4592 17.9883 10.5878C18.0377 10.7165 18.0599 10.8539 18.0537 10.9916C18.0474 11.1292 18.0127 11.2641 17.9519 11.3877C17.891 11.5114 17.8053 11.6211 17.7 11.71L12.7 16.71C12.5131 16.8932 12.2618 16.9959 12 16.9959C11.7382 16.9959 11.4869 16.8932 11.3 16.71L6.3 11.71C6.19474 11.6211 6.10898 11.5114 6.04812 11.3877C5.98726 11.2641 5.95261 11.1292 5.94634 10.9916C5.94007 10.8539 5.96231 10.7165 6.01167 10.5878C6.06104 10.4592 6.13646 10.3421 6.2332 10.244C6.32994 10.1459 6.44592 10.0688 6.57385 10.0176C6.70179 9.96644 6.83892 9.94224 6.97665 9.94656C7.11438 9.95088 7.24972 9.98362 7.3742 10.0427C7.49868 10.1018 7.6096 10.186 7.7 10.29L11 13.59V3C11 2.73478 11.1054 2.48043 11.2929 2.29289C11.4804 2.10536 11.7348 2 12 2ZM3 20C2.73478 20 2.48043 20.1054 2.29289 20.2929C2.10536 20.4804 2 20.7348 2 21C2 21.2652 2.10536 21.5196 2.29289 21.7071C2.48043 21.8946 2.73478 22 3 22H21C21.2652 22 21.5196 21.8946 21.7071 21.7071C21.8946 21.5196 22 21.2652 22 21C22 20.7348 21.8946 20.4804 21.7071 20.2929C21.5196 20.1054 21.2652 20 21 20H3Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function PatchHelperIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M7.79997 15.7699C8.49996 16.1999 8.99997 16.9099 8.99997 17.7299V20.9999C8.99997 21.2651 9.10533 21.5195 9.29286 21.707C9.48039 21.8945 9.73476 21.9999 9.99996 21.9999H14C14.2652 21.9999 14.5196 21.8945 14.7071 21.707C14.8946 21.5195 15 21.2651 15 20.9999V17.7299C15 16.9099 15.5 16.1999 16.2 15.7699C17.357 15.0536 18.3137 14.056 18.9812 12.8701C19.6486 11.6842 20.0048 10.3487 20.0168 8.98795C20.0288 7.62724 19.6961 6.28564 19.0497 5.08819C18.4032 3.89074 17.4642 2.87647 16.32 2.13989C15.72 1.74989 15 2.22989 15 2.93989V8.91988C15 9.18511 14.8946 9.43945 14.7071 9.62701C14.5196 9.81454 14.2652 9.9199 14 9.9199H9.99996C9.73476 9.9199 9.48039 9.81454 9.29286 9.62701C9.10533 9.43945 8.99997 9.18511 8.99997 8.91988V2.93989C8.99997 2.22989 8.27997 1.74989 7.67997 2.13989C6.53577 2.87647 5.59671 3.89074 4.9503 5.08819C4.30386 6.28564 3.97113 7.62724 3.9831 8.98795C3.9951 10.3487 4.35138 11.6842 5.01879 12.8701C5.6862 14.056 6.64299 15.0536 7.79997 15.7699Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function VesktopSettingsIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M18.157.056a1.224 1.224 0 0 0-.709.628c-.105.229-.114.305-.115 1.081v.836l-.351.176a5.545 5.545 0 0 0-.586.342l-.233.167-.543-.311c-.899-.515-.944-.535-1.293-.539-.562-.004-1.018.352-1.17.917-.07.262-.021.567.138.858.151.28.21.325 1.02.794l.606.35v1.38l-.606.35c-.81.469-.869.514-1.02.793-.16.291-.208.597-.137.859.151.564.607.92 1.17.916.347-.004.393-.023 1.289-.537l.54-.308.313.21c.172.116.438.262.588.325l.275.114v.85c.001.793.009.87.115 1.098a1.19 1.19 0 0 0 1.969.282c.269-.307.292-.412.292-1.37v-.869l.315-.148c.173-.081.435-.228.582-.325l.266-.177.706.4c.796.45 1.029.523 1.408.438.882-.198 1.235-1.287.635-1.96-.085-.097-.457-.348-.827-.56l-.67-.385V5.359l.67-.386c.37-.212.742-.463.827-.56.6-.672.247-1.762-.635-1.96-.38-.084-.612-.012-1.405.437l-.7.397-.235-.18a3.792 3.792 0 0 0-.586-.344l-.35-.163v-.848c0-.935-.023-1.044-.292-1.35a1.2 1.2 0 0 0-1.26-.346M4.007 1.25a4.15 4.15 0 0 0-1.21.44c-.354.207-1.102.955-1.309 1.309-.199.34-.374.837-.441 1.252-.07.434-.07 10.426 0 10.86.127.792.42 1.343 1.039 1.963.477.478.81.697 1.317.874.59.204.818.216 4.125.217h3.163v2.42l-1.975.014c-2.202.015-2.148.008-2.5.362-.255.253-.346.474-.346.84s.09.587.345.84c.376.376-.09.348 5.688.348 5.75 0 5.309.025 5.67-.327a1.1 1.1 0 0 0 .362-.86c.002-.367-.09-.586-.344-.84-.353-.355-.3-.348-2.5-.363l-1.976-.014v-2.42h3.164c3.306-.001 3.535-.013 4.124-.217.508-.177.84-.396 1.318-.874.882-.882 1.113-1.57 1.082-3.213-.018-.956-.047-1.068-.364-1.384-.253-.255-.474-.346-.84-.346s-.586.091-.84.346c-.317.316-.343.42-.372 1.47-.023.846-.036.966-.13 1.14a1.22 1.22 0 0 1-.597.553c-.217.098-.276.098-7.757.098-7.48 0-7.54 0-7.757-.098a1.153 1.153 0 0 1-.612-.602l-.114-.242V9.68c.001-5.041.003-5.119.1-5.333.122-.27.3-.462.553-.599.19-.103.242-.104 2.958-.128 3.08-.027 2.927-.01 3.288-.372.255-.254.345-.474.345-.84s-.09-.587-.345-.84c-.364-.365-.204-.347-3.288-.355-1.52-.004-2.881.012-3.024.036m15.047 3.693c.207.108.452.361.57.59.143.278.143.755 0 1.028-.285.54-1.08.81-1.636.556a1.357 1.357 0 0 1-.59-.625c-.107-.255-.092-.7.032-.956.121-.251.46-.568.69-.645.213-.073.755-.043.934.052\"\n            />\n        </Icon>\n    );\n}\n\nexport function CloudDownloadIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.83 7.13 7.39 5.75 8.95 4.38 11 4.08V12.15L9.4 10.6L8 12L12 16L16 12L14.6 10.6L13 12.15V4.08Q15.58 4.43 17.29 6.39 19 8.35 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function CloudUploadIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M11 20H6.5Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13V12.85L14.6 14.4L16 13L12 9L8 13L9.4 14.4L11 12.85Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function ClockIcon(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                fill={props.fill || \"currentColor\"}\n                d=\"M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm1-18a1 1 0 1 0-2 0v7c0 .27.1.52.3.7l3 3a1 1 0 0 0 1.4-1.4L13 11.58V5Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function DownArrow(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z\"\n            />\n        </Icon>\n    );\n}\n\nexport function RightArrow(props: IconProps) {\n    return (\n        <Icon\n            {...props}\n            viewBox=\"0 0 24 24\"\n        >\n            <path\n                fill={props.fill || \"currentColor\"}\n                d=\"M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z\"\n            />\n        </Icon>\n    );\n}\n"
  },
  {
    "path": "src/components/Link.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { React } from \"@webpack/common\";\n\ninterface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {\n    disabled?: boolean;\n}\n\nexport function Link(props: React.PropsWithChildren<Props>) {\n    if (props.disabled) {\n        props.style ??= {};\n        props.style.pointerEvents = \"none\";\n        props[\"aria-disabled\"] = true;\n    }\n\n    props.rel ??= \"noreferrer\";\n\n    return (\n        <a role=\"link\" target=\"_blank\" {...props}>\n            {props.children}\n        </a>\n    );\n}\n"
  },
  {
    "path": "src/components/Paragraph.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { BaseText, type BaseTextProps } from \"./BaseText\";\n\nexport type ParagraphProps = BaseTextProps<\"p\">;\n\nexport function Paragraph({ children, ...restProps }: ParagraphProps) {\n    return (\n        <BaseText tag=\"p\" size=\"sm\" weight=\"normal\" {...restProps}>\n            {children}\n        </BaseText>\n    );\n}\n"
  },
  {
    "path": "src/components/Span.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { BaseText, type BaseTextProps } from \"./BaseText\";\n\nexport type SpanProps = BaseTextProps<\"span\">;\n\nexport function Span({ children, ...restProps }: SpanProps) {\n    return (\n        <BaseText tag=\"span\" size=\"sm\" weight=\"normal\" {...restProps}>\n            {children}\n        </BaseText>\n    );\n}\n"
  },
  {
    "path": "src/components/Switch.css",
    "content": ".vc-switch-container {\n    background: var(--primary-400);\n    border: 1px solid transparent;\n    border-radius: 16px;\n    box-sizing: border-box;\n    cursor: pointer;\n    height: 28px;\n    position: relative;\n    width: 44px;\n\n    .high-contrast-mode & {\n        border-color: var(--border-strong);\n    }\n}\n\n.vc-switch-checked {\n    background: var(--brand-500);\n    border-color: var(--control-primary-border-default);\n}\n\n.vc-switch-disabled {\n    cursor: not-allowed;\n    opacity: 0.3;\n}\n\n.vc-switch-focusVisible {\n    /* stylelint-disable-next-line custom-property-pattern */\n    box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--border-focus, #00b0f4));\n}\n\n.vc-switch-slider {\n    display: block;\n    height: 20px;\n    left: 0;\n    margin: 3px;\n    position: absolute;\n    width: 28px;\n    transition: 100ms transform ease-in-out;\n    overflow: visible;\n}\n\n.vc-switch-input {\n    border-radius: 14px;\n    cursor: pointer;\n    height: 100%;\n    left: 0;\n    margin: 0;\n    opacity: 0;\n    position: absolute;\n    top: 0;\n    width: 100%;\n\n    &:disabled {\n        pointer-events: none;\n        cursor: not-allowed\n    }\n}"
  },
  {
    "path": "src/components/Switch.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./Switch.css\";\n\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport { useState } from \"@webpack/common\";\nimport type { FocusEvent } from \"react\";\n\nconst switchCls = classNameFactory(\"vc-switch-\");\n\nconst SWITCH_ON = \"var(--brand-500)\";\nconst SWITCH_OFF = \"var(--primary-400)\";\n\nexport interface SwitchProps {\n    disabled?: boolean;\n    checked: boolean;\n    onChange: (checked: boolean) => void;\n}\n\nexport function Switch({ checked, onChange, disabled }: SwitchProps) {\n    const [focusVisible, setFocusVisible] = useState(false);\n\n    // Due to how we wrap the invisible input, there is no good way to do this with css.\n    // We need it on the parent, not the input itself. For this, you can use either:\n    // - :focus-within ~ this shows also when clicking, not just on keyboard focus => SUCKS\n    // - :has(:focus-visible) ~ works but :has performs terribly inside Discord\n    // - JS event handlers ~ what we are using now\n    const handleFocusChange = (event: FocusEvent<HTMLInputElement>) => {\n        const target = event.currentTarget;\n        setFocusVisible(target.matches(\":focus-visible\"));\n    };\n\n    return (\n        <div>\n            <div className={classes(switchCls(\"container\", { checked, disabled, focusVisible }))}>\n                <svg\n                    className={switchCls(\"slider\")}\n                    viewBox=\"0 0 28 20\"\n                    preserveAspectRatio=\"xMinYMid meet\"\n                    aria-hidden=\"true\"\n                    style={{\n                        transform: checked ? \"translateX(12px)\" : \"translateX(-3px)\",\n                    }}\n                >\n                    <rect fill=\"white\" x=\"4\" y=\"0\" height=\"20\" width=\"20\" rx=\"10\" />\n                    <svg viewBox=\"0 0 20 20\" fill=\"none\">\n                        {checked ? (\n                            <>\n                                <path fill={SWITCH_ON} d=\"M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z\" />\n                                <path fill={SWITCH_ON} d=\"M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z\" />\n                            </>\n                        ) : (\n                            <>\n                                <path fill={SWITCH_OFF} d=\"M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z\" />\n                                <path fill={SWITCH_OFF} d=\"M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z\" />\n                            </>\n                        )}\n\n                    </svg>\n                </svg>\n                <input\n                    onFocus={handleFocusChange}\n                    onBlur={handleFocusChange}\n                    disabled={disabled}\n                    type=\"checkbox\"\n                    className={switchCls(\"input\")}\n                    tabIndex={0}\n                    checked={checked}\n                    onChange={e => onChange(e.currentTarget.checked)}\n                />\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/TooltipContainer.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { TooltipProps } from \"@vencord/discord-types\";\nimport { Tooltip } from \"@webpack/common\";\n\nexport function TooltipContainer({ children, ...props }: Omit<TooltipProps, \"children\"> & { children: React.ReactNode; }) {\n    return (\n        <Tooltip {...props}>\n            {tooltipProps =>\n                <div {...tooltipProps}>\n                    {children}\n                </div>\n            }\n        </Tooltip>\n    );\n}\n"
  },
  {
    "path": "src/components/TooltipFallback.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Tooltip } from \"@vencord/discord-types\";\n\nconst NOOP = () => { };\n\n/** Don't use this */\nexport const TooltipFallback: Tooltip = ({ children }) => {\n    if (typeof children !== \"function\") {\n        return null;\n    }\n\n    const node = children({\n        onBlur: NOOP,\n        onFocus: NOOP,\n        onMouseEnter: NOOP,\n        onMouseLeave: NOOP,\n        onClick: NOOP,\n        onContextMenu: NOOP\n    });\n\n    return <>{node}</>;\n};\n\nTooltipFallback.Colors = {} as any;\n"
  },
  {
    "path": "src/components/handleComponentFailed.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { maybePromptToUpdate } from \"@utils/updater\";\n\nexport function handleComponentFailed() {\n    maybePromptToUpdate(\n        \"Uh Oh! Failed to render this Page.\" +\n        \" However, there is an update available that might fix it.\" +\n        \" Would you like to update and restart now?\"\n    );\n}\n"
  },
  {
    "path": "src/components/iconStyles.css",
    "content": ".vc-owner-crown-icon {\n    color: var(--status-warning);\n}"
  },
  {
    "path": "src/components/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport * from \"./BaseText\";\nexport * from \"./Button\";\nexport * from \"./Card\";\nexport * from \"./CheckedTextInput\";\nexport * from \"./CodeBlock\";\nexport * from \"./Divider\";\nexport { default as ErrorBoundary } from \"./ErrorBoundary\";\nexport * from \"./ErrorCard\";\nexport * from \"./Flex\";\nexport * from \"./FormSwitch\";\nexport * from \"./Grid\";\nexport * from \"./Heading\";\nexport * from \"./Heart\";\nexport * from \"./Icons\";\nexport * from \"./Link\";\nexport * from \"./margins\";\nexport * from \"./Paragraph\";\nexport * from \"./settings\";\nexport * from \"./Span\";\nexport * from \"./Switch\";\n"
  },
  {
    "path": "src/components/margins.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { classNameFactory } from \"@utils/css\";\n\nconst marginCls = classNameFactory(\"vc-margin-\");\n\nconst Directions = [\"top\", \"bottom\", \"left\", \"right\"] as const;\nconst Sizes = [8, 16, 20] as const;\n\nexport type MarginDirection = (typeof Directions)[number];\nexport type MarginSize = (typeof Sizes)[number];\n\nexport const Margins: Record<`${MarginDirection}${MarginSize}`, string> = {} as any;\n\nexport function generateMarginCss() {\n    let css = \"\";\n\n    for (const direction of Directions) {\n        for (const size of Sizes) {\n            const cl = marginCls(`${direction}-${size}`);\n            Margins[`${direction}${size}`] = cl;\n            css += `.${cl}{margin-${direction}:${size}px;}`;\n        }\n    }\n\n    return css;\n}\n"
  },
  {
    "path": "src/components/settings/AddonCard.css",
    "content": ".vc-addon-card {\n    background-color: var(--card-background-default);\n    color: var(--interactive-icon-active);\n    border: 1px solid var(--border-subtle);\n    border-radius: 8px;\n    display: block;\n    height: 100%;\n    padding: 12px;\n    width: 100%;\n    transition: 0.1s ease-out;\n    transition-property: box-shadow, transform, background, opacity;\n    box-sizing: border-box;\n}\n\n.vc-addon-card-disabled {\n    opacity: 0.6;\n}\n\n.vc-addon-card:hover {\n    transform: translateY(-1px);\n    box-shadow: var(--elevation-high);\n}\n\n.vc-addon-header {\n    margin-top: auto;\n    display: flex;\n    width: 100%;\n    justify-content: flex-end;\n    align-items: center;\n    gap: 8px;\n    margin-bottom: 0.5em;\n}\n\n.vc-addon-note {\n    height: 36px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    line-clamp: 2;\n    -webkit-box-orient: vertical;\n    /* stylelint-disable-next-line property-no-unknown */\n    box-orient: vertical;\n}\n\n.vc-addon-name-author {\n    width: 100%;\n}\n\n.vc-addon-name {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    flex-grow: 1;\n    gap: 8px;\n}\n\n.vc-addon-author {\n    font-size: 0.8em;\n}\n\n.vc-addon-author::before {\n    content: \"by \";\n}\n\n.vc-addon-title-container {\n    width: 100%;\n    overflow: hidden;\n    height: 1.25em;\n    position: relative;\n}\n\n.vc-addon-title {\n    position: absolute;\n    inset: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n@keyframes vc-addon-title {\n    0% {\n        transform: translateX(0);\n    }\n\n    50% {\n        transform: translateX(var(--offset));\n    }\n\n    100% {\n        transform: translateX(0);\n    }\n}\n\n.vc-addon-title:hover {\n    overflow: visible;\n    animation: vc-addon-title var(--duration) linear infinite;\n}"
  },
  {
    "path": "src/components/settings/AddonCard.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./AddonCard.css\";\n\nimport { AddonBadge } from \"@components/settings/PluginBadge\";\nimport { Switch } from \"@components/Switch\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Text, useRef } from \"@webpack/common\";\nimport type { MouseEventHandler, ReactNode } from \"react\";\n\nconst cl = classNameFactory(\"vc-addon-\");\n\ninterface Props {\n    name: ReactNode;\n    description: ReactNode;\n    enabled: boolean;\n    setEnabled: (enabled: boolean) => void;\n    disabled?: boolean;\n    isNew?: boolean;\n    onMouseEnter?: MouseEventHandler<HTMLDivElement>;\n    onMouseLeave?: MouseEventHandler<HTMLDivElement>;\n\n    infoButton?: ReactNode;\n    footer?: ReactNode;\n    author?: ReactNode;\n}\n\nexport function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {\n    const titleRef = useRef<HTMLDivElement>(null);\n    const titleContainerRef = useRef<HTMLDivElement>(null);\n\n    return (\n        <div\n            className={cl(\"card\", { \"card-disabled\": disabled })}\n            onMouseEnter={onMouseEnter}\n            onMouseLeave={onMouseLeave}\n        >\n            <div className={cl(\"header\")}>\n                <div className={cl(\"name-author\")}>\n                    <Text variant=\"text-md/bold\" className={cl(\"name\")}>\n                        <div ref={titleContainerRef} className={cl(\"title-container\")}>\n                            <div\n                                ref={titleRef}\n                                className={cl(\"title\")}\n                                onMouseOver={() => {\n                                    const title = titleRef.current!;\n                                    const titleContainer = titleContainerRef.current!;\n\n                                    title.style.setProperty(\"--offset\", `${titleContainer.clientWidth - title.scrollWidth}px`);\n                                    title.style.setProperty(\"--duration\", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);\n                                }}\n                            >\n                                {name}\n                            </div>\n                        </div>\n                        {isNew && <AddonBadge text=\"NEW\" color=\"#ED4245\" />}\n                    </Text>\n\n                    {!!author && (\n                        <Text variant=\"text-md/normal\" className={cl(\"author\")}>\n                            {author}\n                        </Text>\n                    )}\n                </div>\n\n                {infoButton}\n\n                <Switch\n                    checked={enabled}\n                    onChange={setEnabled}\n                    disabled={disabled}\n                />\n            </div>\n\n            <Text className={cl(\"note\")} variant=\"text-sm/normal\">{description}</Text>\n\n            {footer}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/DonateButton.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Heart } from \"@components/Heart\";\nimport { ButtonProps } from \"@vencord/discord-types\";\nimport { Button } from \"@webpack/common\";\n\nexport default function DonateButton({\n    look = Button.Looks.LINK,\n    color = Button.Colors.TRANSPARENT,\n    ...props\n}: Partial<ButtonProps>) {\n    return (\n        <Button\n            {...props}\n            look={look}\n            color={color}\n            onClick={() => VencordNative.native.openExternal(\"https://github.com/sponsors/Vendicated\")}\n            className=\"vc-donate-button\"\n        >\n            <Heart />\n            Donate\n        </Button>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/PluginBadge.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function AddonBadge({ text, color }) {\n    return (\n        <div className=\"vc-addon-badge\" style={{\n            backgroundColor: color,\n            justifySelf: \"flex-end\",\n            marginLeft: \"auto\"\n        }}>\n            {text}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/QuickAction.css",
    "content": ".vc-settings-quickActions-card {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 0.5em;\n    padding: 0.5em;\n    margin-bottom: 1em;\n}\n\n@media (width <=1040px) {\n    .vc-settings-quickActions-card {\n        grid-template-columns: repeat(2, 1fr);\n    }\n}\n\n.vc-settings-quickActions-pill {\n    all: unset;\n    cursor: pointer;\n    background: var(--control-secondary-background-default);\n    color: var(--control-secondary-text-default, var(--text-default));\n    display: flex;\n    align-items: center;\n    gap: 0.5em;\n    padding: 8px 9px;\n    border-radius: 8px;\n    transition: 0.1s ease-out;\n    box-sizing: border-box;\n}\n\n.vc-settings-quickActions-pill:hover {\n    background: var(--control-secondary-background-hover);\n    transform: translateY(-1px);\n    box-shadow: var(--elevation-high);\n}\n\n.vc-settings-quickActions-pill:focus-visible {\n    outline: 2px solid var(--border-focus);\n    outline-offset: 2px;\n}\n\n.vc-settings-quickActions-img {\n    width: 24px;\n    height: 24px;\n}"
  },
  {
    "path": "src/components/settings/QuickAction.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./QuickAction.css\";\n\nimport { Card } from \"@components/Card\";\nimport { classNameFactory } from \"@utils/css\";\nimport type { ComponentType, PropsWithChildren, ReactNode } from \"react\";\n\nconst cl = classNameFactory(\"vc-settings-quickActions-\");\n\nexport interface QuickActionProps {\n    Icon: ComponentType<{ className?: string; }>;\n    text: ReactNode;\n    action?: () => void;\n    disabled?: boolean;\n}\n\nexport function QuickAction(props: QuickActionProps) {\n    const { Icon, action, text, disabled } = props;\n\n    return (\n        <button className={cl(\"pill\")} onClick={action} disabled={disabled}>\n            <Icon className={cl(\"img\")} />\n            {text}\n        </button>\n    );\n}\n\nexport function QuickActionCard(props: PropsWithChildren) {\n    return (\n        <Card className={cl(\"card\")}>\n            {props.children}\n        </Card>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/SpecialCard.css",
    "content": ".vc-donate-button {\n    overflow: visible !important;\n}\n\n.vc-donate-button .vc-heart-icon {\n    transition: transform 0.3s;\n    margin-right: 0.5em;\n}\n\n.vc-donate-button:hover .vc-heart-icon {\n    transform: scale(1.1);\n    z-index: 10;\n    position: relative;\n}\n\n.vc-special-card-special {\n    padding: 1em 1.5em;\n    margin-bottom: 1em;\n    background-size: cover;\n    background-position: center;\n}\n\n.vc-special-card-flex {\n    display: flex;\n    flex-direction: row;\n}\n\n.vc-special-card-flex-main {\n    width: 100%;\n}\n\n.vc-special-title {\n    color: black;\n}\n\n.vc-special-subtitle {\n    color: black;\n    font-size: 1.2em;\n    font-weight: bold;\n    margin-top: 0.5em;\n}\n\n.vc-special-text {\n    color: black;\n    font-size: 1em;\n    margin-top: .75em;\n    white-space: pre-line;\n}\n\n.vc-special-seperator {\n    margin-top: .75em;\n    border-top: 1px solid white;\n    opacity: 0.4;\n}\n\n.vc-special-hyperlink {\n    margin-top: 1em;\n    cursor: pointer;\n\n    .vc-special-hyperlink-text {\n        color: black;\n        font-size: 1em;\n        font-weight: bold;\n        text-align: center;\n        transition: text-decoration 0.5s;\n        cursor: pointer;\n    }\n\n    &:hover .vc-special-hyperlink-text {\n        text-decoration: underline;\n    }\n}\n\n.vc-special-image-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin-left: 1em;\n    flex-shrink: 0;\n    width: 100px;\n    height: 100px;\n    border-radius: 50%;\n    background-color: white;\n}\n\n.vc-special-image {\n    width: 65%;\n}"
  },
  {
    "path": "src/components/settings/SpecialCard.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./SpecialCard.css\";\n\nimport { Card } from \"@components/Card\";\nimport { Divider } from \"@components/Divider\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Clickable, Forms } from \"@webpack/common\";\nimport type { PropsWithChildren } from \"react\";\n\nconst cl = classNameFactory(\"vc-special-\");\n\ninterface StyledCardProps {\n    title: string;\n    subtitle?: string;\n    description: string;\n    cardImage?: string;\n    backgroundImage?: string;\n    backgroundColor?: string;\n    buttonTitle?: string;\n    buttonOnClick?: () => void;\n}\n\nexport function SpecialCard({ title, subtitle, description, cardImage, backgroundImage, backgroundColor, buttonTitle, buttonOnClick: onClick, children }: PropsWithChildren<StyledCardProps>) {\n    const cardStyle: React.CSSProperties = {\n        backgroundColor: backgroundColor || \"#9c85ef\",\n        backgroundImage: `url(${backgroundImage || \"\"})`,\n    };\n\n    return (\n        <Card className={cl(\"card\", \"card-special\")} style={cardStyle}>\n            <div className={cl(\"card-flex\")}>\n                <div className={cl(\"card-flex-main\")}>\n                    <Forms.FormTitle className={cl(\"title\")} tag=\"h5\">{title}</Forms.FormTitle>\n                    <Forms.FormText className={cl(\"subtitle\")}>{subtitle}</Forms.FormText>\n                    <Forms.FormText className={cl(\"text\")}>{description}</Forms.FormText>\n\n                    {children}\n                </div>\n                {cardImage && (\n                    <div className={cl(\"image-container\")}>\n                        <img\n                            role=\"presentation\"\n                            src={cardImage}\n                            alt=\"\"\n                            className={cl(\"image\")}\n                        />\n                    </div>\n                )}\n            </div>\n            {buttonTitle && (\n                <>\n                    <Divider className={cl(\"seperator\")} />\n                    <Clickable onClick={onClick} className={cl(\"hyperlink\")}>\n                        <Forms.FormText className={cl(\"hyperlink-text\")}>\n                            {buttonTitle}\n                        </Forms.FormText>\n                    </Clickable>\n                </>\n            )}\n        </Card>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport * from \"../Switch\";\nexport * from \"./AddonCard\";\nexport * from \"./DonateButton\";\nexport * from \"./PluginBadge\";\nexport * from \"./QuickAction\";\nexport * from \"./SpecialCard\";\nexport * from \"./tabs\";\n"
  },
  {
    "path": "src/components/settings/tabs/BaseTab.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { handleComponentFailed } from \"@components/handleComponentFailed\";\nimport { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { onlyOnce } from \"@utils/onlyOnce\";\nimport type { ComponentType, PropsWithChildren } from \"react\";\n\nexport function SettingsTab({ children }: PropsWithChildren) {\n    return (\n        <section className=\"vc-settings-tab\">{children}</section>\n    );\n}\n\nexport const handleSettingsTabError = onlyOnce(handleComponentFailed);\n\nexport function wrapTab(component: ComponentType<any>, tab: string) {\n    const wrapped = ErrorBoundary.wrap(component, {\n        displayName: `${tab}SettingsTab`,\n        message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,\n        onError: handleSettingsTabError,\n    });\n\n    return wrapped;\n}\n\nexport function openSettingsTabModal(Tab: ComponentType<any>) {\n    try {\n        openModal(wrapTab((modalProps: ModalProps) => (\n            <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>\n                <ModalContent className=\"vc-settings-modal\">\n                    <ModalCloseButton onClick={modalProps.onClose} className=\"vc-settings-modal-close\" />\n                    <Tab />\n                </ModalContent>\n            </ModalRoot>\n        ), Tab.displayName || \"Settings Tab\"));\n    } catch {\n        handleSettingsTabError();\n    }\n}\n"
  },
  {
    "path": "src/components/settings/tabs/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nexport * from \"./BaseTab\";\nexport { default as PatchHelperTab } from \"./patchHelper\";\nexport { default as PluginsTab } from \"./plugins\";\nexport { openContributorModal } from \"./plugins/ContributorModal\";\nexport { openPluginModal } from \"./plugins/PluginModal\";\nexport { default as BackupAndRestoreTab } from \"./sync/BackupAndRestoreTab\";\nexport { default as CloudTab } from \"./sync/CloudTab\";\nexport { default as ThemesTab } from \"./themes\";\nexport { default as UpdaterTab } from \"./updater\";\nexport { default as VencordTab } from \"./vencord\";\n"
  },
  {
    "path": "src/components/settings/tabs/patchHelper/FullPatchInput.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Margins } from \"@utils/margins\";\nimport { Patch, ReplaceFn } from \"@utils/types\";\nimport { Forms, TextArea, useEffect, useRef, useState } from \"@webpack/common\";\n\nexport interface FullPatchInputProps {\n    setFind(v: string): void;\n    setParsedFind(v: string | RegExp): void;\n    setMatch(v: string): void;\n    setReplacement(v: string | ReplaceFn): void;\n}\n\nexport function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {\n    const [patch, setPatch] = useState<string>(\"\");\n    const [error, setError] = useState<string>(\"\");\n\n    const textAreaRef = useRef<HTMLTextAreaElement>(null);\n\n    function update() {\n        if (patch === \"\") {\n            setError(\"\");\n\n            setFind(\"\");\n            setParsedFind(\"\");\n            setMatch(\"\");\n            setReplacement(\"\");\n            return;\n        }\n\n        try {\n            let { find, replacement } = (0, eval)(`([${patch}][0])`) as Patch;\n\n            if (!find) throw new Error(\"No 'find' field\");\n            if (!replacement) throw new Error(\"No 'replacement' field\");\n\n            if (replacement instanceof Array) {\n                if (replacement.length === 0) throw new Error(\"Invalid replacement\");\n\n                // Only test the first replacement\n                replacement = replacement[0];\n            }\n\n            if (!replacement.match) throw new Error(\"No 'replacement.match' field\");\n            if (replacement.replace == null) throw new Error(\"No 'replacement.replace' field\");\n\n            setFind(find instanceof RegExp ? `/${find.source}/` : find);\n            setParsedFind(find);\n            setMatch(replacement.match instanceof RegExp ? replacement.match.source : replacement.match);\n            setReplacement(replacement.replace);\n            setError(\"\");\n        } catch (e) {\n            setError((e as Error).message);\n        }\n    }\n\n    useEffect(() => {\n        const { current: textArea } = textAreaRef;\n        if (textArea) {\n            textArea.style.height = \"auto\";\n            textArea.style.height = `${textArea.scrollHeight}px`;\n        }\n    }, [patch]);\n\n    return (\n        <>\n            <Forms.FormText className={Margins.bottom8}>\n                Paste your full JSON patch here to fill out the fields\n            </Forms.FormText>\n            <TextArea\n                inputRef={textAreaRef}\n                value={patch}\n                onChange={setPatch}\n                onBlur={update}\n            />\n            {error !== \"\" && <Forms.FormText style={{ color: \"var(--text-feedback-critical)\" }}>{error}</Forms.FormText>}\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/patchHelper/PatchPreview.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Margins } from \"@utils/margins\";\nimport { canonicalizeMatch, canonicalizeReplace } from \"@utils/patches\";\nimport { makeCodeblock } from \"@utils/text\";\nimport { ReplaceFn } from \"@utils/types\";\nimport { Button, Forms, Parser, useMemo, useState } from \"@webpack/common\";\nimport type { Change } from \"diff\";\n\n// Do not include diff in non dev builds (side effects import)\nif (IS_DEV) {\n    var differ = require(\"diff\") as typeof import(\"diff\");\n}\n\ninterface PatchPreviewProps {\n    module: [id: number, factory: Function];\n    match: string;\n    replacement: string | ReplaceFn;\n    setReplacementError(error: any): void;\n}\n\nfunction makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {\n    if (!match || original === patched) return null;\n\n    const changeSize = patched.length - original.length;\n\n    // Use 200 surrounding characters of context\n    const start = Math.max(0, match.index! - 200);\n    const end = Math.min(original.length, match.index! + match[0].length + 200);\n    // (changeSize may be negative)\n    const endPatched = end + changeSize;\n\n    const context = original.slice(start, end);\n    const patchedContext = patched.slice(start, endPatched);\n\n    return differ.diffWordsWithSpace(context, patchedContext);\n}\n\nfunction Match({ matchResult }: { matchResult: RegExpMatchArray | null; }) {\n    if (!matchResult)\n        return null;\n\n    const fullMatch = matchResult[0]\n        ? makeCodeblock(matchResult[0], \"js\")\n        : \"\";\n    const groups = matchResult.length > 1\n        ? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join(\"\\n\"), \"yml\")\n        : \"\";\n\n    return (\n        <>\n            <Forms.FormTitle>Match</Forms.FormTitle>\n            <div style={{ userSelect: \"text\" }}>{Parser.parse(fullMatch)}</div>\n            <div style={{ userSelect: \"text\" }}>{Parser.parse(groups)}</div>\n        </>\n    );\n}\n\nfunction Diff({ diff }: { diff: Change[] | null; }) {\n    if (!diff?.length)\n        return null;\n\n    const diffLines = diff.map((p, idx) => {\n        const color = p.added\n            ? \"lime\"\n            : p.removed\n                ? \"red\"\n                : \"grey\";\n\n        return (\n            <div\n                key={idx}\n                style={{ color, userSelect: \"text\", wordBreak: \"break-all\", lineBreak: \"anywhere\" }}\n            >\n                {p.value}\n            </div>\n        );\n    });\n\n    return (\n        <>\n            <Forms.FormTitle>Diff</Forms.FormTitle>\n            {diffLines}\n        </>\n    );\n}\n\nexport function PatchPreview({ module, match, replacement, setReplacementError }: PatchPreviewProps) {\n    const [id, fact] = module;\n    const [compileResult, setCompileResult] = useState<[boolean, string]>();\n\n    const [patchedCode, matchResult, diff] = useMemo<[string, RegExpMatchArray | null, Change[] | null]>(() => {\n        const src: string = fact.toString().replaceAll(\"\\n\", \"\");\n\n        try {\n            new RegExp(match);\n        } catch (e) {\n            return [\"\", null, null];\n        }\n\n        const canonicalMatch = canonicalizeMatch(new RegExp(match));\n        try {\n            const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins[\"YourPlugin\"]');\n            var patched = src.replace(canonicalMatch, canonicalReplace as string);\n            setReplacementError(void 0);\n        } catch (e) {\n            setReplacementError((e as Error).message);\n            return [\"\", null, null];\n        }\n\n        const m = src.match(canonicalMatch);\n        return [patched, m, makeDiff(src, patched, m)];\n    }, [id, match, replacement]);\n\n    return (\n        <>\n            <Forms.FormTitle>Module {id}</Forms.FormTitle>\n\n            <Match matchResult={matchResult} />\n            <Diff diff={diff} />\n\n            {!!diff?.length && (\n                <Button\n                    className={Margins.top20}\n                    onClick={() => {\n                        try {\n                            Function(patchedCode.replace(/^(?=function\\()/, \"0,\"));\n                            setCompileResult([true, \"Compiled successfully\"]);\n                        } catch (err) {\n                            setCompileResult([false, (err as Error).message]);\n                        }\n                    }}\n                >\n                    Compile\n                </Button>\n            )}\n\n            {compileResult && (\n                <Forms.FormText style={{ color: compileResult[0] ? \"var(--status-positive)\" : \"var(--text-feedback-critical)\" }}>\n                    {compileResult[1]}\n                </Forms.FormText>\n            )}\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/patchHelper/ReplacementInput.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { FormSwitch } from \"@components/FormSwitch\";\nimport { Margins } from \"@utils/margins\";\nimport { Forms, Parser, TextInput, useEffect, useState } from \"@webpack/common\";\n\nconst RegexGuide = {\n    \"\\\\i\": \"Special regex escape sequence that matches identifiers (varnames, classnames, etc.)\",\n    \"$$\": \"Insert a $\",\n    \"$&\": \"Insert the entire match\",\n    \"$`\\u200b\": \"Insert the substring before the match\",\n    \"$'\": \"Insert the substring after the match\",\n    \"$n\": \"Insert the nth capturing group ($1, $2...)\",\n    \"$self\": \"Insert the plugin instance\",\n} as const;\n\nexport function ReplacementInput({ replacement, setReplacement, replacementError }) {\n    const [isFunc, setIsFunc] = useState(false);\n    const [error, setError] = useState<string>();\n\n    function onChange(v: string) {\n        setError(void 0);\n\n        if (isFunc) {\n            try {\n                const func = (0, eval)(v);\n                if (typeof func === \"function\")\n                    setReplacement(() => func);\n\n                else\n                    setError(\"Replacement must be a function\");\n            } catch (e) {\n                setReplacement(v);\n                setError((e as Error).message);\n            }\n        } else {\n            setReplacement(v);\n        }\n    }\n\n    useEffect(() => {\n        if (isFunc)\n            onChange(replacement);\n        else\n            setError(void 0);\n    }, [isFunc]);\n\n    return (\n        <>\n            {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}\n            <Forms.FormTitle className=\"\">Replacement</Forms.FormTitle>\n            <TextInput\n                value={replacement?.toString()}\n                onChange={onChange}\n                error={error ?? replacementError}\n            />\n            {!isFunc && (\n                <div>\n                    <Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>\n\n                    {Object.entries(RegexGuide).map(([placeholder, desc]) => (\n                        <Forms.FormText key={placeholder}>\n                            {Parser.parse(\"`\" + placeholder + \"`\")}: {desc}\n                        </Forms.FormText>\n                    ))}\n                </div>\n            )}\n\n            <FormSwitch\n                className={Margins.top16}\n                value={isFunc}\n                onChange={setIsFunc}\n                title={\"Treat Replacement as function\"}\n                description='\"Replacement\" will be evaluated as a function if this is enabled'\n                hideBorder\n            />\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/patchHelper/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { CodeBlock } from \"@components/CodeBlock\";\nimport { Divider } from \"@components/Divider\";\nimport { Flex } from \"@components/Flex\";\nimport { HeadingTertiary } from \"@components/Heading\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { debounce } from \"@shared/debounce\";\nimport { copyWithToast } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { stripIndent } from \"@utils/text\";\nimport { ReplaceFn } from \"@utils/types\";\nimport { search } from \"@webpack\";\nimport { Button, React, TextInput, useMemo, useState } from \"@webpack/common\";\n\nimport { FullPatchInput } from \"./FullPatchInput\";\nimport { PatchPreview } from \"./PatchPreview\";\nimport { ReplacementInput } from \"./ReplacementInput\";\n\nconst findCandidates = debounce(function ({ find, setModule, setError }) {\n    const candidates = search(find);\n    const keys = Object.keys(candidates);\n    const len = keys.length;\n\n    if (len === 0)\n        setError(\"No match. Perhaps that module is lazy loaded?\");\n    else if (len !== 1)\n        setError(\"Multiple matches. Please refine your filter\");\n    else\n        setModule([keys[0], candidates[keys[0]]]);\n});\n\nfunction PatchHelper() {\n    const [find, setFind] = useState(\"\");\n    const [match, setMatch] = useState(\"\");\n    const [replacement, setReplacement] = useState<string | ReplaceFn>(\"\");\n\n    const [parsedFind, setParsedFind] = useState<string | RegExp>(\"\");\n\n    const [findError, setFindError] = useState<string>();\n    const [matchError, setMatchError] = useState<string>();\n    const [replacementError, setReplacementError] = useState<string>();\n\n    const [module, setModule] = useState<[number, Function]>();\n\n    const code = useMemo(() => {\n        const find = parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind);\n        const replace = typeof replacement === \"function\" ? replacement.toString() : JSON.stringify(replacement);\n\n        return stripIndent`\n            {\n                find: ${find},\n                replacement: {\n                    match: /${match.replace(/(?<!\\\\)\\//g, \"\\\\/\")}/,\n                    replace: ${replace}\n                }\n            }\n        `;\n    }, [parsedFind, match, replacement]);\n\n    function onFindChange(v: string) {\n        setFind(v);\n\n        try {\n            let parsedFind = v as string | RegExp;\n            if (/^\\/.+?\\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));\n\n            setFindError(void 0);\n            setParsedFind(parsedFind);\n\n            if (v.length) {\n                findCandidates({ find: parsedFind, setModule, setError: setFindError });\n            }\n        } catch (e: any) {\n            setFindError((e as Error).message);\n        }\n    }\n\n    function onMatchChange(v: string) {\n        setMatch(v);\n\n        try {\n            new RegExp(v);\n            setMatchError(void 0);\n        } catch (e: any) {\n            setMatchError((e as Error).message);\n        }\n    }\n\n    return (\n        <SettingsTab>\n            <HeadingTertiary>Full patch</HeadingTertiary>\n            <FullPatchInput\n                setFind={onFindChange}\n                setParsedFind={setParsedFind}\n                setMatch={onMatchChange}\n                setReplacement={setReplacement}\n            />\n\n            <HeadingTertiary className={Margins.top8}>Find</HeadingTertiary>\n            <TextInput\n                type=\"text\"\n                value={find}\n                onChange={onFindChange}\n                error={findError}\n            />\n\n            <HeadingTertiary className={Margins.top8}>Match</HeadingTertiary>\n            <TextInput\n                type=\"text\"\n                value={match}\n                onChange={onMatchChange}\n                error={matchError}\n            />\n\n            <div className={Margins.top8} />\n            <ReplacementInput\n                replacement={replacement}\n                setReplacement={setReplacement}\n                replacementError={replacementError}\n            />\n\n            <Divider />\n            {module && (\n                <PatchPreview\n                    module={module}\n                    match={match}\n                    replacement={replacement}\n                    setReplacementError={setReplacementError}\n                />\n            )}\n\n            {!!(find && match && replacement) && (\n                <>\n                    <HeadingTertiary className={Margins.top20}>Code</HeadingTertiary>\n                    <CodeBlock lang=\"js\" content={code} />\n                    <Flex className={Margins.top16}>\n                        <Button onClick={() => copyWithToast(code)}>\n                            Copy to Clipboard\n                        </Button>\n                        <Button onClick={() => copyWithToast(\"```ts\\n\" + code + \"\\n```\")}>\n                            Copy as Codeblock\n                        </Button>\n                    </Flex>\n                </>\n            )}\n        </SettingsTab>\n    );\n}\n\nexport default IS_DEV ? wrapTab(PatchHelper, \"PatchHelper\") : null;\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/ContributorModal.css",
    "content": ".vc-author-modal-root {\n    padding: 1em;\n}\n\n.vc-author-modal-header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 1em;\n}\n\n.vc-author-modal-name {\n    text-transform: none;\n    flex-grow: 0;\n    background: var(--background-base-lowest);\n    border-radius: 0 9999px 9999px 0;\n    padding: 6px 0.8em 6px 0.5em;\n    font-size: 20px;\n    height: 20px;\n    position: relative;\n    text-wrap: nowrap;\n}\n\n.vc-author-modal-name::before {\n    content: \"\";\n    display: block;\n    position: absolute;\n    height: 100%;\n    width: 32px;\n    background: var(--background-base-lowest);\n    z-index: -1;\n    left: -32px;\n    top: 0;\n    border-top-left-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n}\n\n.vc-author-modal-avatar {\n    height: 32px;\n    width: 32px;\n    border-radius: 50%;\n}\n\n.vc-author-modal-links {\n    margin-left: auto;\n}\n\n.vc-author-modal-plugins {\n    display: grid;\n    gap: 0.5em;\n    margin-top: 0.75em;\n}"
  },
  {
    "path": "src/components/settings/tabs/plugins/ContributorModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./ContributorModal.css\";\n\nimport { useSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Link } from \"@components/Link\";\nimport { DevsById } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { fetchUserProfile } from \"@utils/discord\";\nimport { classes, pluralise } from \"@utils/misc\";\nimport { ModalContent, ModalRoot, openModal } from \"@utils/modal\";\nimport { User } from \"@vencord/discord-types\";\nimport { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from \"@webpack/common\";\n\nimport Plugins from \"~plugins\";\n\nimport { GithubButton, WebsiteButton } from \"./LinkIconButton\";\nimport { PluginCard } from \"./PluginCard\";\n\nconst cl = classNameFactory(\"vc-author-modal-\");\n\nexport function openContributorModal(user: User) {\n    openModal(modalProps =>\n        <ModalRoot {...modalProps}>\n            <ErrorBoundary>\n                <ModalContent className={cl(\"root\")}>\n                    <ContributorModal user={user} />\n                </ModalContent>\n            </ErrorBoundary>\n        </ModalRoot>\n    );\n}\n\nfunction ContributorModal({ user }: { user: User; }) {\n    useSettings();\n\n    const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));\n\n    useEffect(() => {\n        if (!profile && !user.bot && user.id)\n            fetchUserProfile(user.id);\n    }, [user.id, user.bot, profile]);\n\n    const githubName = profile?.connectedAccounts?.find(a => a.type === \"github\")?.name;\n    const website = profile?.connectedAccounts?.find(a => a.type === \"domain\")?.name;\n\n    const plugins = useMemo(() => {\n        const allPlugins = Object.values(Plugins);\n        const pluginsByAuthor = DevsById[user.id]\n            ? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))\n            : allPlugins.filter(p => p.authors.some(a => a.name === user.username));\n\n        return pluginsByAuthor\n            .filter(p => !p.name.endsWith(\"API\"))\n            .sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));\n    }, [user.id, user.username]);\n\n    const ContributedHyperLink = <Link href=\"https://vencord.dev/source\">contributed</Link>;\n\n    return (\n        <>\n            <div className={cl(\"header\")}>\n                <img\n                    className={cl(\"avatar\")}\n                    src={user.getAvatarURL(void 0, 512, true)}\n                    alt=\"\"\n                />\n                <Forms.FormTitle tag=\"h2\" className={cl(\"name\")}>{user.username}</Forms.FormTitle>\n\n                <div className={classes(\"vc-settings-modal-links\", cl(\"links\"))}>\n                    {website && (\n                        <WebsiteButton\n                            text={website}\n                            href={`https://${website}`}\n                        />\n                    )}\n                    {githubName && (\n                        <GithubButton\n                            text={githubName}\n                            href={`https://github.com/${githubName}`}\n                        />\n                    )}\n                </div>\n            </div>\n\n            {plugins.length ? (\n                <Forms.FormText>\n                    This person has {ContributedHyperLink} to {pluralise(plugins.length, \"plugin\")}!\n                </Forms.FormText>\n            ) : (\n                <Forms.FormText>\n                    This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!\n                </Forms.FormText>\n            )}\n\n            {!!plugins.length && (\n                <div className={cl(\"plugins\")}>\n                    {plugins.map(p =>\n                        <PluginCard\n                            key={p.name}\n                            plugin={p}\n                            disabled={p.required ?? false}\n                            onRestartNeeded={() => showToast(\"Restart to apply changes!\")}\n                        />\n                    )}\n                </div>\n            )}\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/LinkIconButton.css",
    "content": ".vc-settings-modal-link-icon {\n    height: 32px;\n    width: 32px;\n    border-radius: 50%;\n    border: 4px solid var(--background-base-lowest);\n    box-sizing: border-box\n}\n\n.vc-settings-modal-links {\n    display: flex;\n    gap: 0.2em;\n}"
  },
  {
    "path": "src/components/settings/tabs/plugins/LinkIconButton.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./LinkIconButton.css\";\n\nimport { GithubIcon, WebsiteIcon } from \"@components/Icons\";\nimport { getTheme, Theme } from \"@utils/discord\";\nimport { MaskedLink, Tooltip } from \"@webpack/common\";\n\nexport function GithubLinkIcon() {\n    const theme = getTheme() === Theme.Light ? \"#000000\" : \"#FFFFFF\";\n    return <GithubIcon aria-hidden fill={theme} className={\"vc-settings-modal-link-icon\"} />;\n}\n\nexport function WebsiteLinkIcon() {\n    const theme = getTheme() === Theme.Light ? \"#000000\" : \"#FFFFFF\";\n    return <WebsiteIcon aria-hidden fill={theme} className={\"vc-settings-modal-link-icon\"} />;\n}\n\ninterface Props {\n    text: string;\n    href: string;\n}\n\nfunction LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {\n    return (\n        <Tooltip text={text}>\n            {props => (\n                <MaskedLink {...props} href={href}>\n                    <Icon />\n                </MaskedLink>\n            )}\n        </Tooltip>\n    );\n}\n\nexport const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;\nexport const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/PluginCard.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { showNotice } from \"@api/Notices\";\nimport { isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from \"@api/PluginManager\";\nimport { CogWheel, InfoIcon } from \"@components/Icons\";\nimport { AddonCard } from \"@components/settings/AddonCard\";\nimport { isObjectEmpty } from \"@utils/misc\";\nimport { Plugin } from \"@utils/types\";\nimport { React, showToast, Toasts } from \"@webpack/common\";\nimport { Settings } from \"Vencord\";\n\nimport { cl, logger } from \".\";\nimport { openPluginModal } from \"./PluginModal\";\n\ninterface PluginCardProps extends React.HTMLProps<HTMLDivElement> {\n    plugin: Plugin;\n    disabled: boolean;\n    onRestartNeeded(name: string, key: string): void;\n    isNew?: boolean;\n}\n\nexport function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {\n    const settings = Settings.plugins[plugin.name];\n\n    const isEnabled = () => isPluginEnabled(plugin.name);\n\n    function toggleEnabled() {\n        const wasEnabled = isEnabled();\n\n        // If we're enabling a plugin, make sure all deps are enabled recursively.\n        if (!wasEnabled) {\n            const { restartNeeded, failures } = startDependenciesRecursive(plugin);\n\n            if (failures.length) {\n                logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(\", \")}`);\n                showNotice(\"Failed to start dependencies: \" + failures.join(\", \"), \"Close\", () => null);\n                return;\n            }\n\n            if (restartNeeded) {\n                // If any dependencies have patches, don't start the plugin yet.\n                settings.enabled = true;\n                onRestartNeeded(plugin.name, \"enabled\");\n                return;\n            }\n        }\n\n        // if the plugin requires a restart, don't use stopPlugin/startPlugin. Wait for restart to apply changes.\n        if (pluginRequiresRestart(plugin)) {\n            settings.enabled = !wasEnabled;\n            onRestartNeeded(plugin.name, \"enabled\");\n            return;\n        }\n\n        // If the plugin is enabled, but hasn't been started, then we can just toggle it off.\n        if (wasEnabled && !plugin.started) {\n            settings.enabled = !wasEnabled;\n            return;\n        }\n\n        const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);\n\n        if (!result) {\n            settings.enabled = false;\n\n            const msg = `Error while ${wasEnabled ? \"stopping\" : \"starting\"} plugin ${plugin.name}`;\n            showToast(msg, Toasts.Type.FAILURE, {\n                position: Toasts.Position.BOTTOM,\n            });\n\n            return;\n        }\n\n        settings.enabled = !wasEnabled;\n    }\n\n    return (\n        <AddonCard\n            name={plugin.name}\n            description={plugin.description}\n            isNew={isNew}\n            enabled={isEnabled()}\n            setEnabled={toggleEnabled}\n            disabled={disabled}\n            onMouseEnter={onMouseEnter}\n            onMouseLeave={onMouseLeave}\n            infoButton={\n                <button\n                    role=\"switch\"\n                    onClick={() => openPluginModal(plugin, onRestartNeeded)}\n                    className={cl(\"info-button\")}\n                >\n                    {plugin.options && !isObjectEmpty(plugin.options)\n                        ? <CogWheel className={cl(\"info-icon\")} />\n                        : <InfoIcon className={cl(\"info-icon\")} />\n                    }\n                </button>\n            } />\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/PluginModal.css",
    "content": ".vc-settings-modal-content {\n    padding-bottom: 24px;\n}\n\n.vc-plugin-modal-info {\n    align-items: center;\n}\n\n.vc-plugin-modal-description {\n    flex-grow: 1;\n}"
  },
  {
    "path": "src/components/settings/tabs/plugins/PluginModal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./PluginModal.css\";\n\nimport { generateId } from \"@api/Commands\";\nimport { useSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { debounce } from \"@shared/debounce\";\nimport { gitRemote } from \"@shared/vencordUserAgent\";\nimport { classNameFactory } from \"@utils/css\";\nimport { proxyLazy } from \"@utils/lazy\";\nimport { Margins } from \"@utils/margins\";\nimport { classes, isObjectEmpty } from \"@utils/misc\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { OptionType, Plugin } from \"@utils/types\";\nimport { User } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Clickable, FluxDispatcher, Forms, React, Text, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from \"@webpack/common\";\nimport { Constructor } from \"type-fest\";\n\nimport { PluginMeta } from \"~plugins\";\n\nimport { OptionComponentMap } from \"./components\";\nimport { openContributorModal } from \"./ContributorModal\";\nimport { GithubButton, WebsiteButton } from \"./LinkIconButton\";\n\nconst cl = classNameFactory(\"vc-plugin-modal-\");\n\nconst AvatarStyles = findCssClassesLazy(\"moreUsers\", \"avatar\", \"clickableAvatar\");\nconst UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;\n\ninterface PluginModalProps extends ModalProps {\n    plugin: Plugin;\n    onRestartNeeded(key: string): void;\n}\n\nfunction makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {\n    const newUser = new UserRecord({\n        username: user.username,\n        id: user.id ?? generateId(),\n        avatar: user.avatar,\n        /** To stop discord making unwanted requests... */\n        bot: true,\n    });\n\n    FluxDispatcher.dispatch({\n        type: \"USER_UPDATE\",\n        user: newUser,\n    });\n\n    return newUser;\n}\n\nexport default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {\n    const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];\n    const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));\n\n    // avoid layout shift by showing dummy users while loading users\n    const fallbackAuthors = useMemo(() => [makeDummyUser({ username: \"Loading...\", id: \"-1465912127305809920\" })], []);\n    const [authors, setAuthors] = useState<Partial<User>[]>([]);\n\n    useEffect(() => {\n        (async () => {\n            for (const user of plugin.authors.slice(0, 6)) {\n                try {\n                    const author = user.id\n                        ? await UserUtils.getUser(String(user.id))\n                            .catch(() => makeDummyUser({ username: user.name }))\n                        : makeDummyUser({ username: user.name });\n\n                    setAuthors(a => [...a, author]);\n                } catch (e) {\n                    continue;\n                }\n            }\n        })();\n    }, [plugin.authors]);\n\n    function renderSettings() {\n        if (!hasSettings || !plugin.options)\n            return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;\n\n        const options = Object.entries(plugin.options).map(([key, setting]) => {\n            if (setting.type === OptionType.CUSTOM || setting.hidden) return null;\n\n            function onChange(newValue: any) {\n                const option = plugin.options?.[key];\n                if (!option || option.type === OptionType.CUSTOM) return;\n\n                pluginSettings[key] = newValue;\n\n                if (option.restartNeeded) onRestartNeeded(key);\n            }\n\n            const Component = OptionComponentMap[setting.type];\n            return (\n                <ErrorBoundary noop key={key}>\n                    <Component\n                        id={key}\n                        option={setting}\n                        onChange={debounce(onChange)}\n                        pluginSettings={pluginSettings}\n                        definedSettings={plugin.settings}\n                    />\n                </ErrorBoundary>\n            );\n        });\n\n        return (\n            <div className=\"vc-plugins-settings\">\n                {options}\n            </div>\n        );\n    }\n\n    function renderMoreUsers(_label: string, count: number) {\n        const sliceCount = plugin.authors.length - count;\n        const sliceStart = plugin.authors.length - sliceCount;\n        const sliceEnd = sliceStart + plugin.authors.length - count;\n\n        return (\n            <Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(\", \")}>\n                {({ onMouseEnter, onMouseLeave }) => (\n                    <div\n                        className={AvatarStyles.moreUsers}\n                        onMouseEnter={onMouseEnter}\n                        onMouseLeave={onMouseLeave}\n                    >\n                        +{sliceCount}\n                    </div>\n                )}\n            </Tooltip>\n        );\n    }\n\n    const pluginMeta = PluginMeta[plugin.name];\n\n    return (\n        <ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>\n            <ModalHeader separator={false} className={Margins.bottom8}>\n                <Text variant=\"heading-xl/bold\" style={{ flexGrow: 1 }}>{plugin.name}</Text>\n                <ModalCloseButton onClick={onClose} />\n            </ModalHeader>\n\n            <ModalContent className={\"vc-settings-modal-content\"}>\n                <section>\n                    <Flex className={cl(\"info\")}>\n                        <Forms.FormText className={cl(\"description\")}>{plugin.description}</Forms.FormText>\n                        {!pluginMeta.userPlugin && (\n                            <div className=\"vc-settings-modal-links\">\n                                <WebsiteButton\n                                    text=\"View more info\"\n                                    href={`https://vencord.dev/plugins/${plugin.name}`}\n                                />\n                                <GithubButton\n                                    text=\"View source code\"\n                                    href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}\n                                />\n                            </div>\n                        )}\n                    </Flex>\n                    <Text variant=\"heading-lg/semibold\" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>\n                    <div style={{ width: \"fit-content\" }}>\n                        <ErrorBoundary noop>\n                            <UserSummaryItem\n                                users={authors.length ? authors : fallbackAuthors}\n                                guildId={undefined}\n                                renderIcon={false}\n                                max={6}\n                                showDefaultAvatarsForNullUsers\n                                renderMoreUsers={renderMoreUsers}\n                                renderUser={(user: User) => (\n                                    <Clickable\n                                        className={AvatarStyles.clickableAvatar}\n                                        onClick={() => openContributorModal(user)}\n                                    >\n                                        <img\n                                            className={AvatarStyles.avatar}\n                                            src={user.getAvatarURL(void 0, 80, true)}\n                                            alt={user.username}\n                                            title={user.username}\n                                        />\n                                    </Clickable>\n                                )}\n                            />\n                        </ErrorBoundary>\n                    </div>\n                </section>\n\n                {!!plugin.settingsAboutComponent && (\n                    <div className={Margins.top16}>\n                        <section>\n                            <ErrorBoundary message=\"An error occurred while rendering this plugin's custom Info Component\">\n                                <plugin.settingsAboutComponent />\n                            </ErrorBoundary>\n                        </section>\n                    </div>\n                )}\n\n                <section>\n                    <Text variant=\"heading-lg/semibold\" className={classes(Margins.top16, Margins.bottom8)}>Settings</Text>\n                    {renderSettings()}\n                </section>\n            </ModalContent>\n        </ModalRoot>\n    );\n}\n\nexport function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string, key: string) => void) {\n    openModal(modalProps => (\n        <PluginModal\n            {...modalProps}\n            plugin={plugin}\n            onRestartNeeded={(key: string) => onRestartNeeded?.(plugin.name, key)}\n        />\n    ));\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/UIElements.css",
    "content": ".vc-plugin-ui-elements-button {\n    display: flex;\n    align-items: center;\n    color: var(--interactive-icon-default);\n    margin-top: 0.5em;\n    cursor: pointer;\n\n    &:hover {\n        background-color: var(--background-mod-subtle);\n        color: var(--interactive-icon-hover);\n    }\n}\n\n.vc-plugin-ui-elements-button-arrow {\n    margin-left: auto;\n    width: 24px;\n    height: 24px;\n}\n\n.vc-plugin-ui-elements-modal-content {\n    padding: 1em;\n    display: flex;\n    flex-direction: column;\n    gap: 1.5em;\n}\n\n.vc-plugin-ui-elements-switches {\n    display: flex;\n    flex-direction: column;\n    gap: 1em;\n}\n\n.vc-plugin-ui-elements-switches-row {\n    display: flex;\n    gap: 1em;\n    width: 100%;\n    align-items: center;\n\n    :last-child {\n        margin-left: auto;\n    }\n}"
  },
  {
    "path": "src/components/settings/tabs/plugins/UIElements.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./UIElements.css\";\n\nimport { ChatBarButtonMap } from \"@api/ChatButtons\";\nimport { MessagePopoverButtonMap } from \"@api/MessagePopover\";\nimport { SettingsPluginUiElements, useSettings } from \"@api/Settings\";\nimport { BaseText } from \"@components/BaseText\";\nimport { Card } from \"@components/Card\";\nimport { PlaceholderIcon } from \"@components/Icons\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Switch } from \"@components/Switch\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { IconComponent } from \"@utils/types\";\nimport { Clickable } from \"@webpack/common\";\n\n\nconst cl = classNameFactory(\"vc-plugin-ui-elements-\");\n\nexport function UIElementsButton() {\n    return (\n        <Clickable onClick={() => openModal(modalProps => <UIElementsModal {...modalProps} />)}>\n            <Card className={cl(\"button\")} defaultPadding>\n                <div className={cl(\"button-description\")}>\n                    <Paragraph size=\"md\" weight=\"semibold\">\n                        Manage plugin UI elements\n                    </Paragraph>\n                    <Paragraph size=\"xs\">\n                        Allows you to hide buttons you don't like\n                    </Paragraph>\n                </div>\n                <svg\n                    className={cl(\"button-arrow\")}\n                    aria-hidden=\"true\"\n                    viewBox=\"0 0 24 24\"\n                >\n                    <path fill=\"currentColor\" d=\"M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z\" />\n                </svg>\n            </Card>\n        </Clickable >\n    );\n}\n\nfunction Section(props: {\n    title: string;\n    description: string;\n    settings: SettingsPluginUiElements;\n    buttonMap: Map<string, { icon: IconComponent; }>;\n}) {\n    const { buttonMap, description, title, settings } = props;\n\n    const switches = Array.from(buttonMap, ([name, { icon }]) => {\n        const Icon = icon ?? PlaceholderIcon;\n        return (\n            <Paragraph size=\"md\" weight=\"semibold\" key={name} className={cl(\"switches-row\")}>\n                <Icon height={20} width={20} />\n                {name}\n                <Switch\n                    checked={settings[name]?.enabled ?? true}\n                    onChange={v => {\n                        settings[name] ??= {} as any;\n                        settings[name].enabled = v;\n                    }}\n                />\n            </Paragraph>\n        );\n    });\n\n    return (\n        <section>\n            <BaseText tag=\"h3\" size=\"xl\" weight=\"bold\">{title}</BaseText>\n            <Paragraph size=\"sm\" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>\n\n            <div className={cl(\"switches\")}>\n                {switches.length === 0 && (\n                    <Paragraph weight=\"medium\" className={cl(\"switches-row\")} style={{ color: \"var(--text-muted)\" }}>\n                        Buttons of enabled plugins will appear here.\n                    </Paragraph>\n                )}\n                {switches}\n            </div>\n        </section>\n    );\n}\n\nfunction UIElementsModal(props: ModalProps) {\n    const { uiElements } = useSettings([\"uiElements.*\"]);\n\n    return (\n        <ModalRoot {...props} size={ModalSize.MEDIUM}>\n            <ModalContent className={cl(\"modal-content\")}>\n                <Section\n                    title=\"Chatbar Buttons\"\n                    description=\"These are the buttons on the right side of the chat input bar\"\n                    buttonMap={ChatBarButtonMap}\n                    settings={uiElements.chatBarButtons}\n                />\n                <Section\n                    title=\"Message Popover Buttons\"\n                    description=\"These are the floating buttons on the right when you hover over a message\"\n                    buttonMap={MessagePopoverButtonMap}\n                    settings={uiElements.messagePopoverButtons}\n                />\n            </ModalContent>\n        </ModalRoot>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/BooleanSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Switch } from \"@components/Switch\";\nimport { PluginOptionBoolean } from \"@utils/types\";\nimport { React, useState } from \"@webpack/common\";\n\nimport { resolveError, SettingProps, SettingsSection } from \"./Common\";\n\nexport function BooleanSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionBoolean>) {\n    const def = pluginSettings[id] ?? option.default;\n\n    const [state, setState] = useState(def ?? false);\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: boolean): void {\n        const isValid = option.isValid?.call(definedSettings, newValue) ?? true;\n\n        setState(newValue);\n        setError(resolveError(isValid));\n\n        if (isValid === true) {\n            onChange(newValue);\n        }\n    }\n\n    return (\n        <SettingsSection tag=\"label\" name={id} description={option.description} error={error} inlineSetting>\n            <Switch\n                checked={state}\n                onChange={handleChange}\n                disabled={option.disabled?.call(definedSettings) ?? false}\n            />\n        </SettingsSection>\n    );\n}\n\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/Common.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport { wordsFromCamel, wordsToTitle } from \"@utils/text\";\nimport { DefinedSettings, PluginOptionBase } from \"@utils/types\";\nimport { Text } from \"@webpack/common\";\nimport { PropsWithChildren } from \"react\";\n\nexport const cl = classNameFactory(\"vc-plugins-setting-\");\n\ninterface SettingBaseProps<T> {\n    option: T;\n    onChange(newValue: any): void;\n    pluginSettings: {\n        [setting: string]: any;\n        enabled: boolean;\n    };\n    id: string;\n    definedSettings?: DefinedSettings;\n}\n\nexport type SettingProps<T extends PluginOptionBase> = SettingBaseProps<T>;\nexport type ComponentSettingProps<T extends Omit<PluginOptionBase, \"description\" | \"placeholder\">> = SettingBaseProps<T>;\n\nexport function resolveError(isValidResult: boolean | string) {\n    if (typeof isValidResult === \"string\") return isValidResult;\n\n    return isValidResult ? null : \"Invalid input provided\";\n}\n\ninterface SettingsSectionProps extends PropsWithChildren {\n    name: string;\n    description: string;\n    error?: string | null;\n    inlineSetting?: boolean;\n    tag?: \"label\" | \"div\";\n}\n\nexport function SettingsSection({ tag: Tag = \"div\", name, description, error, inlineSetting, children }: SettingsSectionProps) {\n    return (\n        <Tag className={cl(\"section\")}>\n            <div className={classes(cl(\"content\"), inlineSetting && cl(\"inline\"))}>\n                <div className={cl(\"label\")}>\n                    {name && <Text className={cl(\"title\")} variant=\"text-md/medium\">{wordsToTitle(wordsFromCamel(name))}</Text>}\n                    {description && <Text className={cl(\"description\")} variant=\"text-sm/normal\">{description}</Text>}\n                </div>\n                {children}\n            </div>\n            {error && <Text className={cl(\"error\")} variant=\"text-sm/normal\">{error}</Text>}\n        </Tag>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/ComponentSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { PluginOptionComponent } from \"@utils/types\";\n\nimport { ComponentSettingProps } from \"./Common\";\n\nexport function ComponentSetting({ option, onChange }: ComponentSettingProps<PluginOptionComponent>) {\n    return option.component({ setValue: onChange, option });\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/NumberSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { OptionType, PluginOptionNumber } from \"@utils/types\";\nimport { React, TextInput, useState } from \"@webpack/common\";\n\nimport { resolveError, SettingProps, SettingsSection } from \"./Common\";\n\nconst MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);\n\nexport function NumberSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionNumber>) {\n    function serialize(value: any) {\n        if (option.type === OptionType.BIGINT) return BigInt(value);\n        return Number(value);\n    }\n\n    const [state, setState] = useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: any) {\n        const isValid = option.isValid?.call(definedSettings, newValue) ?? true;\n\n        setError(resolveError(isValid));\n\n        if (isValid === true) {\n            onChange(serialize(newValue));\n        }\n\n        if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {\n            setState(`${Number.MAX_SAFE_INTEGER}`);\n        } else {\n            setState(newValue);\n        }\n    }\n\n    return (\n        <SettingsSection name={id} description={option.description} error={error}>\n            <TextInput\n                type=\"number\"\n                pattern=\"-?[0-9]+\"\n                placeholder={option.placeholder ?? \"Enter a number\"}\n                value={state}\n                onChange={handleChange}\n                disabled={option.disabled?.call(definedSettings) ?? false}\n                {...option.componentProps}\n            />\n        </SettingsSection>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/SelectSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { PluginOptionSelect } from \"@utils/types\";\nimport { React, Select, useState } from \"@webpack/common\";\n\nimport { resolveError, SettingProps, SettingsSection } from \"./Common\";\n\nexport function SelectSetting({ option, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginOptionSelect>) {\n    const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;\n\n    const [state, setState] = useState<any>(def ?? null);\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: any) {\n        const isValid = option.isValid?.call(definedSettings, newValue) ?? true;\n\n        setState(newValue);\n        setError(resolveError(isValid));\n\n        if (isValid === true) {\n            onChange(newValue);\n        }\n    }\n\n    return (\n        <SettingsSection name={id} description={option.description} error={error}>\n            <Select\n                placeholder={option.placeholder ?? \"Select an option\"}\n                options={option.options}\n                maxVisibleItems={5}\n                closeOnSelect={true}\n                select={handleChange}\n                isSelected={v => v === state}\n                serialize={v => String(v)}\n                isDisabled={option.disabled?.call(definedSettings) ?? false}\n                {...option.componentProps}\n            />\n        </SettingsSection>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/SliderSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { PluginOptionSlider } from \"@utils/types\";\nimport { React, Slider, useState } from \"@webpack/common\";\n\nimport { resolveError, SettingProps, SettingsSection } from \"./Common\";\n\nexport function SliderSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionSlider>) {\n    const def = pluginSettings[id] ?? option.default;\n\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: number): void {\n        const isValid = option.isValid?.call(definedSettings, newValue) ?? true;\n\n        setError(resolveError(isValid));\n\n        if (isValid === true) {\n            onChange(newValue);\n        }\n    }\n\n    return (\n        <SettingsSection name={id} description={option.description} error={error}>\n            <Slider\n                markers={option.markers}\n                minValue={option.markers[0]}\n                maxValue={option.markers[option.markers.length - 1]}\n                initialValue={def}\n                onValueChange={handleChange}\n                onValueRender={(v: number) => String(v.toFixed(2))}\n                stickToMarkers={option.stickToMarkers ?? true}\n                disabled={option.disabled?.call(definedSettings) ?? false}\n                {...option.componentProps}\n            />\n        </SettingsSection>\n    );\n}\n\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/TextSetting.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { PluginOptionString } from \"@utils/types\";\nimport { React, TextArea, TextInput, useState } from \"@webpack/common\";\n\nimport { resolveError, SettingProps, SettingsSection } from \"./Common\";\n\nexport function TextSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionString>) {\n    const [state, setState] = useState(pluginSettings[id] ?? option.default ?? null);\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: string) {\n        const isValid = option.isValid?.call(definedSettings, newValue) ?? true;\n\n        setState(newValue);\n        setError(resolveError(isValid));\n\n        if (isValid === true) {\n            onChange(newValue);\n        }\n    }\n\n    return (\n        <SettingsSection name={id} description={option.description} error={error}>\n            {option.multiline\n                ? <TextArea\n                    placeholder={option.placeholder ?? \"Enter a value\"}\n                    value={state}\n                    onChange={handleChange}\n                    disabled={option.disabled?.call(definedSettings) ?? false}\n                    {...option.componentProps} />\n                : <TextInput\n                    type=\"text\"\n                    placeholder={option.placeholder ?? \"Enter a value\"}\n                    value={state}\n                    onChange={handleChange}\n                    maxLength={null}\n                    disabled={option.disabled?.call(definedSettings) ?? false}\n                    {...option.componentProps}\n                />\n            }\n        </SettingsSection>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { OptionType } from \"@utils/types\";\nimport { ComponentType } from \"react\";\n\nimport { BooleanSetting } from \"./BooleanSetting\";\nimport { ComponentSettingProps, SettingProps } from \"./Common\";\nimport { ComponentSetting } from \"./ComponentSetting\";\nimport { NumberSetting } from \"./NumberSetting\";\nimport { SelectSetting } from \"./SelectSetting\";\nimport { SliderSetting } from \"./SliderSetting\";\nimport { TextSetting } from \"./TextSetting\";\n\nexport const OptionComponentMap: Record<OptionType, ComponentType<SettingProps<any> | ComponentSettingProps<any>>> = {\n    [OptionType.STRING]: TextSetting,\n    [OptionType.NUMBER]: NumberSetting,\n    [OptionType.BIGINT]: NumberSetting,\n    [OptionType.BOOLEAN]: BooleanSetting,\n    [OptionType.SELECT]: SelectSetting,\n    [OptionType.SLIDER]: SliderSetting,\n    [OptionType.COMPONENT]: ComponentSetting,\n    [OptionType.CUSTOM]: () => null,\n};\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/components/styles.css",
    "content": ".vc-plugins-setting-section {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5em;\n}\n\n.vc-plugins-setting-content {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5em;\n}\n\n.vc-plugins-setting-inline {\n    flex-direction: row;\n    align-items: center;\n    justify-content: space-between;\n}\n\n.vc-plugins-setting-label {\n    display: flex;\n    flex-direction: column;\n    gap: 0.25em;\n}\n\n.vc-plugins-setting-title {\n    color: var(--text-strong);\n}\n\n.vc-plugins-setting-description {\n    color: var(--text-default);\n}\n\n.vc-plugins-setting-error {\n    color: var(--text-feedback-critical, #FF5C5C);\n}"
  },
  {
    "path": "src/components/settings/tabs/plugins/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport * as DataStore from \"@api/DataStore\";\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { useSettings } from \"@api/Settings\";\nimport { Card } from \"@components/Card\";\nimport { Divider } from \"@components/Divider\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { HeadingTertiary } from \"@components/Heading\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { ChangeList } from \"@utils/ChangeList\";\nimport { classNameFactory } from \"@utils/css\";\nimport { isTruthy } from \"@utils/guards\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { useAwaiter, useCleanupEffect } from \"@utils/react\";\nimport { Alerts, Button, lodash, Parser, React, Select, TextInput, Tooltip, useMemo, useState } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nimport Plugins, { ExcludedPlugins, PluginMeta } from \"~plugins\";\n\nimport { PluginCard } from \"./PluginCard\";\nimport { UIElementsButton } from \"./UIElements\";\n\nexport const cl = classNameFactory(\"vc-plugins-\");\nexport const logger = new Logger(\"PluginSettings\", \"#a6d189\");\n\nfunction ReloadRequiredCard({ required }: { required: boolean; }) {\n    return (\n        <Card variant={required ? \"warning\" : \"normal\"} className={cl(\"info-card\")}>\n            {required\n                ? (\n                    <>\n                        <HeadingTertiary>Restart required!</HeadingTertiary>\n                        <Paragraph className={cl(\"dep-text\")}>\n                            Restart now to apply new plugins and their settings\n                        </Paragraph>\n                        <Button onClick={() => location.reload()} className={cl(\"restart-button\")}>\n                            Restart\n                        </Button>\n                    </>\n                )\n                : (\n                    <>\n                        <HeadingTertiary>Plugin Management</HeadingTertiary>\n                        <Paragraph>Press the cog wheel or info icon to get more info on a plugin</Paragraph>\n                        <Paragraph>Plugins with a cog wheel have settings you can modify!</Paragraph>\n                    </>\n                )}\n        </Card>\n    );\n}\n\nconst enum SearchStatus {\n    ALL,\n    ENABLED,\n    DISABLED,\n    NEW,\n    USER_PLUGINS,\n    API_PLUGINS\n}\n\nfunction ExcludedPluginsList({ search }: { search: string; }) {\n    const matchingExcludedPlugins = search\n        ? Object.entries(ExcludedPlugins)\n            .filter(([name]) => name.toLowerCase().includes(search))\n        : [];\n\n    const ExcludedReasons: Record<\"web\" | \"discordDesktop\" | \"vesktop\" | \"desktop\" | \"dev\", string> = {\n        desktop: \"Discord Desktop app or Vesktop\",\n        discordDesktop: \"Discord Desktop app\",\n        vesktop: \"Vesktop app\",\n        web: \"Vesktop app and the Web version of Discord\",\n        dev: \"Developer version of Vencord\"\n    };\n\n    return (\n        <Paragraph className={Margins.top16}>\n            {matchingExcludedPlugins.length\n                ? <>\n                    <Paragraph>Are you looking for:</Paragraph>\n                    <ul>\n                        {matchingExcludedPlugins.map(([name, reason]) => (\n                            <li key={name}>\n                                <b>{name}</b>: Only available on the {ExcludedReasons[reason]}\n                            </li>\n                        ))}\n                    </ul>\n                </>\n                : \"No plugins meet the search criteria.\"\n            }\n        </Paragraph>\n    );\n}\n\nfunction PluginSettings() {\n    const settings = useSettings();\n    const changes = useMemo(() => new ChangeList<string>(), []);\n\n    useCleanupEffect(() => {\n        if (changes.hasChanges)\n            Alerts.show({\n                title: \"Restart required\",\n                body: (\n                    <>\n                        <p>The following plugins require a restart:</p>\n                        <div>{changes.map((s, i) => (\n                            <>\n                                {i > 0 && \", \"}\n                                {Parser.parse(\"`\" + s.split(\".\")[0] + \"`\")}\n                            </>\n                        ))}</div>\n                    </>\n                ),\n                confirmText: \"Restart now\",\n                cancelText: \"Later!\",\n                onConfirm: () => location.reload()\n            });\n    }, []);\n\n    const depMap = useMemo(() => {\n        const o = {} as Record<string, string[]>;\n        for (const plugin in Plugins) {\n            const deps = Plugins[plugin].dependencies;\n            if (deps) {\n                for (const dep of deps) {\n                    o[dep] ??= [];\n                    o[dep].push(plugin);\n                }\n            }\n        }\n        return o;\n    }, []);\n\n    const sortedPlugins = useMemo(() =>\n        Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)),\n        []\n    );\n\n    const hasUserPlugins = useMemo(() => !IS_STANDALONE && Object.values(PluginMeta).some(m => m.userPlugin), []);\n\n    const [searchValue, setSearchValue] = useState({ value: \"\", status: SearchStatus.ALL });\n\n    const search = searchValue.value.toLowerCase();\n    const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));\n    const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));\n\n    const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {\n        const { status } = searchValue;\n        const enabled = isPluginEnabled(plugin.name);\n\n        switch (status) {\n            case SearchStatus.DISABLED:\n                if (enabled) return false;\n                break;\n            case SearchStatus.ENABLED:\n                if (!enabled) return false;\n                break;\n            case SearchStatus.NEW:\n                if (!newPlugins?.includes(plugin.name)) return false;\n                break;\n            case SearchStatus.USER_PLUGINS:\n                if (!PluginMeta[plugin.name]?.userPlugin) return false;\n                break;\n            case SearchStatus.API_PLUGINS:\n                if (!plugin.name.endsWith(\"API\")) return false;\n                break;\n        }\n\n        if (!search.length) return true;\n\n        return (\n            plugin.name.toLowerCase().includes(search) ||\n            plugin.description.toLowerCase().includes(search) ||\n            plugin.tags?.some(t => t.toLowerCase().includes(search))\n        );\n    };\n\n    const [newPlugins] = useAwaiter(() => DataStore.get(\"Vencord_existingPlugins\").then((cachedPlugins: Record<string, number> | undefined) => {\n        const now = Date.now() / 1000;\n        const existingTimestamps: Record<string, number> = {};\n        const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);\n\n        const newPlugins: string[] = [];\n        for (const { name: p } of sortedPlugins) {\n            const time = existingTimestamps[p] = cachedPlugins?.[p] ?? now;\n            if ((time + 60 * 60 * 24 * 2) > now) {\n                newPlugins.push(p);\n            }\n        }\n        DataStore.set(\"Vencord_existingPlugins\", existingTimestamps);\n\n        return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;\n    }));\n\n    const plugins = [] as JSX.Element[];\n    const requiredPlugins = [] as JSX.Element[];\n\n    const showApi = searchValue.status === SearchStatus.API_PLUGINS;\n    for (const p of sortedPlugins) {\n        if (p.hidden || (!p.options && p.name.endsWith(\"API\") && !showApi))\n            continue;\n\n        if (!pluginFilter(p)) continue;\n\n        const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);\n\n        if (isRequired) {\n            const tooltipText = p.required || !depMap[p.name]\n                ? \"This plugin is required for Vencord to function.\"\n                : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));\n\n            requiredPlugins.push(\n                <Tooltip text={tooltipText} key={p.name}>\n                    {({ onMouseLeave, onMouseEnter }) => (\n                        <PluginCard\n                            onMouseLeave={onMouseLeave}\n                            onMouseEnter={onMouseEnter}\n                            onRestartNeeded={(name, key) => changes.handleChange(`${name}.${key}`)}\n                            disabled={true}\n                            plugin={p}\n                            key={p.name}\n                        />\n                    )}\n                </Tooltip>\n            );\n        } else {\n            plugins.push(\n                <PluginCard\n                    onRestartNeeded={(name, key) => changes.handleChange(`${name}.${key}`)}\n                    disabled={false}\n                    plugin={p}\n                    isNew={newPlugins?.includes(p.name)}\n                    key={p.name}\n                />\n            );\n        }\n    }\n\n    return (\n        <SettingsTab>\n            <ReloadRequiredCard required={changes.hasChanges} />\n\n            <UIElementsButton />\n\n            <HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>\n                Filters\n            </HeadingTertiary>\n\n            <div className={classes(Margins.bottom20, cl(\"filter-controls\"))}>\n                <ErrorBoundary noop>\n                    <TextInput autoFocus value={searchValue.value} placeholder=\"Search for a plugin...\" onChange={onSearch} />\n                </ErrorBoundary>\n                <div>\n                    <ErrorBoundary noop>\n                        <Select\n                            options={[\n                                { label: \"Show All\", value: SearchStatus.ALL, default: true },\n                                { label: \"Show Enabled\", value: SearchStatus.ENABLED },\n                                { label: \"Show Disabled\", value: SearchStatus.DISABLED },\n                                { label: \"Show New\", value: SearchStatus.NEW },\n                                hasUserPlugins && { label: \"Show UserPlugins\", value: SearchStatus.USER_PLUGINS },\n                                { label: \"Show API Plugins\", value: SearchStatus.API_PLUGINS },\n                            ].filter(isTruthy)}\n                            serialize={String}\n                            select={onStatusChange}\n                            isSelected={v => v === searchValue.status}\n                            closeOnSelect={true}\n                        />\n                    </ErrorBoundary>\n                </div>\n            </div>\n\n            <HeadingTertiary className={Margins.top20}>Plugins</HeadingTertiary>\n\n            {plugins.length || requiredPlugins.length\n                ? (\n                    <div className={cl(\"grid\")}>\n                        {plugins.length\n                            ? plugins\n                            : <Paragraph>No plugins meet the search criteria.</Paragraph>\n                        }\n                    </div>\n                )\n                : <ExcludedPluginsList search={search} />\n            }\n\n\n            <Divider className={Margins.top20} />\n\n            <HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>\n                Required Plugins\n            </HeadingTertiary>\n            <div className={cl(\"grid\")}>\n                {requiredPlugins.length\n                    ? requiredPlugins\n                    : <Paragraph>No plugins meet the search criteria.</Paragraph>\n                }\n            </div>\n        </SettingsTab >\n    );\n}\n\nfunction makeDependencyList(deps: string[]) {\n    return (\n        <>\n            <Paragraph>This plugin is required by:</Paragraph>\n            {deps.map((dep: string) => <Paragraph key={dep} className={cl(\"dep-text\")}>{dep}</Paragraph>)}\n        </>\n    );\n}\n\nexport default wrapTab(PluginSettings, \"Plugins\");\n"
  },
  {
    "path": "src/components/settings/tabs/plugins/styles.css",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n.vc-plugins-grid {\n    margin-top: 16px;\n    display: grid;\n    gap: 16px;\n    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n}\n\n.vc-plugins-info-button {\n    height: 24px;\n    width: 24px;\n    padding: 0;\n    background: transparent;\n    margin-right: 8px;\n    color: var(--icon-muted);\n    transition: background-color .2s ease;\n\n    &:hover {\n        color: var(--interactive-icon-hover)\n    }\n}\n\n.vc-plugins-settings-button:hover {\n    color: var(--interactive-icon-hover);\n}\n\n.vc-plugins-filter-controls {\n    display: grid;\n    height: 40px;\n    gap: 10px;\n    grid-template-columns: 1fr 150px;\n}\n\n.vc-addon-badge {\n    padding: 0 6px;\n    font-family: var(--font-display);\n    font-weight: 500;\n    border-radius: 8px;\n    height: 16px;\n    font-size: 12px;\n    line-height: 16px;\n    color: var(--white-500);\n    text-align: center;\n}\n\n.vc-plugins-dep-name {\n    margin: 0 auto;\n}\n\n.vc-plugins-info-card {\n    padding: 1em;\n    height: 8em;\n    display: flex;\n    flex-direction: column;\n    gap: 0.25em;\n}\n\n.vc-plugins-restart-button {\n    margin-top: 0.5em;\n    background: var(--icon-status-idle) !important;\n}\n\n.vc-plugins-info-icon:not(:hover, :focus) {\n    color: var(--text-muted);\n}\n\n.vc-plugins-settings {\n    display: flex;\n    flex-direction: column;\n    gap: 1.25em;\n}"
  },
  {
    "path": "src/components/settings/tabs/styles.css",
    "content": ".vc-settings-tab-bar {\n    margin-top: 20px;\n    margin-bottom: 10px;\n    border-bottom: 1px solid var(--border-subtle);\n}\n\n.vc-settings-tab-bar-item {\n    margin-right: 32px;\n    padding-bottom: 16px;\n    margin-bottom: -2px;\n}\n\n.vc-settings-donate {\n    display: flex;\n    flex-direction: row;\n}\n\n.vc-settings-theme-links {\n    /* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */\n    display: inline-block !important;\n    color: var(--text-default) !important;\n    padding: 0.5em 1em;\n    border: 1px solid var(--input-border-default);\n    max-height: unset;\n    background-color: transparent;\n    box-sizing: border-box;\n    resize: none;\n    width: 100%;\n    font-size: 1em;\n    line-height: 2em;\n    white-space: nowrap;\n}\n\n.vc-settings-theme-links::placeholder {\n    color: var(--text-muted) !important;\n}\n\n.vc-settings-theme-links:focus {\n    background-color: var(--background-base-lowest);\n}\n\n.vc-cloud-settings-sync-grid {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 1em;\n    margin-top: 16px;\n}\n\n.vc-cloud-erase-data-danger-btn {\n    color: var(--control-critical-primary-text-default);\n    background-color: var(--control-critical-primary-background-default);\n}\n\n.vc-cloud-icon-with-button {\n    display: flex;\n    gap: 0.5em;\n    padding-inline: 0.5em 1em;\n}\n\n.vc-cloud-button-icon {\n    height: 1.25em;\n}\n\n.vc-settings-modal {\n    padding: 1.5em !important;\n}\n\n.vc-settings-modal-close {\n    float: right;\n}"
  },
  {
    "path": "src/components/settings/tabs/sync/BackupAndRestoreTab.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { downloadSettingsBackup, uploadSettingsBackup } from \"@api/SettingsSync/offline\";\nimport { Card } from \"@components/Card\";\nimport { Flex } from \"@components/Flex\";\nimport { Heading } from \"@components/Heading\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { Margins } from \"@utils/margins\";\nimport { Button, Text } from \"@webpack/common\";\n\nfunction BackupAndRestoreTab() {\n    return (\n        <SettingsTab>\n            <Flex flexDirection=\"column\" gap=\"0.5em\">\n                <Card variant=\"warning\">\n                    <Heading tag=\"h4\">Warning</Heading>\n                    <Paragraph>Importing a settings file will overwrite your current settings.</Paragraph>\n                </Card>\n\n                <Text variant=\"text-md/normal\" className={Margins.bottom8}>\n                    You can import and export your Vencord settings as a JSON file.\n                    This allows you to easily transfer your settings to another device,\n                    or recover your settings after reinstalling Vencord or Discord.\n                </Text>\n\n                <Heading tag=\"h4\">Settings Export contains:</Heading>\n                <Text variant=\"text-md/normal\" className={Margins.bottom8}>\n                    <ul>\n                        <li>&mdash; Custom QuickCSS</li>\n                        <li>&mdash; Theme Links</li>\n                        <li>&mdash; Plugin Settings</li>\n                    </ul>\n                </Text>\n\n                <Flex>\n                    <Button onClick={() => uploadSettingsBackup()}>\n                        Import Settings\n                    </Button>\n                    <Button onClick={downloadSettingsBackup}>\n                        Export Settings\n                    </Button>\n                </Flex>\n            </Flex>\n        </SettingsTab >\n    );\n}\n\nexport default wrapTab(BackupAndRestoreTab, \"Backup & Restore\");\n"
  },
  {
    "path": "src/components/settings/tabs/sync/CloudTab.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { useSettings } from \"@api/Settings\";\nimport { authorizeCloud, deauthorizeCloud } from \"@api/SettingsSync/cloudSetup\";\nimport { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from \"@api/SettingsSync/cloudSync\";\nimport { BaseText } from \"@components/BaseText\";\nimport { Button, ButtonProps } from \"@components/Button\";\nimport { CheckedTextInput } from \"@components/CheckedTextInput\";\nimport { Divider } from \"@components/Divider\";\nimport { Flex } from \"@components/Flex\";\nimport { FormSwitch } from \"@components/FormSwitch\";\nimport { Grid } from \"@components/Grid\";\nimport { Heading } from \"@components/Heading\";\nimport { CloudDownloadIcon, CloudUploadIcon, DeleteIcon, RestartIcon } from \"@components/Icons\";\nimport { Link } from \"@components/Link\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { localStorage } from \"@utils/localStorage\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { IconComponent } from \"@utils/types\";\nimport { Alerts, Select, Tooltip } from \"@webpack/common\";\n\nfunction validateUrl(url: string) {\n    try {\n        new URL(url);\n        return true;\n    } catch {\n        return \"Invalid URL\";\n    }\n}\n\nconst SectionHeading = ({ text }: { text: string; }) => (\n    <BaseText\n        tag=\"h5\"\n        size=\"lg\"\n        weight=\"semibold\"\n        className={Margins.bottom16}\n    >\n        {text}\n    </BaseText>\n);\n\nfunction ButtonWithIcon({ children, Icon, className, ...buttonProps }: ButtonProps & { Icon: IconComponent; }) {\n    return (\n        <Button {...buttonProps} className={classes(\"vc-cloud-icon-with-button\", className)}>\n            <Icon className={\"vc-cloud-button-icon\"} />\n            {children}\n        </Button>\n    );\n}\n\nfunction CloudSetupSection() {\n    const { cloud } = useSettings([\"cloud.authenticated\", \"cloud.url\"]);\n\n    return (\n        <section>\n            <SectionHeading text=\"Cloud Integrations\" />\n\n            <Paragraph size=\"md\" className={Margins.bottom20}>\n                Vencord comes with a cloud integration that adds goodies like settings sync across devices.\n                It <Link href=\"https://vencord.dev/cloud/privacy\">respects your privacy</Link>, and\n                the <Link href=\"https://github.com/Vencord/Backend\">source code</Link> is AGPL 3.0 licensed so you\n                can host it yourself.\n            </Paragraph>\n            <FormSwitch\n                key=\"backend\"\n                title=\"Enable Cloud Integrations\"\n                description=\"This will request authorization if you have not yet set up cloud integrations.\"\n                value={cloud.authenticated}\n                onChange={v => {\n                    if (v)\n                        authorizeCloud();\n                    else\n                        cloud.authenticated = v;\n                }}\n            />\n            <Heading tag=\"h5\" className={Margins.top16}>Backend URL</Heading>\n            <Paragraph className={Margins.bottom8}>\n                Which backend to use when using cloud integrations.\n            </Paragraph>\n            <CheckedTextInput\n                key=\"backendUrl\"\n                value={cloud.url}\n                onChange={async v => {\n                    cloud.url = v;\n                    cloud.authenticated = false;\n                    deauthorizeCloud();\n                }}\n                validate={validateUrl}\n            />\n\n            <Grid columns={1} gap=\"1em\" className={Margins.top8}>\n                <ButtonWithIcon\n                    variant=\"primary\"\n                    disabled={!cloud.authenticated}\n                    onClick={async () => {\n                        await deauthorizeCloud();\n                        cloud.authenticated = false;\n                        await authorizeCloud();\n                    }}\n                    Icon={RestartIcon}\n                >\n                    Reauthorise\n                </ButtonWithIcon>\n            </Grid>\n        </section>\n    );\n}\n\nfunction SettingsSyncSection() {\n    const { cloud } = useSettings([\"cloud.authenticated\", \"cloud.settingsSync\"]);\n    const sectionEnabled = cloud.authenticated && cloud.settingsSync;\n\n    return (\n        <section>\n            <SectionHeading text=\"Settings Sync\" />\n            <Flex flexDirection=\"column\" gap=\"1em\">\n                <FormSwitch\n                    key=\"cloud-sync\"\n                    title=\"Enable Settings Sync\"\n                    description=\"Save your Vencord settings to the cloud so you can easily keep them the same on all your devices\"\n                    value={cloud.settingsSync}\n                    onChange={v => { cloud.settingsSync = v; }}\n                    disabled={!cloud.authenticated}\n                    hideBorder\n                />\n\n                <div>\n                    <Heading tag=\"h5\">\n                        Sync Rules for This Device\n                    </Heading>\n                    <Paragraph className={Margins.bottom8}>\n                        This setting controls how settings move between <strong>this device</strong> and the cloud.\n                        You can let changes flow both ways, or choose one place to be the main source of truth.\n                    </Paragraph>\n                    <Select\n                        options={[\n                            {\n                                label: \"Two-way sync (changes go both directions)\",\n                                value: \"both\",\n                                default: true,\n                            },\n                            {\n                                label: \"This device is the source (upload only)\",\n                                value: \"push\",\n                            },\n                            {\n                                label: \"The cloud is the source (download only)\",\n                                value: \"pull\",\n                            },\n                            {\n                                label: \"Do not sync automatically (manual sync via buttons below only)\",\n                                value: \"manual\",\n                            }\n                        ]}\n                        isSelected={v => v === localStorage.Vencord_cloudSyncDirection}\n                        serialize={v => String(v)}\n                        select={v => {\n                            localStorage.Vencord_cloudSyncDirection = v;\n                        }}\n                        closeOnSelect={true}\n                    />\n                </div>\n\n                <Grid columns={2} gap=\"1em\" className={Margins.top20}>\n                    <ButtonWithIcon\n                        variant=\"positive\"\n                        disabled={!sectionEnabled}\n                        onClick={() => putCloudSettings(true)}\n                        Icon={CloudUploadIcon}\n                    >\n                        Upload Settings\n                    </ButtonWithIcon>\n                    <Tooltip text=\"This will replace your current settings with the ones saved in the cloud. Be careful!\">\n                        {({ onMouseLeave, onMouseEnter }) => (\n                            <ButtonWithIcon\n                                variant=\"dangerPrimary\"\n                                onMouseLeave={onMouseLeave}\n                                onMouseEnter={onMouseEnter}\n                                disabled={!sectionEnabled}\n                                onClick={() => getCloudSettings(true, true)}\n                                Icon={CloudDownloadIcon}\n                            >\n                                Download Settings\n                            </ButtonWithIcon>\n                        )}\n                    </Tooltip>\n                </Grid>\n            </Flex>\n        </section>\n    );\n}\n\nfunction ResetSection() {\n    const { authenticated, settingsSync } = useSettings([\"cloud.authenticated\", \"cloud.settingsSync\"]).cloud;\n\n    return (\n        <section>\n            <SectionHeading text=\"Reset Cloud Data\" />\n\n            <Grid columns={2} gap=\"1em\">\n                <ButtonWithIcon\n                    variant=\"dangerPrimary\"\n                    disabled={!authenticated || !settingsSync}\n                    onClick={() => deleteCloudSettings()}\n                    Icon={DeleteIcon}\n                >\n                    Delete Settings from Cloud\n                </ButtonWithIcon>\n                <ButtonWithIcon\n                    variant=\"dangerPrimary\"\n                    disabled={!authenticated}\n                    onClick={() => Alerts.show({\n                        title: \"Are you sure?\",\n                        body: \"Once your data is erased, we cannot recover it. There's no going back!\",\n                        onConfirm: eraseAllCloudData,\n                        confirmText: \"Erase it!\",\n                        confirmColor: \"vc-cloud-erase-data-danger-btn\",\n                        cancelText: \"Nevermind\"\n                    })}\n                    Icon={DeleteIcon}\n                >\n                    Delete your Cloud Account\n                </ButtonWithIcon>\n            </Grid>\n        </section>\n    );\n}\n\nfunction CloudTab() {\n    return (\n        <SettingsTab>\n            <Flex flexDirection=\"column\" gap=\"1em\">\n                <CloudSetupSection />\n                <Divider />\n                <SettingsSyncSection />\n                <Divider />\n                <ResetSection />\n            </Flex>\n        </SettingsTab>\n    );\n}\n\nexport default wrapTab(CloudTab, \"Cloud\");\n"
  },
  {
    "path": "src/components/settings/tabs/themes/CspErrorCard.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Divider } from \"@components/Divider\";\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Link } from \"@components/Link\";\nimport { CspBlockedUrls, useCspErrors } from \"@utils/cspViolations\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { relaunch } from \"@utils/native\";\nimport { useForceUpdater } from \"@utils/react\";\nimport { Alerts, Button, Forms } from \"@webpack/common\";\n\nexport function CspErrorCard() {\n    if (IS_WEB) return null;\n\n    const errors = useCspErrors();\n    const forceUpdate = useForceUpdater();\n\n    if (!errors.length) return null;\n\n    const isImgurHtmlDomain = (url: string) => url.startsWith(\"https://imgur.com/\");\n\n    const allowUrl = async (url: string) => {\n        const { origin: baseUrl, host } = new URL(url);\n\n        const result = await VencordNative.csp.requestAddOverride(baseUrl, [\"connect-src\", \"img-src\", \"style-src\", \"font-src\"], \"Vencord Themes\");\n        if (result !== \"ok\") return;\n\n        CspBlockedUrls.forEach(url => {\n            if (new URL(url).host === host) {\n                CspBlockedUrls.delete(url);\n            }\n        });\n\n        forceUpdate();\n\n        Alerts.show({\n            title: \"Restart Required\",\n            body: \"A restart is required to apply this change\",\n            confirmText: \"Restart now\",\n            cancelText: \"Later!\",\n            onConfirm: relaunch\n        });\n    };\n\n    const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);\n\n    return (\n        <ErrorCard className={Margins.bottom16}>\n            <Forms.FormTitle tag=\"h5\">Blocked Resources</Forms.FormTitle>\n            <Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>\n            <Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>\n            <Forms.FormText>\n                After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? \"Discord\" : \"Vesktop\"} to apply the change.\n            </Forms.FormText>\n\n            <Forms.FormTitle tag=\"h5\" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>\n            <div className=\"vc-settings-csp-list\">\n                {errors.map((url, i) => (\n                    <div key={url}>\n                        {i !== 0 && <Divider className={Margins.bottom8} />}\n                        <div className=\"vc-settings-csp-row\">\n                            <Link href={url}>{url}</Link>\n                            <Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>\n                                Allow\n                            </Button>\n                        </div>\n                    </div>\n                ))}\n            </div>\n\n            {hasImgurHtmlDomain && (\n                <>\n                    <Divider className={classes(Margins.top8, Margins.bottom16)} />\n                    <Forms.FormText>\n                        Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>\n                    </Forms.FormText>\n                    <Forms.FormText>To obtain a direct link, right-click the image and select \"Copy image address\".</Forms.FormText>\n                </>\n            )}\n        </ErrorCard>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/themes/LocalThemesTab.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { Settings, useSettings } from \"@api/Settings\";\nimport { Card } from \"@components/Card\";\nimport { Flex } from \"@components/Flex\";\nimport { FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from \"@components/Icons\";\nimport { Link } from \"@components/Link\";\nimport { QuickAction, QuickActionCard } from \"@components/settings/QuickAction\";\nimport { openPluginModal } from \"@components/settings/tabs/plugins/PluginModal\";\nimport { UserThemeHeader } from \"@main/themes\";\nimport ClientThemePlugin from \"@plugins/clientTheme\";\nimport { classNameFactory } from \"@utils/css\";\nimport { findLazy } from \"@webpack\";\nimport { Forms, useEffect, useRef, useState } from \"@webpack/common\";\nimport type { ComponentType, Ref, SyntheticEvent } from \"react\";\n\nimport { ThemeCard } from \"./ThemeCard\";\n\nconst cl = classNameFactory(\"vc-settings-theme-\");\n\ntype FileInput = ComponentType<{\n    ref: Ref<HTMLInputElement>;\n    onChange: (e: SyntheticEvent<HTMLInputElement>) => void;\n    multiple?: boolean;\n    filters?: { name?: string; extensions: string[]; }[];\n}>;\n\nconst FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);\n\n// When a local theme is enabled/disabled, update the settings\nfunction onLocalThemeChange(fileName: string, value: boolean) {\n    if (value) {\n        if (Settings.enabledThemes.includes(fileName)) return;\n        Settings.enabledThemes = [...Settings.enabledThemes, fileName];\n    } else {\n        Settings.enabledThemes = Settings.enabledThemes.filter(f => f !== fileName);\n    }\n}\n\nasync function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {\n    e.stopPropagation();\n    e.preventDefault();\n\n    if (!e.currentTarget?.files?.length) return;\n    const { files } = e.currentTarget;\n\n    const uploads = Array.from(files, file => {\n        const { name } = file;\n        if (!name.endsWith(\".css\")) return;\n\n        return new Promise<void>((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onload = () => {\n                VencordNative.themes.uploadTheme(name, reader.result as string)\n                    .then(resolve)\n                    .catch(reject);\n            };\n            reader.readAsText(file);\n        });\n    });\n\n    await Promise.all(uploads);\n}\n\nexport function LocalThemesTab() {\n    const settings = useSettings([\"enabledThemes\"]);\n\n    const fileInputRef = useRef<HTMLInputElement>(null);\n\n    const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);\n\n    useEffect(() => {\n        refreshLocalThemes();\n    }, []);\n\n    async function refreshLocalThemes() {\n        const themes = await VencordNative.themes.getThemesList();\n        setUserThemes(themes);\n    }\n\n    return (\n        <Flex flexDirection=\"column\" gap=\"1em\">\n            <Card>\n                <Forms.FormTitle tag=\"h5\">Find Themes:</Forms.FormTitle>\n                <div style={{ marginBottom: \".5em\", display: \"flex\", flexDirection: \"column\" }}>\n                    <Link style={{ marginRight: \".5em\" }} href=\"https://betterdiscord.app/themes\">\n                        BetterDiscord Themes\n                    </Link>\n                    <Link href=\"https://github.com/search?q=discord+theme\">GitHub</Link>\n                </div>\n                <Forms.FormText>If using the BD site, click on \"Download\" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>\n            </Card>\n\n            <Card>\n                <Forms.FormTitle tag=\"h5\">External Resources</Forms.FormTitle>\n                <Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>\n                <Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>\n            </Card>\n\n            <section>\n                <Forms.FormTitle tag=\"h5\">Local Themes</Forms.FormTitle>\n                <QuickActionCard>\n                    <>\n                        {IS_WEB ?\n                            (\n                                <QuickAction\n                                    text={\n                                        <span style={{ position: \"relative\" }}>\n                                            Upload Theme\n                                            <FileInput\n                                                ref={fileInputRef}\n                                                onChange={async e => {\n                                                    await onFileUpload(e);\n                                                    refreshLocalThemes();\n                                                }}\n                                                multiple={true}\n                                                filters={[{ extensions: [\"css\"] }]}\n                                            />\n                                        </span>\n                                    }\n                                    Icon={PlusIcon}\n                                />\n                            ) : (\n                                <QuickAction\n                                    text=\"Open Themes Folder\"\n                                    action={() => VencordNative.themes.openFolder()}\n                                    Icon={FolderIcon}\n                                />\n                            )}\n                        <QuickAction\n                            text=\"Load missing Themes\"\n                            action={refreshLocalThemes}\n                            Icon={RestartIcon}\n                        />\n                        <QuickAction\n                            text=\"Edit QuickCSS\"\n                            action={() => VencordNative.quickCss.openEditor()}\n                            Icon={PaintbrushIcon}\n                        />\n\n                        {isPluginEnabled(ClientThemePlugin.name) && (\n                            <QuickAction\n                                text=\"Edit ClientTheme\"\n                                action={() => openPluginModal(ClientThemePlugin)}\n                                Icon={PencilIcon}\n                            />\n                        )}\n                    </>\n                </QuickActionCard>\n\n                <div className={cl(\"grid\")}>\n                    {userThemes?.map(theme => (\n                        <ThemeCard\n                            key={theme.fileName}\n                            enabled={settings.enabledThemes.includes(theme.fileName)}\n                            onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}\n                            onDelete={async () => {\n                                onLocalThemeChange(theme.fileName, false);\n                                await VencordNative.themes.deleteTheme(theme.fileName);\n                                refreshLocalThemes();\n                            }}\n                            theme={theme}\n                        />\n                    ))}\n                </div>\n            </section>\n        </Flex>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/themes/OnlineThemesTab.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { useSettings } from \"@api/Settings\";\nimport { Card } from \"@components/Card\";\nimport { Flex } from \"@components/Flex\";\nimport { Forms, TextArea, useState } from \"@webpack/common\";\n\nexport function OnlineThemesTab() {\n    const settings = useSettings([\"themeLinks\"]);\n\n    const [themeText, setThemeText] = useState(settings.themeLinks.join(\"\\n\"));\n\n    // When the user leaves the online theme textbox, update the settings\n    function onBlur() {\n        settings.themeLinks = [...new Set(\n            themeText\n                .trim()\n                .split(/\\n+/)\n                .map(s => s.trim())\n                .filter(Boolean)\n        )];\n    }\n\n    return (\n        <Flex flexDirection=\"column\" gap=\"1em\">\n            <Card variant=\"warning\" defaultPadding>\n                <Forms.FormText size=\"md\">\n                    This section is for advanced users. If you are having difficulties using it, use the\n                    Local Themes tab instead.\n                </Forms.FormText>\n            </Card>\n            <Card>\n                <Forms.FormTitle tag=\"h5\">Paste links to css files here</Forms.FormTitle>\n                <Forms.FormText>One link per line</Forms.FormText>\n                <Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>\n                <Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>\n            </Card>\n\n            <section>\n                <Forms.FormTitle tag=\"h5\">Online Themes</Forms.FormTitle>\n                <TextArea\n                    value={themeText}\n                    onChange={setThemeText}\n                    className={\"vc-settings-theme-links\"}\n                    placeholder=\"Enter Theme Links...\"\n                    spellCheck={false}\n                    onBlur={onBlur}\n                    rows={10}\n                />\n            </section>\n        </Flex>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/themes/ThemeCard.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Flex } from \"@components/Flex\";\nimport { DeleteIcon } from \"@components/Icons\";\nimport { Link } from \"@components/Link\";\nimport { AddonCard } from \"@components/settings/AddonCard\";\nimport { UserThemeHeader } from \"@main/themes\";\nimport { openInviteModal } from \"@utils/discord\";\nimport { showToast } from \"@webpack/common\";\n\ninterface ThemeCardProps {\n    theme: UserThemeHeader;\n    enabled: boolean;\n    onChange: (enabled: boolean) => void;\n    onDelete: () => void;\n}\n\nexport function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {\n    return (\n        <AddonCard\n            name={theme.name}\n            description={theme.description}\n            author={theme.author}\n            enabled={enabled}\n            setEnabled={onChange}\n            infoButton={\n                IS_WEB && (\n                    <div style={{ cursor: \"pointer\", color: \"var(--status-danger\" }} onClick={onDelete}>\n                        <DeleteIcon />\n                    </div>\n                )\n            }\n            footer={\n                <Flex flexDirection=\"row\" gap=\"0.2em\">\n                    {!!theme.website && <Link href={theme.website}>Website</Link>}\n                    {!!(theme.website && theme.invite) && \" • \"}\n                    {!!theme.invite && (\n                        <Link\n                            href={`https://discord.gg/${theme.invite}`}\n                            onClick={async e => {\n                                e.preventDefault();\n                                theme.invite != null && openInviteModal(theme.invite).catch(() => showToast(\"Invalid or expired invite\"));\n                            }}\n                        >\n                            Discord Server\n                        </Link>\n                    )}\n                </Flex>\n            }\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/themes/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { Card } from \"@components/Card\";\nimport { Link } from \"@components/Link\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { getStylusWebStoreUrl } from \"@utils/web\";\nimport { Forms, React, TabBar, useState } from \"@webpack/common\";\n\nimport { CspErrorCard } from \"./CspErrorCard\";\nimport { LocalThemesTab } from \"./LocalThemesTab\";\nimport { OnlineThemesTab } from \"./OnlineThemesTab\";\n\nconst enum ThemeTab {\n    LOCAL,\n    ONLINE\n}\n\nfunction ThemesTab() {\n    const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);\n\n    return (\n        <SettingsTab>\n            <TabBar\n                type=\"top\"\n                look=\"brand\"\n                className=\"vc-settings-tab-bar\"\n                selectedItem={currentTab}\n                onItemSelect={setCurrentTab}\n            >\n                <TabBar.Item\n                    className=\"vc-settings-tab-bar-item\"\n                    id={ThemeTab.LOCAL}\n                >\n                    Local Themes\n                </TabBar.Item>\n                <TabBar.Item\n                    className=\"vc-settings-tab-bar-item\"\n                    id={ThemeTab.ONLINE}\n                >\n                    Online Themes\n                </TabBar.Item>\n            </TabBar>\n\n            <CspErrorCard />\n\n            {currentTab === ThemeTab.LOCAL && <LocalThemesTab />}\n            {currentTab === ThemeTab.ONLINE && <OnlineThemesTab />}\n        </SettingsTab>\n    );\n}\n\nfunction UserscriptThemesTab() {\n    return (\n        <SettingsTab>\n            <Card variant=\"danger\">\n                <Forms.FormTitle tag=\"h5\">Themes are not supported on the Userscript!</Forms.FormTitle>\n\n                <Forms.FormText>\n                    You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!\n                </Forms.FormText>\n            </Card>\n        </SettingsTab>\n    );\n}\n\nexport default IS_USERSCRIPT\n    ? wrapTab(UserscriptThemesTab, \"Themes\")\n    : wrapTab(ThemesTab, \"Themes\");\n"
  },
  {
    "path": "src/components/settings/tabs/themes/styles.css",
    "content": ".vc-settings-theme-grid {\n    display: grid;\n    gap: 16px;\n    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n}\n\n.vc-settings-csp-list {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n}\n\n.vc-settings-csp-row {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    gap: 8px;\n\n\n    & a {\n        text-overflow: ellipsis;\n        overflow: hidden;\n        white-space: nowrap;\n        line-height: 1.2em;\n    }\n\n    --custom-button-button-md-height: 26px;\n}"
  },
  {
    "path": "src/components/settings/tabs/updater/Components.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Card } from \"@components/Card\";\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Flex } from \"@components/Flex\";\nimport { Link } from \"@components/Link\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { relaunch } from \"@utils/native\";\nimport { changes, checkForUpdates, update, updateError } from \"@utils/updater\";\nimport { Alerts, Button, Forms, React, Toasts, useState } from \"@webpack/common\";\n\nimport { runWithDispatch } from \"./runWithDispatch\";\n\nexport interface CommonProps {\n    repo: string;\n    repoPending: boolean;\n}\n\nexport function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {\n    return (\n        <Link href={`${repo}/commit/${hash}`} disabled={disabled}>\n            {hash}\n        </Link>\n    );\n}\n\nexport function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {\n    return (\n        <Card style={{ padding: \"0 0.5em\" }} defaultPadding={false}>\n            {updates.map(({ hash, author, message }) => (\n                <div\n                    key={hash}\n                    style={{\n                        marginTop: \"0.5em\",\n                        marginBottom: \"0.5em\"\n                    }}\n                >\n                    <code>\n                        <HashLink {...{ repo, hash }} disabled={repoPending} />\n                    </code>\n\n                    <span style={{\n                        marginLeft: \"0.5em\",\n                        color: \"var(--text-default)\"\n                    }}>\n                        {message} - {author}\n                    </span>\n                </div>\n            ))}\n        </Card>\n    );\n}\n\nexport function Newer(props: CommonProps) {\n    return (\n        <>\n            <Forms.FormText className={Margins.bottom8}>\n                Your local copy has more recent commits. Please stash or reset them.\n            </Forms.FormText>\n            <Changes {...props} updates={changes} />\n        </>\n    );\n}\n\nexport function Updatable(props: CommonProps) {\n    const [updates, setUpdates] = useState(changes);\n    const [isChecking, setIsChecking] = useState(false);\n    const [isUpdating, setIsUpdating] = useState(false);\n\n    const isOutdated = (updates?.length ?? 0) > 0;\n\n    return (\n        <>\n            {!updates && updateError ? (\n                <>\n                    <Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>\n                    <ErrorCard style={{ padding: \"1em\" }}>\n                        <p>{updateError.stderr || updateError.stdout || \"An unknown error occurred\"}</p>\n                    </ErrorCard>\n                </>\n            ) : (\n                <Forms.FormText className={Margins.bottom8}>\n                    {isOutdated ? (updates.length === 1 ? \"There is 1 Update\" : `There are ${updates.length} Updates`) : \"Up to Date!\"}\n                </Forms.FormText>\n            )}\n\n            {isOutdated && <Changes updates={updates} {...props} />}\n\n            <Flex className={classes(Margins.bottom8, Margins.top8)}>\n                {isOutdated && (\n                    <Button\n                        disabled={isUpdating || isChecking}\n                        onClick={runWithDispatch(setIsUpdating, async () => {\n                            if (await update()) {\n                                setUpdates([]);\n\n                                await new Promise<void>(r => {\n                                    Alerts.show({\n                                        title: \"Update Success!\",\n                                        body: \"Successfully updated. Restart now to apply the changes?\",\n                                        confirmText: \"Restart\",\n                                        cancelText: \"Not now!\",\n                                        onConfirm() {\n                                            relaunch();\n                                            r();\n                                        },\n                                        onCancel: r\n                                    });\n                                });\n                            }\n                        })}\n                    >\n                        Update Now\n                    </Button>\n                )}\n                <Button\n                    disabled={isUpdating || isChecking}\n                    onClick={runWithDispatch(setIsChecking, async () => {\n                        const outdated = await checkForUpdates();\n\n                        if (outdated) {\n                            setUpdates(changes);\n                        } else {\n                            setUpdates([]);\n\n                            Toasts.show({\n                                message: \"No updates found!\",\n                                id: Toasts.genId(),\n                                type: Toasts.Type.MESSAGE,\n                                options: {\n                                    position: Toasts.Position.BOTTOM\n                                }\n                            });\n                        }\n                    })}\n                >\n                    Check for Updates\n                </Button>\n            </Flex>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/updater/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { useSettings } from \"@api/Settings\";\nimport { Button } from \"@components/Button\";\nimport { Card } from \"@components/Card\";\nimport { Divider } from \"@components/Divider\";\nimport { Flex } from \"@components/Flex\";\nimport { FormSwitch } from \"@components/FormSwitch\";\nimport { HeadingSecondary } from \"@components/Heading\";\nimport { Link } from \"@components/Link\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { useAwaiter } from \"@utils/react\";\nimport { getRepo, isNewer, UpdateLogger } from \"@utils/updater\";\nimport { Forms, React } from \"@webpack/common\";\n\nimport gitHash from \"~git-hash\";\n\nimport { CommonProps, HashLink, Newer, Updatable } from \"./Components\";\n\nfunction VesktopSection() {\n    if (!IS_VESKTOP) return null;\n\n    const [isVesktopOutdated] = useAwaiter<boolean>(VesktopNative.app.isOutdated, { fallbackValue: false });\n\n    return (\n        <Flex className={Margins.bottom20} flexDirection=\"column\" gap=\"1em\">\n            <Card variant=\"info\">\n                <HeadingSecondary>Vesktop & Vencord</HeadingSecondary>\n                <Paragraph>Vesktop and Vencord are two separate things. This updater is for Vencord.</Paragraph>\n                <Paragraph className={Margins.top8}>\n                    You receive separate popups for Vesktop updates. You can also manually update by installing the <Link href=\"https://vesktop.dev/install\">latest version</Link>.\n                </Paragraph>\n            </Card>\n\n            {isVesktopOutdated && (\n                <Card variant=\"warning\">\n                    <HeadingSecondary>Vesktop Outdated</HeadingSecondary>\n                    <Flex flexDirection=\"column\" gap=\"0.5em\">\n                        <Paragraph>Your version of Vesktop is outdated!</Paragraph>\n                        <Button variant=\"link\" onClick={() => VesktopNative.app.openUpdater()}>Open Vesktop Updater</Button>\n                    </Flex>\n                </Card>\n            )}\n        </Flex>\n    );\n}\n\nfunction Updater() {\n    const settings = useSettings([\"autoUpdate\", \"autoUpdateNotification\"]);\n\n    const [repo, err, repoPending] = useAwaiter(getRepo, {\n        fallbackValue: \"Loading...\",\n        onError: e => UpdateLogger.error(\"Failed to retrieve repo\", err)\n    });\n\n    const commonProps: CommonProps = {\n        repo,\n        repoPending\n    };\n\n    return (\n        <SettingsTab>\n            <VesktopSection />\n\n            <FormSwitch\n                title=\"Automatically update\"\n                description=\"Automatically update Vencord without confirmation prompt\"\n                value={settings.autoUpdate}\n                onChange={(v: boolean) => settings.autoUpdate = v}\n            />\n            <FormSwitch\n                title=\"Get notified when an automatic update completes\"\n                description=\"Show a notification when Vencord automatically updates\"\n                value={settings.autoUpdateNotification}\n                onChange={(v: boolean) => settings.autoUpdateNotification = v}\n                disabled={!settings.autoUpdate}\n            />\n\n            <Forms.FormTitle tag=\"h5\" className={Margins.top20}>Repo</Forms.FormTitle>\n\n            <Forms.FormText>\n                {repoPending\n                    ? repo\n                    : err\n                        ? \"Failed to retrieve - check console\"\n                        : (\n                            <Link href={repo}>\n                                {repo.split(\"/\").slice(-2).join(\"/\")}\n                            </Link>\n                        )\n                }\n                {\" \"}\n                (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)\n            </Forms.FormText>\n\n            <Divider className={classes(Margins.top16, Margins.bottom16)} />\n\n            <Forms.FormTitle tag=\"h5\">Updates</Forms.FormTitle>\n\n            {isNewer\n                ? <Newer {...commonProps} />\n                : <Updatable {...commonProps} />\n            }\n        </SettingsTab>\n    );\n}\n\nexport default IS_UPDATER_DISABLED\n    ? null\n    : wrapTab(Updater, \"Updater\");\n"
  },
  {
    "path": "src/components/settings/tabs/updater/runWithDispatch.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { UpdateLogger } from \"@utils/updater\";\nimport { Alerts, Parser } from \"@webpack/common\";\n\nfunction getErrorMessage(e: any) {\n    if (!e?.code || !e.cmd)\n        return \"An unknown error occurred.\\nPlease try again or see the console for more info.\";\n\n    const { code, path, cmd, stderr } = e;\n\n    if (code === \"ENOENT\")\n        return `Command \\`${path}\\` not found.\\nPlease install it and try again.`;\n\n    const extra = stderr || `Code \\`${code}\\`. See the console for more info.`;\n\n    return `An error occurred while running \\`${cmd}\\`:\\n${extra}`;\n}\n\nexport function runWithDispatch(dispatch: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {\n    return async () => {\n        dispatch(true);\n\n        try {\n            await action();\n        } catch (e: any) {\n            UpdateLogger.error(e);\n\n            const err = getErrorMessage(e);\n\n            Alerts.show({\n                title: \"Oops!\",\n                body: (\n                    <ErrorCard>\n                        {err.split(\"\\n\").map((line, idx) =>\n                            <div key={idx}>{Parser.parse(line)}</div>\n                        )}\n                    </ErrorCard>\n                )\n            });\n        } finally {\n            dispatch(false);\n        }\n    };\n}\n"
  },
  {
    "path": "src/components/settings/tabs/vencord/DonateButton.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport DonateButton from \"@components/settings/DonateButton\";\nimport BadgeAPI from \"@plugins/_api/badges\";\nimport { DONOR_ROLE_ID, VENCORD_GUILD_ID } from \"@utils/constants\";\nimport { Button, GuildMemberStore } from \"@webpack/common\";\n\nexport const isDonor = (userId: string) => !!(\n    BadgeAPI.getDonorBadges(userId)?.length > 0\n    || GuildMemberStore?.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID)\n);\n\nexport function DonateButtonComponent() {\n    return (\n        <DonateButton\n            look={Button.Looks.FILLED}\n            color={Button.Colors.WHITE}\n            style={{ marginTop: \"1em\" }}\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/vencord/MacVibrancySettings.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { useSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Margins } from \"@utils/margins\";\nimport { identity } from \"@utils/misc\";\nimport { Forms, Select } from \"@webpack/common\";\n\nexport function VibrancySettings() {\n    const settings = useSettings([\"macosVibrancyStyle\"]);\n\n    return (\n        <>\n            <Forms.FormTitle tag=\"h5\">Window vibrancy style (requires restart)</Forms.FormTitle>\n            <ErrorBoundary noop>\n                <Select\n                    className={Margins.bottom20}\n                    placeholder=\"Window vibrancy style\"\n                    options={[\n                        // Sorted from most opaque to most transparent\n                        {\n                            label: \"No vibrancy\", value: undefined\n                        },\n                        {\n                            label: \"Under Page (window tinting)\",\n                            value: \"under-page\"\n                        },\n                        {\n                            label: \"Content\",\n                            value: \"content\"\n                        },\n                        {\n                            label: \"Window\",\n                            value: \"window\"\n                        },\n                        {\n                            label: \"Selection\",\n                            value: \"selection\"\n                        },\n                        {\n                            label: \"Titlebar\",\n                            value: \"titlebar\"\n                        },\n                        {\n                            label: \"Header\",\n                            value: \"header\"\n                        },\n                        {\n                            label: \"Sidebar\",\n                            value: \"sidebar\"\n                        },\n                        {\n                            label: \"Tooltip\",\n                            value: \"tooltip\"\n                        },\n                        {\n                            label: \"Menu\",\n                            value: \"menu\"\n                        },\n                        {\n                            label: \"Popover\",\n                            value: \"popover\"\n                        },\n                        {\n                            label: \"Fullscreen UI (transparent but slightly muted)\",\n                            value: \"fullscreen-ui\"\n                        },\n                        {\n                            label: \"HUD (Most transparent)\",\n                            value: \"hud\"\n                        },\n                    ]}\n                    select={v => settings.macosVibrancyStyle = v}\n                    isSelected={v => settings.macosVibrancyStyle === v}\n                    serialize={identity}\n                />\n            </ErrorBoundary>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/vencord/NotificationSettings.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { openNotificationLogModal } from \"@api/Notifications/notificationLog\";\nimport { useSettings } from \"@api/Settings\";\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Flex } from \"@components/Flex\";\nimport { Margins } from \"@utils/margins\";\nimport { identity } from \"@utils/misc\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { Button, Forms, Select, Slider, Text } from \"@webpack/common\";\n\nexport function NotificationSection() {\n    return (\n        <section className={Margins.top16}>\n            <Forms.FormTitle tag=\"h5\">Notifications</Forms.FormTitle>\n            <Forms.FormText className={Margins.bottom8}>\n                Settings for Notifications sent by Vencord.\n                This does NOT include Discord notifications (messages, etc)\n            </Forms.FormText>\n            <Flex>\n                <Button onClick={openNotificationSettingsModal}>\n                    Notification Settings\n                </Button>\n                <Button onClick={openNotificationLogModal}>\n                    View Notification Log\n                </Button>\n            </Flex>\n        </section>\n    );\n}\n\nexport function openNotificationSettingsModal() {\n    openModal(props => (\n        <ModalRoot {...props} size={ModalSize.MEDIUM}>\n            <ModalHeader>\n                <Text variant=\"heading-lg/semibold\" style={{ flexGrow: 1 }}>Notification Settings</Text>\n                <ModalCloseButton onClick={props.onClose} />\n            </ModalHeader>\n\n            <ModalContent>\n                <NotificationSettings />\n            </ModalContent>\n        </ModalRoot>\n    ));\n}\n\nfunction NotificationSettings() {\n    const settings = useSettings([\"notifications.*\"]).notifications;\n\n    return (\n        <div style={{ padding: \"1em 0\" }}>\n            <Forms.FormTitle tag=\"h5\">Notification Style</Forms.FormTitle>\n            {settings.useNative !== \"never\" && Notification?.permission === \"denied\" && (\n                <ErrorCard style={{ padding: \"1em\" }} className={Margins.bottom8}>\n                    <Forms.FormTitle tag=\"h5\">Desktop Notification Permission denied</Forms.FormTitle>\n                    <Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>\n                </ErrorCard>\n            )}\n            <Forms.FormText className={Margins.bottom8}>\n                Some plugins may show you notifications. These come in two styles:\n                <ul>\n                    <li><strong>Vencord Notifications</strong>: These are in-app notifications</li>\n                    <li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>\n                </ul>\n            </Forms.FormText>\n            <Select\n                placeholder=\"Notification Style\"\n                options={[\n                    { label: \"Only use Desktop notifications when Discord is not focused\", value: \"not-focused\", default: true },\n                    { label: \"Always use Desktop notifications\", value: \"always\" },\n                    { label: \"Always use Vencord notifications\", value: \"never\" },\n                ] satisfies Array<{ value: typeof settings[\"useNative\"]; } & Record<string, any>>}\n                closeOnSelect={true}\n                select={v => settings.useNative = v}\n                isSelected={v => v === settings.useNative}\n                serialize={identity}\n            />\n\n            <Forms.FormTitle tag=\"h5\" className={Margins.top16 + \" \" + Margins.bottom8}>Notification Position</Forms.FormTitle>\n            <Select\n                isDisabled={settings.useNative === \"always\"}\n                placeholder=\"Notification Position\"\n                options={[\n                    { label: \"Bottom Right\", value: \"bottom-right\", default: true },\n                    { label: \"Top Right\", value: \"top-right\" },\n                ] satisfies Array<{ value: typeof settings[\"position\"]; } & Record<string, any>>}\n                select={v => settings.position = v}\n                isSelected={v => v === settings.position}\n                serialize={identity}\n            />\n\n            <Forms.FormTitle tag=\"h5\" className={Margins.top16 + \" \" + Margins.bottom8}>Notification Timeout</Forms.FormTitle>\n            <Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>\n            <Slider\n                disabled={settings.useNative === \"always\"}\n                markers={[0, 1000, 2500, 5000, 10_000, 20_000]}\n                minValue={0}\n                maxValue={20_000}\n                initialValue={settings.timeout}\n                onValueChange={v => settings.timeout = v}\n                onValueRender={v => (v / 1000).toFixed(2) + \"s\"}\n                onMarkerRender={v => (v / 1000) + \"s\"}\n                stickToMarkers={false}\n            />\n\n            <Forms.FormTitle tag=\"h5\" className={Margins.top16 + \" \" + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>\n            <Forms.FormText className={Margins.bottom16}>\n                The amount of notifications to save in the log until old ones are removed.\n                Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications\n            </Forms.FormText>\n            <Slider\n                markers={[0, 25, 50, 75, 100, 200]}\n                minValue={0}\n                maxValue={200}\n                stickToMarkers={true}\n                initialValue={settings.logLimit}\n                onValueChange={v => settings.logLimit = v}\n                onValueRender={v => v === 200 ? \"∞\" : v}\n                onMarkerRender={v => v === 200 ? \"∞\" : v}\n            />\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/settings/tabs/vencord/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { openNotificationLogModal } from \"@api/Notifications/notificationLog\";\nimport { useSettings } from \"@api/Settings\";\nimport { Divider } from \"@components/Divider\";\nimport { FormSwitch } from \"@components/FormSwitch\";\nimport { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from \"@components/Icons\";\nimport { QuickAction, QuickActionCard } from \"@components/settings/QuickAction\";\nimport { SpecialCard } from \"@components/settings/SpecialCard\";\nimport { SettingsTab, wrapTab } from \"@components/settings/tabs/BaseTab\";\nimport { openContributorModal } from \"@components/settings/tabs/plugins/ContributorModal\";\nimport { openPluginModal } from \"@components/settings/tabs/plugins/PluginModal\";\nimport { gitRemote } from \"@shared/vencordUserAgent\";\nimport { IS_MAC, IS_WINDOWS } from \"@utils/constants\";\nimport { Margins } from \"@utils/margins\";\nimport { isPluginDev } from \"@utils/misc\";\nimport { relaunch } from \"@utils/native\";\nimport { Alerts, Forms, React, useMemo, UserStore } from \"@webpack/common\";\n\nimport { DonateButtonComponent, isDonor } from \"./DonateButton\";\nimport { VibrancySettings } from \"./MacVibrancySettings\";\nimport { NotificationSection } from \"./NotificationSettings\";\n\nconst DEFAULT_DONATE_IMAGE = \"https://cdn.discordapp.com/emojis/1026533090627174460.png\";\nconst SHIGGY_DONATE_IMAGE = \"https://media.discordapp.net/stickers/1039992459209490513.png\";\nconst VENNIE_DONATOR_IMAGE = \"https://cdn.discordapp.com/emojis/1238120638020063377.png\";\nconst COZY_CONTRIB_IMAGE = \"https://cdn.discordapp.com/emojis/1026533070955872337.png\";\nconst DONOR_BACKGROUND_IMAGE = \"https://media.discordapp.net/stickers/1311070116305436712.png?size=2048\";\nconst CONTRIB_BACKGROUND_IMAGE = \"https://media.discordapp.net/stickers/1311070166481895484.png?size=2048\";\n\ntype KeysOfType<Object, Type> = {\n    [K in keyof Object]: Object[K] extends Type ? K : never;\n}[keyof Object];\n\nfunction Switches() {\n    const settings = useSettings([\"useQuickCss\", \"enableReactDevtools\", \"frameless\", \"winNativeTitleBar\", \"transparent\", \"winCtrlQ\", \"disableMinSize\"]);\n\n    const Switches = [\n        {\n            key: \"useQuickCss\",\n            title: \"Enable Custom CSS\",\n        },\n        !IS_WEB && {\n            key: \"enableReactDevtools\",\n            title: \"Enable React Developer Tools\",\n            restartRequired: true\n        },\n        !IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS ? {\n            key: \"frameless\",\n            title: \"Disable the window frame\",\n            restartRequired: true\n        } : {\n            key: \"winNativeTitleBar\",\n            title: \"Use Windows' native title bar instead of Discord's custom one\",\n            restartRequired: true\n        }),\n        !IS_WEB && {\n            key: \"transparent\",\n            title: \"Enable window transparency\",\n            description: \"A theme that supports transparency is required or this will do nothing. Stops the window from being resizable as a side effect\",\n            restartRequired: true\n        },\n        IS_DISCORD_DESKTOP && {\n            key: \"disableMinSize\",\n            title: \"Disable minimum window size\",\n            restartRequired: true\n        },\n        !IS_WEB && IS_WINDOWS && {\n            key: \"winCtrlQ\",\n            title: \"Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)\",\n            restartRequired: true\n        },\n    ] satisfies Array<false | {\n        key: KeysOfType<typeof settings, boolean>;\n        title: string;\n        description?: string;\n        restartRequired?: boolean;\n    }>;\n\n    return Switches.map(setting => {\n        if (!setting) {\n            return null;\n        }\n\n        const { key, title, description, restartRequired } = setting;\n\n        return (\n            <FormSwitch\n                key={key}\n                title={title}\n                description={description}\n                value={settings[key]}\n                onChange={v => {\n                    settings[key] = v;\n\n                    if (restartRequired) {\n                        Alerts.show({\n                            title: \"Restart Required\",\n                            body: \"A restart is required to apply this change\",\n                            confirmText: \"Restart now\",\n                            cancelText: \"Later!\",\n                            onConfirm: relaunch\n                        });\n                    }\n                }}\n            />\n        );\n    });\n}\n\nfunction VencordSettings() {\n    const donateImage = useMemo(() =>\n        Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE,\n        []\n    );\n\n    const needsVibrancySettings = IS_DISCORD_DESKTOP && IS_MAC;\n\n    const user = UserStore?.getCurrentUser();\n\n    return (\n        <SettingsTab>\n            {isDonor(user?.id)\n                ? (\n                    <SpecialCard\n                        title=\"Donations\"\n                        subtitle=\"Thank you for donating!\"\n                        description=\"You can manage your perks at any time by messaging @vending.machine.\"\n                        cardImage={VENNIE_DONATOR_IMAGE}\n                        backgroundImage={DONOR_BACKGROUND_IMAGE}\n                        backgroundColor=\"#ED87A9\"\n                    >\n                        <DonateButtonComponent />\n                    </SpecialCard>\n                )\n                : (\n                    <SpecialCard\n                        title=\"Support the Project\"\n                        description=\"Please consider supporting the development of Vencord by donating!\"\n                        cardImage={donateImage}\n                        backgroundImage={DONOR_BACKGROUND_IMAGE}\n                        backgroundColor=\"#c3a3ce\"\n                    >\n                        <DonateButtonComponent />\n                    </SpecialCard>\n                )\n            }\n\n            {isPluginDev(user?.id) && (\n                <SpecialCard\n                    title=\"Contributions\"\n                    subtitle=\"Thank you for contributing!\"\n                    description=\"Since you've contributed to Vencord you now have a cool new badge!\"\n                    cardImage={COZY_CONTRIB_IMAGE}\n                    backgroundImage={CONTRIB_BACKGROUND_IMAGE}\n                    backgroundColor=\"#EDCC87\"\n                    buttonTitle=\"See what you've contributed to\"\n                    buttonOnClick={() => openContributorModal(user)}\n                />\n            )}\n\n            <section>\n                <Forms.FormTitle tag=\"h5\">Quick Actions</Forms.FormTitle>\n\n                <QuickActionCard>\n                    <QuickAction\n                        Icon={LogIcon}\n                        text=\"Notification Log\"\n                        action={openNotificationLogModal}\n                    />\n                    <QuickAction\n                        Icon={PaintbrushIcon}\n                        text=\"Edit QuickCSS\"\n                        action={() => VencordNative.quickCss.openEditor()}\n                    />\n                    {!IS_WEB && (\n                        <>\n                            <QuickAction\n                                Icon={RestartIcon}\n                                text=\"Relaunch Discord\"\n                                action={relaunch}\n                            />\n                            <QuickAction\n                                Icon={FolderIcon}\n                                text=\"Open Settings Folder\"\n                                action={() => VencordNative.settings.openFolder()}\n                            />\n                        </>\n                    )}\n                    <QuickAction\n                        Icon={GithubIcon}\n                        text=\"View Source Code\"\n                        action={() => VencordNative.native.openExternal(\"https://github.com/\" + gitRemote)}\n                    />\n                </QuickActionCard>\n            </section>\n\n            <Divider />\n\n            <section className={Margins.top16}>\n                <Forms.FormTitle tag=\"h5\">Settings</Forms.FormTitle>\n                <Forms.FormText className={Margins.bottom20} style={{ color: \"var(--text-muted)\" }}>\n                    Hint: You can change the position of this settings section in the{\" \"}\n                    <a onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}>\n                        settings of the Settings plugin\n                    </a>!\n                </Forms.FormText>\n\n                <Switches />\n            </section>\n\n\n            {needsVibrancySettings && <VibrancySettings />}\n\n            <NotificationSection />\n        </SettingsTab>\n    );\n}\n\nexport default wrapTab(VencordSettings, \"Vencord Settings\");\n"
  },
  {
    "path": "src/debug/Tracer.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Logger } from \"@utils/Logger\";\n\nif (IS_DEV || IS_REPORTER) {\n    var traces = {} as Record<string, [number, any[]]>;\n    var logger = new Logger(\"Tracer\", \"#FFD166\");\n}\n\nexport const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :\n    function beginTrace(name: string, ...args: any[]) {\n        if (name in traces) {\n            throw new Error(`Trace ${name} already exists!`);\n        }\n\n        traces[name] = [performance.now(), args];\n    };\n\nexport const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :\n    function finishTrace(name: string) {\n        const end = performance.now();\n\n        const [start, args] = traces[name];\n        delete traces[name];\n\n        const totalTime = end - start;\n        logger.debug(`${name} took ${totalTime}ms`, args);\n\n        return totalTime;\n    };\n\ntype Func = (...args: any[]) => any;\ntype TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;\n\nfunction noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {\n    return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {\n        return [f.apply(this, args), 0];\n    };\n}\n\nfunction noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {\n    return f;\n}\n\nexport const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)\n    ? noopTracerWithResults\n    : function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {\n        return function (this: unknown, ...args: Parameters<F>) {\n            const traceName = mapper?.(...args) ?? name;\n\n            beginTrace(traceName, ...arguments);\n            try {\n                return [f.apply(this, args), finishTrace(traceName)];\n            } catch (e) {\n                finishTrace(traceName);\n                throw e;\n            }\n        };\n    };\n\nexport const traceFunction = !(IS_DEV || IS_REPORTER)\n    ? noopTracer\n    : function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {\n        return function (this: unknown, ...args: Parameters<F>) {\n            const traceName = mapper?.(...args) ?? name;\n\n            beginTrace(traceName, ...arguments);\n            try {\n                return f.apply(this, args);\n            } finally {\n                finishTrace(traceName);\n            }\n        } as F;\n    };\n"
  },
  {
    "path": "src/debug/loadLazyChunks.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Logger } from \"@utils/Logger\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport { ModuleFactory } from \"@vencord/discord-types/webpack\";\nimport * as Webpack from \"@webpack\";\nimport { wreq } from \"@webpack\";\nimport pLimit from \"p-limit\";\nimport { AnyModuleFactory } from \"webpack\";\n\nfunction getWebpackChunkMap() {\n    const sym = Symbol();\n    let v: Record<PropertyKey, string> | null = null;\n\n    Object.defineProperty(Object.prototype, sym, {\n        get() {\n            v = this;\n            return \"\";\n        },\n        configurable: true\n    });\n\n    wreq.u(sym);\n    delete Object.prototype[sym];\n\n    return v;\n}\n\nexport async function loadLazyChunks() {\n    const LazyChunkLoaderLogger = new Logger(\"LazyChunkLoader\");\n    const queue = pLimit(50);\n\n    try {\n        LazyChunkLoaderLogger.log(\"Loading all chunks...\");\n\n        const validChunks = new Set<PropertyKey>();\n        const invalidChunks = new Set<PropertyKey>();\n        const deferredRequires = new Set<PropertyKey>();\n\n        const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();\n\n        // True if resolved, false otherwise\n        const chunksSearchPromises = [] as Array<() => boolean>;\n\n        // This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those\n        const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\\.all\\(\\[)?((?:\\i\\.e\\(\"?[^)]+?\"?\\),?)+?)(?:\\]\\))?)\\.then\\(\\i(?:\\.\\i)?\\.bind\\(\\i,\"?([^)]+?)\"?(?:,[^)]+?)?\\)\\)/g);\n        const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\\.all\\(\\[)?((?:\\i\\.e\\(\"?[^)]+?\"?\\),?)+?)(?:\\]\\))?)\\.then\\(\\i\\.bind\\(\\i,\"?([^)]+?)\"?\\)\\)/g);\n\n        let foundCssDebuggingLoad = false;\n\n        async function searchAndLoadLazyChunks(factoryCode: string) {\n            // Workaround to avoid loading the CSS debugging chunk which turns the app pink\n            const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(\".cssDebuggingEnabled&&\"));\n\n            const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex);\n            const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();\n\n            const shouldForceDefer = false;\n\n            await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {\n                const chunkIds = rawChunkIds\n                    ?.matchAll(Webpack.ChunkIdsRegex)\n                    .map(m => {\n                        const numChunkId = Number(m[1]);\n                        return Number.isNaN(numChunkId) ? m[1] : numChunkId;\n                    })\n                    .toArray()\n                    ?? [];\n\n                if (chunkIds.length === 0) {\n                    return;\n                }\n\n                let invalidChunkGroup = false;\n\n                for (const id of chunkIds) {\n                    if (hasCssDebuggingLoad) {\n                        if (chunkIds.length > 1) {\n                            throw new Error(\"Found multiple chunks in factory that loads the CSS debugging chunk\");\n                        }\n\n                        invalidChunks.add(id);\n                        invalidChunkGroup = true;\n                        break;\n                    }\n\n                    if (wreq.u(id) == null || wreq.u(id) === \"undefined.js\") continue;\n\n                    const isWorkerAsset = await queue(() =>\n                        fetch(wreq.p + wreq.u(id))\n                            .then(r => r.text())\n                            .then(t => /importScripts\\(|self\\.postMessage/.test(t))\n                    );\n\n                    if (isWorkerAsset) {\n                        invalidChunks.add(id);\n                        invalidChunkGroup = true;\n                        continue;\n                    }\n\n                    validChunks.add(id);\n                }\n\n                if (!invalidChunkGroup) {\n                    const numEntryPoint = Number(entryPoint);\n                    validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);\n                }\n            }));\n\n            // Loads all found valid chunk groups\n            await Promise.all(\n                Array.from(validChunkGroups)\n                    .map(([chunkIds]) =>\n                        Promise.all(chunkIds.map(id => wreq.e(id)))\n                    )\n            );\n\n            // Requires the entry points for all valid chunk groups\n            for (const [, entryPoint] of validChunkGroups) {\n                try {\n                    if (shouldForceDefer) {\n                        deferredRequires.add(entryPoint);\n                        continue;\n                    }\n\n                    if (wreq.m[entryPoint]) wreq(entryPoint);\n                } catch (err) {\n                    console.error(err);\n                }\n            }\n\n            // setImmediate to only check if all chunks were loaded after this function resolves\n            // We check if all chunks were loaded every time a factory is loaded\n            // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved\n            // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them\n            setTimeout(() => {\n                let allResolved = true;\n\n                for (let i = 0; i < chunksSearchPromises.length; i++) {\n                    const isResolved = chunksSearchPromises[i]();\n\n                    if (isResolved) {\n                        // Remove finished promises to avoid having to iterate through a huge array everytime\n                        chunksSearchPromises.splice(i--, 1);\n                    } else {\n                        allResolved = false;\n                    }\n                }\n\n                if (allResolved) chunksSearchingResolve();\n            }, 0);\n        }\n\n        function factoryListener(factory: AnyModuleFactory | ModuleFactory) {\n            let isResolved = false;\n            searchAndLoadLazyChunks(String(factory))\n                .then(() => isResolved = true)\n                .catch(() => isResolved = true);\n\n            chunksSearchPromises.push(() => isResolved);\n        }\n\n        Webpack.factoryListeners.add(factoryListener);\n        for (const moduleId in wreq.m) {\n            factoryListener(wreq.m[moduleId]);\n        }\n\n        await chunksSearchingDone;\n        Webpack.factoryListeners.delete(factoryListener);\n\n        // Require deferred entry points\n        for (const deferredRequire of deferredRequires) {\n            wreq(deferredRequire);\n        }\n\n        // All chunks Discord has mapped to asset files, even if they are not used anymore\n        const chunkMap = getWebpackChunkMap();\n        if (!chunkMap) throw new Error(\"Failed to get chunk map\");\n\n        const allChunks = Object.keys(chunkMap).map(id => Number.isNaN(Number(id)) ? id : Number(id));\n        if (allChunks.length === 0) throw new Error(\"Failed to get all chunks\");\n\n        // Chunks which our regex could not catch to load\n        // It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently\n        const chunksLeft = allChunks.filter(id => {\n            return !(validChunks.has(id) || invalidChunks.has(id));\n        });\n\n        await Promise.all(chunksLeft.map(async id => queue(async () => {\n            const isWorkerAsset = await fetch(wreq.p + wreq.u(id))\n                .then(r => r.text())\n                .then(t => /importScripts\\(|self\\.postMessage/.test(t));\n\n            // Loads the chunk. Currently this only happens with the language packs which are loaded differently\n            if (!isWorkerAsset) {\n                await wreq.e(id);\n            }\n        })));\n\n        LazyChunkLoaderLogger.log(\"Finished loading all chunks!\");\n    } catch (e) {\n        LazyChunkLoaderLogger.log(\"A fatal error occurred:\", e);\n    }\n}\n"
  },
  {
    "path": "src/debug/runReporter.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { addPatch } from \"@api/PluginManager\";\nimport { Logger } from \"@utils/Logger\";\nimport * as Webpack from \"@webpack\";\nimport { getBuildNumber, patches, patchTimings } from \"@webpack/patcher\";\n\nimport { loadLazyChunks } from \"./loadLazyChunks\";\n\nasync function runReporter() {\n    const ReporterLogger = new Logger(\"Reporter\");\n\n    try {\n        ReporterLogger.log(\"Starting test...\");\n\n        const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();\n\n        // The main patch for starting the reporter chunk loading\n        addPatch({\n            find: '\"Could not find app-mount\"',\n            replacement: {\n                match: /(?<=\"use strict\";)/,\n                replace: \"Vencord.Webpack._initReporter();\"\n            }\n        }, \"Vencord Reporter\");\n\n        // @ts-expect-error\n        Vencord.Webpack._initReporter = function () {\n            // initReporter is called in the patched entry point of Discord\n            // setImmediate to only start searching for lazy chunks after Discord initialized the app\n            setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);\n        };\n\n        await loadLazyChunksDone;\n\n        if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {\n            console.log(\"[REPORTER_META]\", {\n                buildNumber: getBuildNumber(),\n                buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId\n            });\n        }\n\n        for (const patch of patches) {\n            if (!patch.all) {\n                new Logger(\"WebpackPatcher\").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);\n            }\n        }\n\n        for (const [plugin, moduleId, match, totalTime] of patchTimings) {\n            if (totalTime > 5) {\n                new Logger(\"WebpackPatcher\").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);\n            }\n        }\n\n        for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {\n            let method = searchType;\n\n            if (searchType === \"findComponent\") method = \"find\";\n            if (searchType === \"findExportedComponent\") method = \"findByProps\";\n            if (searchType === \"waitFor\" || searchType === \"waitForComponent\") {\n                if (typeof args[0] === \"string\") method = \"findByProps\";\n                else method = \"find\";\n            }\n            if (searchType === \"waitForStore\") method = \"findStore\";\n\n            let result: any;\n            try {\n                if (method === \"proxyLazyWebpack\" || method === \"LazyComponentWebpack\") {\n                    const [factory] = args;\n                    result = factory();\n                } else if (method === \"extractAndLoadChunks\") {\n                    const [code, matcher] = args;\n\n                    result = await Webpack.extractAndLoadChunks(code, matcher);\n                    if (result === false) result = null;\n                } else if (method === \"mapMangledModule\") {\n                    const [code, mapper, includeBlacklistedExports] = args;\n\n                    result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);\n                    if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error(\"Webpack Find Fail\");\n                } else {\n                    result = Webpack[method](...args);\n                }\n\n                if (result == null || (result.$$vencordGetWrappedComponent != null && result.$$vencordGetWrappedComponent() == null)) throw new Error(\"Webpack Find Fail\");\n            } catch (e) {\n                let logMessage = searchType;\n                if (method === \"find\" || method === \"proxyLazyWebpack\" || method === \"LazyComponentWebpack\") {\n                    if (args[0].$$vencordProps != null) {\n                        logMessage += `(${args[0].$$vencordProps.map(arg => `\"${arg}\"`).join(\", \")})`;\n                    } else {\n                        logMessage += `(${args[0].toString().slice(0, 147)}...)`;\n                    }\n                } else if (method === \"extractAndLoadChunks\") {\n                    logMessage += `([${args[0].map(arg => `\"${arg}\"`).join(\", \")}], ${args[1].toString()})`;\n                } else if (method === \"mapMangledModule\") {\n                    const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);\n\n                    logMessage += `(\"${args[0]}\", {\\n${failedMappings.map(mapping => `\\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(\",\\n\")}\\n})`;\n                } else {\n                    logMessage += `(${args.map(arg => `\"${arg}\"`).join(\", \")})`;\n                }\n\n                ReporterLogger.log(\"Webpack Find Fail:\", logMessage);\n            }\n        }\n\n        ReporterLogger.log(\"Finished test\");\n    } catch (e) {\n        ReporterLogger.log(\"A fatal error occurred:\", e);\n    }\n}\n\n// Run after the Vencord object has been created.\n// We need to add extra properties to it, and it is only created after all of Vencord code has ran\nsetTimeout(runReporter, 0);\n"
  },
  {
    "path": "src/globals.d.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Style } from \"@api/Styles\";\n\ndeclare global {\n    /**\n     * This exists only at build time, so references to it in patches should insert it\n     * via String interpolation OR use different replacement code based on this\n     * but NEVER reference it inside the patched code\n     *\n     * @example\n     * // BAD\n     * replace: \"IS_WEB?foo:bar\"\n     * // GOOD\n     * replace: IS_WEB ? \"foo\" : \"bar\"\n     * // also okay\n     * replace: `${IS_WEB}?foo:bar`\n     */\n    export var IS_WEB: boolean;\n    export var IS_EXTENSION: boolean;\n    export var IS_USERSCRIPT: boolean;\n    export var IS_STANDALONE: boolean;\n    export var IS_UPDATER_DISABLED: boolean;\n    export var IS_DEV: boolean;\n    export var IS_REPORTER: boolean;\n    export var IS_ANTI_CRASH_TEST: boolean;\n    export var IS_DISCORD_DESKTOP: boolean;\n    export var IS_VESKTOP: boolean;\n    export var VERSION: string;\n    export var BUILD_TIMESTAMP: number;\n\n    export var VencordNative: typeof import(\"./VencordNative\").default;\n    export var Vencord: typeof import(\"./Vencord\");\n    export var VencordStyles: Map<string, Style>;\n    export var appSettings: {\n        set(setting: string, v: any): void;\n    };\n    /**\n     * Only available when running in Electron, undefined on web.\n     * Thus, avoid using this or only use it inside an {@link IS_WEB} guard.\n     *\n     * If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)\n     */\n    export var DiscordNative: any;\n    export var Vesktop: any;\n    export var VesktopNative: any;\n\n    interface Window extends Record<PropertyKey, any> { }\n}\n\nexport { };\n"
  },
  {
    "path": "src/main/csp/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { NativeSettings } from \"@main/settings\";\nimport { session } from \"electron\";\n\ntype PolicyMap = Record<string, string[]>;\n\nexport const ConnectSrc = [\"connect-src\"];\nexport const ImageSrc = [...ConnectSrc, \"img-src\"];\nexport const CssSrc = [\"style-src\", \"font-src\"];\nexport const ImageAndCssSrc = [...ImageSrc, ...CssSrc];\nexport const ImageScriptsAndCssSrc = [...ImageAndCssSrc, \"script-src\", \"worker-src\"];\n\n// Plugins can whitelist their own domains by importing this object in their native.ts\n// script and just adding to it. But generally, you should just edit this file instead\n\nexport const CspPolicies: PolicyMap = {\n    \"http://localhost:*\": ImageAndCssSrc,\n    \"http://127.0.0.1:*\": ImageAndCssSrc,\n    \"localhost:*\": ImageAndCssSrc,\n    \"127.0.0.1:*\": ImageAndCssSrc,\n\n    \"*.github.io\": ImageAndCssSrc, // GitHub pages, used by most themes\n    \"github.com\": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes\n    \"raw.githubusercontent.com\": ImageAndCssSrc, // GitHub raw, used by some themes\n    \"*.gitlab.io\": ImageAndCssSrc, // GitLab pages, used by some themes\n    \"gitlab.com\": ImageAndCssSrc, // GitLab raw, used by some themes\n    \"*.codeberg.page\": ImageAndCssSrc, // Codeberg pages, used by some themes\n    \"codeberg.org\": ImageAndCssSrc, // Codeberg raw, used by some themes\n\n    \"*.githack.com\": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes\n    \"jsdelivr.net\": ImageAndCssSrc, // jsDelivr, used by very few themes\n\n    \"fonts.googleapis.com\": CssSrc, // Google Fonts, used by many themes\n\n    \"i.imgur.com\": ImageSrc, // Imgur, used by some themes\n    \"i.ibb.co\": ImageSrc, // ImgBB, used by some themes\n    \"i.pinimg.com\": ImageSrc, // Pinterest, used by some themes\n    \"*.tenor.com\": ImageSrc, // Tenor, used by some themes\n    \"files.catbox.moe\": ImageAndCssSrc, // Catbox, used by some themes\n\n    \"cdn.discordapp.com\": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media\n    \"media.discordapp.net\": ImageSrc, // Discord media CDN, possible alternative to Discord CDN\n\n    // CDNs used for some things by Vencord.\n    // FIXME: we really should not be using CDNs anymore\n    \"cdnjs.cloudflare.com\": ImageScriptsAndCssSrc,\n    \"cdn.jsdelivr.net\": ImageScriptsAndCssSrc,\n\n    // Function Specific\n    \"api.github.com\": ConnectSrc, // used for updating Vencord itself\n    \"ws.audioscrobbler.com\": ConnectSrc, // Last.fm API\n    \"translate-pa.googleapis.com\": ConnectSrc, // Google Translate API\n    \"*.vencord.dev\": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)\n    \"manti.vendicated.dev\": ImageSrc, // ReviewDB API\n    \"decor.fieryflames.dev\": ConnectSrc, // Decor API\n    \"ugc.decor.fieryflames.dev\": ImageSrc, // Decor CDN\n    \"sponsor.ajay.app\": ConnectSrc, // Dearrow API\n    \"dearrow-thumb.ajay.app\": ImageSrc, // Dearrow Thumbnail CDN\n    \"usrbg.is-hardly.online\": ImageSrc, // USRBG API\n    \"icons.duckduckgo.com\": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search)\n};\n\nconst findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {\n    return Object.keys(headers).find(h => h.toLowerCase() === headerName);\n};\n\nconst parsePolicy = (policy: string): PolicyMap => {\n    const result: PolicyMap = {};\n    policy.split(\";\").forEach(directive => {\n        const [directiveKey, ...directiveValue] = directive.trim().split(/\\s+/g);\n        if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {\n            result[directiveKey] = directiveValue;\n        }\n    });\n\n    return result;\n};\n\nconst stringifyPolicy = (policy: PolicyMap): string =>\n    Object.entries(policy)\n        .filter(([, values]) => values?.length)\n        .map(directive => directive.flat().join(\" \"))\n        .join(\"; \");\n\n\nconst patchCsp = (headers: PolicyMap) => {\n    const reportOnlyHeader = findHeader(headers, \"content-security-policy-report-only\");\n    if (reportOnlyHeader)\n        delete headers[reportOnlyHeader];\n\n    const header = findHeader(headers, \"content-security-policy\");\n\n    if (header) {\n        const csp = parsePolicy(headers[header][0]);\n\n        const pushDirective = (directive: string, ...values: string[]) => {\n            csp[directive] ??= [...(csp[\"default-src\"] ?? [])];\n            csp[directive].push(...values);\n        };\n\n        pushDirective(\"style-src\", \"'unsafe-inline'\");\n        // we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/\n        // HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline\n        // Once they stop using it, we also should\n        pushDirective(\"script-src\", \"'unsafe-inline'\", \"'unsafe-eval'\");\n\n        for (const directive of [\"style-src\", \"connect-src\", \"img-src\", \"font-src\", \"media-src\", \"worker-src\"]) {\n            pushDirective(directive, \"blob:\", \"data:\", \"vencord:\", \"vesktop:\");\n        }\n\n        for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) {\n            for (const directive of directives) {\n                pushDirective(directive, host);\n            }\n        }\n\n        for (const [host, directives] of Object.entries(CspPolicies)) {\n            for (const directive of directives) {\n                pushDirective(directive, host);\n            }\n        }\n\n        headers[header] = [stringifyPolicy(csp)];\n    }\n};\n\nexport function initCsp() {\n    session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {\n        if (responseHeaders) {\n            if (resourceType === \"mainFrame\")\n                patchCsp(responseHeaders);\n\n            // Fix hosts that don't properly set the css content type, such as\n            // raw.githubusercontent.com\n            if (resourceType === \"stylesheet\") {\n                const header = findHeader(responseHeaders, \"content-type\");\n                if (header)\n                    responseHeaders[header] = [\"text/css\"];\n            }\n        }\n\n        cb({ cancel: false, responseHeaders });\n    });\n\n    // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.\n    // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it\n    // impossible to load css from github raw despite our fix above\n    session.defaultSession.webRequest.onHeadersReceived = () => { };\n}\n"
  },
  {
    "path": "src/main/csp/manager.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { NativeSettings } from \"@main/settings\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { dialog, ipcMain, IpcMainInvokeEvent } from \"electron\";\n\nimport { CspPolicies, ImageAndCssSrc } from \".\";\n\nexport type CspRequestResult = \"invalid\" | \"cancelled\" | \"unchecked\" | \"ok\" | \"conflict\";\n\nexport function registerCspIpcHandlers() {\n    ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule);\n    ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule);\n    ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed);\n}\n\nfunction validate(url: string, directives: string[]) {\n    try {\n        const { host } = new URL(url);\n\n        if (/[;'\"\\\\]/.test(host)) return false;\n    } catch {\n        return false;\n    }\n\n    if (directives.length === 0) return false;\n    if (directives.some(d => !ImageAndCssSrc.includes(d))) return false;\n\n    return true;\n}\n\nfunction getMessage(url: string, directives: string[], callerName: string) {\n    const domain = new URL(url).host;\n\n    const message = `${callerName} wants to allow connections to ${domain}`;\n\n    let detail =\n        `Unless you recognise and fully trust ${domain}, you should cancel this request!\\n\\n` +\n        `You will have to fully close and restart ${IS_DISCORD_DESKTOP ? \"Discord\" : \"Vesktop\"} for the changes to take effect.`;\n\n    if (directives.length === 1 && directives[0] === \"connect-src\") {\n        return { message, detail };\n    }\n\n    const contentTypes = directives\n        .filter(type => type !== \"connect-src\")\n        .map(type => {\n            switch (type) {\n                case \"img-src\":\n                    return \"Images\";\n                case \"style-src\":\n                    return \"CSS & Themes\";\n                case \"font-src\":\n                    return \"Fonts\";\n                default:\n                    throw new Error(`Illegal CSP directive: ${type}`);\n            }\n        })\n        .sort()\n        .join(\", \");\n\n    detail = `The following types of content will be allowed to load from ${domain}:\\n${contentTypes}\\n\\n${detail}`;\n\n    return { message, detail };\n}\n\nasync function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise<CspRequestResult> {\n    if (!validate(url, directives)) {\n        return \"invalid\";\n    }\n\n    const domain = new URL(url).host;\n\n    if (domain in NativeSettings.store.customCspRules) {\n        return \"conflict\";\n    }\n\n    const { checkboxChecked, response } = await dialog.showMessageBox({\n        ...getMessage(url, directives, callerName),\n        type: callerName ? \"info\" : \"warning\",\n        title: \"Vencord Host Permissions\",\n        buttons: [\"Cancel\", \"Allow\"],\n        defaultId: 0,\n        cancelId: 0,\n        checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`,\n        checkboxChecked: false,\n    });\n\n    if (response !== 1) {\n        return \"cancelled\";\n    }\n\n    if (!checkboxChecked) {\n        return \"unchecked\";\n    }\n\n    NativeSettings.store.customCspRules[domain] = directives;\n    return \"ok\";\n}\n\nfunction removeCspRule(_: IpcMainInvokeEvent, domain: string) {\n    if (domain in NativeSettings.store.customCspRules) {\n        delete NativeSettings.store.customCspRules[domain];\n        return true;\n    }\n\n    return false;\n}\n\nfunction isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) {\n    try {\n        const domain = new URL(url).host;\n\n        const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain];\n        if (!ruleForDomain) return false;\n\n        return directives.every(d => ruleForDomain.includes(d));\n    } catch (e) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { app, net, protocol } from \"electron\";\nimport { join } from \"path\";\nimport { pathToFileURL } from \"url\";\n\nimport { initCsp } from \"./csp\";\nimport { ensureSafePath } from \"./ipcMain\";\nimport { RendererSettings } from \"./settings\";\nimport { IS_VANILLA, THEMES_DIR } from \"./utils/constants\";\nimport { installExt } from \"./utils/extensions\";\n\nif (IS_VESKTOP || !IS_VANILLA) {\n    app.whenReady().then(() => {\n        protocol.handle(\"vencord\", ({ url: unsafeUrl }) => {\n            let url = decodeURI(unsafeUrl).slice(\"vencord://\".length).replace(/\\?v=\\d+$/, \"\");\n\n            if (url.endsWith(\"/\")) url = url.slice(0, -1);\n\n            if (url.startsWith(\"/themes/\")) {\n                const theme = url.slice(\"/themes/\".length);\n\n                const safeUrl = ensureSafePath(THEMES_DIR, theme);\n                if (!safeUrl) {\n                    return new Response(null, {\n                        status: 404\n                    });\n                }\n\n                return net.fetch(pathToFileURL(safeUrl).toString());\n            }\n\n            // Source Maps! Maybe there's a better way but since the renderer is executed\n            // from a string I don't think any other form of sourcemaps would work\n\n            switch (url) {\n                case \"renderer.js.map\":\n                case \"vencordDesktopRenderer.js.map\":\n                case \"preload.js.map\":\n                case \"vencordDesktopPreload.js.map\":\n                case \"patcher.js.map\":\n                case \"vencordDesktopMain.js.map\":\n                    return net.fetch(pathToFileURL(join(__dirname, url)).toString());\n                default:\n                    return new Response(null, {\n                        status: 404\n                    });\n            }\n        });\n\n        try {\n            if (RendererSettings.store.enableReactDevtools)\n                installExt(\"fmkadmapgofadopljbjfkapdkoienihi\")\n                    .then(() => console.info(\"[Vencord] Installed React Developer Tools\"))\n                    .catch(err => console.error(\"[Vencord] Failed to install React Developer Tools\", err));\n        } catch { }\n\n\n        initCsp();\n    });\n}\n\nif (IS_DISCORD_DESKTOP) {\n    require(\"./patcher\");\n}\n"
  },
  {
    "path": "src/main/ipcMain.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./updater\";\nimport \"./ipcPlugins\";\nimport \"./settings\";\n\nimport { debounce } from \"@shared/debounce\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { BrowserWindow, ipcMain, nativeTheme, shell, systemPreferences } from \"electron\";\nimport monacoHtml from \"file://monacoWin.html?minify&base64\";\nimport { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from \"fs\";\nimport { open, readdir, readFile } from \"fs/promises\";\nimport { join, normalize } from \"path\";\n\nimport { registerCspIpcHandlers } from \"./csp/manager\";\nimport { getThemeInfo, stripBOM, UserThemeHeader } from \"./themes\";\nimport { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from \"./utils/constants\";\nimport { makeLinksOpenExternally } from \"./utils/externalLinks\";\n\nconst RENDERER_CSS_PATH = join(__dirname, IS_VESKTOP ? \"vencordDesktopRenderer.css\" : \"renderer.css\");\n\nmkdirSync(THEMES_DIR, { recursive: true });\n\nregisterCspIpcHandlers();\n\nexport function ensureSafePath(basePath: string, path: string) {\n    const normalizedBasePath = normalize(basePath + \"/\");\n    const newPath = join(basePath, path);\n    const normalizedPath = normalize(newPath);\n    return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;\n}\n\nfunction readCss() {\n    return readFile(QUICK_CSS_PATH, \"utf-8\").catch(() => \"\");\n}\n\nasync function listThemes(): Promise<UserThemeHeader[]> {\n    const files = await readdir(THEMES_DIR).catch(() => []);\n\n    const themeInfo: UserThemeHeader[] = [];\n\n    for (const fileName of files) {\n        if (!fileName.endsWith(\".css\")) continue;\n\n        const data = await getThemeData(fileName).then(stripBOM).catch(() => null);\n        if (data == null) continue;\n\n        themeInfo.push(getThemeInfo(data, fileName));\n    }\n\n    return themeInfo;\n}\n\nfunction getThemeData(fileName: string) {\n    fileName = fileName.replace(/\\?v=\\d+$/, \"\");\n    const safePath = ensureSafePath(THEMES_DIR, fileName);\n    if (!safePath) return Promise.reject(`Unsafe path ${fileName}`);\n    return readFile(safePath, \"utf-8\");\n}\n\nipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICK_CSS_PATH));\n\nipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {\n    try {\n        var { protocol } = new URL(url);\n    } catch {\n        throw \"Malformed URL\";\n    }\n    if (!ALLOWED_PROTOCOLS.includes(protocol))\n        throw \"Disallowed protocol.\";\n\n    shell.openExternal(url);\n});\n\n\nipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());\nipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>\n    writeFileSync(QUICK_CSS_PATH, css)\n);\n\nipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());\nipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));\nipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => {\n    let accentColor = systemPreferences.getAccentColor?.() ?? \"\";\n\n    if (accentColor.length && accentColor[0] !== \"#\") {\n        accentColor = `#${accentColor}`;\n    }\n\n    return {\n        \"os-accent-color\": accentColor\n    };\n});\n\nipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));\nipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));\n\nipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {\n    let quickCssWatcher: FSWatcher | undefined;\n    let rendererCssWatcher: FSWatcher | undefined;\n\n    open(QUICK_CSS_PATH, \"a+\").then(fd => {\n        fd.close();\n        quickCssWatcher = watch(QUICK_CSS_PATH, { persistent: false }, debounce(async () => {\n            sender.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());\n        }, 50));\n    }).catch(() => { });\n\n    const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => {\n        sender.postMessage(IpcEvents.THEME_UPDATE, void 0);\n    }));\n\n    if (IS_DEV) {\n        rendererCssWatcher = watch(RENDERER_CSS_PATH, { persistent: false }, async () => {\n            sender.postMessage(IpcEvents.RENDERER_CSS_UPDATE, await readFile(RENDERER_CSS_PATH, \"utf-8\"));\n        });\n    }\n\n    sender.once(\"destroyed\", () => {\n        quickCssWatcher?.close();\n        themesWatcher.close();\n        rendererCssWatcher?.close();\n    });\n});\n\nipcMain.on(IpcEvents.GET_MONACO_THEME, e => {\n    e.returnValue = nativeTheme.shouldUseDarkColors ? \"vs-dark\" : \"vs-light\";\n});\n\nipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {\n    const title = \"Vencord QuickCSS Editor\";\n    const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title);\n    if (existingWindow && !existingWindow.isDestroyed()) {\n        existingWindow.focus();\n        return;\n    }\n\n    const win = new BrowserWindow({\n        title,\n        autoHideMenuBar: true,\n        darkTheme: true,\n        webPreferences: {\n            preload: join(__dirname, IS_DISCORD_DESKTOP ? \"preload.js\" : \"vencordDesktopPreload.js\"),\n            contextIsolation: true,\n            nodeIntegration: false,\n            sandbox: false\n        }\n    });\n\n    makeLinksOpenExternally(win);\n\n    await win.loadURL(`data:text/html;base64,${monacoHtml}`);\n});\n\nipcMain.handle(IpcEvents.GET_RENDERER_CSS, () => readFile(RENDERER_CSS_PATH, \"utf-8\"));\n\nif (IS_DISCORD_DESKTOP) {\n    ipcMain.on(IpcEvents.PRELOAD_GET_RENDERER_JS, e => {\n        e.returnValue = readFileSync(join(__dirname, \"renderer.js\"), \"utf-8\");\n    });\n}\n"
  },
  {
    "path": "src/main/ipcPlugins.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { ipcMain } from \"electron\";\n\nimport PluginNatives from \"~pluginNatives\";\n\nconst PluginIpcMappings = {} as Record<string, Record<string, string>>;\nexport type PluginIpcMappings = typeof PluginIpcMappings;\n\nfor (const [plugin, methods] of Object.entries(PluginNatives)) {\n    const entries = Object.entries(methods);\n    if (!entries.length) continue;\n\n    const mappings = PluginIpcMappings[plugin] = {};\n\n    for (const [methodName, method] of entries) {\n        const key = `VencordPluginNative_${plugin}_${methodName}`;\n        ipcMain.handle(key, method);\n        mappings[methodName] = key;\n    }\n}\n\nipcMain.on(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP, e => {\n    e.returnValue = PluginIpcMappings;\n});\n"
  },
  {
    "path": "src/main/monacoWin.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\" />\n        <title>Vencord QuickCSS Editor</title>\n        <link\n            rel=\"stylesheet\"\n            href=\"https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/editor/editor.main.css\"\n            integrity=\"sha256-tiJPQ2O04z/pZ/AwdyIghrOMzewf+PIvEl1YKbQvsZk=\"\n            crossorigin=\"anonymous\"\n            referrerpolicy=\"no-referrer\"\n        />\n        <style>\n            html,\n            body,\n            #container {\n                position: absolute;\n                left: 0;\n                top: 0;\n                width: 100%;\n                height: 100%;\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n        </style>\n    </head>\n\n    <body>\n        <div id=\"container\"></div>\n        <script\n            src=\"https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/loader.js\"\n            integrity=\"sha256-KcU48TGr84r7unF7J5IgBo95aeVrEbrGe04S7TcFUjs=\"\n            crossorigin=\"anonymous\"\n            referrerpolicy=\"no-referrer\"\n        ></script>\n\n        <script>\n            require.config({\n                paths: {\n                    vs: \"https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs\",\n                },\n            });\n\n            require([\"vs/editor/editor.main\"], () => {\n                getCurrentCss().then((css) => {\n                    var editor = monaco.editor.create(\n                        document.getElementById(\"container\"),\n                        {\n                            value: css,\n                            language: \"css\",\n                            theme: getTheme(),\n                        }\n                    );\n                    editor.onDidChangeModelContent(() =>\n                        setCss(editor.getValue())\n                    );\n                    window.addEventListener(\"resize\", () => {\n                        // make monaco re-layout\n                        editor.layout();\n                    });\n                });\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "src/main/patchWin32Updater.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { app } from \"electron\";\nimport { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from \"original-fs\";\nimport { basename, dirname, join } from \"path\";\n\nfunction isNewer($new: string, old: string) {\n    const newParts = $new.slice(4).split(\".\").map(Number);\n    const oldParts = old.slice(4).split(\".\").map(Number);\n\n    for (let i = 0; i < oldParts.length; i++) {\n        if (newParts[i] > oldParts[i]) return true;\n        if (newParts[i] < oldParts[i]) return false;\n    }\n    return false;\n}\n\nfunction patchLatest() {\n    if (process.env.DISABLE_UPDATER_AUTO_PATCHING) return;\n\n    try {\n        const currentAppPath = dirname(process.execPath);\n        const currentVersion = basename(currentAppPath);\n        const discordPath = join(currentAppPath, \"..\");\n\n        const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {\n            return (curr.startsWith(\"app-\") && isNewer(curr, prev))\n                ? curr\n                : prev;\n        }, currentVersion as string);\n\n        if (latestVersion === currentVersion) return;\n\n        const resources = join(discordPath, latestVersion, \"resources\");\n        const app = join(resources, \"app.asar\");\n        const _app = join(resources, \"_app.asar\");\n\n        if (!existsSync(app) || statSync(app).isDirectory()) return;\n\n        console.info(\"[Vencord] Detected Host Update. Repatching...\");\n\n        renameSync(app, _app);\n        mkdirSync(app);\n        writeFileSync(join(app, \"package.json\"), JSON.stringify({\n            name: \"discord\",\n            main: \"index.js\"\n        }));\n        writeFileSync(join(app, \"index.js\"), `require(${JSON.stringify(join(__dirname, \"patcher.js\"))});`);\n    } catch (err) {\n        console.error(\"[Vencord] Failed to repatch latest host update\", err);\n    }\n}\n\n// Try to patch latest on before-quit\n// Discord's Win32 updater will call app.quit() on restart and open new version on will-quit\napp.on(\"before-quit\", patchLatest);\n"
  },
  {
    "path": "src/main/patcher.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { onceDefined } from \"@shared/onceDefined\";\nimport electron, { app, BrowserWindowConstructorOptions, Menu } from \"electron\";\nimport { dirname, join } from \"path\";\n\nimport { RendererSettings } from \"./settings\";\nimport { IS_VANILLA } from \"./utils/constants\";\n\nconsole.log(\"[Vencord] Starting up...\");\n\n// Our injector file at app/index.js\nconst injectorPath = require.main!.filename;\n\n// special discord_arch_electron injection method\nconst asarName = require.main!.path.endsWith(\"app.asar\") ? \"_app.asar\" : \"app.asar\";\n\n// The original app.asar\nconst asarPath = join(dirname(injectorPath), \"..\", asarName);\n\nconst discordPkg = require(join(asarPath, \"package.json\"));\nrequire.main!.filename = join(asarPath, discordPkg.main);\n\n// @ts-expect-error Untyped method? Dies from cringe\napp.setAppPath(asarPath);\n\nif (!IS_VANILLA) {\n    const settings = RendererSettings.store;\n    // Repatch after host updates on Windows\n    if (process.platform === \"win32\") {\n        require(\"./patchWin32Updater\");\n\n        if (settings.winCtrlQ) {\n            const originalBuild = Menu.buildFromTemplate;\n            Menu.buildFromTemplate = function (template) {\n                if (template[0]?.label === \"&File\") {\n                    const { submenu } = template[0];\n                    if (Array.isArray(submenu)) {\n                        submenu.push({\n                            label: \"Quit (Hidden)\",\n                            visible: false,\n                            acceleratorWorksWhenHidden: true,\n                            accelerator: \"Control+Q\",\n                            click: () => app.quit()\n                        });\n                    }\n                }\n                return originalBuild.call(this, template);\n            };\n        }\n    }\n\n    class BrowserWindow extends electron.BrowserWindow {\n        constructor(options: BrowserWindowConstructorOptions) {\n            if (options?.webPreferences?.preload && options.title) {\n                const original = options.webPreferences.preload;\n                options.webPreferences.preload = join(__dirname, \"preload.js\");\n                options.webPreferences.sandbox = false;\n                // work around discord unloading when in background\n                options.webPreferences.backgroundThrottling = false;\n\n                if (settings.frameless) {\n                    options.frame = false;\n                } else if (process.platform === \"win32\" && settings.winNativeTitleBar) {\n                    delete options.frame;\n                }\n\n                if (settings.transparent) {\n                    options.transparent = true;\n                    options.backgroundColor = \"#00000000\";\n                }\n\n                if (settings.disableMinSize) {\n                    options.minWidth = 0;\n                    options.minHeight = 0;\n                }\n\n                const needsVibrancy = process.platform === \"darwin\" && settings.macosVibrancyStyle;\n\n                if (needsVibrancy) {\n                    options.backgroundColor = \"#00000000\";\n                    if (settings.macosVibrancyStyle) {\n                        options.vibrancy = settings.macosVibrancyStyle;\n                    }\n                }\n\n                process.env.DISCORD_PRELOAD = original;\n\n                super(options);\n\n                if (settings.disableMinSize) {\n                    // Disable the Electron call entirely so that Discord can't dynamically change the size\n                    this.setMinimumSize = (width: number, height: number) => { };\n                }\n            } else super(options);\n        }\n    }\n    Object.assign(BrowserWindow, electron.BrowserWindow);\n    // esbuild may rename our BrowserWindow, which leads to it being excluded\n    // from getFocusedWindow(), so this is necessary\n    // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62\n    Object.defineProperty(BrowserWindow, \"name\", { value: \"BrowserWindow\", configurable: true });\n\n    // Replace electrons exports with our custom BrowserWindow\n    const electronPath = require.resolve(\"electron\");\n    delete require.cache[electronPath]!.exports;\n    require.cache[electronPath]!.exports = {\n        ...electron,\n        BrowserWindow\n    };\n\n    // Patch appSettings to force enable devtools\n    onceDefined(global, \"appSettings\", s => {\n        s.set(\"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING\", true);\n    });\n\n    process.env.DATA_DIR = join(app.getPath(\"userData\"), \"..\", \"Vencord\");\n\n    // Monkey patch commandLine to:\n    // - disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790\n    // - disable UseEcoQoSForBackgroundProcess: Work around Discord unloading when in background\n    const originalAppend = app.commandLine.appendSwitch;\n    app.commandLine.appendSwitch = function (...args) {\n        if (args[0] === \"disable-features\") {\n            const disabledFeatures = new Set((args[1] ?? \"\").split(\",\"));\n            disabledFeatures.add(\"WidgetLayering\");\n            disabledFeatures.add(\"UseEcoQoSForBackgroundProcess\");\n            args[1] += [...disabledFeatures].join(\",\");\n        }\n        return originalAppend.apply(this, args);\n    };\n\n    // disable renderer backgrounding to prevent the app from unloading when in the background\n    // https://github.com/electron/electron/issues/2822\n    // https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling\n    // Work around discord unloading when in background\n    // Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too\n    app.commandLine.appendSwitch(\"disable-renderer-backgrounding\");\n    app.commandLine.appendSwitch(\"disable-background-timer-throttling\");\n    app.commandLine.appendSwitch(\"disable-backgrounding-occluded-windows\");\n} else {\n    console.log(\"[Vencord] Running in vanilla mode. Not loading Vencord\");\n}\n\nconsole.log(\"[Vencord] Loading original Discord app.asar\");\nrequire(require.main!.filename);\n"
  },
  {
    "path": "src/main/settings.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport type { Settings } from \"@api/Settings\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { SettingsStore } from \"@shared/SettingsStore\";\nimport { mergeDefaults } from \"@utils/mergeDefaults\";\nimport { ipcMain } from \"electron\";\nimport { mkdirSync, readFileSync, writeFileSync } from \"fs\";\n\nimport { NATIVE_SETTINGS_FILE, SETTINGS_DIR, SETTINGS_FILE } from \"./utils/constants\";\n\nmkdirSync(SETTINGS_DIR, { recursive: true });\n\nfunction readSettings<T = object>(name: string, file: string): Partial<T> {\n    try {\n        return JSON.parse(readFileSync(file, \"utf-8\"));\n    } catch (err: any) {\n        if (err?.code !== \"ENOENT\")\n            console.error(`Failed to read ${name} settings`, err);\n\n        return {};\n    }\n}\n\nexport const RendererSettings = new SettingsStore(readSettings<Settings>(\"renderer\", SETTINGS_FILE));\n\nRendererSettings.addGlobalChangeListener(() => {\n    try {\n        writeFileSync(SETTINGS_FILE, JSON.stringify(RendererSettings.plain, null, 4));\n    } catch (e) {\n        console.error(\"Failed to write renderer settings\", e);\n    }\n});\n\nipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);\n\nipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {\n    RendererSettings.setData(data, pathToNotify);\n});\n\nexport interface NativeSettings {\n    plugins: {\n        [plugin: string]: {\n            [setting: string]: any;\n        };\n    };\n    customCspRules: Record<string, string[]>;\n}\n\nconst DefaultNativeSettings: NativeSettings = {\n    plugins: {},\n    customCspRules: {}\n};\n\nconst nativeSettings = readSettings<NativeSettings>(\"native\", NATIVE_SETTINGS_FILE);\nmergeDefaults(nativeSettings, DefaultNativeSettings);\n\nexport const NativeSettings = new SettingsStore(nativeSettings as NativeSettings);\n\nNativeSettings.addGlobalChangeListener(() => {\n    try {\n        writeFileSync(NATIVE_SETTINGS_FILE, JSON.stringify(NativeSettings.plain, null, 4));\n    } catch (e) {\n        console.error(\"Failed to write native settings\", e);\n    }\n});\n"
  },
  {
    "path": "src/main/themes/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "src/main/themes/index.ts",
    "content": "/* eslint-disable simple-header/header */\n\n/*!\n * BetterDiscord addon meta parser\n * Copyright 2023 BetterDiscord contributors\n * Copyright 2023 Vendicated and Vencord contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst splitRegex = /[^\\S\\r\\n]*?\\r?(?:\\r\\n|\\n)[^\\S\\r\\n]*?\\*[^\\S\\r\\n]?/;\nconst escapedAtRegex = /^\\\\@/;\n\nexport interface UserThemeHeader {\n    fileName: string;\n    name: string;\n    author: string;\n    description: string;\n    version?: string;\n    license?: string;\n    source?: string;\n    website?: string;\n    invite?: string;\n}\n\nfunction makeHeader(fileName: string, opts: Partial<UserThemeHeader> = {}): UserThemeHeader {\n    return {\n        fileName,\n        name: opts.name ?? fileName.replace(/\\.css$/i, \"\"),\n        author: opts.author ?? \"Unknown Author\",\n        description: opts.description ?? \"A Discord Theme.\",\n        version: opts.version,\n        license: opts.license,\n        source: opts.source,\n        website: opts.website,\n        invite: opts.invite\n    };\n}\n\nexport function stripBOM(fileContent: string) {\n    if (fileContent.charCodeAt(0) === 0xFEFF) {\n        fileContent = fileContent.slice(1);\n    }\n    return fileContent;\n}\n\nexport function getThemeInfo(css: string, fileName: string): UserThemeHeader {\n    if (!css) return makeHeader(fileName);\n\n    const block = css.split(\"/**\", 2)?.[1]?.split(\"*/\", 1)?.[0];\n    if (!block) return makeHeader(fileName);\n\n    const header: Partial<UserThemeHeader> = {};\n    let field = \"\";\n    let accum = \"\";\n    for (const line of block.split(splitRegex)) {\n        if (line.length === 0) continue;\n        if (line.charAt(0) === \"@\" && line.charAt(1) !== \" \") {\n            header[field] = accum.trim();\n            const l = line.indexOf(\" \");\n            field = line.substring(1, l);\n            accum = line.substring(l + 1);\n        }\n        else {\n            accum += \" \" + line.replace(\"\\\\n\", \"\\n\").replace(escapedAtRegex, \"@\");\n        }\n    }\n    header[field] = accum.trim();\n    delete header[\"\"];\n    return makeHeader(fileName, header);\n}\n"
  },
  {
    "path": "src/main/updater/common.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const VENCORD_FILES = [\n    IS_DISCORD_DESKTOP ? \"patcher.js\" : \"vencordDesktopMain.js\",\n    IS_DISCORD_DESKTOP ? \"preload.js\" : \"vencordDesktopPreload.js\",\n    IS_DISCORD_DESKTOP ? \"renderer.js\" : \"vencordDesktopRenderer.js\",\n    IS_DISCORD_DESKTOP ? \"renderer.css\" : \"vencordDesktopRenderer.css\",\n];\n\nexport function serializeErrors(func: (...args: any[]) => any) {\n    return async function () {\n        try {\n            return {\n                ok: true,\n                value: await func(...arguments)\n            };\n        } catch (e: any) {\n            return {\n                ok: false,\n                error: e instanceof Error ? {\n                    // prototypes get lost, so turn error into plain object\n                    ...e,\n                    message: e.message,\n                    name: e.name,\n                    stack: e.stack\n                } : e\n            };\n        }\n    };\n}\n"
  },
  {
    "path": "src/main/updater/git.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { execFile as cpExecFile } from \"child_process\";\nimport { ipcMain } from \"electron\";\nimport { join } from \"path\";\nimport { promisify } from \"util\";\n\nimport { serializeErrors } from \"./common\";\n\nconst VENCORD_SRC_DIR = join(__dirname, \"..\");\n\nconst execFile = promisify(cpExecFile);\n\nconst isFlatpak = process.platform === \"linux\" && !!process.env.FLATPAK_ID;\n\nif (process.platform === \"darwin\") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;\n\nfunction git(...args: string[]) {\n    const opts = { cwd: VENCORD_SRC_DIR };\n\n    if (isFlatpak) return execFile(\"flatpak-spawn\", [\"--host\", \"git\", ...args], opts);\n    else return execFile(\"git\", args, opts);\n}\n\nasync function getRepo() {\n    const res = await git(\"remote\", \"get-url\", \"origin\");\n    return res.stdout.trim()\n        .replace(/git@(.+):/, \"https://$1/\")\n        .replace(/\\.git$/, \"\");\n}\n\nasync function calculateGitChanges() {\n    await git(\"fetch\");\n\n    const branch = (await git(\"branch\", \"--show-current\")).stdout.trim();\n\n    const existsOnOrigin = (await git(\"ls-remote\", \"origin\", branch)).stdout.length > 0;\n    if (!existsOnOrigin) return [];\n\n    const res = await git(\"log\", `HEAD...origin/${branch}`, \"--pretty=format:%an/%h/%s\");\n\n    const commits = res.stdout.trim();\n    return commits ? commits.split(\"\\n\").map(line => {\n        const [author, hash, ...rest] = line.split(\"/\");\n        return {\n            hash, author,\n            message: rest.join(\"/\").split(\"\\n\")[0]\n        };\n    }) : [];\n}\n\nasync function pull() {\n    const res = await git(\"pull\");\n    return res.stdout.includes(\"Fast-forward\");\n}\n\nasync function build() {\n    const opts = { cwd: VENCORD_SRC_DIR };\n\n    const command = isFlatpak ? \"flatpak-spawn\" : \"node\";\n    const args = isFlatpak ? [\"--host\", \"node\", \"scripts/build/build.mjs\"] : [\"scripts/build/build.mjs\"];\n\n    if (IS_DEV) args.push(\"--dev\");\n\n    const res = await execFile(command, args, opts);\n\n    return !res.stderr.includes(\"Build failed\");\n}\n\nipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));\nipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));\nipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));\nipcMain.handle(IpcEvents.BUILD, serializeErrors(build));\n"
  },
  {
    "path": "src/main/updater/http.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { fetchBuffer, fetchJson } from \"@main/utils/http\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { VENCORD_USER_AGENT } from \"@shared/vencordUserAgent\";\nimport { ipcMain } from \"electron\";\nimport { writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport gitHash from \"~git-hash\";\nimport gitRemote from \"~git-remote\";\n\nimport { serializeErrors, VENCORD_FILES } from \"./common\";\n\nconst API_BASE = `https://api.github.com/repos/${gitRemote}`;\nlet PendingUpdates = [] as [string, string][];\n\nasync function githubGet<T = any>(endpoint: string) {\n    return fetchJson<T>(API_BASE + endpoint, {\n        headers: {\n            Accept: \"application/vnd.github+json\",\n            // \"All API requests MUST include a valid User-Agent header.\n            // Requests with no User-Agent header will be rejected.\"\n            \"User-Agent\": VENCORD_USER_AGENT\n        }\n    });\n}\n\nasync function calculateGitChanges() {\n    const isOutdated = await fetchUpdates();\n    if (!isOutdated) return [];\n\n    const data = await githubGet(`/compare/${gitHash}...HEAD`);\n\n    return data.commits.map((c: any) => ({\n        // github api only sends the long sha\n        hash: c.sha.slice(0, 7),\n        author: c.author.login,\n        message: c.commit.message.split(\"\\n\")[0]\n    }));\n}\n\nasync function fetchUpdates() {\n    const data = await githubGet(\"/releases/latest\");\n\n    const hash = data.name.slice(data.name.lastIndexOf(\" \") + 1);\n    if (hash === gitHash)\n        return false;\n\n    data.assets.forEach(({ name, browser_download_url }) => {\n        if (VENCORD_FILES.some(s => name.startsWith(s))) {\n            PendingUpdates.push([name, browser_download_url]);\n        }\n    });\n\n    return true;\n}\n\nasync function applyUpdates() {\n    const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => {\n        const contents = await fetchBuffer(url);\n        return [join(__dirname, name), contents] as const;\n    }));\n\n    await Promise.all(fileContents.map(async ([filename, contents]) =>\n        writeFile(filename, contents))\n    );\n\n    PendingUpdates = [];\n    return true;\n}\n\nipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));\nipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));\nipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));\nipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));\n"
  },
  {
    "path": "src/main/updater/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nif (!IS_UPDATER_DISABLED)\n    require(IS_STANDALONE ? \"./http\" : \"./git\");\n"
  },
  {
    "path": "src/main/utils/constants.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { app } from \"electron\";\nimport { join } from \"path\";\n\nexport const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (\n    process.env.DISCORD_USER_DATA_DIR\n        ? join(process.env.DISCORD_USER_DATA_DIR, \"..\", \"VencordData\")\n        : join(app.getPath(\"userData\"), \"..\", \"Vencord\")\n);\nexport const SETTINGS_DIR = join(DATA_DIR, \"settings\");\nexport const THEMES_DIR = join(DATA_DIR, \"themes\");\nexport const QUICK_CSS_PATH = join(SETTINGS_DIR, \"quickCss.css\");\nexport const SETTINGS_FILE = join(SETTINGS_DIR, \"settings.json\");\nexport const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, \"native-settings.json\");\nexport const ALLOWED_PROTOCOLS = [\n    \"https:\",\n    \"http:\",\n    \"steam:\",\n    \"spotify:\",\n    \"com.epicgames.launcher:\",\n    \"tidal:\",\n    \"itunes:\",\n];\n\nexport const IS_VANILLA = /* @__PURE__ */ process.argv.includes(\"--vanilla\");\n"
  },
  {
    "path": "src/main/utils/crxToZip.ts",
    "content": "/* eslint-disable simple-header/header */\n\n/*!\n * crxToZip\n * Copyright (c) 2013 Rob Wu <rob@robwu.nl>\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.\n */\n\nexport function crxToZip(buf: Buffer) {\n    function calcLength(a: number, b: number, c: number, d: number) {\n        let length = 0;\n\n        length += a << 0;\n        length += b << 8;\n        length += c << 16;\n        length += d << 24 >>> 0;\n        return length;\n    }\n\n    // 50 4b 03 04\n    // This is actually a zip file\n    if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {\n        return buf;\n    }\n\n    // 43 72 32 34 (Cr24)\n    if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {\n        throw new Error(\"Invalid header: Does not start with Cr24\");\n    }\n\n    // 02 00 00 00\n    // or\n    // 03 00 00 00\n    const isV3 = buf[4] === 3;\n    const isV2 = buf[4] === 2;\n\n    if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {\n        throw new Error(\"Unexpected crx format version number.\");\n    }\n\n    if (isV2) {\n        const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);\n        const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);\n\n        // 16 = Magic number (4), CRX format version (4), lengths (2x4)\n        const zipStartOffset = 16 + publicKeyLength + signatureLength;\n\n        return buf.subarray(zipStartOffset, buf.length);\n    }\n    // v3 format has header size and then header\n    const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);\n    const zipStartOffset = 12 + headerSize;\n\n    return buf.subarray(zipStartOffset, buf.length);\n}\n"
  },
  {
    "path": "src/main/utils/extensions.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { session } from \"electron\";\nimport { unzip } from \"fflate\";\nimport { constants as fsConstants } from \"fs\";\nimport { access, mkdir, rm, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { DATA_DIR } from \"./constants\";\nimport { crxToZip } from \"./crxToZip\";\nimport { fetchBuffer } from \"./http\";\n\nconst extensionCacheDir = join(DATA_DIR, \"ExtensionCache\");\n\nasync function extract(data: Buffer, outDir: string) {\n    await mkdir(outDir, { recursive: true });\n    return new Promise<void>((resolve, reject) => {\n        unzip(data, (err, files) => {\n            if (err) return void reject(err);\n            Promise.all(Object.keys(files).map(async f => {\n                // Signature stuff\n                // 'Cannot load extension with file or directory name\n                // _metadata. Filenames starting with \"_\" are reserved for use by the system.';\n                if (f.startsWith(\"_metadata/\")) return;\n\n                if (f.endsWith(\"/\")) return void mkdir(join(outDir, f), { recursive: true });\n\n                const pathElements = f.split(\"/\");\n                const name = pathElements.pop()!;\n                const directories = pathElements.join(\"/\");\n                const dir = join(outDir, directories);\n\n                if (directories) {\n                    await mkdir(dir, { recursive: true });\n                }\n\n                await writeFile(join(dir, name), files[f]);\n            }))\n                .then(() => resolve())\n                .catch(err => {\n                    rm(outDir, { recursive: true, force: true });\n                    reject(err);\n                });\n        });\n    });\n}\n\nexport async function installExt(id: string) {\n    const extDir = join(extensionCacheDir, `${id}`);\n\n    try {\n        await access(extDir, fsConstants.F_OK);\n    } catch (err) {\n        const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;\n\n        const buf = await fetchBuffer(url, {\n            headers: {\n                \"User-Agent\": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`\n            }\n        });\n\n        await extract(crxToZip(buf), extDir)\n            .catch(err => console.error(`Failed to extract extension ${id}`, err));\n    }\n\n    session.defaultSession.loadExtension(extDir);\n}\n"
  },
  {
    "path": "src/main/utils/externalLinks.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { type BrowserWindow, shell } from \"electron\";\n\nexport function makeLinksOpenExternally(win: BrowserWindow) {\n    win.webContents.setWindowOpenHandler(({ url }) => {\n        switch (url) {\n            case \"about:blank\":\n            case \"https://discord.com/popout\":\n            case \"https://ptb.discord.com/popout\":\n            case \"https://canary.discord.com/popout\":\n                return { action: \"allow\" };\n        }\n\n        try {\n            var { protocol } = new URL(url);\n        } catch {\n            return { action: \"deny\" };\n        }\n\n        switch (protocol) {\n            case \"http:\":\n            case \"https:\":\n            case \"mailto:\":\n            case \"steam:\":\n            case \"spotify:\":\n                shell.openExternal(url);\n        }\n\n        return { action: \"deny\" };\n    });\n}\n"
  },
  {
    "path": "src/main/utils/http.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { createWriteStream } from \"original-fs\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\n\ntype Url = string | URL;\n\nexport async function checkedFetch(url: Url, options?: RequestInit) {\n    try {\n        var res = await fetch(url, options);\n    } catch (err) {\n        if (err instanceof Error && err.cause) {\n            err = err.cause;\n        }\n\n        throw new Error(`${options?.method ?? \"GET\"} ${url} failed: ${err}`);\n    }\n\n    if (res.ok) {\n        return res;\n    }\n\n    let message = `${options?.method ?? \"GET\"} ${url}: ${res.status} ${res.statusText}`;\n    try {\n        const reason = await res.text();\n        message += `\\n${reason}`;\n    } catch { }\n\n    throw new Error(message);\n}\n\nexport async function fetchJson<T = any>(url: Url, options?: RequestInit) {\n    const res = await checkedFetch(url, options);\n    return res.json() as Promise<T>;\n}\n\nexport async function fetchBuffer(url: Url, options?: RequestInit) {\n    const res = await checkedFetch(url, options);\n    const buf = await res.arrayBuffer();\n\n    return Buffer.from(buf);\n}\n\nexport async function downloadToFile(url: Url, path: string, options?: RequestInit) {\n    const res = await checkedFetch(url, options);\n    if (!res.body) {\n        throw new Error(`Download ${url}: response body is empty`);\n    }\n\n    // @ts-expect-error weird type conflict\n    const body = Readable.fromWeb(res.body);\n    await finished(body.pipe(createWriteStream(path)));\n}\n"
  },
  {
    "path": "src/modules.d.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\ndeclare module \"~plugins\" {\n    const plugins: Record<string, import(\"./utils/types\").Plugin>;\n    export default plugins;\n    export const PluginMeta: Record<string, {\n        folderName: string;\n        userPlugin: boolean;\n    }>;\n    export const ExcludedPlugins: Record<string, \"web\" | \"discordDesktop\" | \"vesktop\" | \"desktop\" | \"dev\">;\n}\n\ndeclare module \"~git-hash\" {\n    const hash: string;\n    export default hash;\n}\ndeclare module \"~git-remote\" {\n    const remote: string;\n    export default remote;\n}\n\ndeclare module \"file://*\" {\n    const content: string;\n    export default content;\n}\n\ndeclare module \"*.css\";\n\ndeclare module \"*.css?managed\" {\n    const name: string;\n    export default name;\n}\n"
  },
  {
    "path": "src/nativeModules.d.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\n/// <reference types=\"standalone-electron-types\"/>\n\ndeclare module \"~pluginNatives\" {\n    const pluginNatives: Record<string, Record<string, (event: Electron.IpcMainInvokeEvent, ...args: unknown[]) => unknown>>;\n    export default pluginNatives;\n}\n"
  },
  {
    "path": "src/plugins/_api/badges/fixDiscordBadgePadding.css",
    "content": "/* the profile popout badge container(s) */\n[class*=\"profile\"] [class*=\"tags\"] [class*=\"container\"] {\n    /* Discord has gap set to 2px instead of 1px, which causes the 12th badge to wrap to a new line. */\n    gap: 1px;\n}"
  },
  {
    "path": "src/plugins/_api/badges/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./fixDiscordBadgePadding.css\";\n\nimport { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from \"@api/Badges\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Heart } from \"@components/Heart\";\nimport DonateButton from \"@components/settings/DonateButton\";\nimport { openContributorModal } from \"@components/settings/tabs\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport { shouldShowContributorBadge } from \"@utils/misc\";\nimport { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from \"@utils/modal\";\nimport definePlugin from \"@utils/types\";\nimport { ContextMenuApi, Forms, Menu, Toasts, UserStore } from \"@webpack/common\";\n\nconst CONTRIBUTOR_BADGE = \"https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64\";\n\nconst ContributorBadge: ProfileBadge = {\n    description: \"Vencord Contributor\",\n    iconSrc: CONTRIBUTOR_BADGE,\n    position: BadgePosition.START,\n    shouldShow: ({ userId }) => shouldShowContributorBadge(userId),\n    onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))\n};\n\nlet DonorBadges = {} as Record<string, Array<Record<\"tooltip\" | \"badge\", string>>>;\n\nasync function loadBadges(noCache = false) {\n    const init = {} as RequestInit;\n    if (noCache)\n        init.cache = \"no-cache\";\n\n    DonorBadges = await fetch(\"https://badges.vencord.dev/badges.json\", init)\n        .then(r => r.json());\n}\n\nlet intervalId: any;\n\nfunction BadgeContextMenu({ badge }: { badge: ProfileBadge & BadgeUserArgs; }) {\n    return (\n        <Menu.Menu\n            navId=\"vc-badge-context\"\n            onClose={ContextMenuApi.closeContextMenu}\n            aria-label=\"Badge Options\"\n        >\n            {badge.description && (\n                <Menu.MenuItem\n                    id=\"vc-badge-copy-name\"\n                    label=\"Copy Badge Name\"\n                    action={() => copyWithToast(badge.description!)}\n                />\n            )}\n            {badge.iconSrc && (\n                <Menu.MenuItem\n                    id=\"vc-badge-copy-link\"\n                    label=\"Copy Badge Image Link\"\n                    action={() => copyWithToast(badge.iconSrc!)}\n                />\n            )}\n        </Menu.Menu>\n    );\n}\n\nexport default definePlugin({\n    name: \"BadgeAPI\",\n    description: \"API to add badges to users\",\n    authors: [Devs.Megu, Devs.Ven, Devs.TheSun],\n    required: true,\n    patches: [\n        {\n            find: \"#{intl::PROFILE_USER_BADGES}\",\n            replacement: [\n                {\n                    match: /alt:\" \",\"aria-hidden\":!0,src:.{0,50}(\\i).iconSrc/,\n                    replace: \"...$1.props,$&\"\n                },\n                {\n                    match: /(?<=forceOpen:.{0,40}?ariaHidden:!0,)children:(?=.{0,50}?(\\i)\\.id)/,\n                    replace: \"children:$1.component?$self.renderBadgeComponent({...$1}) :\"\n                },\n                // handle onClick and onContextMenu\n                {\n                    match: /href:(\\i)\\.link/,\n                    replace: \"...$self.getBadgeMouseEventHandlers($1),$&\"\n                }\n            ]\n        },\n        {\n            find: \"getLegacyUsername(){\",\n            replacement: {\n                match: /getBadges\\(\\)\\{.{0,100}?return\\[/,\n                replace: \"$&...$self.getBadges(this),\"\n            }\n        }\n    ],\n\n    // for access from the console or other plugins\n    get DonorBadges() {\n        return DonorBadges;\n    },\n\n    toolboxActions: {\n        async \"Refetch Badges\"() {\n            await loadBadges(true);\n            Toasts.show({\n                id: Toasts.genId(),\n                message: \"Successfully refetched badges!\",\n                type: Toasts.Type.SUCCESS\n            });\n        }\n    },\n\n    userProfileBadge: ContributorBadge,\n\n    async start() {\n        await loadBadges();\n\n        clearInterval(intervalId);\n        intervalId = setInterval(loadBadges, 1000 * 60 * 30); // 30 minutes\n    },\n\n    async stop() {\n        clearInterval(intervalId);\n    },\n\n    getBadges(profile: { userId: string; guildId: string; }) {\n        if (!profile) return [];\n\n        try {\n            return _getBadges(profile);\n        } catch (e) {\n            new Logger(\"BadgeAPI#getBadges\").error(e);\n            return [];\n        }\n    },\n\n    renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {\n        const Component = badge.component!;\n        return <Component {...badge} />;\n    }, { noop: true }),\n\n\n    getBadgeMouseEventHandlers(badge: ProfileBadge & BadgeUserArgs) {\n        const handlers = {} as Record<string, (e: React.MouseEvent) => void>;\n\n        if (!badge) return handlers; // sanity check\n\n        const { onClick, onContextMenu } = badge;\n\n        if (onClick) handlers.onClick = e => onClick(e, badge);\n        if (onContextMenu) handlers.onContextMenu = e => onContextMenu(e, badge);\n\n        return handlers;\n    },\n\n    getDonorBadges(userId: string) {\n        return DonorBadges[userId]?.map(badge => ({\n            iconSrc: badge.badge,\n            description: badge.tooltip,\n            position: BadgePosition.START,\n            props: {\n                style: {\n                    borderRadius: \"50%\",\n                    transform: \"scale(0.9)\" // The image is a bit too big compared to default badges\n                }\n            },\n            onContextMenu(event, badge) {\n                ContextMenuApi.openContextMenu(event, () => <BadgeContextMenu badge={badge} />);\n            },\n            onClick() {\n                const modalKey = openModal(props => (\n                    <ErrorBoundary noop onError={() => {\n                        closeModal(modalKey);\n                        VencordNative.native.openExternal(\"https://github.com/sponsors/Vendicated\");\n                    }}>\n                        <ModalRoot {...props}>\n                            <ModalHeader>\n                                <Forms.FormTitle\n                                    tag=\"h2\"\n                                    style={{\n                                        width: \"100%\",\n                                        textAlign: \"center\",\n                                        margin: 0\n                                    }}\n                                >\n                                    <Flex justifyContent=\"center\" alignItems=\"center\" gap=\"0.5em\">\n                                        <Heart />\n                                        Vencord Donor\n                                    </Flex>\n                                </Forms.FormTitle>\n                            </ModalHeader>\n                            <ModalContent>\n                                <Flex>\n                                    <img\n                                        role=\"presentation\"\n                                        src=\"https://cdn.discordapp.com/emojis/1026533070955872337.png\"\n                                        alt=\"\"\n                                        style={{ margin: \"auto\" }}\n                                    />\n                                    <img\n                                        role=\"presentation\"\n                                        src=\"https://cdn.discordapp.com/emojis/1026533090627174460.png\"\n                                        alt=\"\"\n                                        style={{ margin: \"auto\" }}\n                                    />\n                                </Flex>\n                                <div style={{ padding: \"1em\" }}>\n                                    <Forms.FormText>\n                                        This Badge is a special perk for Vencord Donors\n                                    </Forms.FormText>\n                                    <Forms.FormText className={Margins.top20}>\n                                        Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!\n                                    </Forms.FormText>\n                                </div>\n                            </ModalContent>\n                            <ModalFooter>\n                                <Flex justifyContent=\"center\" style={{ width: \"100%\" }}>\n                                    <DonateButton />\n                                </Flex>\n                            </ModalFooter>\n                        </ModalRoot>\n                    </ErrorBoundary>\n                ));\n            },\n        } satisfies ProfileBadge));\n    }\n});\n"
  },
  {
    "path": "src/plugins/_api/chatButtons.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ChatInputButtonAPI\",\n    description: \"API to add buttons to the chat input\",\n    authors: [Devs.Ven],\n\n    patches: [\n        {\n            find: '\"sticker\")',\n            replacement: {\n                match: /0===(\\i)\\.length(?=.{0,25}?\\(0,\\i\\.jsxs?\\)\\(.{0,75}?children:\\1)/,\n                replace: \"(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),$&)\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/commands.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"CommandsAPI\",\n    authors: [Devs.Arjix],\n    description: \"Api required by anything that uses commands\",\n    patches: [\n        // obtain BUILT_IN_COMMANDS instance\n        {\n            find: ',\"tenor\"',\n            replacement: [\n                {\n                    // Matches BUILT_IN_COMMANDS. This is not exported so this is\n                    // the only way. _init() just returns the same object to make the\n                    // patch simpler\n\n                    // textCommands = builtInCommands.filter(...)\n                    match: /(?<=\\w=)(\\w)(\\.filter\\(.{0,60}tenor)/,\n                    replace: \"Vencord.Api.Commands._init($1)$2\",\n                }\n            ],\n        },\n        // command error handling\n        {\n            find: \"Unexpected value for option\",\n            replacement: {\n                // return [2, cmd.execute(args, ctx)]\n                match: /,(\\i)\\.execute\\((\\i),(\\i)\\)/,\n                replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})`\n            }\n        },\n        // Show plugin name instead of \"Built-In\"\n        {\n            find: \"#{intl::COMMANDS_OPTIONAL_COUNT}\",\n            replacement: [\n                {\n                    // ...children: p?.name\n                    match: /(?<=:(\\i)\\.displayDescription\\}.{0,200}children:).{0,50}\\.name(?=\\}\\))/,\n                    replace: \"$1.plugin||($&)\",\n                    noWarn: true // TODO: remove legacy compatibility code in the future\n                },\n                {\n                    match: /children:(?=\\i\\?\\?\\i\\?\\.name)(?<=command:(\\i),.+?)/,\n                    replace: \"children:$1.plugin??\"\n                }\n            ]\n        }\n    ],\n});\n"
  },
  {
    "path": "src/plugins/_api/contextMenu.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ContextMenuAPI\",\n    description: \"API for adding/removing items to/from context menus.\",\n    authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi],\n    required: true,\n\n    patches: [\n        {\n            find: \"♫ (つ｡◕‿‿◕｡)つ ♪\",\n            replacement: {\n                match: /(?=let{navId:)(?<=function \\i\\((\\i)\\).+?)/,\n                replace: \"$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);\"\n            }\n        },\n        {\n            find: \"navId:\",\n            all: true,\n            noWarn: true,\n            replacement: [\n                {\n                    match: /navId:(?=.+?([,}].*?\\)))/g,\n                    replace: (m, rest) => {\n                        // Check if this navId: match is a destructuring statement, ignore it if it is\n                        const destructuringMatch = rest.match(/}=.+/);\n                        if (destructuringMatch == null) {\n                            return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;\n                        }\n                        return m;\n                    }\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/dynamicImageModalApi.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\n\nexport default definePlugin({\n    name: \"DynamicImageModalAPI\",\n    authors: [Devs.sadan, Devs.Nuckyz],\n    description: \"Allows you to omit either width or height when opening an image modal\",\n    patches: [\n        {\n            // TODO: bundler compat\n            find: \".renderLinkComponent\",\n            replacement: {\n                // widthAndHeightPassed = w != null && w !== 0 && h == null || h === 0\n                match: /(?<=\\i=)(null!=\\i&&0!==\\i)&&(null!=\\i&&0!==\\i)/,\n                replace: \"($1)||($2)\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/memberListDecorators/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nimport managedStyle from \"./style.css?managed\";\n\nexport default definePlugin({\n    name: \"MemberListDecoratorsAPI\",\n    description: \"API to add decorators to member list (both in servers and DMs)\",\n    authors: [Devs.TheSun, Devs.Ven],\n\n    managedStyle,\n\n    patches: [\n        {\n            find: \"#{intl::GUILD_OWNER}),children:\",\n            replacement: [\n                {\n                    match: /children:\\[(?=.{0,300},lostPermissionTooltipText:)/,\n                    replace: \"children:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'guild'),\"\n                }\n            ]\n        },\n        {\n            find: \"PrivateChannel.renderAvatar\",\n            replacement: {\n                match: /decorators:(\\i\\.isSystemDM\\(\\)\\?.+?:null)/,\n                replace: \"decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'dm'),$1]\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/memberListDecorators/style.css",
    "content": ".vc-member-list-decorators-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 0.25em;\n}\n\n.vc-member-list-decorators-wrapper:not(:empty) {\n    /* Margin to match default Discord decorators */\n    margin-left: 0.25em;\n}\n"
  },
  {
    "path": "src/plugins/_api/menuItemDemangler.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport definePlugin from \"@utils/types\";\n\n// duplicate values have multiple branches with different types. Just include all to be safe\nconst nameMap = {\n    radio: \"MenuRadioItem\",\n    separator: \"MenuSeparator\",\n    checkbox: \"MenuCheckboxItem\",\n    groupstart: \"MenuGroup\",\n\n    control: \"MenuControlItem\",\n    compositecontrol: \"MenuControlItem\",\n\n    item: \"MenuItem\",\n    customitem: \"MenuItem\",\n};\n\nexport default definePlugin({\n    name: \"MenuItemDemanglerAPI\",\n    description: \"Demangles Discord's Menu Item module\",\n    authors: [Devs.Ven],\n    required: true,\n    patches: [\n        {\n            find: \"Menu API only allows Items\",\n            replacement: {\n                match: /function.{0,80}type===(\\i\\.\\i)\\).{0,50}navigable:.+?Menu API/s,\n                replace: (m, mod) => {\n                    const nameAssignments = [] as string[];\n\n                    // if (t.type === m.MenuItem)\n                    const typeCheckRe = canonicalizeMatch(/\\(\\i\\.type===(\\i\\.\\i)\\)/g);\n                    // push({type:\"item\"})\n                    const pushTypeRe = /type:\"(\\w+)\"/g;\n\n                    let typeMatch: RegExpExecArray | null;\n                    // for each if (t.type === ...)\n                    while ((typeMatch = typeCheckRe.exec(m)) !== null) {\n                        // extract the current menu item\n                        const item = typeMatch[1];\n                        // Set the starting index of the second regex to that of the first to start\n                        // matching from after the if\n                        pushTypeRe.lastIndex = typeCheckRe.lastIndex;\n                        // extract the first type: \"...\"\n                        const type = pushTypeRe.exec(m)?.[1];\n                        if (type && type in nameMap) {\n                            const name = nameMap[type];\n                            nameAssignments.push(`Object.defineProperty(${item},\"name\",{value:\"${name}\"})`);\n                        }\n                    }\n                    if (nameAssignments.length < 6) {\n                        console.warn(\"[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped\", nameAssignments.length);\n                    }\n\n                    // Merge all our redefines with the actual module\n                    return `${nameAssignments.join(\";\")};${m}`;\n                },\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/_api/messageAccessories.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"MessageAccessoriesAPI\",\n    description: \"API to add message accessories.\",\n    authors: [Devs.Cyn],\n    patches: [\n        {\n            find: \"#{intl::REMOVE_ATTACHMENT_BODY}\",\n            replacement: {\n                match: /children:(\\[[^\\]]{0,100}?this.renderSuppressConfirmModal[^\\]]{0,100}?\\])/,\n                replace: \"children:Vencord.Api.MessageAccessories._modifyAccessories($1,this.props)\",\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/_api/messageDecorations/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nimport managedStyle from \"./style.css?managed\";\n\nexport default definePlugin({\n    name: \"MessageDecorationsAPI\",\n    description: \"API to add decorations to messages\",\n    authors: [Devs.TheSun],\n\n    managedStyle,\n\n    patches: [\n        {\n            find: \"#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\",\n            replacement: {\n                match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?renderPopout:.+?(?=\\])/,\n                replace: \"$&,Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/messageDecorations/style.css",
    "content": ".vc-message-decorations-wrapper {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: 0.25em;\n}\n\n.vc-message-decorations-wrapper:not(:empty) {\n    /* Margin to match default Discord decorators */\n    margin-left: 0.25em;\n\n    /* Align vertically */\n    position: relative;\n    vertical-align: top;\n    top: 0.1rem;\n    height: calc(1rem + 4px);\n    max-height: calc(1rem + 4px)\n}\n"
  },
  {
    "path": "src/plugins/_api/messageEvents.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"MessageEventsAPI\",\n    description: \"Api required by anything using message events.\",\n    authors: [Devs.Arjix, Devs.hunt, Devs.Ven],\n    patches: [\n        {\n            find: \"#{intl::EDIT_TEXTAREA_HELP}\",\n            replacement: {\n                match: /(?<=,channel:\\i\\}\\)\\.then\\().+?(?=\\i\\.content!==this\\.props\\.message\\.content&&\\i\\((.+?)\\)\\})/,\n                replace: (match, args) => \"\" +\n                    `async ${match}` +\n                    `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +\n                    \"return Promise.resolve({shouldClear:false,shouldRefocus:true});\"\n            }\n        },\n        {\n            find: \".handleSendMessage,onResize:\",\n            replacement: {\n                // https://regex101.com/r/7iswuk/1\n                match: /let (\\i)=\\i\\.\\i\\.parse\\((\\i),.+?\\.getSendMessageOptions\\(\\{.+?\\}\\)?;(?=.+?(\\i)\\.flags=)(?<=\\)\\(({.+?})\\)\\.then.+?)/,\n                replace: (m, parsedMessage, channel, replyOptions, extra) => m +\n                    `if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +\n                    \"return{shouldClear:false,shouldRefocus:true};\"\n            }\n        },\n        {\n            find: '(\"interactionUsernameProfile',\n            replacement: {\n                match: /let\\{id:\\i}=(\\i),{id:\\i}=(\\i);return \\i\\.useCallback\\((\\i)=>\\{/,\n                replace: (m, message, channel, event) =>\n                    `const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg,vcChan,${event});`\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/messagePopover.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"MessagePopoverAPI\",\n    description: \"API to add buttons to message popovers.\",\n    authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],\n    patches: [\n        {\n            find: \"#{intl::MESSAGE_UTILITIES_A11Y_LABEL}\",\n            replacement: {\n                match: /(?<=\\]\\}\\)),(.{0,40}togglePopout:.+?\\}\\))\\]\\}\\):null,(?<=\\((\\i\\.\\i),\\{label:.+?:null,(\\i)\\?\\(0,\\i\\.jsxs?\\)\\(\\i\\.Fragment.+?message:(\\i).+?)/,\n                replace: (_, ReactButton, ButtonComponent, showReactButton, message) => \"\" +\n                    `]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,`\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/messageUpdater.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"MessageUpdaterAPI\",\n    description: \"API for updating and re-rendering messages.\",\n    authors: [Devs.Nuckyz],\n\n    patches: [\n        {\n            // Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference\n            find: \"}renderStickersAccessories(\",\n            replacement: {\n                match: /(?<=this.props,\\i,\\[)\"message\",/,\n                replace: \"\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/notices.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoticesAPI\",\n    description: \"Fixes notices being automatically dismissed\",\n    authors: [Devs.Ven],\n    required: true,\n    patches: [\n        {\n            find: '\"NoticeStore\"',\n            replacement: [\n                {\n                    match: /(?<=!1;)\\i=null;(?=.{0,80}getPremiumSubscription\\(\\))/g,\n                    replace: \"if(Vencord.Api.Notices.currentNotice)return false;$&\"\n                },\n                {\n                    match: /(?<=,NOTICE_DISMISS:function\\(\\i\\){)return null!=(\\i)/,\n                    replace: (m, notice) => `if(${notice}?.id==\"VencordNotice\")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`\n                },\n                // FIXME(Bundler agressive inline): Remove the non used compability once enough time has passed\n                {\n                    match: /(?<=function (\\i)\\(\\i\\){)return null!=(\\i)(?=.+?NOTICE_DISMISS:\\1)/,\n                    replace: (m, _, notice) => `if(${notice}?.id==\"VencordNotice\")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`,\n                    noWarn: true\n                }\n            ]\n        }\n    ],\n});\n"
  },
  {
    "path": "src/plugins/_api/serverList.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ServerListAPI\",\n    authors: [Devs.kemo],\n    description: \"Api required for plugins that modify the server list\",\n    patches: [\n        {\n            find: \"#{intl::DISCODO_DISABLED}\",\n            replacement: {\n                match: /(?<=#{intl::DISCODO_DISABLED}.+?return)(\\(.{0,150}?tutorialId:\"friends-list\".+?}\\))(?=}function)/,\n                replace: \"[$1].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))\"\n            }\n        },\n        {\n            find: \".setGuildsTree(\",\n            replacement: {\n                match: /(?<=#{intl::SERVERS}\\),gap:\"xs\",children:)\\i\\.map\\(.{0,50}\\.length\\)/,\n                replace: \"Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_api/userSettings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"UserSettingsAPI\",\n    description: \"Patches Discord's UserSettings to expose their group and name.\",\n    authors: [Devs.Nuckyz],\n\n    patches: [\n        {\n            find: \",updateSetting:\",\n            replacement: [\n                // Main setting definition\n                {\n                    match: /\\.updateAsync\\(.+?(?=,useSetting:)/,\n                    replace: \"$&,userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1]\"\n                },\n                // Selective wrapper\n                {\n                    match: /updateSetting:.{0,100}SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE/,\n                    replace: \"userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&\"\n                },\n                // Override wrapper\n                {\n                    match: /updateSetting:.{0,60}USER_SETTINGS_OVERRIDE_CLEAR/,\n                    replace: \"userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&\"\n                }\n\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/_core/noTrack.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType, StartAt } from \"@utils/types\";\nimport { WebpackRequire } from \"@vencord/discord-types/webpack\";\n\nconst settings = definePluginSettings({\n    disableAnalytics: {\n        type: OptionType.BOOLEAN,\n        description: \"Disable Discord's tracking (analytics/'science')\",\n        default: true,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"NoTrack\",\n    description: \"Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting\",\n    authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],\n    required: true,\n\n    settings,\n\n    patches: [\n        {\n            find: \"AnalyticsActionHandlers.handle\",\n            predicate: () => settings.store.disableAnalytics,\n            replacement: {\n                match: /^.+$/,\n                replace: \"()=>{}\",\n            },\n        },\n        {\n            find: \".METRICS_V2\",\n            replacement: [\n                {\n                    match: /this\\._intervalId=/,\n                    replace: \"this._intervalId=void 0&&\"\n                },\n                {\n                    match: /(?:increment|distribution)\\(\\i(?:,\\i)?\\){/g,\n                    replace: \"$&return;\"\n                }\n            ]\n        },\n        {\n            find: \".BetterDiscord||null!=\",\n            replacement: {\n                // Make hasClientMods return false\n                match: /(?=let \\i=window;)/,\n                replace: \"return false;\"\n            }\n        }\n    ],\n\n    // The TRACK event takes an optional `resolve` property that is called when the tracking event was submitted to the server.\n    // A few spots in Discord await this callback before continuing (most notably the Voice Debug Logging toggle).\n    // Since we NOOP the AnalyticsActionHandlers module, there is no handler for the TRACK event, so we have to handle it ourselves\n    flux: {\n        TRACK(event) {\n            event?.resolve?.();\n        }\n    },\n\n    startAt: StartAt.Init,\n    start() {\n        // Sentry is initialized in its own WebpackInstance.\n        // It has everything it needs preloaded, so, it doesn't include any chunk loading functionality.\n        // Because of that, its WebpackInstance doesnt export wreq.m or wreq.c\n\n        // To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set.\n        // When that happens we are gonna forcefully throw an error and abort everything.\n        Object.defineProperty(Function.prototype, \"g\", {\n            configurable: true,\n\n            set(this: WebpackRequire, globalObj: WebpackRequire[\"g\"]) {\n                Object.defineProperty(this, \"g\", {\n                    value: globalObj,\n                    configurable: true,\n                    enumerable: true,\n                    writable: true\n                });\n\n                // Ensure this is most likely the Sentry WebpackInstance.\n                // Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it\n                const { stack } = new Error();\n                if (this.c != null || !stack?.includes(\"http\") || !String(this).includes(\"exports:{}\")) {\n                    return;\n                }\n\n                const assetPath = stack.match(/http.+?(?=:\\d+?:\\d+?$)/m)?.[0];\n                if (!assetPath) {\n                    return;\n                }\n\n                const srcRequest = new XMLHttpRequest();\n                srcRequest.open(\"GET\", assetPath, false);\n                srcRequest.send();\n\n                // Final condition to see if this is the Sentry WebpackInstance\n                // This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies\n                if (!srcRequest.responseText.includes(\".DiscordSentry=\")) {\n                    return;\n                }\n\n                new Logger(\"NoTrack\", \"#8caaee\").info(\"Disabling Sentry by erroring its WebpackInstance\");\n\n                Reflect.deleteProperty(Function.prototype, \"g\");\n                Reflect.deleteProperty(window, \"DiscordSentry\");\n\n                throw new Error(\"Sentry successfully disabled\");\n            }\n        });\n\n        Object.defineProperty(window, \"DiscordSentry\", {\n            configurable: true,\n\n            set() {\n                new Logger(\"NoTrack\", \"#8caaee\").error(\"Failed to disable Sentry. Falling back to deleting window.DiscordSentry\");\n\n                Reflect.deleteProperty(Function.prototype, \"g\");\n                Reflect.deleteProperty(window, \"DiscordSentry\");\n            }\n        });\n    }\n});\n"
  },
  {
    "path": "src/plugins/_core/settings.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and Megumin\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from \"@components/Icons\";\nimport { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from \"@components/settings/tabs\";\nimport { Devs } from \"@utils/constants\";\nimport { isTruthy } from \"@utils/guards\";\nimport definePlugin, { IconProps, OptionType } from \"@utils/types\";\nimport { waitFor } from \"@webpack\";\nimport { React } from \"@webpack/common\";\nimport type { ComponentType, PropsWithChildren, ReactNode } from \"react\";\n\nimport gitHash from \"~git-hash\";\n\nlet LayoutTypes = {\n    SECTION: 1,\n    SIDEBAR_ITEM: 2,\n    PANEL: 3,\n    CATEGORY: 5,\n    CUSTOM: 19,\n};\nwaitFor([\"SECTION\", \"SIDEBAR_ITEM\", \"PANEL\", \"CUSTOM\"], v => LayoutTypes = v);\n\nconst FallbackSectionTypes = {\n    HEADER: \"HEADER\",\n    DIVIDER: \"DIVIDER\",\n    CUSTOM: \"CUSTOM\"\n};\ntype SectionTypes = typeof FallbackSectionTypes;\n\ntype SettingsLocation =\n    | \"top\"\n    | \"aboveNitro\"\n    | \"belowNitro\"\n    | \"aboveActivity\"\n    | \"belowActivity\"\n    | \"bottom\";\n\ninterface SettingsLayoutNode {\n    type: number;\n    key?: string;\n    legacySearchKey?: string;\n    getLegacySearchKey?(): string;\n    useLabel?(): string;\n    useTitle?(): string;\n    buildLayout?(): SettingsLayoutNode[];\n    icon?(): ReactNode;\n    render?(): ReactNode;\n    StronglyDiscouragedCustomComponent?(): ReactNode;\n}\n\ninterface EntryOptions {\n    key: string,\n    title: string,\n    panelTitle?: string,\n    Component: ComponentType<{}>,\n    Icon: ComponentType<IconProps>;\n}\ninterface SettingsLayoutBuilder {\n    key?: string;\n    buildLayout(): SettingsLayoutNode[];\n}\n\nconst settings = definePluginSettings({\n    settingsLocation: {\n        type: OptionType.SELECT,\n        description: \"Where to put the Vencord settings section\",\n        options: [\n            { label: \"At the very top\", value: \"top\" },\n            { label: \"Above the Nitro section\", value: \"aboveNitro\", default: true },\n            { label: \"Below the Nitro section\", value: \"belowNitro\" },\n            { label: \"Above Activity Settings\", value: \"aboveActivity\" },\n            { label: \"Below Activity Settings\", value: \"belowActivity\" },\n            { label: \"At the very bottom\", value: \"bottom\" },\n        ] as { label: string; value: SettingsLocation; default?: boolean; }[]\n    }\n});\n\nconst settingsSectionMap: [string, string][] = [\n    [\"VencordSettings\", \"vencord_main_panel\"],\n    [\"VencordPlugins\", \"vencord_plugins_panel\"],\n    [\"VencordThemes\", \"vencord_themes_panel\"],\n    [\"VencordUpdater\", \"vencord_updater_panel\"],\n    [\"VencordCloud\", \"vencord_cloud_panel\"],\n    [\"VencordBackupAndRestore\", \"vencord_backup_restore_panel\"],\n    [\"VencordPatchHelper\", \"vencord_patch_helper_panel\"]\n];\n\nexport default definePlugin({\n    name: \"Settings\",\n    description: \"Adds Settings UI and debug info\",\n    authors: [Devs.Ven, Devs.Megu],\n    required: true,\n\n    settings,\n    settingsSectionMap,\n\n    patches: [\n        {\n            find: \"#{intl::COPY_VERSION}\",\n            replacement: [\n                {\n                    match: /\"text-xxs\\/normal\".{0,300}?(?=null!=(\\i)&&(.{0,20}\\i\\.Text.{0,200}?,children:).{0,15}?(\"span\"),({className:\\i\\.\\i,children:\\[\"Build Override: \",\\1\\.id\\]\\})\\)\\}\\))/,\n                    replace: (m, _buildOverride, makeRow, component, props) => {\n                        props = props.replace(/children:\\[.+\\]/, \"\");\n                        return `${m},$self.makeInfoElements(${component},${props}).map(e=>${makeRow}e})),`;\n                    }\n                },\n                {\n                    match: /\"text-xs\\/normal\".{0,300}?\\[\\(0,\\i\\.jsxs?\\)\\((.{1,10}),(\\{[^{}}]+\\{.{0,20}className:\\i.\\i,.+?\\})\\),\" \"/,\n                    replace: (m, component, props) => {\n                        props = props.replace(/children:\\[.+\\]/, \"\");\n                        return `${m},$self.makeInfoElements(${component},${props})`;\n                    }\n                },\n                {\n                    match: /copyValue:\\i\\.join\\(\" \"\\)/g,\n                    replace: \"$& + $self.getInfoString()\"\n                }\n            ]\n        },\n        {\n            find: \".buildLayout().map\",\n            replacement: {\n                match: /(\\i)\\.buildLayout\\(\\)(?=\\.map)/,\n                replace: \"$self.buildLayout($1)\"\n            }\n        },\n        {\n            find: \"getWebUserSettingFromSection\",\n            replacement: {\n                match: /new Map\\(\\[(?=\\[.{0,10}\\.ACCOUNT,.{0,10}\\.ACCOUNT_PANEL)/,\n                replace: \"new Map([...$self.getSettingsSectionMappings(),\"\n            }\n        }\n    ],\n\n    buildEntry(options: EntryOptions): SettingsLayoutNode {\n        const { key, title, panelTitle = title, Component, Icon } = options;\n\n        const panel: SettingsLayoutNode = {\n            key: key + \"_panel\",\n            type: LayoutTypes.PANEL,\n            useTitle: () => panelTitle,\n            buildLayout: () => [{\n                type: LayoutTypes.CATEGORY,\n                key: key + \"_category\",\n                buildLayout: () => [{\n                    type: LayoutTypes.CUSTOM,\n                    key: key + \"_custom\",\n                    Component: Component,\n                    useSearchTerms: () => [title]\n                }]\n            }]\n        };\n\n        return ({\n            key,\n            type: LayoutTypes.SIDEBAR_ITEM,\n            useTitle: () => title,\n            icon: () => <Icon width={20} height={20} />,\n            buildLayout: () => [panel]\n        });\n    },\n\n    getSettingsSectionMappings() {\n        return settingsSectionMap;\n    },\n\n    buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) {\n        const layout = originalLayoutBuilder.buildLayout();\n        if (originalLayoutBuilder.key !== \"$Root\") return layout;\n        if (!Array.isArray(layout)) return layout;\n\n        if (layout.some(s => s?.key === \"vencord_section\")) return layout;\n\n        const { buildEntry } = this;\n\n        const vencordEntries: SettingsLayoutNode[] = [\n            buildEntry({\n                key: \"vencord_main\",\n                title: \"Vencord\",\n                panelTitle: \"Vencord Settings\",\n                Component: VencordTab,\n                Icon: MainSettingsIcon\n            }),\n            buildEntry({\n                key: \"vencord_plugins\",\n                title: \"Plugins\",\n                Component: PluginsTab,\n                Icon: PluginsIcon\n            }),\n            buildEntry({\n                key: \"vencord_themes\",\n                title: \"Themes\",\n                Component: ThemesTab,\n                Icon: PaintbrushIcon\n            }),\n            !IS_UPDATER_DISABLED && UpdaterTab && buildEntry({\n                key: \"vencord_updater\",\n                title: \"Updater\",\n                panelTitle: \"Vencord Updater\",\n                Component: UpdaterTab,\n                Icon: UpdaterIcon\n            }),\n            buildEntry({\n                key: \"vencord_cloud\",\n                title: \"Cloud\",\n                panelTitle: \"Vencord Cloud\",\n                Component: CloudTab,\n                Icon: CloudIcon\n            }),\n            buildEntry({\n                key: \"vencord_backup_restore\",\n                title: \"Backup & Restore\",\n                Component: BackupAndRestoreTab,\n                Icon: BackupRestoreIcon\n            }),\n            IS_DEV && PatchHelperTab && buildEntry({\n                key: \"vencord_patch_helper\",\n                title: \"Patch Helper\",\n                Component: PatchHelperTab,\n                Icon: PatchHelperIcon\n            }),\n            ...this.customEntries.map(buildEntry),\n            // TODO: Remove deprecated customSections in a future update\n            ...this.customSections.map((func, i) => {\n                const { section, element, label } = func(FallbackSectionTypes);\n                if (Object.values(FallbackSectionTypes).includes(section)) return null;\n\n                return buildEntry({\n                    key: `vencord_deprecated_custom_${section}`,\n                    title: label,\n                    Component: element,\n                    Icon: section === \"Vesktop\" ? VesktopSettingsIcon : PlaceholderIcon\n                });\n            })\n        ].filter(isTruthy);\n\n        const vencordSection: SettingsLayoutNode = {\n            key: \"vencord_section\",\n            type: LayoutTypes.SECTION,\n            useTitle: () => \"Vencord Settings\",\n            buildLayout: () => vencordEntries\n        };\n\n        const { settingsLocation } = settings.store;\n\n        const places: Record<SettingsLocation, string> = {\n            top: \"user_section\",\n            aboveNitro: \"billing_section\",\n            belowNitro: \"billing_section\",\n            aboveActivity: \"activity_section\",\n            belowActivity: \"activity_section\",\n            bottom: \"logout_section\"\n        };\n\n        const key = places[settingsLocation] ?? places.top;\n        let idx = layout.findIndex(s => typeof s?.key === \"string\" && s.key === key);\n\n        if (idx === -1) {\n            idx = 2;\n        } else if (settingsLocation.startsWith(\"below\")) {\n            idx += 1;\n        }\n\n        layout.splice(idx, 0, vencordSection);\n\n        return layout;\n    },\n\n    /** @deprecated Use customEntries */\n    customSections: [] as ((SectionTypes: SectionTypes) => any)[],\n    customEntries: [] as EntryOptions[],\n\n    get electronVersion() {\n        return VencordNative.native.getVersions().electron || window.legcord?.electron || null;\n    },\n\n    get chromiumVersion() {\n        try {\n            return VencordNative.native.getVersions().chrome\n                // @ts-expect-error Typescript will add userAgentData IMMEDIATELY\n                || navigator.userAgentData?.brands?.find(b => b.brand === \"Chromium\" || b.brand === \"Google Chrome\")?.version\n                || null;\n        } catch { // inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium\n            return null;\n        }\n    },\n\n    get additionalInfo() {\n        if (IS_DEV) return \" (Dev)\";\n        if (IS_WEB) return \" (Web)\";\n        if (IS_VESKTOP) return ` (Vesktop v${VesktopNative.app.getVersion()})`;\n        if (IS_STANDALONE) return \" (Standalone)\";\n        return \"\";\n    },\n\n    getInfoRows() {\n        const { electronVersion, chromiumVersion, additionalInfo } = this;\n\n        const rows = [`Vencord ${gitHash}${additionalInfo}`];\n\n        if (electronVersion) rows.push(`Electron ${electronVersion}`);\n        if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);\n\n        return rows;\n    },\n\n    getInfoString() {\n        return \"\\n\" + this.getInfoRows().join(\"\\n\");\n    },\n\n    makeInfoElements(Component: ComponentType<PropsWithChildren>, props: PropsWithChildren) {\n        return this.getInfoRows().map((text, i) =>\n            <Component key={i} {...props}>{text}</Component>\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/_core/supportHelper.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport { Card } from \"@components/Card\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Link } from \"@components/Link\";\nimport { openSettingsTabModal, UpdaterTab } from \"@components/settings\";\nimport { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CATEGORY_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from \"@utils/constants\";\nimport { sendMessage } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport { isPluginDev, tryOrElse } from \"@utils/misc\";\nimport { relaunch } from \"@utils/native\";\nimport { onlyOnce } from \"@utils/onlyOnce\";\nimport { makeCodeblock } from \"@utils/text\";\nimport definePlugin from \"@utils/types\";\nimport { checkForUpdates, isOutdated, update } from \"@utils/updater\";\nimport { Channel } from \"@vencord/discord-types\";\nimport { Alerts, Button, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nimport gitHash from \"~git-hash\";\nimport plugins, { PluginMeta } from \"~plugins\";\n\nimport SettingsPlugin from \"./settings\";\n\nconst CodeBlockRe = /```js\\n(.+?)```/s;\n\nconst AdditionalAllowedChannelIds = [\n    \"1024286218801926184\", // Vencord > #bot-spam\n];\n\nconst TrustedRolesIds = [\n    CONTRIB_ROLE_ID, // contributor\n    REGULAR_ROLE_ID, // regular\n    DONOR_ROLE_ID, // donor\n];\n\nconst AsyncFunction = async function () { }.constructor;\n\nconst ShowCurrentGame = getUserSettingLazy<boolean>(\"status\", \"showCurrentGame\")!;\n\nconst isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);\n\nasync function forceUpdate() {\n    const outdated = await checkForUpdates();\n    if (outdated) {\n        await update();\n        relaunch();\n    }\n\n    return outdated;\n}\n\nasync function generateDebugInfoMessage() {\n    const { RELEASE_CHANNEL } = window.GLOBAL_ENV;\n\n    const client = (() => {\n        if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;\n        if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;\n        if (\"legcord\" in window) return `Legcord v${window.legcord.version}`;\n\n        // @ts-expect-error\n        const name = typeof unsafeWindow !== \"undefined\" ? \"UserScript\" : \"Web\";\n        return `${name} (${navigator.userAgent})`;\n    })();\n\n    const info = {\n        Vencord:\n            `v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +\n            `${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat(\"en-GB\", { dateStyle: \"medium\" }).format(BUILD_TIMESTAMP)}`,\n        Client: `${RELEASE_CHANNEL} ~ ${client}`,\n        Platform: navigator.platform\n    };\n\n    if (IS_DISCORD_DESKTOP) {\n        info[\"Last Crash Reason\"] = (await tryOrElse(() => DiscordNative.processUtils.getLastCrash(), undefined))?.rendererCrashReason ?? \"N/A\";\n    }\n\n    const commonIssues = {\n        \"Activity Sharing disabled\": tryOrElse(() => !ShowCurrentGame.getSetting(), false),\n        \"Vencord DevBuild\": !IS_STANDALONE,\n        \"Has UserPlugins\": Object.values(PluginMeta).some(m => m.userPlugin),\n        \"More than two weeks out of date\": BUILD_TIMESTAMP < Date.now() - 12096e5,\n    };\n\n    let content = `>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join(\"\\n\")}`;\n    content += \"\\n\" + Object.entries(commonIssues)\n        .filter(([, v]) => v).map(([k]) => `⚠️ ${k}`)\n        .join(\"\\n\");\n\n    return content.trim();\n}\n\nfunction generatePluginList() {\n    const isApiPlugin = (plugin: string) => plugin.endsWith(\"API\") || plugins[plugin].required;\n\n    const enabledPlugins = Object.keys(plugins)\n        .filter(p => isPluginEnabled(p) && !isApiPlugin(p));\n\n    const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin);\n    const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin);\n\n\n    let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\\n${makeCodeblock(enabledStockPlugins.join(\", \"))}`;\n\n    if (enabledUserPlugins.length) {\n        content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\\n${makeCodeblock(enabledUserPlugins.join(\", \"))}`;\n    }\n\n    return content;\n}\n\nconst checkForUpdatesOnce = onlyOnce(checkForUpdates);\n\nconst settings = definePluginSettings({}).withPrivateSettings<{\n    dismissedDevBuildWarning?: boolean;\n}>();\n\nexport default definePlugin({\n    name: \"SupportHelper\",\n    required: true,\n    description: \"Helps us provide support to you\",\n    authors: [Devs.Ven],\n    dependencies: [\"UserSettingsAPI\"],\n\n    settings,\n\n    patches: [{\n        find: \"#{intl::BEGINNING_DM}\",\n        replacement: {\n            match: /#{intl::BEGINNING_DM},{.+?}\\),(?=.{0,300}(\\i)\\.isMultiUserDM)/,\n            replace: \"$& $self.renderContributorDmWarningCard({ channel: $1 }),\"\n        }\n    }],\n\n    commands: [\n        {\n            name: \"vencord-debug\",\n            description: \"Send Vencord debug info\",\n            predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),\n            execute: async () => ({ content: await generateDebugInfoMessage() })\n        },\n        {\n            name: \"vencord-plugins\",\n            description: \"Send Vencord plugin list\",\n            predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),\n            execute: () => ({ content: generatePluginList() })\n        }\n    ],\n\n    flux: {\n        async CHANNEL_SELECT({ channelId }) {\n            const isSupportChannel = channelId === SUPPORT_CHANNEL_ID || ChannelStore.getChannel(channelId)?.parent_id === SUPPORT_CATEGORY_ID;\n            if (!isSupportChannel) return;\n\n            const selfId = UserStore.getCurrentUser()?.id;\n            if (!selfId || isPluginDev(selfId)) return;\n\n            if (!IS_UPDATER_DISABLED) {\n                await checkForUpdatesOnce().catch(() => { });\n\n                if (isOutdated) {\n                    return Alerts.show({\n                        title: \"Hold on!\",\n                        body: <div>\n                            <Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>\n                            <Forms.FormText className={Margins.top8}>\n                                Please first update before asking for support!\n                            </Forms.FormText>\n                        </div>,\n                        onCancel: () => openSettingsTabModal(UpdaterTab!),\n                        cancelText: \"View Updates\",\n                        confirmText: \"Update & Restart Now\",\n                        onConfirm: forceUpdate,\n                        secondaryConfirmText: \"I know what I'm doing or I can't update\"\n                    });\n                }\n            }\n\n            const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;\n            if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;\n\n            if (!IS_WEB && IS_UPDATER_DISABLED) {\n                return Alerts.show({\n                    title: \"Hold on!\",\n                    body: <div>\n                        <Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>\n                        <Forms.FormText className={Margins.top8}>\n                            Please either switch to an <Link href=\"https://vencord.dev/download\">officially supported version of Vencord</Link>, or\n                            contact your package maintainer for support instead.\n                        </Forms.FormText>\n                    </div>\n                });\n            }\n\n            if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {\n                return Alerts.show({\n                    title: \"Hold on!\",\n                    body: <div>\n                        <Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>\n\n                        <Forms.FormText className={Margins.top8}>\n                            We only provide support for <Link href=\"https://vencord.dev/download\">official builds</Link>.\n                            Either <Link href=\"https://vencord.dev/download\">switch to an official build</Link> or figure your issue out yourself.\n                        </Forms.FormText>\n\n                        <Text variant=\"text-md/bold\" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>\n                    </div>,\n                    confirmText: \"Understood\",\n                    secondaryConfirmText: \"Don't show again\",\n                    onConfirmSecondary: () => settings.store.dismissedDevBuildWarning = true\n                });\n            }\n        }\n    },\n\n    renderMessageAccessory(props) {\n        const buttons = [] as JSX.Element[];\n\n        const shouldAddUpdateButton =\n            !IS_UPDATER_DISABLED\n            && (\n                (props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||\n                (props.channel.parent_id === SUPPORT_CATEGORY_ID && props.message.author.id === VENBOT_USER_ID)\n            )\n            && props.message.content?.toLowerCase().includes(\"update\");\n\n        if (shouldAddUpdateButton) {\n            buttons.push(\n                <Button\n                    key=\"vc-update\"\n                    color={Button.Colors.GREEN}\n                    onClick={async () => {\n                        try {\n                            if (await forceUpdate())\n                                showToast(\"Success! Restarting...\", Toasts.Type.SUCCESS);\n                            else\n                                showToast(\"Already up to date!\", Toasts.Type.MESSAGE);\n                        } catch (e) {\n                            new Logger(this.name).error(\"Error while updating:\", e);\n                            showToast(\"Failed to update :(\", Toasts.Type.FAILURE);\n                        }\n                    }}\n                >\n                    Update Now\n                </Button>\n            );\n        }\n\n        if (props.channel.parent_id === SUPPORT_CATEGORY_ID && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel)) {\n            if (props.message.content.includes(\"/vencord-debug\") || props.message.content.includes(\"/vencord-plugins\")) {\n                buttons.push(\n                    <Button\n                        key=\"vc-dbg\"\n                        color={Button.Colors.PRIMARY}\n                        onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}\n                    >\n                        Run /vencord-debug\n                    </Button>,\n                    <Button\n                        key=\"vc-plg-list\"\n                        color={Button.Colors.PRIMARY}\n                        onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}\n                    >\n                        Run /vencord-plugins\n                    </Button>\n                );\n            }\n\n            if (props.message.author.id === VENBOT_USER_ID) {\n                const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || \"\");\n                if (match) {\n                    buttons.push(\n                        <Button\n                            key=\"vc-run-snippet\"\n                            onClick={async () => {\n                                try {\n                                    await AsyncFunction(match[1])();\n                                    showToast(\"Success!\", Toasts.Type.SUCCESS);\n                                } catch (e) {\n                                    new Logger(this.name).error(\"Error while running snippet:\", e);\n                                    showToast(\"Failed to run snippet :(\", Toasts.Type.FAILURE);\n                                }\n                            }}\n                        >\n                            Run Snippet\n                        </Button>\n                    );\n                }\n            }\n        }\n\n        return buttons.length\n            ? <Flex>{buttons}</Flex>\n            : null;\n    },\n\n    renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {\n        const userId = channel.getRecipientId();\n        if (!isPluginDev(userId)) return null;\n        if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;\n\n        return (\n            <Card variant=\"warning\" className={Margins.top8} defaultPadding>\n                Please do not private message Vencord plugin developers for support!\n                <br />\n                Instead, use the Vencord support channel: {Parser.parse(\"https://discord.com/channels/1015060230222131221/1026515880080842772\")}\n                {!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && \" (Click the link to join)\"}\n            </Card>\n        );\n    }, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/accountPanelServerProfile/README.md",
    "content": "# AccountPanelServerProfile\n\nRight click your account panel in the bottom left to view your profile in the current server\n\n![](https://github.com/user-attachments/assets/3228497d-488f-479c-93d2-a32ccdb08f0f)\n\n![](https://github.com/user-attachments/assets/6fc45363-d95f-4810-812f-2f9fb28b41b5)\n"
  },
  {
    "path": "src/plugins/accountPanelServerProfile/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { getCurrentChannel } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { User } from \"@vencord/discord-types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { ContextMenuApi, Menu } from \"@webpack/common\";\n\ninterface UserProfileProps {\n    popoutProps: Record<string, any>;\n    currentUser: User;\n    originalRenderPopout: () => React.ReactNode;\n}\n\nconst UserProfile = findComponentByCodeLazy(\".POPOUT,user\");\n\nlet openAlternatePopout = false;\nlet accountPanelRef: React.RefObject<HTMLDivElement | null> = { current: null };\n\nconst AccountPanelContextMenu = ErrorBoundary.wrap(() => {\n    const { prioritizeServerProfile } = settings.use([\"prioritizeServerProfile\"]);\n\n    return (\n        <Menu.Menu\n            navId=\"vc-ap-server-profile\"\n            onClose={ContextMenuApi.closeContextMenu}\n        >\n            <Menu.MenuItem\n                id=\"vc-ap-view-alternate-popout\"\n                label={prioritizeServerProfile ? \"View Account Profile\" : \"View Server Profile\"}\n                disabled={getCurrentChannel()?.getGuildId() == null}\n                action={e => {\n                    openAlternatePopout = true;\n                    accountPanelRef.current?.click();\n                }}\n            />\n            <Menu.MenuCheckboxItem\n                id=\"vc-ap-prioritize-server-profile\"\n                label=\"Prioritize Server Profile\"\n                checked={prioritizeServerProfile}\n                action={() => settings.store.prioritizeServerProfile = !prioritizeServerProfile}\n            />\n        </Menu.Menu>\n    );\n}, { noop: true });\n\nconst settings = definePluginSettings({\n    prioritizeServerProfile: {\n        type: OptionType.BOOLEAN,\n        description: \"Prioritize Server Profile when left clicking your account panel\",\n        default: false\n    }\n});\n\nexport default definePlugin({\n    name: \"AccountPanelServerProfile\",\n    description: \"Right click your account panel in the bottom left to view your profile in the current server\",\n    authors: [Devs.Nuckyz, Devs.relitrix],\n    settings,\n\n    patches: [\n        {\n            find: \"handleOpenSettingsContextMenu=\",\n            group: true,\n            replacement: [\n                {\n                    match: /(\\.AVATAR,children:.+?renderPopout:(\\i)=>){(.+?)}(?=,position)(?<=currentUser:(\\i).+?)/,\n                    replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`\n                },\n                {\n                    match: /\\.AVATAR,children:.+?onRequestClose:\\(\\)=>\\{/,\n                    replace: \"$&$self.onPopoutClose();\"\n                },\n                {\n                    match: /ref:(\\i),style:\\i(?=.{0,250}#{intl::USER_PROFILE_ACCOUNT_POPOUT_BUTTON_A11Y_LABEL})/,\n                    replace: \"$&,onContextMenu:($self.grabRef($1),$self.openAccountPanelContextMenu)\"\n                }\n            ]\n        }\n    ],\n\n    get accountPanelRef() {\n        return accountPanelRef;\n    },\n\n    grabRef(ref: React.RefObject<HTMLDivElement>) {\n        accountPanelRef = ref;\n        return ref;\n    },\n\n    openAccountPanelContextMenu(event: React.UIEvent) {\n        ContextMenuApi.openContextMenu(event, AccountPanelContextMenu);\n    },\n\n    onPopoutClose() {\n        openAlternatePopout = false;\n    },\n\n    UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalRenderPopout }: UserProfileProps) => {\n        if (\n            (settings.store.prioritizeServerProfile && openAlternatePopout) ||\n            (!settings.store.prioritizeServerProfile && !openAlternatePopout)\n        ) {\n            return originalRenderPopout();\n        }\n\n        const currentChannel = getCurrentChannel();\n        if (currentChannel?.getGuildId() == null || !UserProfile.$$vencordGetWrappedComponent()) {\n            return originalRenderPopout();\n        }\n\n        return (\n            <UserProfile\n                {...popoutProps}\n                user={currentUser}\n                currentUser={currentUser}\n                guildId={currentChannel.getGuildId()}\n                channelId={currentChannel.id}\n            />\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/alwaysAnimate/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"AlwaysAnimate\",\n    description: \"Animates anything that can be animated\",\n    authors: [Devs.FieryFlames],\n\n    patches: [\n        {\n            find: \"canAnimate:\",\n            all: true,\n            // Some modules match the find but the replacement is returned untouched\n            noWarn: true,\n            replacement: {\n                match: /canAnimate:.+?([,}].*?\\))/g,\n                replace: (m, rest) => {\n                    const destructuringMatch = rest.match(/}=.+/);\n                    if (destructuringMatch == null) return `canAnimate:!0${rest}`;\n                    return m;\n                }\n            }\n        },\n        {\n            // Status emojis\n            find: \"#{intl::GUILD_OWNER}),children:\",\n            replacement: {\n                match: /(\\.CUSTOM_STATUS.+?animateEmoji:)\\i/,\n                replace: \"$1!0\"\n            }\n        },\n        {\n            // Guild Banner\n            find: \"#{intl::DISCOVERABLE_GUILD_HEADER_PUBLIC_INFO}\",\n            replacement: {\n                match: /(guildBanner:\\i,animate:)\\i(?=}\\):null)/,\n                replace: \"$1!0\"\n            }\n        },\n        {\n            // Nameplates\n            find: \".MINI_PREVIEW,[\",\n            replacement: {\n                match: /animate:\\i,loop:/,\n                replace: \"animate:true,loop:true,_loop:\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/alwaysExpandRoles/README.md",
    "content": "# Always Expand Roles\n\nAlways expands the role list in profile popouts\n"
  },
  {
    "path": "src/plugins/alwaysExpandRoles/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nmigratePluginSettings(\"AlwaysExpandRoles\", \"ShowAllRoles\");\nexport default definePlugin({\n    name: \"AlwaysExpandRoles\",\n    description: \"Always expands the role list in profile popouts\",\n    authors: [Devs.surgedevs],\n    patches: [\n        {\n            find: \"hasDeveloperContextMenu:\",\n            replacement: [\n                {\n                    match: /(?<=\\?\\i\\.current\\[\\i\\].{0,100}?)useState\\(!1\\)/,\n                    replace: \"useState(!0)\"\n                },\n                {\n                    // Fix not calculating non-expanded roles because the above patch makes the default \"expanded\",\n                    // which makes the collapse button never show up and calculation never occur\n                    match: /(?<=useLayoutEffect\\(\\(\\)=>\\{if\\()\\i/,\n                    replace: \"false\"\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/alwaysTrust/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    domain: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Remove the untrusted domain popup when opening links\",\n        restartNeeded: true\n    },\n    file: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Remove the 'Potentially Dangerous Download' popup when opening links\",\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"AlwaysTrust\",\n    description: \"Removes the annoying untrusted domain and suspicious file popup\",\n    authors: [Devs.zt, Devs.Trwy],\n    patches: [\n        {\n            find: '=\"MaskedLinkStore\",',\n            replacement: {\n                match: /(?<=isTrustedDomain\\(\\i\\){)return \\i\\(\\i\\)/,\n                replace: \"return true\"\n            },\n            predicate: () => settings.store.domain\n        },\n        {\n            find: \"bitbucket.org\",\n            replacement: {\n                match: /function \\i\\(\\i\\){(?=.{0,30}pathname:\\i)/,\n                replace: \"$&return null;\"\n            },\n            predicate: () => settings.store.file\n        }\n    ],\n    settings\n});\n"
  },
  {
    "path": "src/plugins/anonymiseFileNames/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { CloudUpload } from \"@vencord/discord-types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { useState } from \"@webpack/common\";\n\nconst ActionBarIcon = findByCodeLazy(\"Children.map\", \"isValidElement\", \"dangerous:\");\n\nconst enum Methods {\n    Random,\n    Consistent,\n    Timestamp,\n}\n\nconst ANONYMISE_UPLOAD_SYMBOL = Symbol(\"vcAnonymise\");\nconst tarExtMatcher = /\\.tar\\.\\w+$/;\n\nconst settings = definePluginSettings({\n    anonymiseByDefault: {\n        description: \"Whether to anonymise file names by default\",\n        type: OptionType.BOOLEAN,\n        default: true,\n    },\n    method: {\n        description: \"Anonymising method\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Random Characters\", value: Methods.Random, default: true },\n            { label: \"Consistent\", value: Methods.Consistent },\n            { label: \"Timestamp\", value: Methods.Timestamp },\n        ],\n    },\n    randomisedLength: {\n        description: \"Random characters length\",\n        type: OptionType.NUMBER,\n        default: 7,\n        disabled: () => settings.store.method !== Methods.Random,\n    },\n    consistent: {\n        description: \"Consistent filename\",\n        type: OptionType.STRING,\n        default: \"image\",\n        disabled: () => settings.store.method !== Methods.Consistent,\n    },\n});\n\nexport default definePlugin({\n    name: \"AnonymiseFileNames\",\n    authors: [Devs.fawn],\n    description: \"Anonymise uploaded file names\",\n    settings,\n\n    patches: [\n        {\n            find: \"async uploadFiles(\",\n            replacement: [\n                {\n                    match: /async uploadFiles\\((\\i)\\){/,\n                    replace: \"$&$1.forEach($self.anonymise);\"\n                }\n            ],\n        },\n        {\n            find: \"#{intl::ATTACHMENT_UTILITIES_SPOILER}\",\n            replacement: {\n                match: /(?<=children:\\[)(?=.{10,80}tooltip:.{0,100}#{intl::ATTACHMENT_UTILITIES_SPOILER})/,\n                replace: \"arguments[0].canEdit!==false?$self.AnonymiseUploadButton(arguments[0]):null,\"\n            },\n        },\n    ],\n\n    AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: CloudUpload; }) => {\n        const [anonymise, setAnonymise] = useState(upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault);\n\n        function onToggleAnonymise() {\n            upload[ANONYMISE_UPLOAD_SYMBOL] = !anonymise;\n            setAnonymise(!anonymise);\n        }\n\n        return (\n            <ActionBarIcon\n                tooltip={anonymise ? \"Using anonymous file name\" : \"Using normal file name\"}\n                onClick={onToggleAnonymise}\n            >\n                {anonymise\n                    ? <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M17.06 13C15.2 13 13.64 14.33 13.24 16.1C12.29 15.69 11.42 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C19.23 21 21 19.21 21 17C21 14.79 19.23 13 17.06 13M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17S5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17S8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17S15.5 14.14 17.06 14.14C18.62 14.14 19.88 15.42 19.88 17S18.61 19.86 17.06 19.86M22 10.5H2V12H22V10.5M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z\" /></svg>\n                    : <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" style={{ transform: \"scale(-1,1)\" }}><path fill=\"currentColor\" d=\"M22.11 21.46L2.39 1.73L1.11 3L6.31 8.2L6 9H7.11L8.61 10.5H2V12H10.11L13.5 15.37C13.38 15.61 13.3 15.85 13.24 16.1C12.29 15.69 11.41 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C17.66 21 18.22 20.86 18.72 20.61L20.84 22.73L22.11 21.46M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17C4.13 15.42 5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17C9.75 18.58 8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17C14.25 16.74 14.29 16.5 14.36 16.25L17.84 19.73C17.59 19.81 17.34 19.86 17.06 19.86M22 12H15.2L13.7 10.5H22V12M17.06 13C19.23 13 21 14.79 21 17C21 17.25 20.97 17.5 20.93 17.73L19.84 16.64C19.68 15.34 18.66 14.32 17.38 14.17L16.29 13.09C16.54 13.03 16.8 13 17.06 13M12.2 9L7.72 4.5L8.43 2.68C8.63 2.17 9.19 1.89 9.72 2.04L9.77 2.05L12 2.79L14.22 2.05C14.75 1.88 15.32 2.14 15.54 2.63L15.56 2.68L18 9H12.2Z\" /></svg>\n                }\n            </ActionBarIcon>\n        );\n    }, { noop: true }),\n\n    anonymise(upload: CloudUpload) {\n        if ((upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault) === false) {\n            return;\n        }\n\n        const originalFileName = upload.filename;\n        const tarMatch = tarExtMatcher.exec(originalFileName);\n        const extIdx = tarMatch?.index ?? originalFileName.lastIndexOf(\".\");\n        const ext = extIdx !== -1 ? originalFileName.slice(extIdx) : \"\";\n\n        const newFilename = (() => {\n            switch (settings.store.method) {\n                case Methods.Random:\n                    const chars = \"0123456789bdfhjkmnpqrstvwxz\";\n                    return Array.from(\n                        { length: settings.store.randomisedLength },\n                        () => chars[Math.floor(Math.random() * chars.length)]\n                    ).join(\"\") + ext;\n                case Methods.Consistent:\n                    return settings.store.consistent + ext;\n                case Methods.Timestamp:\n                    return Date.now() + ext;\n            }\n        })();\n\n        upload.filename = newFilename;\n    }\n});\n"
  },
  {
    "path": "src/plugins/appleMusic.desktop/README.md",
    "content": "# AppleMusicRichPresence\n\nThis plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)\n\n![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)\n\n## Configuration\n\nFor the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.\n"
  },
  {
    "path": "src/plugins/appleMusic.desktop/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport definePlugin, { OptionType, PluginNative, ReporterTestable } from \"@utils/types\";\nimport { Activity, ActivityAssets, ActivityButton } from \"@vencord/discord-types\";\nimport { ActivityFlags, ActivityStatusDisplayType, ActivityType } from \"@vencord/discord-types/enums\";\nimport { ApplicationAssetUtils, FluxDispatcher, Forms } from \"@webpack/common\";\n\nconst Native = VencordNative.pluginHelpers.AppleMusicRichPresence as PluginNative<typeof import(\"./native\")>;\n\nexport interface TrackData {\n    name: string;\n    album?: string;\n    artist?: string;\n\n    appleMusicLink?: string;\n    songLink?: string;\n\n    albumArtwork?: string;\n    artistArtwork?: string;\n\n    playerPosition?: number;\n    duration?: number;\n}\n\nconst enum AssetImageType {\n    Album = \"Album\",\n    Artist = \"Artist\",\n    Disabled = \"Disabled\"\n}\n\nconst applicationId = \"1239490006054207550\";\n\nfunction setActivity(activity: Activity | null) {\n    FluxDispatcher.dispatch({\n        type: \"LOCAL_ACTIVITY_UPDATE\",\n        activity,\n        socketId: \"AppleMusic\",\n    });\n}\n\nconst settings = definePluginSettings({\n    activityType: {\n        type: OptionType.SELECT,\n        description: \"Which type of activity\",\n        options: [\n            { label: \"Playing\", value: ActivityType.PLAYING, default: true },\n            { label: \"Listening\", value: ActivityType.LISTENING }\n        ],\n    },\n    statusDisplayType: {\n        description: \"Show the track / artist name in the member list\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Don't show (shows generic listening message)\",\n                value: \"off\",\n                default: true\n            },\n            {\n                label: \"Show artist name\",\n                value: \"artist\"\n            },\n            {\n                label: \"Show track name\",\n                value: \"track\"\n            }\n        ]\n    },\n    refreshInterval: {\n        type: OptionType.SLIDER,\n        description: \"The interval between activity refreshes (seconds)\",\n        markers: [1, 2, 2.5, 3, 5, 10, 15],\n        default: 5,\n        restartNeeded: true,\n    },\n    enableTimestamps: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether or not to enable timestamps\",\n        default: true,\n    },\n    enableButtons: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether or not to enable buttons\",\n        default: true,\n    },\n    nameString: {\n        type: OptionType.STRING,\n        description: \"Activity name format string\",\n        default: \"Apple Music\"\n    },\n    detailsString: {\n        type: OptionType.STRING,\n        description: \"Activity details format string\",\n        default: \"{name}\"\n    },\n    stateString: {\n        type: OptionType.STRING,\n        description: \"Activity state format string\",\n        default: \"{artist} · {album}\"\n    },\n    largeImageType: {\n        type: OptionType.SELECT,\n        description: \"Activity assets large image type\",\n        options: [\n            { label: \"Album artwork\", value: AssetImageType.Album, default: true },\n            { label: \"Artist artwork\", value: AssetImageType.Artist },\n            { label: \"Disabled\", value: AssetImageType.Disabled }\n        ],\n    },\n    largeTextString: {\n        type: OptionType.STRING,\n        description: \"Activity assets large text format string\",\n        default: \"{album}\"\n    },\n    smallImageType: {\n        type: OptionType.SELECT,\n        description: \"Activity assets small image type\",\n        options: [\n            { label: \"Album artwork\", value: AssetImageType.Album },\n            { label: \"Artist artwork\", value: AssetImageType.Artist, default: true },\n            { label: \"Disabled\", value: AssetImageType.Disabled }\n        ],\n    },\n    smallTextString: {\n        type: OptionType.STRING,\n        description: \"Activity assets small text format string\",\n        default: \"{artist}\"\n    },\n});\n\nfunction customFormat(formatStr: string, data: TrackData) {\n    return formatStr\n        .replaceAll(\"{name}\", data.name)\n        .replaceAll(\"{album}\", data.album ?? \"\")\n        .replaceAll(\"{artist}\", data.artist ?? \"\");\n}\n\nfunction getImageAsset(type: AssetImageType, data: TrackData) {\n    const source = type === AssetImageType.Album\n        ? data.albumArtwork\n        : data.artistArtwork;\n\n    if (!source) return undefined;\n\n    return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);\n}\n\nexport default definePlugin({\n    name: \"AppleMusicRichPresence\",\n    description: \"Discord rich presence for your Apple Music!\",\n    authors: [Devs.RyanCaoDev],\n    hidden: !IS_MAC,\n    reporterTestable: ReporterTestable.None,\n\n    settingsAboutComponent() {\n        return <>\n            <Forms.FormText>\n                For the customizable activity format strings, you can use several special strings to include track data in activities!{\" \"}\n                <code>{\"{name}\"}</code> is replaced with the track name; <code>{\"{artist}\"}</code> is replaced with the artist(s)' name(s); and <code>{\"{album}\"}</code> is replaced with the album name.\n            </Forms.FormText>\n        </>;\n    },\n\n    settings,\n\n    start() {\n        this.updatePresence();\n        this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);\n    },\n\n    stop() {\n        clearInterval(this.updateInterval);\n        FluxDispatcher.dispatch({ type: \"LOCAL_ACTIVITY_UPDATE\", activity: null });\n    },\n\n    updatePresence() {\n        this.getActivity().then(activity => { setActivity(activity); });\n    },\n\n    async getActivity(): Promise<Activity | null> {\n        const trackData = await Native.fetchTrackData();\n        if (!trackData) return null;\n\n        const [largeImageAsset, smallImageAsset] = await Promise.all([\n            getImageAsset(settings.store.largeImageType, trackData),\n            getImageAsset(settings.store.smallImageType, trackData)\n        ]);\n\n        const assets: ActivityAssets = {};\n\n        const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);\n\n        if (settings.store.largeImageType !== AssetImageType.Disabled) {\n            assets.large_image = largeImageAsset;\n            if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);\n        }\n\n        if (settings.store.smallImageType !== AssetImageType.Disabled) {\n            assets.small_image = smallImageAsset;\n            if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);\n        }\n\n        const buttons: ActivityButton[] = [];\n\n        if (settings.store.enableButtons) {\n            if (trackData.appleMusicLink)\n                buttons.push({\n                    label: \"Listen on Apple Music\",\n                    url: trackData.appleMusicLink,\n                });\n\n            if (trackData.songLink)\n                buttons.push({\n                    label: \"View on SongLink\",\n                    url: trackData.songLink,\n                });\n        }\n\n        return {\n            application_id: applicationId,\n\n            name: customFormat(settings.store.nameString, trackData),\n            details: customFormat(settings.store.detailsString, trackData),\n            state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),\n\n            timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {\n                start: Date.now() - (trackData.playerPosition * 1000),\n                end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),\n            } : undefined,\n\n            assets,\n\n            buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,\n            metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,\n\n            type: settings.store.activityType,\n            status_display_type: {\n                \"off\": ActivityStatusDisplayType.NAME,\n                \"artist\": ActivityStatusDisplayType.STATE,\n                \"track\": ActivityStatusDisplayType.DETAILS\n            }[settings.store.statusDisplayType],\n            flags: ActivityFlags.INSTANCE,\n        };\n    }\n});\n"
  },
  {
    "path": "src/plugins/appleMusic.desktop/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { VENCORD_USER_AGENT } from \"@shared/vencordUserAgent\";\nimport { execFile } from \"child_process\";\nimport { promisify } from \"util\";\n\nimport type { TrackData } from \".\";\n\nconst exec = promisify(execFile);\n\nasync function applescript(cmds: string[]) {\n    const { stdout } = await exec(\"osascript\", cmds.map(c => [\"-e\", c]).flat());\n    return stdout;\n}\n\ninterface RemoteData {\n    appleMusicLink?: string,\n    songLink?: string,\n    albumArtwork?: string,\n    artistArtwork?: string;\n}\n\nlet cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;\n\nasync function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {\n    if (id === cachedRemoteData?.id) {\n        if (\"data\" in cachedRemoteData) return cachedRemoteData.data;\n        if (\"failures\" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;\n    }\n\n    try {\n        const dataUrl = new URL(\"https://itunes.apple.com/search\");\n        dataUrl.searchParams.set(\"term\", `${name} ${artist} ${album}`);\n        dataUrl.searchParams.set(\"media\", \"music\");\n        dataUrl.searchParams.set(\"entity\", \"song\");\n\n        const songData = await fetch(dataUrl, {\n            headers: {\n                \"user-agent\": VENCORD_USER_AGENT,\n            },\n        })\n            .then(r => r.json())\n            .then(data => data.results.find(song => song.collectionName === album) || data.results[0]);\n\n        const artistArtworkURL = await fetch(songData.artistViewUrl)\n            .then(r => r.text())\n            .then(data => {\n                const match = data.match(/<meta property=\"og:image\" content=\"(.+?)\">/);\n                return match ? match[1].replace(/[0-9]+x.+/, \"220x220bb-60.png\") : undefined;\n            })\n            .catch(() => void 0);\n\n        cachedRemoteData = {\n            id,\n            data: {\n                appleMusicLink: songData.trackViewUrl,\n                songLink: `https://song.link/i/${new URL(songData.trackViewUrl).searchParams.get(\"i\")}`,\n                albumArtwork: (songData.artworkUrl100).replace(\"100x100\", \"512x512\"),\n                artistArtwork: artistArtworkURL\n            }\n        };\n\n        return cachedRemoteData.data;\n    } catch (e) {\n        console.error(\"[AppleMusicRichPresence] Failed to fetch remote data:\", e);\n        cachedRemoteData = {\n            id,\n            failures: (id === cachedRemoteData?.id && \"failures\" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1\n        };\n        return null;\n    }\n}\n\nexport async function fetchTrackData(): Promise<TrackData | null> {\n    try {\n        await exec(\"pgrep\", [\"^Music$\"]);\n    } catch (error) {\n        return null;\n    }\n\n    const playerState = await applescript(['tell application \"Music\"', \"get player state\", \"end tell\"])\n        .then(out => out.trim());\n    if (playerState !== \"playing\") return null;\n\n    const playerPosition = await applescript(['tell application \"Music\"', \"get player position\", \"end tell\"])\n        .then(text => Number.parseFloat(text.trim()));\n\n    const stdout = await applescript([\n        'set output to \"\"',\n        'tell application \"Music\"',\n        \"set t_id to database id of current track\",\n        \"set t_name to name of current track\",\n        \"set t_album to album of current track\",\n        \"set t_artist to artist of current track\",\n        \"set t_duration to duration of current track\",\n        'set output to \"\" & t_id & \"\\\\n\" & t_name & \"\\\\n\" & t_album & \"\\\\n\" & t_artist & \"\\\\n\" & t_duration',\n        \"end tell\",\n        \"return output\"\n    ]);\n\n    const [id, name, album, artist, durationStr] = stdout.split(\"\\n\").filter(k => !!k);\n    const duration = Number.parseFloat(durationStr);\n\n    const remoteData = await fetchRemoteData({ id, name, artist, album });\n\n    return { name, album, artist, playerPosition, duration, ...remoteData };\n}\n"
  },
  {
    "path": "src/plugins/arRPC.web/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 OpenAsar\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { popNotice, showNotice } from \"@api/Notices\";\nimport { Link } from \"@components/Link\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { ReporterTestable } from \"@utils/types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from \"@webpack/common\";\n\nconst fetchApplicationsRPC = findByCodeLazy('\"Invalid Origin\"', \".application\");\n\nasync function lookupAsset(applicationId: string, key: string): Promise<string> {\n    return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];\n}\n\nconst apps: any = {};\nasync function lookupApp(applicationId: string): Promise<string> {\n    const socket: any = {};\n    await fetchApplicationsRPC(socket, applicationId);\n    return socket.application;\n}\n\nlet ws: WebSocket;\nexport default definePlugin({\n    name: \"WebRichPresence (arRPC)\",\n    description: \"Client plugin for arRPC to enable RPC on Discord Web (experimental)\",\n    authors: [Devs.Ducko],\n    reporterTestable: ReporterTestable.None,\n    hidden: IS_VESKTOP || \"legcord\" in window,\n\n    settingsAboutComponent: () => (\n        <>\n            <Forms.FormTitle tag=\"h3\">How to use arRPC</Forms.FormTitle>\n            <Forms.FormText>\n                <Link href=\"https://github.com/OpenAsar/arrpc/tree/main#server\">Follow the instructions in the GitHub repo</Link> to get the server running, and then enable the plugin.\n            </Forms.FormText>\n        </>\n    ),\n\n    async handleEvent(e: MessageEvent<any>) {\n        const data = JSON.parse(e.data);\n\n        const { activity } = data;\n        const assets = activity?.assets;\n\n        if (assets?.large_image) assets.large_image = await lookupAsset(activity.application_id, assets.large_image);\n        if (assets?.small_image) assets.small_image = await lookupAsset(activity.application_id, assets.small_image);\n\n        if (activity) {\n            const appId = activity.application_id;\n            apps[appId] ||= await lookupApp(appId);\n\n            const app = apps[appId];\n            activity.name ||= app.name;\n        }\n\n        FluxDispatcher.dispatch({ type: \"LOCAL_ACTIVITY_UPDATE\", ...data });\n    },\n\n    async start() {\n        if (ws) ws.close();\n        ws = new WebSocket(\"ws://127.0.0.1:1337\"); // try to open WebSocket\n\n        ws.onmessage = this.handleEvent;\n\n        const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 5000)); // check if open after 5s\n        if (!connectionSuccessful) {\n            showNotice(\"Failed to connect to arRPC, is it running?\", \"Retry\", () => { // show notice about failure to connect, with retry/ignore\n                popNotice();\n                this.start();\n            });\n            return;\n        }\n\n        Toasts.show({ // show toast on success\n            message: \"Connected to arRPC\",\n            type: Toasts.Type.SUCCESS,\n            id: Toasts.genId(),\n            options: {\n                duration: 1000,\n                position: Toasts.Position.BOTTOM\n            }\n        });\n    },\n\n    stop() {\n        FluxDispatcher.dispatch({ type: \"LOCAL_ACTIVITY_UPDATE\", activity: null }); // clear status\n        ws?.close(); // close WebSocket\n    }\n});\n"
  },
  {
    "path": "src/plugins/autoDndWhilePlaying.discordDesktop/README.md",
    "content": "# AutoDNDWhilePlaying\n\nThis plugin automatically updates your online status (online, idle, dnd) when launching games.\n\nIt will change your status back to the prior status when the game is closed.\n"
  },
  {
    "path": "src/plugins/autoDndWhilePlaying.discordDesktop/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nlet savedStatus: string | null;\n\nconst StatusSettings = getUserSettingLazy<string>(\"status\", \"status\")!;\n\nconst settings = definePluginSettings({\n    statusToSet: {\n        type: OptionType.SELECT,\n        description: \"Status to set while playing a game\",\n        options: [\n            {\n                label: \"Online\",\n                value: \"online\",\n            },\n            {\n                label: \"Idle\",\n                value: \"idle\",\n            },\n            {\n                label: \"Do Not Disturb\",\n                value: \"dnd\",\n                default: true\n            },\n            {\n                label: \"Invisible\",\n                value: \"invisible\",\n            }\n        ]\n    }\n});\n\nexport default definePlugin({\n    name: \"AutoDNDWhilePlaying\",\n    description: \"Automatically updates your online status (online, idle, dnd) when launching games\",\n    authors: [Devs.thororen],\n    settings,\n    flux: {\n        RUNNING_GAMES_CHANGE({ games }) {\n            const status = StatusSettings.getSetting();\n\n            if (games.length > 0) {\n                if (status !== settings.store.statusToSet) {\n                    savedStatus = status;\n                    StatusSettings.updateSetting(settings.store.statusToSet);\n                }\n            } else if (savedStatus && savedStatus !== settings.store.statusToSet) {\n                StatusSettings.updateSetting(savedStatus);\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterFolders/FolderSideBar.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { Animations, ChannelRTCStore, useStateFromStores } from \"@webpack/common\";\nimport type { CSSProperties } from \"react\";\n\nimport { ExpandedGuildFolderStore, settings, SortedGuildStore } from \".\";\n\nconst GuildsBar = findComponentByCodeLazy('(\"guildsnav\")');\n\nfunction getExpandedFolderIds() {\n    const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders();\n    const folders = SortedGuildStore.getGuildFolders();\n\n    const expandedFolderIds = new Set<string>();\n\n    for (const folder of folders) {\n        if (expandedFolders.has(folder.folderId) && folder.guildIds?.length) {\n            expandedFolderIds.add(folder.folderId);\n        }\n    }\n\n    return expandedFolderIds;\n}\n\nexport default ErrorBoundary.wrap(guildsBarProps => {\n    const expandedFolderIds = useStateFromStores([ExpandedGuildFolderStore, SortedGuildStore], () => getExpandedFolderIds());\n    const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());\n\n    const Sidebar = (\n        <GuildsBar\n            {...guildsBarProps}\n            isBetterFolders={true}\n            betterFoldersExpandedIds={expandedFolderIds}\n        />\n    );\n\n    const visible = !!expandedFolderIds.size;\n    const guilds = document.querySelector(guildsBarProps.className.split(\" \").map(c => `.${c}`).join(\"\"));\n\n    // We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it.\n    // Also display flex otherwise to fix scrolling.\n    const sidebarStyle = {\n        display: isFullscreen ? \"none\" : \"flex\"\n    } satisfies CSSProperties;\n\n    if (!guilds || !settings.store.sidebarAnim) {\n        return visible\n            ? <div className=\"vc-betterFolders-sidebar\" style={sidebarStyle}>{Sidebar}</div>\n            : null;\n    }\n\n    return (\n        <Animations.Transition\n            items={visible}\n            from={{ width: 0 }}\n            enter={{ width: guilds.getBoundingClientRect().width }}\n            leave={{ width: 0 }}\n            config={{ duration: 200 }}\n        >\n            {(animationStyle: any, show: any) =>\n                show && (\n                    <Animations.animated.div className=\"vc-betterFolders-sidebar\" style={{ ...animationStyle, ...sidebarStyle }}>\n                        {Sidebar}\n                    </Animations.animated.div>\n                )\n            }\n        </Animations.Transition>\n    );\n}, { noop: true });\n"
  },
  {
    "path": "src/plugins/betterFolders/README.md",
    "content": "# Better Folders\n\nBetter Folders offers a variety of options to improve your folder experience\n\nAlways show the folder icon, regardless of if the folder is open or not\n\nOnly have one folder open at a time\n\nOpen folders in a sidebar:\n\n![A folder open in a separate sidebar](https://github.com/user-attachments/assets/432d3146-8091-4bae-9c1e-c19046c72947)\n"
  },
  {
    "path": "src/plugins/betterFolders/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByPropsLazy, findStoreLazy } from \"@webpack\";\nimport { FluxDispatcher } from \"@webpack/common\";\nimport { ReactNode } from \"react\";\n\nimport FolderSideBar from \"./FolderSideBar\";\n\nenum FolderIconDisplay {\n    Never,\n    Always,\n    MoreThanOneFolderExpanded\n}\n\nexport const ExpandedGuildFolderStore = findStoreLazy(\"ExpandedGuildFolderStore\");\nexport const SortedGuildStore = findStoreLazy(\"SortedGuildStore\");\nconst FolderUtils = findByPropsLazy(\"move\", \"toggleGuildFolderExpand\");\n\nlet lastGuildId = null as string | null;\nlet dispatchingFoldersClose = false;\n\nfunction getGuildFolder(id: string) {\n    return SortedGuildStore.getGuildFolders().find(folder => folder.guildIds.includes(id));\n}\n\nfunction closeFolders() {\n    for (const id of ExpandedGuildFolderStore.getExpandedFolders())\n        FolderUtils.toggleGuildFolderExpand(id);\n}\n\n// Nuckyz: Unsure if this should be a general utility or not\nfunction filterTreeWithTargetNode(children: any, predicate: (node: any) => boolean) {\n    if (children == null) {\n        return false;\n    }\n\n    if (!Array.isArray(children)) {\n        if (predicate(children)) {\n            return true;\n        }\n\n        return filterTreeWithTargetNode(children.props?.children, predicate);\n    }\n\n    let childIsTargetChild = false;\n    for (let i = 0; i < children.length; i++) {\n        const shouldKeep = filterTreeWithTargetNode(children[i], predicate);\n        if (shouldKeep) {\n            childIsTargetChild = true;\n            continue;\n        }\n\n        children.splice(i--, 1);\n    }\n\n    return childIsTargetChild;\n}\n\nexport const settings = definePluginSettings({\n    sidebar: {\n        type: OptionType.BOOLEAN,\n        description: \"Display servers from folder on dedicated sidebar\",\n        restartNeeded: true,\n        default: true\n    },\n    sidebarAnim: {\n        type: OptionType.BOOLEAN,\n        description: \"Animate opening the folder sidebar\",\n        default: true\n    },\n    closeAllFolders: {\n        type: OptionType.BOOLEAN,\n        description: \"Close all folders when selecting a server not in a folder\",\n        default: false\n    },\n    closeAllHomeButton: {\n        type: OptionType.BOOLEAN,\n        description: \"Close all folders when clicking on the home button\",\n        restartNeeded: true,\n        default: false\n    },\n    closeOthers: {\n        type: OptionType.BOOLEAN,\n        description: \"Close other folders when opening a folder\",\n        default: false\n    },\n    forceOpen: {\n        type: OptionType.BOOLEAN,\n        description: \"Force a folder to open when switching to a server of that folder\",\n        default: false\n    },\n    keepIcons: {\n        type: OptionType.BOOLEAN,\n        description: \"Keep showing guild icons in the primary guild bar folder when it's open in the BetterFolders sidebar\",\n        restartNeeded: true,\n        default: false\n    },\n    showFolderIcon: {\n        type: OptionType.SELECT,\n        description: \"Show the folder icon above the folder guilds in the BetterFolders sidebar\",\n        options: [\n            { label: \"Never\", value: FolderIconDisplay.Never },\n            { label: \"Always\", value: FolderIconDisplay.Always, default: true },\n            { label: \"When more than one folder is expanded\", value: FolderIconDisplay.MoreThanOneFolderExpanded }\n        ],\n        restartNeeded: true\n    }\n});\n\nconst IS_BETTER_FOLDERS_VAR = \"typeof isBetterFolders!=='undefined'?isBetterFolders:arguments[0]?.isBetterFolders\";\nconst BETTER_FOLDERS_EXPANDED_IDS_VAR = \"typeof betterFoldersExpandedIds!=='undefined'?betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds\";\nconst GRID_STYLE_NAME = \"vc-betterFolders-sidebar-grid\";\n\nexport default definePlugin({\n    name: \"BetterFolders\",\n    description: \"Shows server folders on dedicated sidebar and adds folder related improvements\",\n    authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz],\n\n    settings,\n\n    patches: [\n        {\n            find: '(\"guildsnav\")',\n            predicate: () => settings.store.sidebar,\n            replacement: [\n                // Create the isBetterFolders and betterFoldersExpandedIds variables in the GuildsBar component\n                // Needed because we access this from a non-arrow closure so we can't use arguments[0]\n                {\n                    match: /let{disableAppDownload:\\i=\\i\\.isPlatformEmbedded,isOverlay:.+?(?=}=\\i)/,\n                    replace: \"$&,isBetterFolders,betterFoldersExpandedIds\"\n                },\n                // Export the isBetterFolders and betterFoldersExpandedIds variable to the Guild List component\n                {\n                    match: /,{guildDiscoveryButton:\\i,/g,\n                    replace: \"$&isBetterFolders:arguments[0]?.isBetterFolders,betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds,\"\n                },\n                // Wrap the guild node (guild or folder) component in a div with display: none if it's not an expanded folder or a guild in an expanded folder\n                {\n                    match: /switch\\((\\i)\\.type\\){.+?default:return null}/,\n                    replace: `return $self.wrapGuildNodeComponent($1,()=>{$&},${IS_BETTER_FOLDERS_VAR},${BETTER_FOLDERS_EXPANDED_IDS_VAR});`\n                },\n                // Export the isBetterFolders variable to the folder component\n                {\n                    match: /switch\\(\\i\\.type\\){case \\i\\.\\i\\.FOLDER:.+?folderNode:\\i,/,\n                    replace: `$&isBetterFolders:${IS_BETTER_FOLDERS_VAR},`\n                },\n                // Make the callback for returning the guild node component depend on isBetterFolders and betterFoldersExpandedIds\n                {\n                    match: /switch\\(\\i\\.type\\).+?,\\i,\\i\\.setNodeRef/,\n                    replace: \"$&,arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds\"\n                },\n                // If we are rendering the Better Folders sidebar, we filter out everything but the guilds and folders from the Guild List children\n                {\n                    match: /lastTargetNode:\\i\\[\\i\\.length-1\\].+?}\\)(?::null)?\\](?=}\\))/,\n                    replace: \"$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))\"\n                },\n                // If we are rendering the Better Folders sidebar, we filter out everything but the Guild List from the Sidebar children\n                {\n                    match: /reverse:!0,.{0,150}?barClassName:.+?\\}\\)\\]/,\n                    replace: \"$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))\"\n                }\n            ]\n        },\n        {\n            // This is the parent folder component\n            find: \".toggleGuildFolderExpand(\",\n            predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,\n            replacement: [\n                {\n                    // Modify the expanded state to instead return the list of expanded folders\n                    match: /(\\],\\(\\)=>)(\\i\\.\\i)\\.isFolderExpanded\\(\\i\\)\\)/,\n                    replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders())`,\n                },\n                {\n                    // Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds\n                    // Also export the list of expanded folders to the child folder component if the patch above succeeds, else export undefined\n                    match: /(?<=folderNode:(\\i),expanded:)\\i(?=,)/,\n                    replace: (isExpandedOrExpandedIds, folderNote) => \"\"\n                        + `typeof ${isExpandedOrExpandedIds}===\"boolean\"?${isExpandedOrExpandedIds}:${isExpandedOrExpandedIds}.has(${folderNote}.id),`\n                        + `betterFoldersExpandedIds:${isExpandedOrExpandedIds} instanceof Set?${isExpandedOrExpandedIds}:void 0`\n                }\n            ]\n        },\n        {\n            find: \".FOLDER_ITEM_ANIMATION_DURATION),\",\n            predicate: () => settings.store.sidebar,\n            replacement: [\n                // We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)\n\n                // If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions\n                {\n                    predicate: () => settings.store.keepIcons,\n                    match: /(?<=let ?(?:\\i,)*?{folderNode:\\i,setNodeRef:\\i,.+?expanded:(\\i),.+?;)(?=let)/,\n                    replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`\n                },\n                // Disable expanding and collapsing folders transition in the normal GuildsBar sidebar\n                {\n                    predicate: () => !settings.store.keepIcons,\n                    match: /(?=,\\{from:\\{height)/,\n                    replace: \"&&$self.shouldShowTransition(arguments[0])\"\n                },\n                // If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded\n                {\n                    predicate: () => !settings.store.keepIcons,\n                    match: /\"--custom-folder-color\".+?(?=\\i\\(\\(\\i,\\i,\\i\\)=>{let{key:.{0,70}\"ul\")(?<=selected:\\i,expanded:(\\i),.+?)/,\n                    replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:`\n                },\n                // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar\n                {\n                    predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,\n                    match: /\"--custom-folder-color\".{0,110}?children:\\[/,\n                    replace: \"$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&\"\n                },\n                // Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar\n                {\n                    predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,\n                    match: /\"--custom-folder-color\".+?className:\\i\\.\\i}\\),(?=\\i,)/,\n                    replace: \"$&!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:\"\n                }\n            ]\n        },\n        {\n            find: \"APPLICATION_LIBRARY,render:\",\n            predicate: () => settings.store.sidebar,\n            group: true,\n            replacement: [\n                {\n                    // Render the Better Folders sidebar\n                    // Discord has two different places where they render the sidebar.\n                    // One is for visual refresh, one is not,\n                    // and each has a bunch of conditions &&ed in front of it.\n                    // Add the betterFolders sidebar to both, keeping the conditions Discord uses.\n                    match: /(?<=[[,])((?:!?\\i&&)+)\\(.{0,50}({className:\\i\\.\\i,themeOverride:\\i})\\)/g,\n                    replace: (m, conditions, props) => `${m},${conditions}$self.FolderSideBar(${props})`\n                },\n                {\n                    // Add grid styles to fix aligment with other visual refresh elements\n                    match: /(?<=className:)\\i\\.\\i(?=,\"data-fullscreen\")/,\n                    replace: `\"${GRID_STYLE_NAME} \"+$&`\n                }\n            ]\n        },\n        {\n            find: \"#{intl::DISCODO_DISABLED}\",\n            predicate: () => settings.store.closeAllHomeButton,\n            replacement: {\n                // Close all folders when clicking the home button\n                match: /(?<=onClick:\\(\\)=>{)(?=.{0,300}\"discodo\")/,\n                replace: \"$self.closeFolders();\"\n            }\n        }\n    ],\n\n    flux: {\n        CHANNEL_SELECT(data) {\n            if (!settings.store.closeAllFolders && !settings.store.forceOpen)\n                return;\n\n            if (lastGuildId !== data.guildId) {\n                lastGuildId = data.guildId;\n                const guildFolder = getGuildFolder(data.guildId);\n\n                if (guildFolder?.folderId) {\n                    if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {\n                        FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);\n                    }\n                } else if (settings.store.closeAllFolders) {\n                    closeFolders();\n                }\n            }\n        },\n\n        TOGGLE_GUILD_FOLDER_EXPAND(data) {\n            if (settings.store.closeOthers && !dispatchingFoldersClose) {\n                dispatchingFoldersClose = true;\n\n                FluxDispatcher.wait(() => {\n                    const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders();\n\n                    if (expandedFolders.size > 1) {\n                        for (const id of expandedFolders) if (id !== data.folderId)\n                            FolderUtils.toggleGuildFolderExpand(id);\n                    }\n\n                    dispatchingFoldersClose = false;\n                });\n            }\n        },\n\n        LOGOUT() {\n            closeFolders();\n        }\n    },\n\n    FolderSideBar,\n    closeFolders,\n\n\n    wrapGuildNodeComponent(node: any, originalComponent: () => ReactNode, isBetterFolders: boolean, expandedFolderIds?: Set<any>) {\n        if (\n            !isBetterFolders ||\n            node.type === \"folder\" && expandedFolderIds?.has(node.id) ||\n            node.type === \"guild\" && expandedFolderIds?.has(node.parentId)\n        ) {\n            return originalComponent();\n        }\n\n        return (\n            <div style={{ display: \"none\" }}>\n                {originalComponent()}\n            </div>\n        );\n    },\n\n    makeGuildsBarGuildListFilter(isBetterFolders: boolean) {\n        return (child: any) => {\n            if (!isBetterFolders) {\n                return true;\n            }\n\n            try {\n                // can cause hang if intl message is not found\n                const serversIntlMsg = getIntlMessage(\"SERVERS\");\n                if (!serversIntlMsg) {\n                    new Logger(\"BetterFolders\").error(\"Failed to get SERVERS intl message\");\n                    return true;\n                }\n                return child?.props?.[\"aria-label\"] === serversIntlMsg;\n            } catch (e) {\n                console.error(e);\n                return true;\n            }\n        };\n    },\n\n    makeGuildsBarSidebarFilter(isBetterFolders: boolean) {\n        return (child: any) => {\n            if (!isBetterFolders) {\n                return true;\n            }\n\n            try {\n                return filterTreeWithTargetNode(child, child => child?.props?.renderTreeNode != null);\n            } catch (e) {\n                console.error(e);\n                return true;\n            }\n        };\n    },\n\n    shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set<any>) {\n        if (!isBetterFolders) {\n            return true;\n        }\n\n        switch (settings.store.showFolderIcon) {\n            case FolderIconDisplay.Never:\n                return false;\n            case FolderIconDisplay.Always:\n                return true;\n            case FolderIconDisplay.MoreThanOneFolderExpanded:\n                return (expandedFolderIds?.size ?? 0) > 1;\n            default:\n                return true;\n        }\n    },\n\n    shouldShowTransition(props: any) {\n        // Pending guilds\n        if (props?.folderNode?.id === 1) return true;\n\n        return !!props?.isBetterFolders;\n    },\n\n    shouldRenderContents(props: any, isExpanded: boolean) {\n        // Pending guilds\n        if (props?.folderNode?.id === 1) return false;\n\n        return !props?.isBetterFolders && isExpanded;\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterFolders/style.css",
    "content": ".vc-betterFolders-sidebar {\n    grid-area: betterFoldersSidebar\n}\n\n/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */\n.vc-betterFolders-sidebar-grid {\n    /* stylelint-disable-next-line value-keyword-case */\n    grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end];\n    grid-template-areas:\n        \"titleBar titleBar titleBar titleBar\"\n        \"guildsList betterFoldersSidebar notice notice\"\n        \"guildsList betterFoldersSidebar channelsList page\";\n}"
  },
  {
    "path": "src/plugins/betterGifAltText/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"BetterGifAltText\",\n    authors: [Devs.Ven],\n    description:\n        \"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename\",\n    patches: [\n        {\n            find: \".modalContext})};\",\n            replacement: {\n                match: /(return.{0,10}\\.jsx.{0,50}isWindowFocused)/,\n                replace:\n                    \"$self.altify(e);$1\",\n            },\n        },\n        {\n            find: \"#{intl::GIF}\",\n            replacement: {\n                match: /alt:(\\i)=(\\i\\.\\i\\.string\\(\\i\\.\\i#{intl::GIF}\\))(?=,[^}]*\\}=(\\i))/,\n                replace:\n                    // rename prop so we can always use default value\n                    \"alt_$$:$1=$self.altify($3)||$2\",\n            },\n        },\n    ],\n\n    altify(props: any) {\n        props.alt ??= \"GIF\";\n        if (props.alt !== \"GIF\") return props.alt;\n\n        let url: string = props.original || props.src;\n        try {\n            url = decodeURI(url);\n        } catch { }\n\n        let name = url\n            .slice(url.lastIndexOf(\"/\") + 1)\n            .replace(/\\d/g, \"\") // strip numbers\n            .replace(/.gif$/, \"\") // strip extension\n            .split(/[,\\-_ ]+/g)\n            .slice(0, 20)\n            .join(\" \");\n        if (name.length > 300) {\n            name = name.slice(0, 300) + \"...\";\n        }\n\n        if (name) props.alt += ` - ${name}`;\n\n        return props.alt;\n    },\n});\n"
  },
  {
    "path": "src/plugins/betterGifPicker/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"BetterGifPicker\",\n    description: \"Makes the gif picker open the favourite category by default\",\n    authors: [Devs.Samwich],\n    patches: [\n        {\n            find: \"renderHeaderContent(){\",\n            replacement: [\n                {\n                    match: /(?<=state={resultType:)null/,\n                    replace: '\"Favorites\"'\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/betterNotes/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings, Settings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    hide: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide notes\",\n        default: false,\n        restartNeeded: true\n    },\n    noSpellCheck: {\n        type: OptionType.BOOLEAN,\n        description: \"Disable spellcheck in notes\",\n        disabled: () => Settings.plugins.BetterNotesBox.hide,\n        default: false\n    }\n});\n\nexport default definePlugin({\n    name: \"BetterNotesBox\",\n    description: \"Hide notes or disable spellcheck (Configure in settings!!)\",\n    authors: [Devs.Ven],\n    settings,\n\n    patches: [\n        {\n            find: \"hideNote:\",\n            all: true,\n            // Some modules match the find but the replacement is returned untouched\n            noWarn: true,\n            predicate: () => settings.store.hide,\n            replacement: {\n                match: /hideNote:.+?(?=([,}].*?\\)))/g,\n                replace: (m, rest) => {\n                    const destructuringMatch = rest.match(/}=.+/);\n                    if (destructuringMatch) {\n                        const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\\i)=!?\\d/));\n                        return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;\n                    }\n\n                    return \"hideNote:!0\";\n                }\n            }\n        },\n        {\n            find: \"#{intl::NOTE_PLACEHOLDER}\",\n            replacement: {\n                match: /#{intl::NOTE_PLACEHOLDER}\\),/,\n                replace: \"$&spellCheck:!$self.noSpellCheck,\"\n            }\n        }\n    ],\n\n    get noSpellCheck() {\n        return settings.store.noSpellCheck;\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterRoleContext/README.md",
    "content": "# BetterRoleContext\n\nAdds options to copy role color, edit role and view role icon when right clicking roles in the user profile\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)\n\n"
  },
  {
    "path": "src/plugins/betterRoleContext/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport { ImageIcon } from \"@components/Icons\";\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { Devs } from \"@utils/constants\";\nimport { getCurrentGuild, openImageModal } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { GuildRoleStore, Menu, PermissionStore } from \"@webpack/common\";\n\nconst GuildSettingsActions = findByPropsLazy(\"open\", \"selectRole\", \"updateGuild\");\n\nconst DeveloperMode = getUserSettingLazy(\"appearance\", \"developerMode\")!;\n\nfunction PencilIcon() {\n    return (\n        <svg\n            role=\"img\"\n            width=\"18\"\n            height=\"18\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n        >\n            <path fill=\"currentColor\" d=\"m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z\" />\n        </svg>\n    );\n}\n\nfunction AppearanceIcon() {\n    return (\n        <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\">\n            <path fill=\"currentColor\" d=\"M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z\" />\n        </svg>\n    );\n}\n\nconst settings = definePluginSettings({\n    roleIconFileFormat: {\n        type: OptionType.SELECT,\n        description: \"File format to use when viewing role icons\",\n        options: [\n            {\n                label: \"png\",\n                value: \"png\",\n                default: true\n            },\n            {\n                label: \"webp\",\n                value: \"webp\",\n            },\n            {\n                label: \"jpg\",\n                value: \"jpg\"\n            }\n        ]\n    }\n});\n\nexport default definePlugin({\n    name: \"BetterRoleContext\",\n    description: \"Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile\",\n    authors: [Devs.Ven, Devs.goodbee],\n    dependencies: [\"UserSettingsAPI\"],\n\n    settings,\n\n    start() {\n        // DeveloperMode needs to be enabled for the context menu to be shown\n        DeveloperMode.updateSetting(true);\n    },\n\n    contextMenus: {\n        \"dev-context\"(children, { id }: { id: string; }) {\n            const guild = getCurrentGuild();\n            if (!guild) return;\n\n            const role = GuildRoleStore.getRole(guild.id, id);\n            if (!role) return;\n\n            if (role.colorString) {\n                children.unshift(\n                    <Menu.MenuItem\n                        id=\"vc-copy-role-color\"\n                        label=\"Copy Role Color\"\n                        action={() => copyToClipboard(role.colorString!)}\n                        icon={AppearanceIcon}\n                    />\n                );\n            }\n\n            if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {\n                children.unshift(\n                    <Menu.MenuItem\n                        id=\"vc-edit-role\"\n                        label=\"Edit Role\"\n                        action={async () => {\n                            await GuildSettingsActions.open(guild.id, \"ROLES\");\n                            GuildSettingsActions.selectRole(id);\n                        }}\n                        icon={PencilIcon}\n                    />\n                );\n            }\n\n            if (role.icon) {\n                children.push(\n                    <Menu.MenuItem\n                        id=\"vc-view-role-icon\"\n                        label=\"View Role Icon\"\n                        action={() => {\n                            openImageModal({\n                                url: `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,\n                                height: 128,\n                                width: 128\n                            });\n                        }}\n                        icon={ImageIcon}\n                    />\n\n                );\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterRoleDot/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"BetterRoleDot\",\n    authors: [Devs.Ven, Devs.AutumnVN],\n    description:\n        \"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously\",\n\n    patches: [\n        {\n            // Class used in this module is dotBorderBase\n            find: \"M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z\",\n            replacement: {\n                match: /,viewBox:\"0 0 20 20\"/,\n                replace: \"$&,onClick:()=>$self.copyToClipBoard(arguments[0].color),style:{cursor:'pointer'}\",\n            },\n        },\n        {\n            find: '\"dot\"===',\n            all: true,\n            noWarn: true,\n            predicate: () => Settings.plugins.BetterRoleDot.bothStyles,\n            replacement: {\n                match: /\"(?:username|dot)\"===\\i(?!\\.\\i)/g,\n                replace: \"true\",\n            },\n        },\n\n        {\n            find: \"#{intl::ADD_ROLE_A11Y_LABEL}\",\n            all: true,\n            predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,\n            noWarn: true,\n            replacement: {\n                match: /\"dot\"===\\i/,\n                replace: \"true\"\n            }\n        },\n        {\n            find: \".roleVerifiedIcon\",\n            all: true,\n            predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,\n            noWarn: true,\n            replacement: {\n                match: /\"dot\"===\\i/,\n                replace: \"true\"\n            }\n        }\n    ],\n\n    options: {\n        bothStyles: {\n            type: OptionType.BOOLEAN,\n            description: \"Show both role dot and coloured names\",\n            restartNeeded: true,\n            default: false,\n        },\n        copyRoleColorInProfilePopout: {\n            type: OptionType.BOOLEAN,\n            description: \"Allow click on role dot in profile popout to copy role color\",\n            restartNeeded: true,\n            default: false\n        }\n    },\n\n    copyToClipBoard(color: string) {\n        copyWithToast(color);\n    },\n});\n"
  },
  {
    "path": "src/plugins/betterSessions/README.md",
    "content": "# BetterSessions\n\nEnhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.\n\n![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8)\n"
  },
  {
    "path": "src/plugins/betterSessions/components/RenameButton.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Button } from \"@components/Button\";\nimport { SessionInfo } from \"@plugins/betterSessions/types\";\nimport { openModal } from \"@utils/modal\";\n\nimport { RenameModal } from \"./RenameModal\";\n\nexport function RenameButton({ session, state }: { session: SessionInfo[\"session\"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {\n    return (\n        <Button\n            variant=\"secondary\"\n            size=\"xs\"\n            className=\"vc-betterSessions-rename-btn\"\n            onClick={() =>\n                openModal(props => (\n                    <RenameModal\n                        props={props}\n                        session={session}\n                        state={state}\n                    />\n                ))\n            }\n        >\n            Rename\n        </Button>\n    );\n}\n"
  },
  {
    "path": "src/plugins/betterSessions/components/RenameModal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { SessionInfo } from \"@plugins/betterSessions/types\";\nimport { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from \"@plugins/betterSessions/utils\";\nimport { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from \"@utils/modal\";\nimport { Button, Forms, React, TextInput } from \"@webpack/common\";\nimport { KeyboardEvent } from \"react\";\n\nexport function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo[\"session\"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {\n    const [title, setTitle] = state;\n    const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? \"\");\n\n    function onSaveClick() {\n        savedSessionsCache.set(session.id_hash, { name: value, isNew: false });\n        if (value !== \"\") {\n            setTitle(`${value}*`);\n        } else {\n            setTitle(getDefaultName(session.client_info));\n        }\n\n        saveSessionsToDataStore();\n        props.onClose();\n    }\n\n    return (\n        <ModalRoot {...props}>\n            <ModalHeader>\n                <Forms.FormTitle tag=\"h4\">Rename</Forms.FormTitle>\n            </ModalHeader>\n\n            <ModalContent>\n                <Forms.FormTitle tag=\"h5\" style={{ marginTop: \"10px\" }}>New device name</Forms.FormTitle>\n                <TextInput\n                    style={{ marginBottom: \"10px\" }}\n                    placeholder={getDefaultName(session.client_info)}\n                    value={value}\n                    onChange={setValue}\n                    onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n                        if (e.key === \"Enter\") {\n                            onSaveClick();\n                        }\n                    }}\n                />\n                <Button\n                    style={{\n                        marginBottom: \"20px\",\n                        paddingLeft: \"1px\",\n                        paddingRight: \"1px\",\n                        opacity: 0.6\n                    }}\n                    look={Button.Looks.LINK}\n                    color={Button.Colors.LINK}\n                    size={Button.Sizes.NONE}\n                    onClick={() => setValue(\"\")}\n                >\n                    Reset Name\n                </Button>\n            </ModalContent>\n\n            <ModalFooter>\n                <div className=\"vc-betterSessions-footer-buttons\">\n                    <Button\n                        color={Button.Colors.PRIMARY}\n                        onClick={() => props.onClose()}\n                    >\n                        Cancel\n                    </Button>\n                    <Button\n                        color={Button.Colors.BRAND}\n                        onClick={onSaveClick}\n                    >\n                        Save\n                    </Button>\n                </div>\n            </ModalFooter>\n        </ModalRoot >\n    );\n}\n"
  },
  {
    "path": "src/plugins/betterSessions/components/icons.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { LazyComponent } from \"@utils/react\";\nimport { findByCode } from \"@webpack\";\nimport { SVGProps } from \"react\";\n\nexport const DiscordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 16 16\"\n    >\n        <path d=\"M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z\" />\n    </svg>\n);\n\nexport const ChromeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 512 512\"\n    >\n        <path d=\"M188.8,255.93A67.2,67.2,0,1,0,256,188.75,67.38,67.38,0,0,0,188.8,255.93Z\" />\n        <path d=\"M476.75,217.79s0,0,0,.05a206.63,206.63,0,0,0-7-28.84h-.11a202.16,202.16,0,0,1,7.07,29h0a203.5,203.5,0,0,0-7.07-29H314.24c19.05,17,31.36,40.17,31.36,67.05a86.55,86.55,0,0,1-12.31,44.73L231,478.45a2.44,2.44,0,0,1,0,.27V479h0v-.26A224,224,0,0,0,256,480c6.84,0,13.61-.39,20.3-1a222.91,222.91,0,0,0,29.78-4.74C405.68,451.52,480,362.4,480,255.94A225.25,225.25,0,0,0,476.75,217.79Z\" />\n        <path d=\"M256,345.5c-33.6,0-61.6-17.91-77.29-44.79L76,123.05l-.14-.24A224,224,0,0,0,207.4,474.55l0-.05,77.69-134.6A84.13,84.13,0,0,1,256,345.5Z\" />\n        <path d=\"M91.29,104.57l77.35,133.25A89.19,89.19,0,0,1,256,166H461.17a246.51,246.51,0,0,0-25.78-43.94l.12.08A245.26,245.26,0,0,1,461.17,166h.17a245.91,245.91,0,0,0-25.66-44,2.63,2.63,0,0,1-.35-.26A223.93,223.93,0,0,0,91.14,104.34l.14.24Z\" />\n    </svg>\n);\n\nexport const EdgeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 24 24\"\n    >\n        <path d=\"M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.58-1.6-.2-.83-.2-1.67 0-1 .32-1.96.33-.97.87-1.8.14.95.55 1.77.41.82 1.02 1.5.6.68 1.38 1.21.78.54 1.64.9.86.36 1.77.56.92.2 1.8.2 1.12 0 2.18-.24 1.06-.23 2.06-.72l.2-.1.2-.05zm-15.5-1.27q0 1.1.27 2.15.27 1.06.78 2.03.51.96 1.24 1.77.74.82 1.66 1.4-1.47-.2-2.8-.74-1.33-.55-2.48-1.37-1.15-.83-2.08-1.9-.92-1.07-1.58-2.33T.36 14.94Q0 13.54 0 12.06q0-.81.32-1.49.31-.68.83-1.23.53-.55 1.2-.96.66-.4 1.35-.66.74-.27 1.5-.39.78-.12 1.55-.12.7 0 1.42.1.72.12 1.4.35.68.23 1.32.57.63.35 1.16.83-.35 0-.7.07-.33.07-.65.23v-.02q-.63.28-1.2.74-.57.46-1.05 1.04-.48.58-.87 1.26-.38.67-.65 1.39-.27.71-.42 1.44-.15.72-.15 1.38zM11.96.06q1.7 0 3.33.39 1.63.38 3.07 1.15 1.43.77 2.62 1.93 1.18 1.16 1.98 2.7.49.94.76 1.96.28 1 .28 2.08 0 .89-.23 1.7-.24.8-.69 1.48-.45.68-1.1 1.22-.64.53-1.45.88-.54.24-1.11.36-.58.13-1.16.13-.42 0-.97-.03-.54-.03-1.1-.12-.55-.1-1.05-.28-.5-.19-.84-.5-.12-.09-.23-.24-.1-.16-.1-.33 0-.15.16-.35.16-.2.35-.5.2-.28.36-.68.16-.4.16-.95 0-1.06-.4-1.96-.4-.91-1.06-1.64-.66-.74-1.52-1.28-.86-.55-1.79-.89-.84-.3-1.72-.44-.87-.14-1.76-.14-1.55 0-3.06.45T.94 7.55q.71-1.74 1.81-3.13 1.1-1.38 2.52-2.35Q6.68 1.1 8.37.58q1.7-.52 3.58-.52Z\" />\n    </svg>\n);\n\nexport const FirefoxIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 512 512\"\n    >\n        <path d=\"M130.22 127.548C130.38 127.558 130.3 127.558 130.22 127.548V127.548ZM481.64 172.898C471.03 147.398 449.56 119.898 432.7 111.168C446.42 138.058 454.37 165.048 457.4 185.168C457.405 185.306 457.422 185.443 457.45 185.578C429.87 116.828 383.098 89.1089 344.9 28.7479C329.908 5.05792 333.976 3.51792 331.82 4.08792L331.7 4.15792C284.99 30.1109 256.365 82.5289 249.12 126.898C232.503 127.771 216.219 131.895 201.19 139.035C199.838 139.649 198.736 140.706 198.066 142.031C197.396 143.356 197.199 144.87 197.506 146.323C197.7 147.162 198.068 147.951 198.586 148.639C199.103 149.327 199.76 149.899 200.512 150.318C201.264 150.737 202.096 150.993 202.954 151.071C203.811 151.148 204.676 151.045 205.491 150.768L206.011 150.558C221.511 143.255 238.408 139.393 255.541 139.238C318.369 138.669 352.698 183.262 363.161 201.528C350.161 192.378 326.811 183.338 304.341 187.248C392.081 231.108 368.541 381.784 246.951 376.448C187.487 373.838 149.881 325.467 146.421 285.648C146.421 285.648 157.671 243.698 227.041 243.698C234.541 243.698 255.971 222.778 256.371 216.698C256.281 214.698 213.836 197.822 197.281 181.518C188.434 172.805 184.229 168.611 180.511 165.458C178.499 163.75 176.392 162.158 174.201 160.688C168.638 141.231 168.399 120.638 173.51 101.058C148.45 112.468 128.96 130.508 114.8 146.428H114.68C105.01 134.178 105.68 93.7779 106.25 85.3479C106.13 84.8179 99.022 89.0159 98.1 89.6579C89.5342 95.7103 81.5528 102.55 74.26 110.088C57.969 126.688 30.128 160.242 18.76 211.318C14.224 231.701 12 255.739 12 263.618C12 398.318 121.21 507.508 255.92 507.508C376.56 507.508 478.939 420.281 496.35 304.888C507.922 228.192 481.64 173.82 481.64 172.898Z\" />\n    </svg>\n);\n\nexport const IEIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 512 512\"\n    >\n        <path d=\"M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z\" />\n    </svg>\n);\n\nexport const OperaIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 496 512\"\n    >\n        <path d=\"M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z\" />\n    </svg>\n);\n\nexport const SafariIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 512 512\"\n    >\n        <path d=\"M274.69,274.69l-37.38-37.38L166,346ZM256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8ZM411.85,182.79l14.78-6.13A8,8,0,0,1,437.08,181h0a8,8,0,0,1-4.33,10.46L418,197.57a8,8,0,0,1-10.45-4.33h0A8,8,0,0,1,411.85,182.79ZM314.43,94l6.12-14.78A8,8,0,0,1,331,74.92h0a8,8,0,0,1,4.33,10.45l-6.13,14.78a8,8,0,0,1-10.45,4.33h0A8,8,0,0,1,314.43,94ZM256,60h0a8,8,0,0,1,8,8V84a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V68A8,8,0,0,1,256,60ZM181,74.92a8,8,0,0,1,10.46,4.33L197.57,94a8,8,0,1,1-14.78,6.12l-6.13-14.78A8,8,0,0,1,181,74.92Zm-63.58,42.49h0a8,8,0,0,1,11.31,0L140,128.72A8,8,0,0,1,140,140h0a8,8,0,0,1-11.31,0l-11.31-11.31A8,8,0,0,1,117.41,117.41ZM60,256h0a8,8,0,0,1,8-8H84a8,8,0,0,1,8,8h0a8,8,0,0,1-8,8H68A8,8,0,0,1,60,256Zm40.15,73.21-14.78,6.13A8,8,0,0,1,74.92,331h0a8,8,0,0,1,4.33-10.46L94,314.43a8,8,0,0,1,10.45,4.33h0A8,8,0,0,1,100.15,329.21Zm4.33-136h0A8,8,0,0,1,94,197.57l-14.78-6.12A8,8,0,0,1,74.92,181h0a8,8,0,0,1,10.45-4.33l14.78,6.13A8,8,0,0,1,104.48,193.24ZM197.57,418l-6.12,14.78a8,8,0,0,1-14.79-6.12l6.13-14.78A8,8,0,1,1,197.57,418ZM264,444a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V428a8,8,0,0,1,8-8h0a8,8,0,0,1,8,8Zm67-6.92h0a8,8,0,0,1-10.46-4.33L314.43,418a8,8,0,0,1,4.33-10.45h0a8,8,0,0,1,10.45,4.33l6.13,14.78A8,8,0,0,1,331,437.08Zm63.58-42.49h0a8,8,0,0,1-11.31,0L372,383.28A8,8,0,0,1,372,372h0a8,8,0,0,1,11.31,0l11.31,11.31A8,8,0,0,1,394.59,394.59ZM286.25,286.25,110.34,401.66,225.75,225.75,401.66,110.34ZM437.08,331h0a8,8,0,0,1-10.45,4.33l-14.78-6.13a8,8,0,0,1-4.33-10.45h0A8,8,0,0,1,418,314.43l14.78,6.12A8,8,0,0,1,437.08,331ZM444,264H428a8,8,0,0,1-8-8h0a8,8,0,0,1,8-8h16a8,8,0,0,1,8,8h0A8,8,0,0,1,444,264Z\" />\n    </svg>\n);\n\nexport const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (\n    <svg\n        {...props}\n        fill=\"currentColor\"\n        viewBox=\"0 0 16 16\"\n    >\n        <path fillRule=\"evenodd\" d=\"M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z\" />\n    </svg>\n);\n\nexport const MobileIcon = LazyComponent(() => findByCode(\"M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38\"));\n"
  },
  {
    "path": "src/plugins/betterSessions/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { showNotification } from \"@api/Notifications\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from \"@webpack\";\nimport { Constants, React, RestAPI, SettingsRouter, Tooltip } from \"@webpack/common\";\n\nimport { RenameButton } from \"./components/RenameButton\";\nimport { Session, SessionInfo } from \"./types\";\nimport { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from \"./utils\";\n\nconst AuthSessionsStore = findStoreLazy(\"AuthSessionsStore\");\n\nconst TimestampClasses = findCssClassesLazy(\"timestamp\", \"blockquoteContainer\");\nconst SessionIconClasses = findCssClassesLazy(\"sessionIcon\");\n\nconst BlobMask = findComponentByCodeLazy(\"!1,lowerBadgeSize:\");\n\nconst settings = definePluginSettings({\n    backgroundCheck: {\n        type: OptionType.BOOLEAN,\n        description: \"Check for new sessions in the background, and display notifications when they are detected\",\n        default: false,\n        restartNeeded: true\n    },\n    checkInterval: {\n        description: \"How often to check for new sessions in the background (if enabled), in minutes\",\n        type: OptionType.NUMBER,\n        default: 20,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"BetterSessions\",\n    description: \"Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.\",\n    authors: [Devs.amia],\n\n    settings: settings,\n\n    patches: [\n        {\n            find: \"#{intl::AUTH_SESSIONS_SESSION_LOG_OUT}\",\n            replacement: [\n                // Replace children with a single label with state\n                {\n                    match: /({variant:\"eyebrow\",className:\\i\\.\\i,children:).{70,110}{children:\"\\\\xb7\"}\\),\\(0,\\i\\.\\i\\)\\(\"span\",{children:\\i\\[\\d+\\]}\\)\\]}\\)\\]/,\n                    replace: \"$1$self.renderName(arguments[0])\"\n                },\n                {\n                    match: /({variant:\"text-sm\\/medium\",className:\\i\\.\\i,children:.{70,110}{children:\"\\\\xb7\"}\\),\\(0,\\i\\.\\i\\)\\(\"span\",{children:)(\\i\\[\\d+\\])}/,\n                    replace: \"$1$self.renderTimestamp({...arguments[0],timeLabel:$2})}\"\n                },\n                // Replace the icon\n                {\n                    match: /children:\\[(?=.{0,125}?width:\"32\")(?<=,icon:(\\i)\\}.+?)/,\n                    replace: \"children:[$self.renderIcon({...arguments[0],DeviceIcon:$1}),false&&\"\n                }\n            ]\n        }\n    ],\n\n    renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {\n        const savedSession = savedSessionsCache.get(session.id_hash);\n\n        const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));\n        const [title, setTitle] = state;\n\n        // Show a \"NEW\" badge if the session is seen for the first time\n        return (\n            <>\n                <span>{title}</span>\n                {(savedSession == null || savedSession.isNew) && (\n                    <div\n                        className=\"vc-addon-badge\"\n                        style={{\n                            backgroundColor: \"#ED4245\",\n                            marginLeft: \"2px\"\n                        }}\n                    >\n                        NEW\n                    </div>\n                )}\n                <RenameButton session={session} state={state} />\n            </>\n        );\n    }, { noop: true }),\n\n    renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {\n        return (\n            <Tooltip text={session.approx_last_used_time.toLocaleString()}>\n                {props => (\n                    <span {...props} className={TimestampClasses.timestamp}>\n                        {timeLabel}\n                    </span>\n                )}\n            </Tooltip>\n        );\n    }, { noop: true }),\n\n    renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType<any>; }) => {\n        const PlatformIcon = GetPlatformIcon(session.client_info.platform);\n\n        return (\n            <BlobMask\n                isFolder\n                style={{ cursor: \"unset\" }}\n                selected={false}\n                lowerBadge={\n                    <div\n                        style={{\n                            width: \"20px\",\n                            height: \"20px\",\n\n                            display: \"flex\",\n                            justifyContent: \"center\",\n                            alignItems: \"center\",\n                            overflow: \"hidden\",\n\n                            borderRadius: \"50%\",\n                            backgroundColor: \"var(--interactive-icon-default)\",\n                            color: \"var(--background-base-lower)\",\n                        }}\n                    >\n                        <PlatformIcon width={14} height={14} />\n                    </div>\n                }\n                lowerBadgeSize={{\n                    width: 20,\n                    height: 20\n                }}\n            >\n                <div\n                    className={SessionIconClasses.sessionIcon}\n                    style={{ backgroundColor: GetOsColor(session.client_info.os) }}\n                >\n                    <DeviceIcon size=\"md\" color=\"currentColor\" />\n                </div>\n            </BlobMask>\n        );\n    }, { noop: true }),\n\n    async checkNewSessions() {\n        const data = await RestAPI.get({\n            url: Constants.Endpoints.AUTH_SESSIONS\n        });\n\n        for (const session of data.body.user_sessions) {\n            if (savedSessionsCache.has(session.id_hash)) continue;\n\n            savedSessionsCache.set(session.id_hash, { name: \"\", isNew: true });\n            showNotification({\n                title: \"BetterSessions\",\n                body: `New session:\\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,\n                permanent: true,\n                onClick: () => SettingsRouter.openUserSettings(\"sessions_panel\")\n            });\n        }\n\n        saveSessionsToDataStore();\n    },\n\n    flux: {\n        USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {\n            const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo[\"session\"]) => session.id_hash);\n\n            // Add new sessions to cache\n            lastFetchedHashes.forEach(idHash => {\n                if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: \"\", isNew: false });\n            });\n\n            // Delete removed sessions from cache\n            if (lastFetchedHashes.length > 0) {\n                savedSessionsCache.forEach((_, idHash) => {\n                    if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);\n                });\n            }\n\n            // Dismiss the \"NEW\" badge of all sessions.\n            // Since the only way for a session to be marked as \"NEW\" is going to the Devices tab,\n            // closing the settings means they've been viewed and are no longer considered new.\n            savedSessionsCache.forEach(data => {\n                data.isNew = false;\n            });\n            saveSessionsToDataStore();\n        }\n    },\n\n    async start() {\n        await fetchNamesFromDataStore();\n\n        this.checkNewSessions();\n        if (settings.store.backgroundCheck) {\n            this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);\n        }\n    },\n\n    stop() {\n        clearInterval(this.checkInterval);\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterSessions/styles.css",
    "content": ".vc-betterSessions-footer-buttons {\n    display: flex;\n    gap: 0.5em;\n    align-items: center;\n}\n\n.vc-betterSessions-rename-btn {\n    margin-left: 4px;\n    translate: 0 -2px;\n}"
  },
  {
    "path": "src/plugins/betterSessions/types.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport interface SessionInfo {\n    session: {\n        id_hash: string;\n        approx_last_used_time: Date;\n        client_info: {\n            os: string;\n            platform: string;\n            location: string;\n        };\n    },\n    current?: boolean;\n}\n\nexport type Session = SessionInfo[\"session\"];\n"
  },
  {
    "path": "src/plugins/betterSessions/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as DataStore from \"@api/DataStore\";\nimport { UserStore } from \"@webpack/common\";\n\nimport { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from \"./components/icons\";\nimport { SessionInfo } from \"./types\";\n\nconst getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`;\n\nexport const savedSessionsCache: Map<string, { name: string, isNew: boolean; }> = new Map();\n\nexport function getDefaultName(clientInfo: SessionInfo[\"session\"][\"client_info\"]) {\n    return `${clientInfo.os} · ${clientInfo.platform}`;\n}\n\nexport function saveSessionsToDataStore() {\n    return DataStore.set(getDataKey(), savedSessionsCache);\n}\n\nexport async function fetchNamesFromDataStore() {\n    const savedSessions = await DataStore.get<Map<string, { name: string, isNew: boolean; }>>(getDataKey()) || new Map();\n    savedSessions.forEach((data, idHash) => {\n        savedSessionsCache.set(idHash, data);\n    });\n}\n\nexport function GetOsColor(os: string) {\n    switch (os) {\n        case \"Windows Mobile\":\n        case \"Windows\":\n            return \"#55a6ef\"; // Light blue\n        case \"Linux\":\n            return \"#cdcd31\"; // Yellow\n        case \"Android\":\n            return \"#7bc958\"; // Green\n        case \"Mac OS X\":\n        case \"iOS\":\n            return \"\"; // Default to white/black (theme-dependent)\n        default:\n            return \"#f3799a\"; // Pink\n    }\n}\n\nexport function GetPlatformIcon(platform: string) {\n    switch (platform) {\n        case \"Discord Android\":\n        case \"Discord iOS\":\n        case \"Discord Client\":\n            return DiscordIcon;\n        case \"Android Chrome\":\n        case \"Chrome iOS\":\n        case \"Chrome\":\n            return ChromeIcon;\n        case \"Edge\":\n            return EdgeIcon;\n        case \"Firefox\":\n            return FirefoxIcon;\n        case \"Internet Explorer\":\n            return IEIcon;\n        case \"Opera Mini\":\n        case \"Opera\":\n            return OperaIcon;\n        case \"Mobile Safari\":\n        case \"Safari\":\n            return SafariIcon;\n        case \"BlackBerry\":\n        case \"Facebook Mobile\":\n        case \"Android Mobile\":\n            return MobileIcon;\n        default:\n            return UnknownIcon;\n    }\n}\n"
  },
  {
    "path": "src/plugins/betterSettings/README.md",
    "content": "# BetterSettings\n\nImproves Discord's Settings via multiple (toggleable) changes:\n- makes opening settings much faster\n- removes the scuffed transition animation\n- organises the settings cog context menu into categories\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)\n\n"
  },
  {
    "path": "src/plugins/betterSettings/fullHeightContext.css",
    "content": "/*\n * Discord has dumb max height logic for their context menus.\n * If a context menu is at the bottom of the screen, its submenus are capped to its max height and can't even grow upwards\n * We unset the variable they use to cap height. This allows submenus to grow as tall as they want\n */\n\n#user-settings-cog,\n[aria-activedescendant^=\"user-settings-cog\"] {\n    --reference-position-layer-max-height: initial !important;\n}"
  },
  {
    "path": "src/plugins/betterSettings/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport { buildPluginMenuEntries, buildThemeMenuEntries } from \"@plugins/vencordToolbox/menu\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from \"@webpack/common\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport fullHeightStyle from \"./fullHeightContext.css?managed\";\n\nconst cl = classNameFactory(\"\");\nconst Classes = findCssClassesLazy(\"animating\", \"baseLayer\", \"bg\", \"layer\", \"layers\");\n\nconst settings = definePluginSettings({\n    disableFade: {\n        description: \"Disable the crossfade animation\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    organizeMenu: {\n        description: \"Organizes the settings cog context menu into categories\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    eagerLoad: {\n        description: \"Removes the loading delay when opening the menu for the first time\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    }\n});\n\ninterface LayerProps extends HTMLAttributes<HTMLDivElement> {\n    mode: \"SHOWN\" | \"HIDDEN\";\n    baseLayer?: boolean;\n}\n\nfunction Layer({ mode, baseLayer = false, ...props }: LayerProps) {\n    const hidden = mode === \"HIDDEN\";\n    const containerRef = useRef<HTMLDivElement>(null);\n\n    useEffect(() => () => {\n        ComponentDispatch.dispatch(\"LAYER_POP_START\");\n        ComponentDispatch.dispatch(\"LAYER_POP_COMPLETE\");\n    }, []);\n\n    const node = (\n        <div\n            ref={containerRef}\n            aria-hidden={hidden}\n            className={cl({\n                [Classes.layer]: true,\n                [Classes.baseLayer]: baseLayer,\n                \"stop-animations\": hidden\n            })}\n            style={{ opacity: hidden ? 0 : undefined }}\n            {...props}\n        />\n    );\n\n    return baseLayer\n        ? node\n        : <FocusLock containerRef={containerRef}>{node}</FocusLock>;\n}\n\nexport default definePlugin({\n    name: \"BetterSettings\",\n    description: \"Enhances your settings-menu-opening experience\",\n    authors: [Devs.Kyuuhachi],\n    settings,\n\n    start() {\n        if (settings.store.organizeMenu)\n            enableStyle(fullHeightStyle);\n    },\n\n    stop() {\n        disableStyle(fullHeightStyle);\n    },\n\n    patches: [\n        {\n            find: \"this.renderArtisanalHack()\",\n            replacement: [\n                {\n                    match: /class (\\i)(?= extends \\i\\.PureComponent.+?static contextType=.+?jsx\\)\\(\\1,\\{mode:)/,\n                    replace: \"var $1=$self.Layer;class VencordPatchedOldFadeLayer\",\n                    predicate: () => settings.store.disableFade\n                },\n                { // Lazy-load contents\n                    match: /createPromise:\\(\\)=>([^:}]*?),webpackId:\"?\\d+\"?,name:(?!=\"CollectiblesShop\")\"[^\"]+\"/g,\n                    replace: \"$&,_:$1\",\n                    predicate: () => settings.store.eagerLoad\n                }\n            ]\n        },\n        { // For some reason standardSidebarView also has a small fade-in\n            find: 'minimal:\"contentColumnMinimal\"',\n            replacement: [\n                {\n                    match: /(?=\\(0,\\i\\.\\i\\)\\((\\i),\\{from:\\{position:\"absolute\")/,\n                    replace: \"(_cb=>_cb(void 0,$1))||\"\n                },\n                {\n                    match: /\\i\\.animated\\.div/,\n                    replace: '\"div\"'\n                }\n            ],\n            predicate: () => settings.store.disableFade\n        },\n        { // Disable fade animations for settings menu\n            find: '\"data-mana-component\":\"layer-modal\"',\n            replacement: [\n                {\n                    match: /(\\i)\\.animated\\.div(?=,\\{\"data-mana-component\":\"layer-modal\")/,\n                    replace: '\"div\"'\n                },\n                {\n                    match: /(?<=\"data-mana-component\":\"layer-modal\"[^}]*?)style:\\i,/,\n                    replace: \"style:{},\"\n                }\n            ],\n            predicate: () => settings.store.disableFade\n        },\n        { // Disable initial and exit delay for settings menu\n            find: \"headerId:void 0,headerIdIsManaged:!1\",\n            replacement: {\n                match: /let (\\i)=300/,\n                replace: \"let $1=0\"\n            },\n            predicate: () => settings.store.disableFade\n        },\n        { // Load menu TOC eagerly\n            find: \"handleOpenSettingsContextMenu=\",\n            replacement: {\n                match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\\i&&.{0,100}?(await [^};]*?\\)\\)))/,\n                replace: \"_vencordBetterSettingsEagerLoad=(async ()=>$1)();\"\n            },\n            predicate: () => settings.store.eagerLoad\n        },\n        { // Settings cog context menu\n            find: \"#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}\",\n            predicate: () => settings.store.organizeMenu,\n            replacement: [\n                {\n                    match: /children:\\[(\\i),(?<=\\1=.{0,30}\\.openUserSettings.+?)/,\n                    replace: \"children:[$self.transformSettingsEntries($1),\",\n                },\n            ]\n        },\n    ],\n\n    // This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary\n    // without possibly also catching unrelated errors of children.\n    //\n    // Thus, we sanity check webpack modules\n    Layer(props: LayerProps) {\n        try {\n            [FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes.layer].forEach(e => e.test);\n        } catch {\n            new Logger(\"BetterSettings\").error(\"Failed to find some components\");\n            return props.children;\n        }\n\n        return <Layer {...props} />;\n    },\n\n    transformSettingsEntries(list) {\n        const items: ReactNode[] = [];\n\n        for (const item of list) {\n            const { key, props } = item;\n            if (!props) continue;\n\n            if (key === \"vencord_plugins\" || key === \"vencord_themes\") {\n                const children = key === \"vencord_plugins\"\n                    ? buildPluginMenuEntries()\n                    : buildThemeMenuEntries();\n\n                items.push(\n                    <Menu.MenuItem key={key} label={props.label} id={props.label} {...props}>\n                        {children}\n                    </Menu.MenuItem>\n                );\n            } else if (key.endsWith(\"_section\") && props.label) {\n                items.push(\n                    <Menu.MenuItem key={key} label={props.label} id={props.label}>\n                        {this.transformSettingsEntries(props.children)}\n                    </Menu.MenuItem>\n                );\n            } else {\n                items.push(item);\n            }\n        }\n\n        return items;\n    }\n});\n"
  },
  {
    "path": "src/plugins/betterUploadButton/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"BetterUploadButton\",\n    authors: [Devs.fawn, Devs.Ven],\n    description: \"Upload with a single click, open menu with right click\",\n    patches: [\n        {\n            find: \".CHAT_INPUT_BUTTON_NOTIFICATION,\",\n            replacement: [\n                {\n                    match: /onClick:(\\i\\?void 0:\\i)(?=,onDoubleClick:(\\i\\?void 0:\\i),)/,\n                    replace: \"$&,...$self.getOverrides(arguments[0],$1,$2)\",\n                },\n            ]\n        },\n    ],\n\n    getOverrides(props: any, onClick: any, onDoubleClick: any) {\n        if (!props?.className?.includes(\"attachButton\")) return {};\n\n        return {\n            onClick: onDoubleClick,\n            onContextMenu: onClick\n        };\n    }\n});\n"
  },
  {
    "path": "src/plugins/biggerStreamPreview/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { ScreenshareIcon } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport { openImageModal } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { Channel, User } from \"@vencord/discord-types\";\nimport { Menu } from \"@webpack/common\";\n\nimport { ApplicationStreamingStore, ApplicationStreamPreviewStore } from \"./webpack/stores\";\nimport { ApplicationStream, Stream } from \"./webpack/types/stores\";\n\nexport interface UserContextProps {\n    channel: Channel,\n    channelSelected: boolean,\n    className: string,\n    config: { context: string; };\n    context: string,\n    onHeightUpdate: Function,\n    position: string,\n    target: HTMLElement,\n    theme: string,\n    user: User;\n}\n\nexport interface StreamContextProps {\n    appContext: string,\n    className: string,\n    config: { context: string; };\n    context: string,\n    exitFullscreen: Function,\n    onHeightUpdate: Function,\n    position: string,\n    target: HTMLElement,\n    stream: Stream,\n    theme: string,\n}\n\nexport const handleViewPreview = async ({ guildId, channelId, ownerId }: ApplicationStream | Stream) => {\n    const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId);\n    if (!previewUrl) return;\n\n    openImageModal({\n        url: previewUrl,\n        height: 720,\n        width: 1280\n    });\n};\n\nexport const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {\n    const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);\n    if (!stream) return;\n\n    const streamPreviewItem = (\n        <Menu.MenuItem\n            label=\"View Stream Preview\"\n            id=\"view-stream-preview\"\n            icon={ScreenshareIcon}\n            action={() => stream && handleViewPreview(stream)}\n            disabled={!stream}\n        />\n    );\n\n    children.push(<Menu.MenuSeparator />, streamPreviewItem);\n};\n\nexport const streamContextPatch: NavContextMenuPatchCallback = (children, { stream }: StreamContextProps) => {\n    return addViewStreamContext(children, { userId: stream.ownerId });\n};\n\nexport const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {\n    if (user) return addViewStreamContext(children, { userId: user.id });\n};\n\nexport default definePlugin({\n    name: \"BiggerStreamPreview\",\n    description: \"This plugin allows you to enlarge stream previews\",\n    authors: [Devs.phil],\n    contextMenus: {\n        \"user-context\": userContextPatch,\n        \"stream-context\": streamContextPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/biggerStreamPreview/webpack/stores.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findStoreLazy } from \"@webpack\";\n\nimport * as t from \"./types/stores\";\n\nexport const ApplicationStreamPreviewStore: t.ApplicationStreamPreviewStore = findStoreLazy(\"ApplicationStreamPreviewStore\");\nexport const ApplicationStreamingStore: t.ApplicationStreamingStore = findStoreLazy(\"ApplicationStreamingStore\");\n"
  },
  {
    "path": "src/plugins/biggerStreamPreview/webpack/types/stores.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { FluxStore } from \"@vencord/discord-types\";\n\nexport interface ApplicationStreamPreviewStore extends FluxStore {\n    getIsPreviewLoading: (guildId: string | bigint | null, channelId: string | bigint, ownerId: string | bigint) => boolean;\n    getPreviewURL: (guildId: string | bigint | null, channelId: string | bigint, ownerId: string | bigint) => Promise<string | null>;\n    getPreviewURLForStreamKey: (streamKey: string) => ReturnType<ApplicationStreamPreviewStore[\"getPreviewURL\"]>;\n}\n\nexport interface ApplicationStream {\n    streamType: string;\n    guildId: string | null;\n    channelId: string;\n    ownerId: string;\n}\n\nexport interface Stream extends ApplicationStream {\n    state: string;\n}\n\nexport interface RTCStream {\n    region: string,\n    streamKey: string,\n    viewerIds: string[];\n}\n\nexport interface StreamMetadata {\n    id: string | null,\n    pid: number | null,\n    sourceName: string | null;\n}\n\nexport interface StreamingStoreState {\n    activeStreams: [string, Stream][];\n    rtcStreams: { [key: string]: RTCStream; };\n    streamerActiveStreamMetadatas: { [key: string]: StreamMetadata | null; };\n    streamsByUserAndGuild: { [key: string]: { [key: string]: ApplicationStream; }; };\n}\n\n/**\n * example how a stream key could look like: `call(type of connection):1116549917987192913(channelId):305238513941667851(ownerId)`\n */\nexport interface ApplicationStreamingStore extends FluxStore {\n    getActiveStreamForApplicationStream: (stream: ApplicationStream) => Stream | null;\n    getActiveStreamForStreamKey: (streamKey: string) => Stream | null;\n    getActiveStreamForUser: (userId: string | bigint, guildId?: string | bigint | null) => Stream | null;\n    getAllActiveStreams: () => Stream[];\n    getAllApplicationStreams: () => ApplicationStream[];\n    getAllApplicationStreamsForChannel: (channelId: string | bigint) => ApplicationStream[];\n    getAllActiveStreamsForChannel: (channelId: string | bigint) => Stream[];\n    getAnyStreamForUser: (userId: string | bigint) => Stream | ApplicationStream | null;\n    getStreamForUser: (userId: string | bigint, guildId?: string | bigint | null) => Stream | null;\n    getCurrentUserActiveStream: () => Stream | null;\n    getLastActiveStream: () => Stream | null;\n    getState: () => StreamingStoreState;\n    getRTCStream: (streamKey: string) => RTCStream | null;\n    getStreamerActiveStreamMetadata: () => StreamMetadata;\n    getViewerIds: (stream: ApplicationStream) => string[];\n    isSelfStreamHidden: (channelId: string | bigint | null) => boolean;\n}\n"
  },
  {
    "path": "src/plugins/blurNsfw/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings } from \"@api/Settings\";\nimport { managedStyleRootNode } from \"@api/Styles\";\nimport { Devs } from \"@utils/constants\";\nimport { createAndAppendStyle } from \"@utils/css\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nlet style: HTMLStyleElement;\n\nfunction setCss() {\n    style.textContent = `\n        .vc-nsfw-img [class*=imageContainer],\n        .vc-nsfw-img [class*=wrapperPaused] {\n            filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);\n            transition: filter 0.2s;\n\n            &:hover {\n                filter: blur(0);\n            }\n        }\n        `;\n}\n\nexport default definePlugin({\n    name: \"BlurNSFW\",\n    description: \"Blur attachments in NSFW channels until hovered\",\n    authors: [Devs.Ven],\n\n    patches: [\n        {\n            find: \"}renderStickersAccessories(\",\n            replacement: [\n                {\n                    match: /(\\.renderReactions\\(\\i\\).+?className:)/,\n                    replace: '$&(this.props?.channel?.nsfw?\"vc-nsfw-img \":\"\")+'\n                }\n            ]\n        }\n    ],\n\n    options: {\n        blurAmount: {\n            type: OptionType.NUMBER,\n            description: \"Blur Amount (in pixels)\",\n            default: 10,\n            onChange: setCss\n        }\n    },\n\n    start() {\n        style = createAndAppendStyle(\"VcBlurNsfw\", managedStyleRootNode);\n\n        setCss();\n    },\n\n    stop() {\n        style?.remove();\n    }\n});\n"
  },
  {
    "path": "src/plugins/callTimer/alignedChatInputFix.css",
    "content": "[class*=\"panels\"] [class*=\"inner\"],\n[class*=\"rtcConnectionStatus\"] {\n    height: fit-content !important;\n}"
  },
  {
    "path": "src/plugins/callTimer/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { useTimer } from \"@utils/react\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { React } from \"@webpack/common\";\n\nimport alignedChatInputFix from \"./alignedChatInputFix.css?managed\";\n\nfunction formatDuration(ms: number) {\n    // here be dragons (moment fucking sucks)\n    const human = Settings.plugins.CallTimer.format === \"human\";\n\n    const format = (n: number) => human ? n : n.toString().padStart(2, \"0\");\n    const unit = (s: string) => human ? s : \"\";\n    const delim = human ? \" \" : \":\";\n\n    // thx copilot\n    const d = Math.floor(ms / 86400000);\n    const h = Math.floor((ms % 86400000) / 3600000);\n    const m = Math.floor(((ms % 86400000) % 3600000) / 60000);\n    const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000);\n\n    let res = \"\";\n    if (d) res += `${d}d `;\n    if (h || res) res += `${format(h)}${unit(\"h\")}${delim}`;\n    if (m || res || !human) res += `${format(m)}${unit(\"m\")}${delim}`;\n    res += `${format(s)}${unit(\"s\")}`;\n\n    return res;\n}\n\nexport default definePlugin({\n    name: \"CallTimer\",\n    description: \"Adds a timer to vcs\",\n    authors: [Devs.Ven],\n    managedStyle: alignedChatInputFix,\n\n    startTime: 0,\n    interval: void 0 as NodeJS.Timeout | undefined,\n\n    options: {\n        format: {\n            type: OptionType.SELECT,\n            description: \"The timer format. This can be any valid moment.js format\",\n            options: [\n                {\n                    label: \"30d 23:00:42\",\n                    value: \"stopwatch\",\n                    default: true\n                },\n                {\n                    label: \"30d 23h 00m 42s\",\n                    value: \"human\"\n                }\n            ]\n        }\n    },\n\n    patches: [{\n        find: \"renderConnectionStatus(){\",\n        replacement: {\n            // in renderConnectionStatus()\n            match: /(lineClamp:1,children:)(\\i)(?=,|}\\))/,\n            replace: \"$1[$2,$self.renderTimer(this.props.channel.id)]\"\n        }\n    }],\n\n    renderTimer(channelId: string) {\n        return <ErrorBoundary noop>\n            <this.Timer channelId={channelId} />\n        </ErrorBoundary>;\n    },\n\n    Timer({ channelId }: { channelId: string; }) {\n        const time = useTimer({\n            deps: [channelId]\n        });\n\n        return <p style={{ margin: 0, fontFamily: \"var(--font-code)\" }}>{formatDuration(time)}</p>;\n    }\n});\n"
  },
  {
    "path": "src/plugins/clearURLs/README.md",
    "content": "# ClearURLs\n\nAutomatically removes tracking elements from URLs you send.\n\nUses data from the [ClearURLs browser extension](https://clearurls.xyz/).\n\n## Example\n\n**Before:** `https://www.amazon.com/dp/exampleProduct/ref=sxin_0_pb?__mk_de_DE=ÅMÅŽÕÑ&keywords=tea&pd_rd_i=exampleProduct&pd_rd_r=8d39e4cd-1e4f-43db-b6e7-72e969a84aa5&pd_rd_w=1pcKM&pd_rd_wg=hYrNl&pf_rd_p=50bbfd25-5ef7-41a2-68d6-74d854b30e30&pf_rd_r=0GMWD0YYKA7XFGX55ADP&qid=1517757263&rnid=2914120011`\n\n**After:** `https://www.amazon.com/dp/exampleProduct/`\n"
  },
  {
    "path": "src/plugins/clearURLs/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport {\n    MessageObject\n} from \"@api/MessageEvents\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nconst CLEAR_URLS_JSON_URL = \"https://raw.githubusercontent.com/ClearURLs/Rules/master/data.min.json\";\n\ninterface Provider {\n    urlPattern: string;\n    completeProvider: boolean;\n    rules?: string[];\n    rawRules?: string[];\n    referralMarketing?: string[];\n    exceptions?: string[];\n    redirections?: string[];\n    forceRedirection?: boolean;\n}\n\ninterface ClearUrlsData {\n    providers: Record<string, Provider>;\n}\n\ninterface RuleSet {\n    name: string;\n    urlPattern: RegExp;\n    rules?: RegExp[];\n    rawRules?: RegExp[];\n    exceptions?: RegExp[];\n}\n\nexport default definePlugin({\n    name: \"ClearURLs\",\n    description: \"Automatically removes tracking elements from URLs you send\",\n    authors: [Devs.adryd, Devs.thororen],\n\n    rules: [] as RuleSet[],\n\n    async start() {\n        await this.createRules();\n    },\n\n    stop() {\n        this.rules = [];\n    },\n\n    onBeforeMessageSend(_, msg) {\n        return this.cleanMessage(msg);\n    },\n\n    onBeforeMessageEdit(_cid, _mid, msg) {\n        return this.cleanMessage(msg);\n    },\n\n    async createRules() {\n        const res = await fetch(CLEAR_URLS_JSON_URL)\n            .then(res => res.json()) as ClearUrlsData;\n\n        this.rules = [];\n\n        for (const [name, provider] of Object.entries(res.providers)) {\n            const urlPattern = new RegExp(provider.urlPattern, \"i\");\n\n            const rules = provider.rules?.map(rule => new RegExp(rule, \"i\"));\n            const rawRules = provider.rawRules?.map(rule => new RegExp(rule, \"i\"));\n            const exceptions = provider.exceptions?.map(ex => new RegExp(ex, \"i\"));\n\n            this.rules.push({\n                name,\n                urlPattern,\n                rules,\n                rawRules,\n                exceptions,\n            });\n        }\n    },\n\n    replacer(match: string) {\n        // Parse URL without throwing errors\n        try {\n            var url = new URL(match);\n        } catch (error) {\n            // Don't modify anything if we can't parse the URL\n            return match;\n        }\n\n        // Cheap way to check if there are any search params\n        if (url.searchParams.entries().next().done) return match;\n\n        // Check rules for each provider that matches\n        this.rules.forEach(({ urlPattern, exceptions, rawRules, rules }) => {\n            if (!urlPattern.test(url.href) || exceptions?.some(ex => ex.test(url.href))) return;\n\n            const toDelete: string[] = [];\n\n            if (rules) {\n                // Add matched params to delete list\n                url.searchParams.forEach((_, param) => {\n                    if (rules.some(rule => rule.test(param))) {\n                        toDelete.push(param);\n                    }\n                });\n            }\n\n            // Delete matched params from list\n            toDelete.forEach(param => url.searchParams.delete(param));\n\n            // Match and remove any raw rules\n            let cleanedUrl = url.href;\n            rawRules?.forEach(rawRule => {\n                cleanedUrl = cleanedUrl.replace(rawRule, \"\");\n            });\n            url = new URL(cleanedUrl);\n        });\n\n        return url.toString();\n    },\n\n    cleanMessage(msg: MessageObject) {\n        // Only run on messages that contain URLs\n        if (/http(s)?:\\/\\//.test(msg.content)) {\n            msg.content = msg.content.replace(\n                /(https?:\\/\\/[^\\s<]+[^<.,:;\"'>)|\\]\\s])/g,\n                match => this.replacer(match)\n            );\n        }\n    },\n});\n"
  },
  {
    "path": "src/plugins/clientTheme/README.md",
    "content": "# Client Theme\n\nRevival of the old client theme experiment (The one that came before the sucky one that we actually got)\n\n![the ClientTheme theme colour picker](https://user-images.githubusercontent.com/37855219/230238053-e90b7098-373a-459a-bb8c-c24e82f69270.png)\n\nhttps://github.com/Vendicated/Vencord/assets/45497981/6c1bcb3b-e0c7-4a02-b0b8-c4c5cd954f38\n"
  },
  {
    "path": "src/plugins/clientTheme/clientTheme.css",
    "content": ".vc-clientTheme-settings {\n    display: flex;\n    flex-direction: column;\n}\n\n.vc-clientTheme-container {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n}\n\n.vc-clientTheme-labels {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n}\n\n.vc-clientTheme-container [class*=\"swatch\"] {\n    border: thin solid var(--input-border-default) !important;\n}\n\n.vc-clientTheme-buttons-container {\n    margin-top: 16px;\n    display: flex;\n    gap: 4px;\n}"
  },
  {
    "path": "src/plugins/clientTheme/components/Settings.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { relativeLuminance } from \"@plugins/clientTheme/utils/colorUtils\";\nimport { createOrUpdateThemeColorVars } from \"@plugins/clientTheme/utils/styleUtils\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Margins } from \"@utils/margins\";\nimport { findByCodeLazy, findStoreLazy } from \"@webpack\";\nimport { Button, ColorPicker, Forms, ThemeStore, useStateFromStores } from \"@webpack/common\";\n\nimport { settings } from \"..\";\n\nconst saveClientTheme = findByCodeLazy('type:\"UNSYNCED_USER_SETTINGS_UPDATE', '\"system\"===');\nconst NitroThemeStore = findStoreLazy(\"ClientThemesBackgroundStore\");\n\nconst cl = classNameFactory(\"vc-clientTheme-\");\n\nconst colorPresets = [\n    \"#1E1514\", \"#172019\", \"#13171B\", \"#1C1C28\", \"#402D2D\",\n    \"#3A483D\", \"#344242\", \"#313D4B\", \"#2D2F47\", \"#322B42\",\n    \"#3C2E42\", \"#422938\", \"#b6908f\", \"#bfa088\", \"#d3c77d\",\n    \"#86ac86\", \"#88aab3\", \"#8693b5\", \"#8a89ba\", \"#ad94bb\",\n];\n\nfunction onPickColor(color: number) {\n    const hexColor = color.toString(16).padStart(6, \"0\");\n\n    settings.store.color = hexColor;\n    createOrUpdateThemeColorVars(hexColor);\n}\n\nfunction setDiscordTheme(theme: string) {\n    saveClientTheme({ theme });\n}\n\nexport function ThemeSettingsComponent() {\n    const currentTheme = useStateFromStores([ThemeStore], () => ThemeStore.theme);\n    const isLightTheme = currentTheme === \"light\";\n    const oppositeTheme = isLightTheme ? \"Dark\" : \"Light\";\n\n    const nitroThemeEnabled = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset != null);\n\n    const selectedLuminance = relativeLuminance(settings.store.color);\n\n    let contrastWarning = false;\n    let fixableContrast = true;\n\n    if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) {\n        contrastWarning = true;\n    }\n\n    if (selectedLuminance < 0.26 && selectedLuminance > 0.12) {\n        fixableContrast = false;\n    }\n\n    // Light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels\n    if (isLightTheme && selectedLuminance > 0.65) {\n        contrastWarning = true;\n        fixableContrast = false;\n    }\n\n    return (\n        <div className={cl(\"settings\")}>\n            <div className={cl(\"container\")}>\n                <div className={cl(\"settings-labels\")}>\n                    <Forms.FormTitle tag=\"h3\">Theme Color</Forms.FormTitle>\n                    <Forms.FormText>Add a color to your Discord client theme</Forms.FormText>\n                </div>\n                <ColorPicker\n                    color={parseInt(settings.store.color, 16)}\n                    onChange={onPickColor}\n                    showEyeDropper={false}\n                    suggestedColors={colorPresets}\n                />\n            </div>\n            {(contrastWarning || nitroThemeEnabled) && (<>\n                <ErrorCard className={Margins.top8}>\n                    <Forms.FormTitle tag=\"h2\">Your theme won't look good!</Forms.FormTitle>\n\n                    {contrastWarning && <Forms.FormText>{\">\"} Selected color won't contrast well with text</Forms.FormText>}\n                    {nitroThemeEnabled && <Forms.FormText>{\">\"} Nitro themes aren't supported</Forms.FormText>}\n\n                    <div className={cl(\"buttons-container\")}>\n                        {(contrastWarning && fixableContrast) && <Button onClick={() => setDiscordTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}\n                        {(nitroThemeEnabled) && <Button onClick={() => setDiscordTheme(currentTheme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}\n                    </div>\n                </ErrorCard>\n            </>)}\n        </div>\n    );\n}\n\nexport function ResetThemeColorComponent() {\n    return (\n        <Button onClick={() => onPickColor(0x313338)}>\n            Reset Theme Color\n        </Button>\n    );\n}\n"
  },
  {
    "path": "src/plugins/clientTheme/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./clientTheme.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType, StartAt } from \"@utils/types\";\n\nimport { ResetThemeColorComponent, ThemeSettingsComponent } from \"./components/Settings\";\nimport { disableClientTheme, startClientTheme } from \"./utils/styleUtils\";\n\nexport const settings = definePluginSettings({\n    color: {\n        type: OptionType.COMPONENT,\n        default: \"313338\",\n        component: ThemeSettingsComponent\n    },\n    resetColor: {\n        type: OptionType.COMPONENT,\n        component: ResetThemeColorComponent\n    }\n});\n\nexport default definePlugin({\n    name: \"ClientTheme\",\n    authors: [Devs.Nuckyz],\n    description: \"Recreation of the old client theme experiment. Add a color to your Discord client theme\",\n    settings,\n\n    startAt: StartAt.DOMContentLoaded,\n    start: () => startClientTheme(settings.store.color),\n    stop: disableClientTheme\n});\n"
  },
  {
    "path": "src/plugins/clientTheme/utils/colorUtils.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\n// https://css-tricks.com/converting-color-spaces-in-javascript/\nexport function hexToHSL(hexCode: string) {\n    // Hex => RGB normalized to 0-1\n    const r = parseInt(hexCode.substring(0, 2), 16) / 255;\n    const g = parseInt(hexCode.substring(2, 4), 16) / 255;\n    const b = parseInt(hexCode.substring(4, 6), 16) / 255;\n\n    // RGB => HSL\n    const cMax = Math.max(r, g, b);\n    const cMin = Math.min(r, g, b);\n    const delta = cMax - cMin;\n\n    let hue: number;\n    let saturation: number;\n    let lightness: number;\n\n    lightness = (cMax + cMin) / 2;\n\n    if (delta === 0) {\n        // If r=g=b then the only thing that matters is lightness\n        hue = 0;\n        saturation = 0;\n    } else {\n        // Magic\n        saturation = delta / (1 - Math.abs(2 * lightness - 1));\n\n        if (cMax === r) {\n            hue = ((g - b) / delta) % 6;\n        } else if (cMax === g) {\n            hue = (b - r) / delta + 2;\n        } else {\n            hue = (r - g) / delta + 4;\n        }\n\n        hue *= 60;\n        if (hue < 0) {\n            hue += 360;\n        }\n    }\n\n    // Move saturation and lightness from 0-1 to 0-100\n    saturation *= 100;\n    lightness *= 100;\n\n    return { hue, saturation, lightness };\n}\n\n// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance\nexport function relativeLuminance(hexCode: string) {\n    const normalize = (x: number) => (\n        x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4\n    );\n\n    const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);\n    const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);\n    const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);\n\n    return r * 0.2126 + g * 0.7152 + b * 0.0722;\n}\n"
  },
  {
    "path": "src/plugins/clientTheme/utils/styleUtils.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { managedStyleRootNode } from \"@api/Styles\";\nimport { createAndAppendStyle } from \"@utils/css\";\n\nimport { hexToHSL } from \"./colorUtils\";\nconst VARS_STYLE_ID = \"vc-clientTheme-vars\";\nconst OVERRIDES_STYLE_ID = \"vc-clientTheme-overrides\";\ntype StyleId = typeof VARS_STYLE_ID | typeof OVERRIDES_STYLE_ID;\n\nconst styleCache = {} as Record<StyleId, HTMLStyleElement | null>;\n\nexport function createOrUpdateThemeColorVars(color: string) {\n    const { hue, saturation, lightness } = hexToHSL(color);\n\n    createOrUpdateStyle(VARS_STYLE_ID, `:root {\n        --theme-h: ${hue};\n        --theme-s: ${saturation}%;\n        --theme-l: ${lightness}%;\n    }`);\n}\n\nexport async function startClientTheme(color: string) {\n    createOrUpdateThemeColorVars(color);\n    createColorsOverrides(await getDiscordStyles());\n}\n\nexport function disableClientTheme() {\n    styleCache[VARS_STYLE_ID]?.remove();\n    styleCache[OVERRIDES_STYLE_ID]?.remove();\n    styleCache[VARS_STYLE_ID] = null;\n    styleCache[OVERRIDES_STYLE_ID] = null;\n}\n\nfunction getOrCreateStyle(styleId: StyleId) {\n    if (!styleCache[styleId]) {\n        styleCache[styleId] = createAndAppendStyle(styleId, managedStyleRootNode);\n    }\n    return styleCache[styleId];\n}\n\nfunction createOrUpdateStyle(styleId: StyleId, css: string) {\n    const style = getOrCreateStyle(styleId);\n    style.textContent = css;\n}\n\n/**\n * @returns A string containing all the CSS styles from the Discord client.\n */\nasync function getDiscordStyles(): Promise<string> {\n    const styleLinkNodes = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\n\n    const cssTexts = await Promise.all(Array.from(styleLinkNodes, async node => {\n        if (!node.href)\n            return null;\n\n        return fetch(node.href).then(res => res.text());\n    }));\n\n    return cssTexts.filter(Boolean).join(\"\\n\");\n}\n\nconst VISUAL_REFRESH_COLORS_VARIABLES_REGEX = /(--neutral-\\d{1,3}?-hsl):.+?([\\d.]+?)%;/g;\n\nfunction createColorsOverrides(styles: string) {\n    const visualRefreshColorsLightness = {} as Record<string, number>;\n\n    for (const [, colorVariableName, lightness] of styles.matchAll(VISUAL_REFRESH_COLORS_VARIABLES_REGEX)) {\n        visualRefreshColorsLightness[colorVariableName] = parseFloat(lightness);\n    }\n\n    const lightThemeBaseLightness = visualRefreshColorsLightness[\"--neutral-2-hsl\"];\n    const darkThemeBaseLightness = visualRefreshColorsLightness[\"--neutral-69-hsl\"];\n\n    createOrUpdateStyle(OVERRIDES_STYLE_ID, [\n        `.theme-light {\\n ${generateNewColorVars(visualRefreshColorsLightness, lightThemeBaseLightness)} \\n}`,\n        `.theme-dark {\\n ${generateNewColorVars(visualRefreshColorsLightness, darkThemeBaseLightness)} \\n}`,\n    ].join(\"\\n\\n\"));\n}\n\nfunction generateNewColorVars(colorsLightess: Record<string, number>, baseLightness: number) {\n    return Object.entries(colorsLightess).map(([colorVariableName, lightness]) => {\n        const lightnessOffset = lightness - baseLightness;\n        const plusOrMinus = lightnessOffset >= 0 ? \"+\" : \"-\";\n\n        return `${colorVariableName}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;\n    }).join(\"\\n\");\n}\n"
  },
  {
    "path": "src/plugins/colorSighted/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ColorSighted\",\n    description: \"Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord\",\n    authors: [Devs.lewisakura],\n    patches: [\n        {\n            find: \"Masks.STATUS_ONLINE\",\n            replacement: {\n                match: /Masks\\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,\n                replace: \"Masks.STATUS_ONLINE\"\n            }\n        },\n        {\n            find: \".AVATAR_STATUS_MOBILE_16;\",\n            replacement: {\n                match: /(fromIsMobile:\\i=!0,.+?)status:(\\i)/,\n                // Rename field to force it to always use \"online\"\n                replace: '$1status_$:$2=\"online\"'\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/consoleJanitor/README.md",
    "content": "# ConsoleJanitor\n\nDisables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and Discord logger messages.\n\nOne of the disabled messages is the \"Window state not initialized\" warning, for example.\n"
  },
  {
    "path": "src/plugins/consoleJanitor/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { SettingsSection } from \"@components/settings/tabs/plugins/components/Common\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { defineDefault, OptionType, StartAt } from \"@utils/types\";\nimport { Checkbox, Text } from \"@webpack/common\";\n\nconst Noop = () => { };\nconst NoopLogger = {\n    logDangerously: Noop,\n    log: Noop,\n    verboseDangerously: Noop,\n    verbose: Noop,\n    info: Noop,\n    warn: Noop,\n    error: Noop,\n    trace: Noop,\n    time: Noop,\n    fileOnly: Noop\n};\n\nconst logAllow = new Set();\n\ninterface AllowLevels {\n    error: boolean;\n    warn: boolean;\n    trace: boolean;\n    log: boolean;\n    info: boolean;\n    debug: boolean;\n}\n\ninterface AllowLevelSettingProps {\n    settingKey: keyof AllowLevels;\n}\n\nfunction AllowLevelSetting({ settingKey }: AllowLevelSettingProps) {\n    const { allowLevel } = settings.use([\"allowLevel\"]);\n    const value = allowLevel[settingKey];\n\n    return (\n        <Checkbox\n            value={value}\n            onChange={(_, newValue) => settings.store.allowLevel[settingKey] = newValue}\n            size={20}\n        >\n            <Text variant=\"text-sm/normal\">{settingKey[0].toUpperCase() + settingKey.slice(1)}</Text>\n        </Checkbox>\n    );\n}\n\nconst AllowLevelSettings = ErrorBoundary.wrap(() => {\n    return (\n        <SettingsSection name=\"Filter List\" description=\"Always allow loggers of these types\">\n            <div style={{ display: \"flex\", flexDirection: \"row\" }}>\n                {Object.keys(settings.store.allowLevel).map(key => (\n                    <AllowLevelSetting key={key} settingKey={key as keyof AllowLevels} />\n                ))}\n            </div>\n        </SettingsSection>\n    );\n});\n\nconst settings = definePluginSettings({\n    disableLoggers: {\n        type: OptionType.BOOLEAN,\n        description: \"Disables Discords loggers\",\n        default: false,\n        restartNeeded: true\n    },\n    disableSpotifyLogger: {\n        type: OptionType.BOOLEAN,\n        description: \"Disable the Spotify logger, which leaks account information and access token\",\n        default: true,\n        restartNeeded: true\n    },\n    whitelistedLoggers: {\n        type: OptionType.STRING,\n        description: \"Semicolon (;) separated list of loggers to allow even if others are hidden\",\n        default: \"GatewaySocket; Routing/Utils\",\n        multiline: true,\n        onChange(newVal: string) {\n            logAllow.clear();\n            newVal.split(\";\").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));\n        }\n    },\n    allowLevel: {\n        type: OptionType.COMPONENT,\n        component: AllowLevelSettings,\n        default: defineDefault<AllowLevels>({\n            error: true,\n            warn: false,\n            trace: false,\n            log: false,\n            info: false,\n            debug: false\n        })\n    }\n});\n\nexport default definePlugin({\n    name: \"ConsoleJanitor\",\n    description: \"Disables annoying console messages/errors\",\n    authors: [Devs.Nuckyz, Devs.sadan],\n    settings,\n\n    startAt: StartAt.Init,\n    start() {\n        logAllow.clear();\n        this.settings.store.whitelistedLoggers?.split(\";\").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));\n    },\n\n    Noop,\n    NoopLogger: () => NoopLogger,\n\n    shouldLog(logger: string, level: keyof AllowLevels) {\n        return logAllow.has(logger) || settings.store.allowLevel[level] === true;\n    },\n\n    patches: [\n        {\n            find: \"https://github.com/highlightjs/highlight.js/issues/2277\",\n            replacement: {\n                match: /\\(console.log\\(`Deprecated.+?`\\),/,\n                replace: \"(\"\n            }\n        },\n        {\n            find: 'The \"interpolate\" function is deprecated in v10 (use \"to\" instead)',\n            replacement: {\n                match: /,console.warn\\(\\i\\+'The \"interpolate\" function is deprecated in v10 \\(use \"to\" instead\\)'\\)/,\n                replace: \"\"\n            }\n        },\n        {\n            find: 'console.warn(\"Window state not initialized\"',\n            replacement: {\n                match: /console\\.warn\\(\"Window state not initialized\",\\i\\),/,\n                replace: \"\"\n            }\n        },\n        {\n            find: \"is not a valid locale.\",\n            replacement: [\n                {\n                    match: /\\i\\.error(?=\\(`\\$\\{\\i\\} is not a valid locale.`)/,\n                    replace: \"$self.Noop\"\n                }\n            ]\n        },\n        {\n            find: '\"AppCrashedFatalReport: getLastCrash not supported.\"',\n            replacement: {\n                match: /console\\.log(?=\\(\"AppCrashedFatalReport: getLastCrash not supported\\.\"\\))/,\n                replace: \"$self.Noop\"\n            }\n        },\n        {\n            find: \"RPCServer:WSS\",\n            replacement: {\n                match: /\\i\\.error\\(`Error: \\$\\{(\\i)\\.message\\}/,\n                replace: '!$1.message.includes(\"EADDRINUSE\")&&$&'\n            }\n        },\n        {\n            find: \"Tried getting Dispatch instance before instantiated\",\n            replacement: {\n                match: /null==\\i&&\\i\\.warn\\(\"Tried getting Dispatch instance before instantiated\"\\),/,\n                replace: \"\"\n            }\n        },\n        {\n            find: \"Unable to determine render window for element\",\n            replacement: {\n                match: /console\\.warn\\(\"Unable to determine render window for element\",\\i\\),/,\n                replace: \"\"\n            }\n        },\n        {\n            find: \"failed to send analytics events\",\n            replacement: [\n                {\n                    match: /console\\.error\\(`\\[analytics\\] failed to send analytics events query: \\$\\{\\i\\}`\\)/,\n                    replace: \"\"\n                }\n            ]\n        },\n        {\n            find: \"Slow dispatch on\",\n            replacement: [\n                {\n                    match: /\\i\\.totalTime>\\i&&\\i\\.verbose\\([`\"]Slow dispatch on.{0,55}\\);/,\n                    replace: \"\"\n                },\n            ]\n        },\n        // Patches Discord generic logger function\n        {\n            find: '\"file-only\"!==',\n            predicate: () => settings.store.disableLoggers,\n            replacement: {\n                match: /(?<=&&)(?=console)/,\n                replace: \"$self.shouldLog(arguments[0],arguments[1])&&\"\n            }\n        },\n        {\n            find: '(\"Spotify\")',\n            predicate: () => settings.store.disableSpotifyLogger,\n            replacement: {\n                match: /new \\i\\.\\i\\(\"Spotify\"\\)/,\n                replace: \"$self.NoopLogger()\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/consoleShortcuts/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport { getCurrentChannel, getCurrentGuild } from \"@utils/discord\";\nimport { runtimeHashMessageKey } from \"@utils/intlHash\";\nimport { SYM_LAZY_CACHED, SYM_LAZY_GET } from \"@utils/lazy\";\nimport { sleep } from \"@utils/misc\";\nimport { ModalAPI } from \"@utils/modal\";\nimport { relaunch } from \"@utils/native\";\nimport { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from \"@utils/patches\";\nimport definePlugin, { PluginNative, StartAt } from \"@utils/types\";\nimport * as Webpack from \"@webpack\";\nimport { extract, filters, findAll, findModuleId, search } from \"@webpack\";\nimport * as Common from \"@webpack/common\";\nimport { loadLazyChunks } from \"debug/loadLazyChunks\";\nimport type { ComponentType } from \"react\";\n\nconst DESKTOP_ONLY = (f: string) => () => {\n    throw new Error(`'${f}' is Discord Desktop only.`);\n};\n\nconst makeVesktopSwitcher = (branch: string) => () => {\n    if (Vesktop.Settings.store.discordBranch === branch)\n        throw new Error(`Already on ${branch}`);\n\n    Vesktop.Settings.store.discordBranch = branch;\n    VesktopNative.app.relaunch();\n};\n\nconst define: typeof Object.defineProperty =\n    (obj, prop, desc) => {\n        if (Object.hasOwn(desc, \"value\"))\n            desc.writable = true;\n\n        return Object.defineProperty(obj, prop, {\n            configurable: true,\n            enumerable: true,\n            ...desc\n        });\n    };\n\nfunction makeShortcuts() {\n    function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn, topLevelOnly = false) {\n        const cache = new Map<string, unknown>();\n\n        return function (...filterProps: unknown[]) {\n            const cacheKey = String(filterProps);\n            if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n            const matches = findAll(filterFactory(...filterProps), { topLevelOnly });\n\n            const result = (() => {\n                switch (matches.length) {\n                    case 0: return null;\n                    case 1: return matches[0];\n                    default:\n                        const uniqueMatches = [...new Set(matches)];\n                        if (uniqueMatches.length > 1)\n                            console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\\n`, uniqueMatches);\n\n                        return matches[0];\n                }\n            })();\n            if (result && cacheKey) cache.set(cacheKey, result);\n            return result;\n        };\n    }\n\n    function findStoreWrapper(findStore: typeof Webpack.findStore) {\n        const cache = new Map<string, unknown>();\n\n        return function (storeName: string) {\n            const cacheKey = String(storeName);\n            if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n            let store: unknown;\n            try {\n                store = findStore(storeName);\n            } catch { }\n            if (store) cache.set(cacheKey, store);\n            return store;\n        };\n    }\n\n    let fakeRenderWin: WeakRef<Window> | undefined;\n    const find = newFindWrapper(f => f);\n    const findByProps = newFindWrapper(filters.byProps);\n\n    return {\n        ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])),\n        wp: Webpack,\n        wpc: { getter: () => Webpack.cache },\n        wreq: { getter: () => Webpack.wreq },\n        wpPatcher: { getter: () => Vencord.WebpackPatcher },\n        wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },\n        wpsearch: search,\n        wpex: extract,\n        wpexs: (code: string) => extract(findModuleId(code)!),\n        loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error(\"loadLazyChunks is dev only.\"); },\n        find,\n        findAll: findAll,\n        findByProps,\n        findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),\n        findByCode: newFindWrapper(filters.byCode),\n        findCssClasses: newFindWrapper(filters.byClassNames, true),\n        findAllByCode: (code: string) => findAll(filters.byCode(code)),\n        findComponentByCode: newFindWrapper(filters.componentByCode),\n        findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),\n        findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],\n        findStore: findStoreWrapper(Webpack.findStore),\n        PluginsApi: { getter: () => Vencord.Plugins },\n        plugins: { getter: () => Vencord.Plugins.plugins },\n        Settings: { getter: () => Vencord.Settings },\n        Api: { getter: () => Vencord.Api },\n        Util: { getter: () => Vencord.Util },\n        reload: () => location.reload(),\n        restart: IS_WEB ? DESKTOP_ONLY(\"restart\") : relaunch,\n        canonicalizeMatch,\n        canonicalizeReplace,\n        canonicalizeReplacement,\n        runtimeHashMessageKey,\n        fakeRender: (component: ComponentType, props: any) => {\n            const prevWin = fakeRenderWin?.deref();\n            const win = prevWin?.closed === false\n                ? prevWin\n                : window.open(\"about:blank\", \"Fake Render\", \"popup,width=500,height=500\")!;\n            fakeRenderWin = new WeakRef(win);\n            win.focus();\n\n            const doc = win.document;\n            doc.body.style.margin = \"1em\";\n\n            if (!win.prepared) {\n                win.prepared = true;\n\n                [...document.querySelectorAll(\"style\"), ...document.querySelectorAll(\"link[rel=stylesheet]\")].forEach(s => {\n                    const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement;\n\n                    if (s.parentElement?.tagName === \"HEAD\")\n                        doc.head.append(n);\n                    else if (n.id?.startsWith(\"vencord-\") || n.id?.startsWith(\"vcd-\"))\n                        doc.documentElement.append(n);\n                    else\n                        doc.body.append(n);\n                });\n            }\n\n            const root = Common.createRoot(doc.body.appendChild(document.createElement(\"div\")));\n            root.render(Common.React.createElement(component, props));\n\n            doc.addEventListener(\"close\", () => root.unmount(), { once: true });\n        },\n\n        preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,\n\n        channel: { getter: () => getCurrentChannel(), preload: false },\n        channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false },\n        guild: { getter: () => getCurrentGuild(), preload: false },\n        guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false },\n        me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },\n        meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },\n        messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },\n        openModal: { getter: () => ModalAPI.openModal },\n        openModalLazy: { getter: () => ModalAPI.openModalLazy },\n\n        Stores: { getter: () => Object.fromEntries(Webpack.fluxStores) },\n\n        // e.g. \"2024-05_desktop_visual_refresh\", 0\n        setExperiment: (id: string, bucket: number) => {\n            Common.FluxDispatcher.dispatch({\n                type: \"EXPERIMENT_OVERRIDE_BUCKET\",\n                experimentId: id,\n                experimentBucket: bucket,\n            });\n        },\n        ...IS_VESKTOP ? {\n            vesktopStable: makeVesktopSwitcher(\"stable\"),\n            vesktopCanary: makeVesktopSwitcher(\"canary\"),\n            vesktopPtb: makeVesktopSwitcher(\"ptb\"),\n        } : {},\n    };\n}\n\nfunction loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {\n    const currentVal = val.getter();\n    if (!currentVal || val.preload === false) return currentVal;\n\n    function unwrapProxy(value: any) {\n        if (value[SYM_LAZY_GET]) {\n            forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];\n        } else if (value.$$vencordGetWrappedComponent) {\n            return forceLoad ? value.$$vencordGetWrappedComponent() : value;\n        }\n\n        return value;\n    }\n\n    const value = unwrapProxy(currentVal);\n    if (typeof value === \"object\" && value !== null) {\n        const descriptors = Object.getOwnPropertyDescriptors(value);\n\n        for (const propKey in descriptors) {\n            if (value[propKey] == null) continue;\n\n            const descriptor = descriptors[propKey];\n            if (descriptor.writable === true || descriptor.set != null) {\n                const currentValue = value[propKey];\n                const newValue = unwrapProxy(currentValue);\n                if (newValue != null && currentValue !== newValue) {\n                    value[propKey] = newValue;\n                }\n            }\n        }\n    }\n\n    if (value != null) {\n        define(window.shortcutList, key, { value });\n        define(window, key, { value });\n    }\n\n    return value;\n}\n\nconst webpackModulesProbablyLoaded = Webpack.onceReady.then(() => sleep(1000));\n\nexport default definePlugin({\n    name: \"ConsoleShortcuts\",\n    description: \"Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.\",\n    authors: [Devs.Ven],\n    startAt: StartAt.Init,\n\n    patches: [\n        {\n            find: \"&&this.initializeIfNeeded()\",\n            replacement: [\n                {\n                    match: /\\i&&this\\.initializeIfNeeded\\(\\)/,\n                    replace: \"$&,Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1})\"\n                }\n            ]\n        }\n    ],\n\n\n    start() {\n        const shortcuts = makeShortcuts();\n        window.shortcutList = {};\n\n        for (const [key, val] of Object.entries(shortcuts)) {\n            if (\"getter\" in val) {\n                define(window.shortcutList, key, {\n                    get: () => loadAndCacheShortcut(key, val, true)\n                });\n\n                define(window, key, {\n                    get: () => window.shortcutList[key]\n                });\n            } else {\n                window.shortcutList[key] = val;\n                window[key] = val;\n            }\n        }\n\n        // unproxy loaded modules\n        this.eagerLoad(false);\n\n        if (!IS_WEB) {\n            const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import(\"./native\")>;\n            Native.initDevtoolsOpenEagerLoad();\n        }\n    },\n\n    async eagerLoad(forceLoad: boolean) {\n        await webpackModulesProbablyLoaded;\n\n        const shortcuts = makeShortcuts();\n\n        for (const [key, val] of Object.entries(shortcuts)) {\n            if (!Object.hasOwn(val, \"getter\") || (val as any).preload === false) continue;\n\n            try {\n                loadAndCacheShortcut(key, val, forceLoad);\n            } catch { } // swallow not found errors in DEV\n        }\n    },\n\n    stop() {\n        delete window.shortcutList;\n        for (const key in makeShortcuts()) {\n            delete window[key];\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/consoleShortcuts/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { IpcMainInvokeEvent } from \"electron\";\n\nexport function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) {\n    const handleDevtoolsOpened = () => e.sender.executeJavaScript(\"Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)\");\n\n    if (e.sender.isDevToolsOpened())\n        handleDevtoolsOpened();\n    else\n        e.sender.once(\"devtools-opened\", () => handleDevtoolsOpened());\n}\n"
  },
  {
    "path": "src/plugins/copyEmojiMarkdown/README.md",
    "content": "# CopyEmojiMarkdown\n\nAllows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒`\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547)\n"
  },
  {
    "path": "src/plugins/copyEmojiMarkdown/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { Menu } from \"@webpack/common\";\n\nconst { convertNameToSurrogate } = findByPropsLazy(\"convertNameToSurrogate\");\n\ninterface Emoji {\n    type: string;\n    id: string;\n    name: string;\n}\n\ninterface Target {\n    dataset: Emoji;\n    firstChild: HTMLImageElement;\n}\n\nfunction getEmojiMarkdown(target: Target, copyUnicode: boolean): string {\n    const { id: emojiId, name: emojiName } = target.dataset;\n\n    if (!emojiId) {\n        return copyUnicode\n            ? convertNameToSurrogate(emojiName)\n            : `:${emojiName}:`;\n    }\n\n    const url = new URL(target.firstChild.src);\n    const hasParam = url.searchParams.get(\"animated\") === \"true\";\n    const isGif = url.pathname.endsWith(\".gif\");\n\n    return `<${(hasParam || isGif) ? \"a\" : \"\"}:${emojiName.replace(/~\\d+$/, \"\")}:${emojiId}>`;\n}\n\nconst settings = definePluginSettings({\n    copyUnicode: {\n        type: OptionType.BOOLEAN,\n        description: \"Copy the raw unicode character instead of :name: for default emojis (👽)\",\n        default: true,\n    },\n});\n\nexport default definePlugin({\n    name: \"CopyEmojiMarkdown\",\n    description: \"Allows you to copy emojis as formatted string (<:blobcatcozy:1026533070955872337>)\",\n    authors: [Devs.HappyEnderman, Devs.Vishnya],\n    settings,\n\n    contextMenus: {\n        \"expression-picker\"(children, { target }: { target: Target; }) {\n            if (target.dataset.type !== \"emoji\") return;\n\n            children.push(\n                <Menu.MenuItem\n                    id=\"vc-copy-emoji-markdown\"\n                    label=\"Copy Emoji Markdown\"\n                    action={() => {\n                        copyWithToast(\n                            getEmojiMarkdown(target, settings.store.copyUnicode),\n                            \"Success! Copied emoji markdown.\"\n                        );\n                    }}\n                />\n            );\n        },\n    },\n});\n"
  },
  {
    "path": "src/plugins/copyFileContents/README.md",
    "content": "# CopyFileContents\n\nAdds a button to text file attachments to copy their contents.\n\n![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca)\n"
  },
  {
    "path": "src/plugins/copyFileContents/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./style.css\";\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { CopyIcon, NoEntrySignIcon } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { Tooltip, useState } from \"@webpack/common\";\n\nconst CheckMarkIcon = () => {\n    return <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n        <path fill=\"currentColor\" d=\"M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z\"></path>\n    </svg>;\n};\n\nexport default definePlugin({\n    name: \"CopyFileContents\",\n    description: \"Adds a button to text file attachments to copy their contents\",\n    authors: [Devs.Obsidian, Devs.Nuckyz],\n    patches: [\n        {\n            find: \"#{intl::PREVIEW_BYTES_LEFT}\",\n            replacement: {\n                match: /fileName:\\i,fileSize:\\i}\\),(?=.{0,75}?setLanguage:)(?<=fileContents:(\\i),bytesLeft:(\\i).+?)/g,\n                replace: \"$&$self.addCopyButton({fileContents:$1,bytesLeft:$2}),\"\n            }\n        }\n    ],\n\n    addCopyButton: ErrorBoundary.wrap(({ fileContents, bytesLeft }: { fileContents: string, bytesLeft: number; }) => {\n        const [recentlyCopied, setRecentlyCopied] = useState(false);\n\n        return (\n            <Tooltip text={recentlyCopied ? \"Copied!\" : bytesLeft > 0 ? \"File too large to copy\" : \"Copy File Contents\"}>\n                {tooltipProps => (\n                    <div\n                        {...tooltipProps}\n                        className=\"vc-cfc-button\"\n                        role=\"button\"\n                        onClick={() => {\n                            if (!recentlyCopied && bytesLeft <= 0) {\n                                copyWithToast(fileContents);\n                                setRecentlyCopied(true);\n                                setTimeout(() => setRecentlyCopied(false), 2000);\n                            }\n                        }}\n                    >\n                        {recentlyCopied ? <CheckMarkIcon /> : bytesLeft > 0 ? <NoEntrySignIcon color=\"var(--channel-icon)\" /> : <CopyIcon />}\n                    </div>\n                )}\n            </Tooltip>\n        );\n    }, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/copyFileContents/style.css",
    "content": ".vc-cfc-button {\n    color: var(--interactive-icon-default);\n    cursor: pointer;\n    padding-left: 4px;\n}\n\n.vc-cfc-button:hover {\n    color: var(--interactive-icon-hover);\n}"
  },
  {
    "path": "src/plugins/copyStickerLinks/README.md",
    "content": "# CopyStickerLinks\n\nAdds \"Copy Link\" and \"Open Link\" options to the context menu of stickers!\n\n![](https://github.com/user-attachments/assets/a0982d5c-ab83-458b-9ca3-834803e0782e)\n"
  },
  {
    "path": "src/plugins/copyStickerLinks/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2025 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport ExpressionClonerPlugin from \"@plugins/expressionCloner\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { Message, Sticker } from \"@vencord/discord-types\";\nimport { Menu, React, StickersStore } from \"@webpack/common\";\n\nconst StickerExt = [, \"png\", \"png\", \"json\", \"gif\"] as const;\n\ntype PartialSticker = Pick<Sticker, \"id\" | \"format_type\">;\n\nfunction getUrl(data: PartialSticker): string {\n    if (data.format_type === 4)\n        return `https:${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.gif?size=512&lossless=true`;\n\n    return `https://${window.GLOBAL_ENV.CDN_HOST}/stickers/${data.id}.${StickerExt[data.format_type]}?size=512&lossless=true`;\n}\n\nfunction buildMenuItem(sticker: PartialSticker, addBottomSeparator: boolean) {\n    return (\n        <>\n            <Menu.MenuGroup>\n                <Menu.MenuItem\n                    id=\"vc-copy-sticker-link\"\n                    key=\"vc-copy-sticker-link\"\n                    label=\"Copy Link\"\n                    action={() => copyWithToast(getUrl(sticker), \"Link copied!\")}\n                />\n\n                <Menu.MenuItem\n                    id=\"vc-open-sticker-link\"\n                    key=\"vc-open-sticker-link\"\n                    label=\"Open Link\"\n                    action={() => VencordNative.native.openExternal(getUrl(sticker))}\n                />\n            </Menu.MenuGroup>\n            {addBottomSeparator && <Menu.MenuSeparator />}\n        </>\n    );\n}\n\nconst messageContextMenuPatch: NavContextMenuPatchCallback = (\n    children,\n    { favoriteableId, favoriteableType, message }: { favoriteableId: string; favoriteableType: string; message: Message; }\n) => {\n    if (!favoriteableId || favoriteableType !== \"sticker\") return;\n\n    const sticker = message.stickerItems.find(s => s.id === favoriteableId);\n    if (!sticker?.format_type) return;\n\n    const idx = children.findIndex(c => Array.isArray(c) && findGroupChildrenByChildId(\"vc-copy-sticker-url\", c) != null);\n\n    children.splice(idx, 0, buildMenuItem(sticker, idx !== -1));\n};\n\nconst expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {\n    const id = props?.target?.dataset?.id;\n    if (!id) return;\n    if (props.target.className?.includes(\"lottieCanvas\")) return;\n\n    const sticker = StickersStore.getStickerById(id);\n    if (sticker) {\n        children.push(buildMenuItem(sticker, isPluginEnabled(ExpressionClonerPlugin.name)));\n    }\n};\n\nexport default definePlugin({\n    name: \"CopyStickerLinks\",\n    description: \"Adds the ability to copy & open Sticker links\",\n    authors: [Devs.Ven, Devs.Byeoon],\n    contextMenus: {\n        \"message\": messageContextMenuPatch,\n        \"expression-picker\": expressionPickerPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/copyUserURLs/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { LinkIcon } from \"@components/Icons\";\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport type { Channel, User } from \"@vencord/discord-types\";\nimport { Menu } from \"@webpack/common\";\n\ninterface UserContextProps {\n    channel: Channel;\n    guildId?: string;\n    user: User;\n}\n\nconst UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {\n    if (!user) return;\n\n    children.push(\n        <Menu.MenuItem\n            id=\"vc-copy-user-url\"\n            label=\"Copy User URL\"\n            action={() => copyToClipboard(`<https://discord.com/users/${user.id}>`)}\n            icon={LinkIcon}\n        />\n    );\n};\n\nexport default definePlugin({\n    name: \"CopyUserURLs\",\n    authors: [Devs.castdrian],\n    description: \"Adds a 'Copy User URL' option to the user context menu.\",\n    contextMenus: {\n        \"user-context\": UserContextMenuPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/crashHandler/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { showNotification } from \"@api/Notifications\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport { closeAllModals } from \"@utils/modal\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { maybePromptToUpdate } from \"@utils/updater\";\nimport { filters, findBulk, proxyLazyWebpack } from \"@webpack\";\nimport { DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from \"@webpack/common\";\n\nconst CrashHandlerLogger = new Logger(\"CrashHandler\");\n\nconst { ModalStack, DraftManager } = proxyLazyWebpack(() => {\n    const [ModalStack, DraftManager] = findBulk(\n        filters.byProps(\"pushLazy\", \"popAll\"),\n        filters.byProps(\"clearDraft\", \"saveDraft\"),\n    );\n\n    return {\n        ModalStack,\n        DraftManager\n    };\n});\n\nconst settings = definePluginSettings({\n    attemptToPreventCrashes: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to attempt to prevent Discord crashes.\",\n        default: true\n    },\n    attemptToNavigateToHome: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to attempt to navigate to the home when preventing Discord crashes.\",\n        default: false\n    }\n});\n\nlet hasCrashedOnce = false;\nlet isRecovering = false;\nlet shouldAttemptRecover = true;\n\nexport default definePlugin({\n    name: \"CrashHandler\",\n    description: \"Utility plugin for handling and possibly recovering from crashes without a restart\",\n    authors: [Devs.Nuckyz],\n    enabledByDefault: true,\n\n    settings,\n\n    patches: [\n        {\n            find: \"#{intl::ERRORS_UNEXPECTED_CRASH}\",\n            replacement: {\n                match: /this\\.setState\\((.+?)\\)/,\n                replace: \"$self.handleCrash(this,$1);\"\n            }\n        }\n    ],\n\n    handleCrash(_this: any, errorState: any) {\n        if (IS_DEV) {\n            try {\n                if (errorState?.info && \"componentStack\" in errorState.info) {\n                    console.error(\"Component Stack:\", errorState.info.componentStack);\n                }\n            } catch { }\n        }\n        _this.setState(errorState);\n\n        // Already recovering, prevent error which happens more than once too fast to trigger another recover\n        if (isRecovering) return;\n        isRecovering = true;\n\n        // 1 ms timeout to avoid react breaking when re-rendering\n        setTimeout(() => {\n            try {\n                // Prevent a crash loop with an error that could not be handled\n                if (!shouldAttemptRecover) {\n                    try {\n                        showNotification({\n                            color: \"#eed202\",\n                            title: \"Discord has crashed!\",\n                            body: \"Awn :( Discord has crashed two times rapidly, not attempting to recover.\",\n                            noPersist: true\n                        });\n                    } catch { }\n\n                    return;\n                }\n\n                shouldAttemptRecover = false;\n                // This is enough to avoid a crash loop\n                setTimeout(() => shouldAttemptRecover = true, 1000);\n            } catch { }\n\n            try {\n                if (!hasCrashedOnce) {\n                    hasCrashedOnce = true;\n                    maybePromptToUpdate(\"Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?\", true);\n                }\n            } catch { }\n\n            try {\n                if (settings.store.attemptToPreventCrashes) {\n                    this.handlePreventCrash(_this);\n                }\n            } catch (err) {\n                CrashHandlerLogger.error(\"Failed to handle crash\", err);\n            }\n        }, 1);\n    },\n\n    handlePreventCrash(_this: any) {\n        try {\n            showNotification({\n                color: \"#eed202\",\n                title: \"Discord has crashed!\",\n                body: \"Attempting to recover...\",\n                noPersist: true\n            });\n        } catch { }\n\n        try {\n            const channelId = SelectedChannelStore.getChannelId();\n\n            for (const key in DraftType) {\n                if (!Number.isNaN(Number(key))) continue;\n\n                DraftManager.clearDraft(channelId, DraftType[key]);\n            }\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to clear drafts.\", err);\n        }\n        try {\n            ExpressionPickerStore.closeExpressionPicker();\n        }\n        catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close expression picker.\", err);\n        }\n        try {\n            FluxDispatcher.dispatch({ type: \"CONTEXT_MENU_CLOSE\" });\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close open context menu.\", err);\n        }\n        try {\n            ModalStack.popAll();\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close old modals.\", err);\n        }\n        try {\n            closeAllModals();\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close all open modals.\", err);\n        }\n        try {\n            FluxDispatcher.dispatch({ type: \"USER_PROFILE_MODAL_CLOSE\" });\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close user popout.\", err);\n        }\n        try {\n            FluxDispatcher.dispatch({ type: \"LAYER_POP_ALL\" });\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to pop all layers.\", err);\n        }\n        try {\n            FluxDispatcher.dispatch({\n                type: \"DEV_TOOLS_SETTINGS_UPDATE\",\n                settings: { displayTools: false, lastOpenTabId: \"analytics\" }\n            });\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to close DevTools.\", err);\n        }\n\n        if (settings.store.attemptToNavigateToHome) {\n            try {\n                NavigationRouter.transitionToGuild(\"@me\");\n            } catch (err) {\n                CrashHandlerLogger.debug(\"Failed to navigate to home\", err);\n            }\n        }\n\n        // Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens\n        setImmediate(() => isRecovering = false);\n\n        try {\n            _this.setState({ error: null, info: null });\n        } catch (err) {\n            CrashHandlerLogger.debug(\"Failed to update crash handler component.\", err);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/ctrlEnterSend/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"CtrlEnterSend\",\n    authors: [Devs.UlyssesZhan],\n    description: \"Use Ctrl+Enter to send messages (customizable)\",\n    settings: definePluginSettings({\n        submitRule: {\n            description: \"The way to send a message\",\n            type: OptionType.SELECT,\n            options: [\n                {\n                    label: \"Ctrl+Enter (Enter or Shift+Enter for new line) (cmd+enter on macOS)\",\n                    value: \"ctrl+enter\"\n                },\n                {\n                    label: \"Shift+Enter (Enter for new line)\",\n                    value: \"shift+enter\"\n                },\n                {\n                    label: \"Enter (Shift+Enter for new line; Discord default)\",\n                    value: \"enter\"\n                }\n            ],\n            default: \"ctrl+enter\"\n        },\n        sendMessageInTheMiddleOfACodeBlock: {\n            description: \"Whether to send a message in the middle of a code block\",\n            type: OptionType.BOOLEAN,\n            default: true,\n        }\n    }),\n    patches: [\n        // Only one of the two patches will be at effect; Discord often updates to switch between them.\n        // See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673\n        {\n            find: \".selectPreviousCommandOption(\",\n            replacement: {\n                match: /(?<=(\\i)\\.which!==\\i\\.\\i.ENTER\\|\\|).{0,100}(\\(0,\\i\\.\\i\\)\\(\\i\\)).{0,100}(?=\\|\\|\\(\\i\\.preventDefault)/,\n                replace: \"!$self.shouldSubmit($1,$2)\"\n            }\n        },\n        {\n            find: \"!this.hasOpenCodeBlock()\",\n            replacement: {\n                match: /!(\\i).shiftKey&&!(this.hasOpenCodeBlock\\(\\))&&\\(.{0,100}?\\)/,\n                replace: \"$self.shouldSubmit($1, $2)\"\n            }\n        }\n    ],\n    shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {\n        let result = false;\n        switch (this.settings.store.submitRule) {\n            case \"shift+enter\":\n                result = event.shiftKey;\n                break;\n            case \"ctrl+enter\":\n                result = IS_MAC ? event.metaKey : event.ctrlKey;\n                break;\n            case \"enter\":\n                result = !event.shiftKey && !event.ctrlKey;\n                break;\n        }\n        if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {\n            result &&= !codeblock;\n        }\n        return result;\n    }\n});\n"
  },
  {
    "path": "src/plugins/customCommands/CreateTagModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2026 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { BaseText } from \"@components/BaseText\";\nimport { Button } from \"@components/Button\";\nimport { Card } from \"@components/Card\";\nimport { InlineCode } from \"@components/CodeBlock\";\nimport { ExpandableSection } from \"@components/ExpandableCard\";\nimport { Flex } from \"@components/Flex\";\nimport { HeadingSecondary } from \"@components/Heading\";\nimport { InfoIcon } from \"@components/Icons\";\nimport { Margins } from \"@components/margins\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { TextArea, TextInput, useState } from \"@webpack/common\";\n\nimport { parseTagArguments } from \".\";\nimport { addTag, getTag, Tag } from \"./settings\";\n\nexport function openCreateTagModal(initialValue: Tag = { name: \"\", message: \"\" }) {\n    openModal(modalProps => (\n        <Modal initialValue={initialValue} modalProps={modalProps} />\n    ));\n}\n\nconst EXAMPLE_RESPONSE = \"Hello {{user}}! I am feeling {{mood = great}}.\";\n\nfunction Modal({ initialValue, modalProps }: { initialValue: Tag; modalProps: ModalProps; }) {\n    const [name, setName] = useState(initialValue.name);\n    const [message, setMessage] = useState(initialValue.message.replaceAll(\"\\\\n\", \"\\n\"));\n\n    const detectedArguments = parseTagArguments(message);\n    const hasReservedEphemeral = detectedArguments.some(arg => arg.name === \"ephemeral\");\n    const nameAlreadyExists = name !== initialValue.name && getTag(name);\n\n    return (\n        <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>\n            <ModalHeader>\n                <BaseText size=\"lg\" weight=\"semibold\" style={{ flexGrow: 1 }}>Create Tag</BaseText>\n                <ModalCloseButton onClick={modalProps.onClose} />\n            </ModalHeader>\n\n            <ModalContent>\n                <Flex flexDirection=\"column\" gap={12}>\n                    <Paragraph>Create a new tag which will be registered as a slash command.</Paragraph>\n\n                    <section className={Margins.top8}>\n                        <HeadingSecondary>Name</HeadingSecondary>\n                        <TextInput value={name} onChange={setName} placeholder=\"greet\" />\n                    </section>\n\n                    <section>\n                        <HeadingSecondary>Response</HeadingSecondary>\n                        <TextArea value={message} onChange={setMessage} placeholder={EXAMPLE_RESPONSE} />\n                    </section>\n\n                    {detectedArguments.length > 0 && (\n                        <section>\n                            <HeadingSecondary>Detected Arguments</HeadingSecondary>\n                            <Paragraph>\n                                <ul>\n                                    {detectedArguments.map(arg => (\n                                        <li key={arg.name}>\n                                            &mdash; <b>{arg.name}</b>{arg.defaultValue ? ` (default: ${arg.defaultValue})` : \"\"}\n                                        </li>\n                                    ))}\n                                </ul>\n                            </Paragraph>\n                        </section>\n                    )}\n\n                    <ExpandableSection\n                        renderContent={() => (\n                            <Flex flexDirection=\"column\" gap={12}>\n                                <Paragraph>\n                                    Your response can include variables wrapped in double curly braces which will become command arguments, for example <InlineCode>{\"Hello {{user}}\"}</InlineCode>.\n                                </Paragraph>\n                                <Paragraph>\n                                    You can specify arguments with default values by using an equals sign, for example <InlineCode>{\"Hello {{user = pal}}\"}</InlineCode>.\n                                </Paragraph>\n\n                                <section>\n                                    <Paragraph><b>Example Command response:</b> <InlineCode>{EXAMPLE_RESPONSE}</InlineCode></Paragraph>\n                                    <Paragraph><b>Example usage:</b> <InlineCode>{\"/greet user:@Clyde\"}</InlineCode></Paragraph>\n                                    <Paragraph><b>Example output:</b> <InlineCode>{\"Hello @Clyde! I am feeling great.\"}</InlineCode></Paragraph>\n                                </section>\n                            </Flex>\n                        )}\n                    >\n                        <Flex alignItems=\"center\" gap={8}>\n                            <InfoIcon color=\"var(--text-muted)\" height={16} width={16} />\n                            View Arguments guide\n                        </Flex>\n                    </ExpandableSection>\n                    {hasReservedEphemeral &&\n                        <Card variant=\"danger\" className={Margins.top8} defaultPadding>\n                            <Paragraph>The argument name \"ephemeral\" is reserved and cannot be used.</Paragraph>\n                        </Card>\n                    }\n                    {nameAlreadyExists &&\n                        <Card variant=\"warning\" className={Margins.top8} defaultPadding>\n                            <Paragraph>A tag with the name <InlineCode>{name}</InlineCode> already exists and will be overwritten.</Paragraph>\n                        </Card>\n                    }\n                </Flex>\n            </ModalContent>\n\n            <ModalFooter>\n                <Flex>\n                    <Button\n                        variant=\"secondary\"\n                        onClick={modalProps.onClose}\n                    >\n                        Cancel\n                    </Button>\n                    <Button\n                        onClick={() => {\n                            const tag = { name, message };\n                            addTag(tag);\n                            modalProps.onClose();\n                        }}\n                        disabled={!name || !message || hasReservedEphemeral}\n                    >\n                        Create\n                    </Button>\n                </Flex>\n            </ModalFooter>\n        </ModalRoot>\n    );\n}\n"
  },
  {
    "path": "src/plugins/customCommands/SettingsTagList.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2026 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { BaseText } from \"@components/BaseText\";\nimport { Button } from \"@components/Button\";\nimport { Card } from \"@components/Card\";\nimport { Flex } from \"@components/Flex\";\nimport { DeleteIcon, PencilIcon } from \"@components/Icons\";\nimport { Margins } from \"@components/margins\";\nimport { Paragraph } from \"@components/Paragraph\";\n\nimport { openCreateTagModal } from \"./CreateTagModal\";\nimport { removeTag, settings } from \"./settings\";\n\nexport function SettingsTagList() {\n    const { tagsList } = settings.use([\"tagsList\"]);\n\n    return (\n        <section className={Margins.top8}>\n            <BaseText size=\"md\" weight=\"semibold\">Registered Tags</BaseText>\n            <Flex flexDirection=\"column\" gap=\"0.5em\" className={Margins.top8}>\n                {Object.values(tagsList).map(tag => (\n                    <Card key={tag.name} className=\"vc-customCommands-card\">\n                        <Paragraph size=\"md\" weight=\"medium\">{tag.name}</Paragraph>\n\n                        <Button variant=\"secondary\" size=\"iconOnly\" onClick={() => openCreateTagModal(tag)}>\n                            <PencilIcon aria-label=\"Edit Tag\" width={20} height={20} />\n                        </Button>\n                        <Button variant=\"dangerSecondary\" size=\"iconOnly\" onClick={() => removeTag(tag.name)}>\n                            <DeleteIcon aria-label=\"Delete Tag\" width={20} height={20} />\n                        </Button>\n                    </Card>\n                ))}\n                <Button onClick={() => openCreateTagModal()}>Create Tag</Button>\n            </Flex>\n        </section>\n    );\n}\n"
  },
  {
    "path": "src/plugins/customCommands/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage } from \"@api/Commands\";\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { sendMessage } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { FluxDispatcher, MessageActions, PendingReplyStore } from \"@webpack/common\";\n\nimport { openCreateTagModal } from \"./CreateTagModal\";\nimport { getTag, getTags, removeTag, settings, Tag } from \"./settings\";\n\nconst CustomCommandsMarker = Symbol(\"CustomCommands\");\nconst ArgumentRegex = /{{(.+?)}}/g;\n\nexport function parseTagArguments(message: string) {\n    const args = [] as { name: string, defaultValue: string | null; }[];\n\n    for (const [, value] of message.matchAll(ArgumentRegex)) {\n        const [name, defaultValue] = value.split(\"=\").map(s => s.trim());\n\n        if (!name) continue;\n        if (args.some(arg => arg.name === name)) continue;\n\n        args.push({ name: name.toLowerCase(), defaultValue: defaultValue ?? null });\n    }\n\n    return args;\n}\n\nexport function registerTagCommand(tag: Tag) {\n    const tagArguments = parseTagArguments(tag.message);\n\n    registerCommand({\n        name: tag.name,\n        description: tag.name,\n        inputType: ApplicationCommandInputType.BUILT_IN,\n        options: [\n            ...tagArguments.map(arg => ({\n                name: arg.name,\n                description: arg.name,\n                type: ApplicationCommandOptionType.STRING,\n                required: arg.defaultValue === null\n            })),\n            {\n                name: \"ephemeral\",\n                description: \"Whether the response should only be visible to you\",\n                type: ApplicationCommandOptionType.BOOLEAN,\n                required: false\n            }\n        ],\n\n        execute: async (args, { channel }) => {\n            const ephemeral = findOption(args, \"ephemeral\", false);\n\n            const response = tag.message\n                .replace(ArgumentRegex, (fullMatch, value: string) => {\n                    const [argName, defaultValue] = value.split(\"=\").map(s => s.trim());\n                    return findOption(args, argName, null) ?? defaultValue ?? fullMatch;\n                })\n                .replaceAll(\"\\\\n\", \"\\n\");\n\n            const doSend = ephemeral ? sendBotMessage : sendMessage;\n            doSend(channel.id, { content: response }, false, MessageActions.getSendMessageOptionsForReply(PendingReplyStore.getPendingReply(channel.id)));\n            FluxDispatcher.dispatch({ type: \"DELETE_PENDING_REPLY\", channelId: channel.id });\n        },\n        [CustomCommandsMarker]: true,\n    }, \"CustomCommands\");\n}\n\n\nmigratePluginSettings(\"CustomCommands\", \"MessageTags\");\nexport default definePlugin({\n    name: \"CustomCommands\",\n    description: \"Allows you to create custom slash commands / tags\",\n    tags: [\"MessageTags\"],\n    authors: [Devs.Ven, Devs.Luna,],\n    settings,\n\n    async start() {\n        const tags = getTags();\n        for (const tagName in tags) {\n            registerTagCommand(tags[tagName]);\n        }\n    },\n\n    commands: [\n        {\n            name: \"tags\",\n            description: \"Manage all custom commands\",\n            inputType: ApplicationCommandInputType.BUILT_IN,\n            options: [\n                {\n                    name: \"create\",\n                    description: \"Create a new tag\",\n                    type: ApplicationCommandOptionType.SUB_COMMAND,\n                },\n                {\n                    name: \"list\",\n                    description: \"List all your tags\",\n                    type: ApplicationCommandOptionType.SUB_COMMAND,\n                    options: []\n                },\n                {\n                    name: \"delete\",\n                    description: \"Remove a tag by name\",\n                    type: ApplicationCommandOptionType.SUB_COMMAND,\n                    options: [\n                        {\n                            name: \"tag-name\",\n                            description: \"The name of the tag\",\n                            type: ApplicationCommandOptionType.STRING,\n                            required: true\n                        }\n                    ]\n                },\n            ],\n\n            async execute(args, ctx) {\n                switch (args[0].name) {\n                    case \"create\": {\n                        openCreateTagModal();\n                        break;\n                    }\n\n                    case \"delete\": {\n                        const name: string = findOption(args[0].options, \"tag-name\", \"\");\n\n                        if (!getTag(name))\n                            return sendBotMessage(ctx.channel.id, {\n                                content: `A Tag with the name **${name}** does not exist!`\n                            });\n\n                        removeTag(name);\n\n                        sendBotMessage(ctx.channel.id, {\n                            content: `Successfully deleted the tag **${name}**!`\n                        });\n\n                        break;\n                    }\n\n                    case \"list\": {\n                        const content = Object.values(getTags())\n                            .map(tag => `\\`${tag.name}\\`: ${tag.message.slice(0, 72).replaceAll(\"\\\\n\", \" \")}${tag.message.length > 72 ? \"...\" : \"\"}`)\n                            .join(\"\\n\");\n\n                        sendBotMessage(ctx.channel.id, {\n                            content: content || \"Woops! There are no tags yet, use `/tags create` to create one!\",\n                        });\n\n                        break;\n                    }\n                }\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/customCommands/settings.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2026 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { unregisterCommand } from \"@api/Commands\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { OptionType } from \"@utils/types\";\n\nimport { registerTagCommand } from \".\";\nimport { SettingsTagList } from \"./SettingsTagList\";\n\nexport const settings = definePluginSettings({\n    tagsList: {\n        type: OptionType.CUSTOM,\n        default: {} as Record<string, Tag>,\n    },\n    tagComponent: {\n        type: OptionType.COMPONENT,\n        component: SettingsTagList\n    }\n});\n\nexport interface Tag {\n    name: string;\n    message: string;\n}\n\nexport function getTags() {\n    return settings.store.tagsList;\n}\n\nexport function getTag(name: string) {\n    return settings.store.tagsList[name];\n}\n\nexport function addTag(tag: Tag) {\n    unregisterCommand(tag.name);\n\n    settings.store.tagsList[tag.name] = tag;\n    registerTagCommand(tag);\n}\n\nexport function removeTag(name: string) {\n    delete settings.store.tagsList[name];\n    unregisterCommand(name);\n}\n"
  },
  {
    "path": "src/plugins/customCommands/styles.css",
    "content": ".vc-customCommands-card {\n    padding: 0.5em;\n    padding-left: 1em;\n    display: grid;\n    grid-template-columns: 1fr min-content min-content;\n    align-items: center;\n    gap: 0.25em;\n}"
  },
  {
    "path": "src/plugins/customIdle/README.md",
    "content": "# CustomIdle\n\nLets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.\n\n![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b)\n"
  },
  {
    "path": "src/plugins/customIdle/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { currentNotice, noticesQueue, popNotice, showNotice } from \"@api/Notices\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { makeRange, OptionType } from \"@utils/types\";\nimport { FluxDispatcher } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    idleTimeout: {\n        description: \"Minutes before Discord goes idle (0 to disable auto-idle)\",\n        type: OptionType.SLIDER,\n        markers: makeRange(0, 60, 5),\n        default: 10,\n        stickToMarkers: false,\n        restartNeeded: true // Because of the setInterval patch\n    },\n    remainInIdle: {\n        description: \"When you come back to Discord, remain idle until you confirm you want to go online\",\n        type: OptionType.BOOLEAN,\n        default: true\n    }\n});\n\nexport default definePlugin({\n    name: \"CustomIdle\",\n    description: \"Allows you to set the time before Discord goes idle (or disable auto-idle)\",\n    authors: [Devs.newwares],\n    settings,\n    patches: [\n        {\n            find: 'type:\"IDLE\",idle:',\n            replacement: [\n                {\n                    match: /(?<=Date\\.now\\(\\)-\\i>)\\i\\.\\i\\|\\|/,\n                    replace: \"$self.getIdleTimeout()||\"\n                },\n                {\n                    match: /Math\\.min\\((\\i\\*\\i\\.\\i\\.\\i\\.SECOND),\\i\\.\\i\\)/,\n                    replace: \"$1\" // Decouple idle from afk (phone notifications will remain at user setting or 10 min maximum)\n                },\n                {\n                    match: /\\i\\.\\i\\.dispatch\\({type:\"IDLE\",idle:!1}\\)/,\n                    replace: \"$self.handleOnline()\"\n                }\n            ]\n        }\n    ],\n\n    handleOnline() {\n        if (!settings.store.remainInIdle) {\n            FluxDispatcher.dispatch({\n                type: \"IDLE\",\n                idle: false\n            });\n            return;\n        }\n\n        const backOnlineMessage = \"Welcome back! Click the button to go online. Click the X to stay idle until reload.\";\n        if (\n            currentNotice?.[1] === backOnlineMessage ||\n            noticesQueue.some(([, noticeMessage]) => noticeMessage === backOnlineMessage)\n        ) return;\n\n        showNotice(backOnlineMessage, \"Exit idle\", () => {\n            popNotice();\n            FluxDispatcher.dispatch({\n                type: \"IDLE\",\n                idle: false\n            });\n        });\n    },\n\n    getIdleTimeout() { // milliseconds, default is 6e5\n        const { idleTimeout } = settings.store;\n        return idleTimeout === 0 ? Infinity : idleTimeout * 60000;\n    }\n});\n"
  },
  {
    "path": "src/plugins/customRPC/README.md",
    "content": "# CustomRPC\n\nAllows you to set a custom Rich Presence (game activity)\n\n![](https://github.com/user-attachments/assets/c924466f-ded3-4661-a360-24ca384741f5)\n"
  },
  {
    "path": "src/plugins/customRPC/RpcSettings.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./settings.css\";\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { Divider } from \"@components/Divider\";\nimport { Heading } from \"@components/Heading\";\nimport { resolveError } from \"@components/settings/tabs/plugins/components/Common\";\nimport { debounce } from \"@shared/debounce\";\nimport { classNameFactory } from \"@utils/css\";\nimport { ActivityType } from \"@vencord/discord-types/enums\";\nimport { Select, Text, TextInput, useState } from \"@webpack/common\";\n\nimport CustomRPCPlugin, { setRpc, settings, TimestampMode } from \".\";\n\nconst cl = classNameFactory(\"vc-customRPC-settings-\");\n\ntype SettingsKey = keyof typeof settings.store;\n\ninterface TextOption<T> {\n    settingsKey: SettingsKey;\n    label: string;\n    disabled?: boolean;\n    transform?: (value: string) => T;\n    isValid?: (value: T) => true | string;\n}\n\ninterface SelectOption<T> {\n    settingsKey: SettingsKey;\n    label: string;\n    disabled?: boolean;\n    options: { label: string; value: T; default?: boolean; }[];\n}\n\nconst makeValidator = (maxLength: number, isRequired = false) => (value: string) => {\n    if (isRequired && !value) return \"This field is required.\";\n    if (value.length > maxLength) return `Must be not longer than ${maxLength} characters.`;\n    return true;\n};\n\nconst maxLength128 = makeValidator(128);\n\nfunction isAppIdValid(value: string) {\n    if (!/^\\d{16,21}$/.test(value)) return \"Must be a valid Discord ID.\";\n    return true;\n}\n\nconst updateRPC = debounce(() => {\n    setRpc(true);\n    if (isPluginEnabled(CustomRPCPlugin.name)) setRpc();\n});\n\nfunction isStreamLinkDisabled() {\n    return settings.store.type !== ActivityType.STREAMING;\n}\n\nfunction isStreamLinkValid(value: string) {\n    if (!isStreamLinkDisabled() && !/https?:\\/\\/(www\\.)?(twitch\\.tv|youtube\\.com)\\/\\w+/.test(value)) return \"Streaming link must be a valid URL.\";\n    if (value && value.length > 512) return \"Streaming link must be not longer than 512 characters.\";\n    return true;\n}\n\nfunction parseNumber(value: string) {\n    return value ? parseInt(value, 10) : 0;\n}\n\nfunction isNumberValid(value: number) {\n    if (isNaN(value)) return \"Must be a number.\";\n    if (value < 0) return \"Must be a positive number.\";\n    return true;\n}\n\nfunction isUrlValid(value: string) {\n    if (value && !/^https?:\\/\\/.+/.test(value)) return \"Must be a valid URL.\";\n    return true;\n}\n\nfunction isImageKeyValid(value: string) {\n    if (/https?:\\/\\/(cdn|media)\\.discordapp\\.(com|net)\\//.test(value)) return \"Don't use a Discord link. Use an Imgur image link instead.\";\n    if (/https?:\\/\\/(?!i\\.)?imgur\\.com\\//.test(value)) return \"Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'\";\n    if (/https?:\\/\\/(?!media\\.)?tenor\\.com\\//.test(value)) return \"Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'\";\n    return true;\n}\n\nfunction PairSetting<T>(props: { data: [TextOption<T>, TextOption<T>]; }) {\n    const [left, right] = props.data;\n\n    return (\n        <div className={cl(\"pair\")}>\n            <SingleSetting {...left} />\n            <SingleSetting {...right} />\n        </div>\n    );\n}\n\nfunction SingleSetting<T>({ settingsKey, label, disabled, isValid, transform }: TextOption<T>) {\n    const [state, setState] = useState(settings.store[settingsKey] ?? \"\");\n    const [error, setError] = useState<string | null>(null);\n\n    function handleChange(newValue: any) {\n        if (transform) newValue = transform(newValue);\n\n        const valid = isValid?.(newValue) ?? true;\n\n        setState(newValue);\n        setError(resolveError(valid));\n\n        if (valid === true) {\n            settings.store[settingsKey] = newValue;\n            updateRPC();\n        }\n    }\n\n    return (\n        <div className={cl(\"single\", { disabled })}>\n            <Heading tag=\"h5\">{label}</Heading>\n            <TextInput\n                type=\"text\"\n                placeholder={\"Enter a value\"}\n                value={state}\n                onChange={handleChange}\n                disabled={disabled}\n            />\n            {error && <Text className={cl(\"error\")} variant=\"text-sm/normal\">{error}</Text>}\n        </div>\n    );\n}\n\nfunction SelectSetting<T>({ settingsKey, label, options, disabled }: SelectOption<T>) {\n    return (\n        <div className={cl(\"single\", { disabled })}>\n            <Heading tag=\"h5\">{label}</Heading>\n            <Select\n                placeholder={\"Select an option\"}\n                options={options}\n                maxVisibleItems={5}\n                closeOnSelect={true}\n                select={v => settings.store[settingsKey] = v}\n                isSelected={v => v === settings.store[settingsKey]}\n                serialize={v => String(v)}\n                isDisabled={disabled}\n            />\n        </div>\n    );\n}\n\nexport function RPCSettings() {\n    const s = settings.use();\n\n    return (\n        <div className={cl(\"root\")}>\n            <SelectSetting\n                settingsKey=\"type\"\n                label=\"Activity Type\"\n                options={[\n                    {\n                        label: \"Playing\",\n                        value: ActivityType.PLAYING,\n                        default: true\n                    },\n                    {\n                        label: \"Streaming\",\n                        value: ActivityType.STREAMING\n                    },\n                    {\n                        label: \"Listening\",\n                        value: ActivityType.LISTENING\n                    },\n                    {\n                        label: \"Watching\",\n                        value: ActivityType.WATCHING\n                    },\n                    {\n                        label: \"Competing\",\n                        value: ActivityType.COMPETING\n                    }\n                ]}\n            />\n\n            <PairSetting data={[\n                { settingsKey: \"appID\", label: \"Application ID\", isValid: isAppIdValid },\n                { settingsKey: \"appName\", label: \"Application Name\", isValid: makeValidator(128, true) },\n            ]} />\n\n            <PairSetting data={[\n                { settingsKey: \"details\", label: \"Detail (line 1)\", isValid: maxLength128 },\n                { settingsKey: \"detailsURL\", label: \"Detail URL\", isValid: isUrlValid },\n            ]} />\n\n            <PairSetting data={[\n                { settingsKey: \"state\", label: \"State (line 2)\", isValid: maxLength128 },\n                { settingsKey: \"stateURL\", label: \"State URL\", isValid: isUrlValid },\n            ]} />\n\n            <SingleSetting\n                settingsKey=\"streamLink\"\n                label=\"Stream Link (Twitch or YouTube, only if activity type is Streaming)\"\n                disabled={s.type !== ActivityType.STREAMING}\n                isValid={isStreamLinkValid}\n            />\n\n            <PairSetting data={[\n                {\n                    settingsKey: \"partySize\",\n                    label: \"Party Size\",\n                    transform: parseNumber,\n                    isValid: isNumberValid,\n                    disabled: s.type !== ActivityType.PLAYING,\n                },\n                {\n                    settingsKey: \"partyMaxSize\",\n                    label: \"Maximum Party Size\",\n                    transform: parseNumber,\n                    isValid: isNumberValid,\n                    disabled: s.type !== ActivityType.PLAYING,\n                },\n            ]} />\n\n            <Divider />\n\n            <PairSetting data={[\n                { settingsKey: \"imageBig\", label: \"Large Image URL/Key\", isValid: isImageKeyValid },\n                { settingsKey: \"imageBigTooltip\", label: \"Large Image Text\", isValid: maxLength128 },\n            ]} />\n            <SingleSetting settingsKey=\"imageBigURL\" label=\"Large Image clickable URL\" isValid={isUrlValid} />\n\n            <PairSetting data={[\n                { settingsKey: \"imageSmall\", label: \"Small Image URL/Key\", isValid: isImageKeyValid },\n                { settingsKey: \"imageSmallTooltip\", label: \"Small Image Text\", isValid: maxLength128 },\n            ]} />\n            <SingleSetting settingsKey=\"imageSmallURL\" label=\"Small Image clickable URL\" isValid={isUrlValid} />\n\n            <Divider />\n\n            <PairSetting data={[\n                { settingsKey: \"buttonOneText\", label: \"Button1 Text\", isValid: makeValidator(31) },\n                { settingsKey: \"buttonOneURL\", label: \"Button1 URL\", isValid: isUrlValid },\n            ]} />\n            <PairSetting data={[\n                { settingsKey: \"buttonTwoText\", label: \"Button2 Text\", isValid: makeValidator(31) },\n                { settingsKey: \"buttonTwoURL\", label: \"Button2 URL\", isValid: isUrlValid },\n            ]} />\n\n            <Divider />\n\n            <SelectSetting\n                settingsKey=\"timestampMode\"\n                label=\"Timestamp Mode\"\n                options={[\n                    {\n                        label: \"None\",\n                        value: TimestampMode.NONE,\n                        default: true\n                    },\n                    {\n                        label: \"Since discord open\",\n                        value: TimestampMode.NOW\n                    },\n                    {\n                        label: \"Same as your current time (not reset after 24h)\",\n                        value: TimestampMode.TIME\n                    },\n                    {\n                        label: \"Custom\",\n                        value: TimestampMode.CUSTOM\n                    }\n                ]}\n            />\n\n            <PairSetting data={[\n                {\n                    settingsKey: \"startTime\",\n                    label: \"Start Timestamp (in milliseconds)\",\n                    transform: parseNumber,\n                    isValid: isNumberValid,\n                    disabled: s.timestampMode !== TimestampMode.CUSTOM,\n                },\n                {\n                    settingsKey: \"endTime\",\n                    label: \"End Timestamp (in milliseconds)\",\n                    transform: parseNumber,\n                    isValid: isNumberValid,\n                    disabled: s.timestampMode !== TimestampMode.CUSTOM,\n                },\n            ]} />\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/plugins/customRPC/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport { Divider } from \"@components/Divider\";\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Flex } from \"@components/Flex\";\nimport { Link } from \"@components/Link\";\nimport { Devs } from \"@utils/constants\";\nimport { isTruthy } from \"@utils/guards\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { useAwaiter } from \"@utils/react\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Activity } from \"@vencord/discord-types\";\nimport { ActivityType } from \"@vencord/discord-types/enums\";\nimport { findByCodeLazy, findComponentByCodeLazy } from \"@webpack\";\nimport { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from \"@webpack/common\";\n\nimport { RPCSettings } from \"./RpcSettings\";\n\nconst useProfileThemeStyle = findByCodeLazy(\"profileThemeStyle:\", \"--profile-gradient-primary-color\");\nconst ActivityView = findComponentByCodeLazy(\".party?(0\", \"USER_PROFILE_ACTIVITY\");\n\nconst ShowCurrentGame = getUserSettingLazy<boolean>(\"status\", \"showCurrentGame\")!;\n\nasync function getApplicationAsset(key: string): Promise<string> {\n    return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];\n}\n\nexport const enum TimestampMode {\n    NONE,\n    NOW,\n    TIME,\n    CUSTOM,\n}\n\nexport const settings = definePluginSettings({\n    config: {\n        type: OptionType.COMPONENT,\n        component: RPCSettings\n    },\n}).withPrivateSettings<{\n    appID?: string;\n    appName?: string;\n    details?: string;\n    detailsURL?: string;\n    state?: string;\n    stateURL?: string;\n    type?: ActivityType;\n    streamLink?: string;\n    timestampMode?: TimestampMode;\n    startTime?: number;\n    endTime?: number;\n    imageBig?: string;\n    imageBigURL?: string;\n    imageBigTooltip?: string;\n    imageSmall?: string;\n    imageSmallURL?: string;\n    imageSmallTooltip?: string;\n    buttonOneText?: string;\n    buttonOneURL?: string;\n    buttonTwoText?: string;\n    buttonTwoURL?: string;\n    partySize?: number;\n    partyMaxSize?: number;\n}>();\n\nasync function createActivity(): Promise<Activity | undefined> {\n    const {\n        appID,\n        appName,\n        details,\n        detailsURL,\n        state,\n        stateURL,\n        type,\n        streamLink,\n        startTime,\n        endTime,\n        imageBig,\n        imageBigURL,\n        imageBigTooltip,\n        imageSmall,\n        imageSmallURL,\n        imageSmallTooltip,\n        buttonOneText,\n        buttonOneURL,\n        buttonTwoText,\n        buttonTwoURL,\n        partyMaxSize,\n        partySize,\n        timestampMode\n    } = settings.store;\n\n    if (!appName) return;\n\n    const activity: Activity = {\n        application_id: appID || \"0\",\n        name: appName,\n        state,\n        details,\n        type: type ?? ActivityType.PLAYING,\n        flags: 1 << 0,\n    };\n\n    if (type === ActivityType.STREAMING) activity.url = streamLink;\n\n    switch (timestampMode) {\n        case TimestampMode.NOW:\n            activity.timestamps = {\n                start: Date.now()\n            };\n            break;\n        case TimestampMode.TIME:\n            activity.timestamps = {\n                start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000\n            };\n            break;\n        case TimestampMode.CUSTOM:\n            if (startTime || endTime) {\n                activity.timestamps = {};\n                if (startTime) activity.timestamps.start = startTime;\n                if (endTime) activity.timestamps.end = endTime;\n            }\n            break;\n        case TimestampMode.NONE:\n        default:\n            break;\n    }\n\n    if (detailsURL) {\n        activity.details_url = detailsURL;\n    }\n\n    if (stateURL) {\n        activity.state_url = stateURL;\n    }\n\n    if (buttonOneText) {\n        activity.buttons = [\n            buttonOneText,\n            buttonTwoText\n        ].filter(isTruthy);\n\n        activity.metadata = {\n            button_urls: [\n                buttonOneURL,\n                buttonTwoURL\n            ].filter(isTruthy)\n        };\n    }\n\n    if (imageBig) {\n        activity.assets = {\n            large_image: await getApplicationAsset(imageBig),\n            large_text: imageBigTooltip || undefined,\n            large_url: imageBigURL || undefined\n        };\n    }\n\n    if (imageSmall) {\n        activity.assets = {\n            ...activity.assets,\n            small_image: await getApplicationAsset(imageSmall),\n            small_text: imageSmallTooltip || undefined,\n            small_url: imageSmallURL || undefined\n        };\n    }\n\n    if (partyMaxSize && partySize) {\n        activity.party = {\n            size: [partySize, partyMaxSize]\n        };\n    }\n\n    for (const k in activity) {\n        if (k === \"type\") continue;\n        const v = activity[k];\n        if (!v || v.length === 0)\n            delete activity[k];\n    }\n\n    return activity;\n}\n\nexport async function setRpc(disable?: boolean) {\n    const activity: Activity | undefined = await createActivity();\n\n    FluxDispatcher.dispatch({\n        type: \"LOCAL_ACTIVITY_UPDATE\",\n        activity: !disable ? activity : null,\n        socketId: \"CustomRPC\",\n    });\n}\n\nexport default definePlugin({\n    name: \"CustomRPC\",\n    description: \"Add a fully customisable Rich Presence (Game status) to your Discord profile\",\n    authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],\n    dependencies: [\"UserSettingsAPI\"],\n    // This plugin's patch is not important for functionality, so don't require a restart\n    requiresRestart: false,\n    settings,\n\n    start: setRpc,\n    stop: () => setRpc(true),\n\n    // Discord hides buttons on your own Rich Presence for some reason. This patch disables that behaviour\n    patches: [\n        {\n            find: \".USER_PROFILE_ACTIVITY_BUTTONS),\",\n            replacement: {\n                match: /.getId\\(\\)===\\i.id/,\n                replace: \"$& && false\"\n            }\n        }\n    ],\n\n    settingsAboutComponent: () => {\n        const [activity] = useAwaiter(createActivity, { fallbackValue: undefined, deps: Object.values(settings.store) });\n        const gameActivityEnabled = ShowCurrentGame.useSetting();\n        const { profileThemeStyle } = useProfileThemeStyle({});\n\n        return (\n            <>\n                {!gameActivityEnabled && (\n                    <ErrorCard\n                        className={classes(Margins.top16, Margins.bottom16)}\n                        style={{ padding: \"1em\" }}\n                    >\n                        <Forms.FormTitle>Notice</Forms.FormTitle>\n                        <Forms.FormText>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>\n\n                        <Button\n                            color={Button.Colors.TRANSPARENT}\n                            className={Margins.top8}\n                            onClick={() => ShowCurrentGame.updateSetting(true)}\n                        >\n                            Enable\n                        </Button>\n                    </ErrorCard>\n                )}\n\n                <Flex flexDirection=\"column\" gap=\".5em\" className={Margins.top16}>\n                    <Forms.FormText>\n                        Go to the <Link href=\"https://discord.com/developers/applications\">Discord Developer Portal</Link> to create an application and\n                        get the application ID.\n                    </Forms.FormText>\n                    <Forms.FormText>\n                        Upload images in the Rich Presence tab to get the image keys.\n                    </Forms.FormText>\n                    <Forms.FormText>\n                        If you want to use an image link, download your image and reupload the image to <Link href=\"https://imgur.com\">Imgur</Link> and get the image link by right-clicking the image and selecting \"Copy image address\".\n                    </Forms.FormText>\n                    <Forms.FormText>\n                        You can't see your own buttons on your profile, but everyone else can see it fine.\n                    </Forms.FormText>\n                    <Forms.FormText>\n                        Some weird unicode text (\"fonts\" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.\n                    </Forms.FormText>\n                </Flex>\n\n                <Divider className={Margins.top8} />\n\n                <div style={{ width: \"284px\", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: \"var(--background-mod-muted)\" }}>\n                    {activity && <ActivityView\n                        activity={activity}\n                        user={UserStore.getCurrentUser()}\n                        currentUser={UserStore.getCurrentUser()}\n                    />}\n                </div>\n            </>\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/customRPC/settings.css",
    "content": ".vc-customRPC-settings-root {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5em;\n\n    & h5 {\n        margin: 0;\n    }\n}\n\n.vc-customRPC-settings-pair {\n    display: grid;\n    grid-template-columns: repeat(2, 1fr);\n    align-items: start;\n    gap: 0.5em;\n    width: 100%;\n}\n\n.vc-customRPC-settings-single {\n    display: grid;\n    gap: 0.5em;\n}\n\n.vc-customRPC-settings-disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n}\n\n.vc-customRPC-settings-error {\n    color: var(--text-feedback-critical);\n}"
  },
  {
    "path": "src/plugins/dearrow/README.md",
    "content": "# Dearrow\n\nMakes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)\n\nhttps://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283\n"
  },
  {
    "path": "src/plugins/dearrow/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Tooltip } from \"@webpack/common\";\nimport type { Component } from \"react\";\n\ninterface Props {\n    embed: {\n        rawTitle: string;\n        provider?: {\n            name: string;\n        };\n        thumbnail: {\n            proxyURL: string;\n        };\n        video: {\n            url: string;\n        };\n\n        dearrow: {\n            enabled: boolean;\n            oldTitle?: string;\n            oldThumb?: string;\n        };\n    };\n}\n\nconst enum ReplaceElements {\n    ReplaceAllElements,\n    ReplaceTitlesOnly,\n    ReplaceThumbnailsOnly\n}\n\nconst embedUrlRe = /https:\\/\\/www\\.youtube\\.com\\/embed\\/([a-zA-Z0-9_-]{11})/;\n\nasync function embedDidMount(this: Component<Props>) {\n    try {\n        const { embed } = this.props;\n        const { replaceElements, dearrowByDefault } = settings.store;\n\n        if (!embed || embed.dearrow || embed.provider?.name !== \"YouTube\" || !embed.video?.url) return;\n\n        const videoId = embedUrlRe.exec(embed.video.url)?.[1];\n        if (!videoId) return;\n\n        const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);\n        if (!res.ok) return;\n\n        const { titles, thumbnails } = await res.json();\n\n        const hasTitle = titles[0]?.votes >= 0;\n        const hasThumb = thumbnails[0]?.votes >= 0 && !thumbnails[0].original;\n\n        if (!hasTitle && !hasThumb) return;\n\n\n        embed.dearrow = {\n            enabled: dearrowByDefault\n        };\n\n        if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {\n            const replacementTitle = titles[0].title.replace(/(^|\\s)>(\\S)/g, \"$1$2\");\n\n            embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;\n            if (dearrowByDefault) embed.rawTitle = replacementTitle;\n        }\n        if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {\n            const replacementProxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;\n\n            embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;\n            if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;\n        }\n\n        this.forceUpdate();\n    } catch (err) {\n        new Logger(\"Dearrow\").error(\"Failed to dearrow embed\", err);\n    }\n}\n\nfunction DearrowButton({ component }: { component: Component<Props>; }) {\n    const { embed } = component.props;\n    if (!embed?.dearrow) return null;\n\n    return (\n        <Tooltip text={embed.dearrow.enabled ? \"This embed has been dearrowed, click to restore\" : \"Click to dearrow\"}>\n            {({ onMouseEnter, onMouseLeave }) => (\n                <button\n                    onMouseEnter={onMouseEnter}\n                    onMouseLeave={onMouseLeave}\n                    className={\"vc-dearrow-toggle-\" + (embed.dearrow.enabled ? \"on\" : \"off\")}\n                    onClick={() => {\n                        const { enabled, oldThumb, oldTitle } = embed.dearrow;\n                        settings.store.dearrowByDefault = !enabled;\n                        embed.dearrow.enabled = !enabled;\n                        if (oldTitle) {\n                            embed.dearrow.oldTitle = embed.rawTitle;\n                            embed.rawTitle = oldTitle;\n                        }\n                        if (oldThumb) {\n                            embed.dearrow.oldThumb = embed.thumbnail.proxyURL;\n                            embed.thumbnail.proxyURL = oldThumb;\n                        }\n\n                        component.forceUpdate();\n                    }}\n                >\n                    {/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}\n                    <svg\n                        xmlns=\"http://www.w3.org/2000/svg\"\n                        width=\"24px\"\n                        height=\"24px\"\n                        viewBox=\"0 0 36 36\"\n                        aria-label=\"Toggle Dearrow\"\n                        className=\"vc-dearrow-icon\"\n                    >\n                        <path\n                            fill=\"#1213BD\"\n                            d=\"M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z\"\n                        />\n                        <path\n                            fill=\"#88c9f9\"\n                            d=\"m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z\"\n                        />\n                        <path\n                            fill=\"#0a62a5\"\n                            d=\"m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z\"\n                        />\n                    </svg>\n\n                </button>\n            )}\n        </Tooltip>\n    );\n}\n\nconst settings = definePluginSettings({\n    hideButton: {\n        description: \"Hides the Dearrow button from YouTube embeds\",\n        type: OptionType.BOOLEAN,\n        default: false,\n        restartNeeded: true\n    },\n    replaceElements: {\n        description: \"Choose which elements of the embed will be replaced\",\n        type: OptionType.SELECT,\n        restartNeeded: true,\n        options: [\n            { label: \"Everything (Titles & Thumbnails)\", value: ReplaceElements.ReplaceAllElements, default: true },\n            { label: \"Titles\", value: ReplaceElements.ReplaceTitlesOnly },\n            { label: \"Thumbnails\", value: ReplaceElements.ReplaceThumbnailsOnly },\n        ],\n    },\n    dearrowByDefault: {\n        description: \"Dearrow videos automatically\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: false\n    }\n});\n\nexport default definePlugin({\n    name: \"Dearrow\",\n    description: \"Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow\",\n    authors: [Devs.Ven],\n    settings,\n\n    embedDidMount,\n    renderButton(component: Component<Props>) {\n        return (\n            <ErrorBoundary noop>\n                <DearrowButton component={component} />\n            </ErrorBoundary>\n        );\n    },\n\n    patches: [{\n        find: \"this.renderInlineMediaEmbed\",\n        replacement: [\n            // patch componentDidMount to replace embed thumbnail and title\n            {\n                match: /render\\(\\)\\{.{0,30}let\\{embed:/,\n                replace: \"componentDidMount=$self.embedDidMount;$&\"\n            },\n\n            // add dearrow button\n            {\n                match: /children:\\[(?=null!=\\i\\?(\\i)\\.renderSuppressButton)/,\n                replace: \"children:[$self.renderButton($1),\",\n                predicate: () => !settings.store.hideButton\n            }\n        ]\n    }],\n});\n"
  },
  {
    "path": "src/plugins/dearrow/styles.css",
    "content": ".vc-dearrow-toggle-off .vc-dearrow-icon {\n    filter: grayscale(1);\n}\n\n.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {\n    all: unset;\n    display: inline;\n    cursor: pointer;\n    position: absolute;\n    top: 0.75rem;\n    right: 0.75rem;\n}\n"
  },
  {
    "path": "src/plugins/decor/README.md",
    "content": "# Decor\n\nCustom avatar decorations!\n\n![Custom decorations in chat](https://github.com/Vendicated/Vencord/assets/30497388/b0c4c4c8-8723-42a8-b50f-195ad4e26136)\n\nCreate and use your own custom avatar decorations, or pick your favorite from the presets.\n\nYou'll be able to see the custom avatar decorations of other users of this plugin, and they'll be able to see your custom avatar decoration.\n\nYou can select and manage your custom avatar decorations under the \"Profiles\" page in settings, or in the plugin settings.\n\n![Custom decorations management](https://github.com/Vendicated/Vencord/assets/30497388/74fe8a9e-a2a2-4b29-bc10-9eaa58208ad4)\n\nReview the [guidelines](https://github.com/decor-discord/.github/blob/main/GUIDELINES.md) before creating your own custom avatar decoration.\n\nJoin the [Discord server](https://discord.gg/dXp2SdxDcP) for support and notifications on your decoration's review.\n"
  },
  {
    "path": "src/plugins/decor/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated, FieryFlames and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./ui/styles.css\";\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { UserStore } from \"@webpack/common\";\n\nimport { CDN_URL, RAW_SKU_ID, SKU_ID } from \"./lib/constants\";\nimport { useAuthorizationStore } from \"./lib/stores/AuthorizationStore\";\nimport { useCurrentUserDecorationsStore } from \"./lib/stores/CurrentUserDecorationsStore\";\nimport { useUserDecorAvatarDecoration, useUsersDecorationsStore } from \"./lib/stores/UsersDecorationsStore\";\nimport { settings } from \"./settings\";\nimport { setDecorationGridDecoration, setDecorationGridItem } from \"./ui/components\";\nimport DecorSection from \"./ui/components/DecorSection\";\n\nexport interface AvatarDecoration {\n    asset: string;\n    skuId: string;\n}\n\nexport default definePlugin({\n    name: \"Decor\",\n    description: \"Create and use your own custom avatar decorations, or pick your favorite from the presets.\",\n    authors: [Devs.FieryFlames],\n    patches: [\n        // Patch MediaResolver to return correct URL for Decor avatar decorations\n        {\n            find: \"getAvatarDecorationURL:\",\n            replacement: {\n                match: /(?<=function \\i\\(\\i\\){)(?=let{avatarDecoration)/,\n                replace: \"const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;\"\n            }\n        },\n        // Patch profile customization settings to include Decor section\n        {\n            find: \"DefaultCustomizationSections\",\n            replacement: {\n                match: /(?<=#{intl::USER_SETTINGS_AVATAR_DECORATION}\\)},\"decoration\"\\),)/,\n                replace: \"$self.DecorSection(),\"\n            }\n        },\n        // Decoration modal module\n        {\n            find: \"80,onlyAnimateOnHoverOrFocus:!\",\n            replacement: [\n                {\n                    match: /(?<==)\\i=>{let{children.{20,200}isSelected:\\i=!1.{0,5}\\}=\\i/,\n                    replace: \"$self.DecorationGridItem=$&\",\n                },\n                {\n                    match: /(?<==)\\i=>{let{user:\\i,avatarDecoration/,\n                    replace: \"$self.DecorationGridDecoration=$&\",\n                },\n                // Remove NEW label from decor avatar decorations\n                {\n                    match: /(?<=\\.\\i\\.PURCHASE)(?=,)(?<=avatarDecoration:(\\i).+?)/,\n                    replace: \"||$1.skuId===$self.SKU_ID\"\n                }\n            ]\n        },\n        {\n            find: \"isAvatarDecorationAnimating:\",\n            group: true,\n            replacement: [\n                // Add Decor avatar decoration hook to avatar decoration hook\n                {\n                    match: /(?<=\\.avatarDecoration,guildId:\\i\\}\\)\\),)(?<=user:(\\i).+?)/,\n                    replace: \"vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),\"\n                },\n                // Use added hook\n                {\n                    match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\\i).+?)/,\n                    replace: \"$1??vcDecorAvatarDecoration??($&)\"\n                },\n                // Make memo depend on added hook\n                {\n                    match: /(?<=size:\\i}\\),\\[)/,\n                    replace: \"vcDecorAvatarDecoration,\"\n                }\n            ]\n        },\n        // Current user area, at bottom of channels/dm list\n        {\n            find: \".DISPLAY_NAME_STYLES_COACHMARK)\",\n            replacement: [\n                // Use Decor avatar decoration hook\n                {\n                    match: /(?<=\\i\\)\\({avatarDecoration:)\\i(?=,)(?<=currentUser:(\\i).+?)/,\n                    replace: \"$self.useUserDecorAvatarDecoration($1)??$&\"\n                }\n            ]\n        },\n        ...[\n            \"#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\", // Messages\n            \"#{intl::COLLECTIBLES_NAMEPLATE_PREVIEW_A11Y}\", // Nameplate preview\n            \"#{intl::COLLECTIBLES_PROFILE_PREVIEW_A11Y}\", // Avatar preview\n        ].map(find => ({\n            find,\n            replacement: {\n                match: /(?<=userValue:)((\\i(?:\\.author)?)\\?\\.avatarDecoration)/,\n                replace: \"$self.useUserDecorAvatarDecoration($2)??$1\"\n            }\n        })),\n        // Patch avatar decoration preview to display Decor avatar decorations as if they are purchased\n        {\n            find: \"#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}\",\n            replacement: {\n                match: /(#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}.+?return null!=(\\i)&&\\()(null==\\i)/,\n                replace: (_, rest, avatarDecoration, hasPurchase) => `${rest}(${avatarDecoration}.skuId!==$self.SKU_ID&&${avatarDecoration}.skuId!==$self.RAW_SKU_ID&&${hasPurchase})`\n            }\n        }\n    ],\n    settings,\n\n    flux: {\n        CONNECTION_OPEN: () => {\n            useAuthorizationStore.getState().init();\n            useCurrentUserDecorationsStore.getState().clear();\n            useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);\n        },\n        USER_PROFILE_MODAL_OPEN: data => {\n            useUsersDecorationsStore.getState().fetch(data.userId, true);\n        },\n    },\n\n    set DecorationGridItem(e: any) {\n        setDecorationGridItem(e);\n    },\n\n    set DecorationGridDecoration(e: any) {\n        setDecorationGridDecoration(e);\n    },\n\n    SKU_ID,\n    RAW_SKU_ID,\n\n    useUserDecorAvatarDecoration,\n\n    async start() {\n        useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);\n    },\n\n    getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {\n        // Only Decor avatar decorations have this SKU ID\n        if (avatarDecoration?.skuId === SKU_ID) {\n            const parts = avatarDecoration.asset.split(\"_\");\n            // Remove a_ prefix if it's animated and animation is disabled\n            if (avatarDecoration.asset.startsWith(\"a_\") && !canAnimate) parts.shift();\n            return `${CDN_URL}/${parts.join(\"_\")}.png`;\n        } else if (avatarDecoration?.skuId === RAW_SKU_ID) {\n            return avatarDecoration.asset;\n        }\n    },\n\n    DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/decor/lib/api.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { API_URL } from \"./constants\";\nimport { useAuthorizationStore } from \"./stores/AuthorizationStore\";\n\nexport interface Preset {\n    id: string;\n    name: string;\n    description: string | null;\n    decorations: Decoration[];\n    authorIds: string[];\n}\n\nexport interface Decoration {\n    hash: string;\n    animated: boolean;\n    alt: string | null;\n    authorId: string | null;\n    reviewed: boolean | null;\n    presetId: string | null;\n}\n\nexport interface NewDecoration {\n    file: File;\n    alt: string | null;\n}\n\nexport async function fetchApi(url: RequestInfo, options?: RequestInit) {\n    const res = await fetch(url, {\n        ...options,\n        headers: {\n            ...options?.headers,\n            Authorization: `Bearer ${useAuthorizationStore.getState().token}`\n        }\n    });\n\n    if (res.ok) return res;\n    else throw new Error(await res.text());\n}\n\nexport const getUsersDecorations = async (ids?: string[]): Promise<Record<string, string | null>> => {\n    if (ids?.length === 0) return {};\n\n    const url = new URL(API_URL + \"/users\");\n    if (ids && ids.length !== 0) url.searchParams.set(\"ids\", JSON.stringify(ids));\n\n    return await fetch(url).then(c => c.json());\n};\n\nexport const getUserDecorations = async (id: string = \"@me\"): Promise<Decoration[]> =>\n    fetchApi(API_URL + `/users/${id}/decorations`).then(c => c.json());\n\nexport const getUserDecoration = async (id: string = \"@me\"): Promise<Decoration | null> =>\n    fetchApi(API_URL + `/users/${id}/decoration`).then(c => c.json());\n\nexport const setUserDecoration = async (decoration: Decoration | NewDecoration | null, id: string = \"@me\"): Promise<string | Decoration> => {\n    const formData = new FormData();\n\n    if (!decoration) {\n        formData.append(\"hash\", \"null\");\n    } else if (\"hash\" in decoration) {\n        formData.append(\"hash\", decoration.hash);\n    } else if (\"file\" in decoration) {\n        formData.append(\"image\", decoration.file);\n        formData.append(\"alt\", decoration.alt ?? \"null\");\n    }\n\n    return fetchApi(API_URL + `/users/${id}/decoration`, { method: \"PUT\", body: formData }).then(c =>\n        decoration && \"file\" in decoration ? c.json() : c.text()\n    );\n};\n\nexport const getDecoration = async (hash: string): Promise<Decoration> => fetch(API_URL + `/decorations/${hash}`).then(c => c.json());\n\nexport const deleteDecoration = async (hash: string): Promise<void> => {\n    await fetchApi(API_URL + `/decorations/${hash}`, { method: \"DELETE\" });\n};\n\nexport const getPresets = async (): Promise<Preset[]> => fetch(API_URL + \"/decorations/presets\").then(c => c.json());\n"
  },
  {
    "path": "src/plugins/decor/lib/constants.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport const BASE_URL = \"https://decor.fieryflames.dev\";\nexport const API_URL = BASE_URL + \"/api\";\nexport const AUTHORIZE_URL = API_URL + \"/authorize\";\nexport const CDN_URL = \"https://ugc.decor.fieryflames.dev\";\nexport const CLIENT_ID = \"1096966363416899624\";\nexport const SKU_ID = \"100101099111114\"; // decor in ascii numbers\nexport const RAW_SKU_ID = \"11497119\"; // raw in ascii numbers\nexport const GUILD_ID = \"1096357702931841148\";\nexport const INVITE_KEY = \"dXp2SdxDcP\";\nexport const DECORATION_FETCH_COOLDOWN = 1000 * 60 * 60 * 4; // 4 hours\n"
  },
  {
    "path": "src/plugins/decor/lib/stores/AuthorizationStore.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport * as DataStore from \"@api/DataStore\";\nimport { AUTHORIZE_URL, CLIENT_ID } from \"@plugins/decor/lib/constants\";\nimport { proxyLazy } from \"@utils/lazy\";\nimport { Logger } from \"@utils/Logger\";\nimport { openModal } from \"@utils/modal\";\nimport { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from \"@webpack/common\";\n\ninterface AuthorizationState {\n    token: string | null;\n    tokens: Record<string, string>;\n    init: () => void;\n    authorize: () => Promise<void>;\n    setToken: (token: string) => void;\n    remove: (id: string) => void;\n    isAuthorized: () => boolean;\n}\n\nconst indexedDBStorage = {\n    async getItem(name: string): Promise<string | null> {\n        return DataStore.get(name).then(v => v ?? null);\n    },\n    async setItem(name: string, value: string): Promise<void> {\n        await DataStore.set(name, value);\n    },\n    async removeItem(name: string): Promise<void> {\n        await DataStore.del(name);\n    },\n};\n\n// TODO: Move switching accounts subscription inside the store?\nexport const useAuthorizationStore = proxyLazy(() => zustandCreate(\n    zustandPersist(\n        (set: any, get: any) => ({\n            token: null,\n            tokens: {},\n            init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },\n            setToken: (token: string) => set({ token, tokens: { ...get().tokens, [UserStore.getCurrentUser().id]: token } }),\n            remove: (id: string) => {\n                const { tokens, init } = get();\n                const newTokens = { ...tokens };\n                delete newTokens[id];\n                set({ tokens: newTokens });\n\n                init();\n            },\n            async authorize() {\n                return new Promise((resolve, reject) => openModal(props =>\n                    <OAuth2AuthorizeModal\n                        {...props}\n                        scopes={[\"identify\"]}\n                        responseType=\"code\"\n                        redirectUri={AUTHORIZE_URL}\n                        permissions={0n}\n                        clientId={CLIENT_ID}\n                        cancelCompletesFlow={false}\n                        callback={async (response: any) => {\n                            try {\n                                const url = new URL(response.location);\n                                url.searchParams.append(\"client\", \"vencord\");\n\n                                const req = await fetch(url);\n\n                                if (req?.ok) {\n                                    const token = await req.text();\n                                    get().setToken(token);\n                                } else {\n                                    throw new Error(\"Request not OK\");\n                                }\n                                resolve(void 0);\n                            } catch (e) {\n                                if (e instanceof Error) {\n                                    showToast(`Failed to authorize: ${e.message}`, Toasts.Type.FAILURE);\n                                    new Logger(\"Decor\").error(\"Failed to authorize\", e);\n                                    reject(e);\n                                }\n                            }\n                        }}\n                    />, {\n                    onCloseCallback() {\n                        reject(new Error(\"Authorization cancelled\"));\n                    },\n                }\n                ));\n            },\n            isAuthorized: () => !!get().token,\n        } as AuthorizationState),\n        {\n            name: \"decor-auth\",\n            storage: indexedDBStorage,\n            partialize: state => ({ tokens: state.tokens }),\n            onRehydrateStorage: () => state => state?.init()\n        }\n    )\n));\n"
  },
  {
    "path": "src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Decoration, deleteDecoration, getUserDecoration, getUserDecorations, NewDecoration, setUserDecoration } from \"@plugins/decor/lib/api\";\nimport { decorationToAsset } from \"@plugins/decor/lib/utils/decoration\";\nimport { proxyLazy } from \"@utils/lazy\";\nimport { UserStore, zustandCreate } from \"@webpack/common\";\n\nimport { useUsersDecorationsStore } from \"./UsersDecorationsStore\";\n\ninterface UserDecorationsState {\n    decorations: Decoration[];\n    selectedDecoration: Decoration | null;\n    fetch: () => Promise<void>;\n    delete: (decoration: Decoration | string) => Promise<void>;\n    create: (decoration: NewDecoration) => Promise<void>;\n    select: (decoration: Decoration | null) => Promise<void>;\n    clear: () => void;\n}\n\nexport const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({\n    decorations: [],\n    selectedDecoration: null,\n    async fetch() {\n        const decorations = await getUserDecorations();\n        const selectedDecoration = await getUserDecoration();\n\n        set({ decorations, selectedDecoration });\n    },\n    async create(newDecoration: NewDecoration) {\n        const decoration = (await setUserDecoration(newDecoration)) as Decoration;\n        set({ decorations: [...get().decorations, decoration] });\n    },\n    async delete(decoration: Decoration | string) {\n        const hash = typeof decoration === \"object\" ? decoration.hash : decoration;\n        await deleteDecoration(hash);\n\n        const { selectedDecoration, decorations } = get();\n        const newState = {\n            decorations: decorations.filter(d => d.hash !== hash),\n            selectedDecoration: selectedDecoration?.hash === hash ? null : selectedDecoration\n        };\n\n        set(newState);\n    },\n    async select(decoration: Decoration | null) {\n        if (get().selectedDecoration === decoration) return;\n        set({ selectedDecoration: decoration });\n        setUserDecoration(decoration);\n        useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);\n    },\n    clear: () => set({ decorations: [], selectedDecoration: null })\n} as UserDecorationsState)));\n"
  },
  {
    "path": "src/plugins/decor/lib/stores/UsersDecorationsStore.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { AvatarDecoration } from \"@plugins/decor\";\nimport { getUsersDecorations } from \"@plugins/decor/lib/api\";\nimport { DECORATION_FETCH_COOLDOWN, SKU_ID } from \"@plugins/decor/lib/constants\";\nimport { debounce } from \"@shared/debounce\";\nimport { proxyLazy } from \"@utils/lazy\";\nimport { User } from \"@vencord/discord-types\";\nimport { useEffect, useState, zustandCreate } from \"@webpack/common\";\n\ninterface UserDecorationData {\n    asset: string | null;\n    fetchedAt: Date;\n}\n\ninterface UsersDecorationsState {\n    usersDecorations: Map<string, UserDecorationData>;\n    fetchQueue: Set<string>;\n    bulkFetch: () => Promise<void>;\n    fetch: (userId: string, force?: boolean) => Promise<void>;\n    fetchMany: (userIds: string[]) => Promise<void>;\n    get: (userId: string) => UserDecorationData | undefined;\n    getAsset: (userId: string) => string | null | undefined;\n    has: (userId: string) => boolean;\n    set: (userId: string, decoration: string | null) => void;\n}\n\nexport const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({\n    usersDecorations: new Map<string, UserDecorationData>(),\n    fetchQueue: new Set(),\n    bulkFetch: debounce(async () => {\n        const { fetchQueue, usersDecorations } = get();\n\n        if (fetchQueue.size === 0) return;\n\n        set({ fetchQueue: new Set() });\n\n        const fetchIds = [...fetchQueue];\n        const fetchedUsersDecorations = await getUsersDecorations(fetchIds);\n\n        const newUsersDecorations = new Map(usersDecorations);\n\n        const now = new Date();\n        for (const fetchId of fetchIds) {\n            const newDecoration = fetchedUsersDecorations[fetchId] ?? null;\n            newUsersDecorations.set(fetchId, { asset: newDecoration, fetchedAt: now });\n        }\n\n        set({ usersDecorations: newUsersDecorations });\n    }),\n    async fetch(userId: string, force: boolean = false) {\n        const { usersDecorations, fetchQueue, bulkFetch } = get();\n\n        const { fetchedAt } = usersDecorations.get(userId) ?? {};\n        if (fetchedAt) {\n            if (!force && Date.now() - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) return;\n        }\n\n        set({ fetchQueue: new Set(fetchQueue).add(userId) });\n        bulkFetch();\n    },\n    async fetchMany(userIds) {\n        if (!userIds.length) return;\n        const { usersDecorations, fetchQueue, bulkFetch } = get();\n\n        const newFetchQueue = new Set(fetchQueue);\n\n        const now = Date.now();\n        for (const userId of userIds) {\n            const { fetchedAt } = usersDecorations.get(userId) ?? {};\n            if (fetchedAt) {\n                if (now - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) continue;\n            }\n            newFetchQueue.add(userId);\n        }\n\n        set({ fetchQueue: newFetchQueue });\n        bulkFetch();\n    },\n    get(userId: string) { return get().usersDecorations.get(userId); },\n    getAsset(userId: string) { return get().usersDecorations.get(userId)?.asset; },\n    has(userId: string) { return get().usersDecorations.has(userId); },\n    set(userId: string, decoration: string | null) {\n        const { usersDecorations } = get();\n        const newUsersDecorations = new Map(usersDecorations);\n\n        newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });\n        set({ usersDecorations: newUsersDecorations });\n    }\n} as UsersDecorationsState)));\n\nexport function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {\n    try {\n        const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);\n\n        useEffect(() => {\n            const destructor = (() => {\n                try {\n                    return useUsersDecorationsStore.subscribe(\n                        state => {\n                            if (!user) return;\n                            const newDecorAvatarDecoration = state.getAsset(user.id);\n                            if (!newDecorAvatarDecoration) return;\n                            if (decorAvatarDecoration !== newDecorAvatarDecoration) setDecorAvatarDecoration(newDecorAvatarDecoration);\n                        }\n                    );\n                } catch {\n                    return () => { };\n                }\n            })();\n\n            try {\n                if (user) {\n                    const { fetch: fetchUserDecorAvatarDecoration } = useUsersDecorationsStore.getState();\n                    fetchUserDecorAvatarDecoration(user.id);\n                }\n            } catch { }\n\n            return destructor;\n        }, []);\n\n        return decorAvatarDecoration ? { asset: decorAvatarDecoration, skuId: SKU_ID } : null;\n    } catch (e) {\n        console.error(e);\n    }\n\n    return null;\n}\n"
  },
  {
    "path": "src/plugins/decor/lib/utils/decoration.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { AvatarDecoration } from \"@plugins/decor\";\nimport { Decoration } from \"@plugins/decor/lib/api\";\nimport { SKU_ID } from \"@plugins/decor/lib/constants\";\n\nexport function decorationToAsset(decoration: Decoration) {\n    return `${decoration.animated ? \"a_\" : \"\"}${decoration.hash}`;\n}\n\nexport function decorationToAvatarDecoration(decoration: Decoration): AvatarDecoration {\n    return { asset: decorationToAsset(decoration), skuId: SKU_ID };\n}\n"
  },
  {
    "path": "src/plugins/decor/settings.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Link } from \"@components/Link\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { closeAllModals } from \"@utils/modal\";\nimport { OptionType } from \"@utils/types\";\nimport { FluxDispatcher, Forms } from \"@webpack/common\";\n\nimport DecorPlugin from \".\";\nimport DecorSection from \"./ui/components/DecorSection\";\n\nexport const settings = definePluginSettings({\n    changeDecoration: {\n        type: OptionType.COMPONENT,\n        component() {\n            if (!DecorPlugin.started) return <Forms.FormText>\n                Enable Decor and restart your client to change your avatar decoration.\n            </Forms.FormText>;\n\n            return <div>\n                <DecorSection hideTitle hideDivider noMargin />\n                <Forms.FormText className={classes(Margins.top8, Margins.bottom8)}>\n                    You can also access Decor decorations from the <Link\n                        href=\"/settings/profile-customization\"\n                        onClick={e => {\n                            e.preventDefault();\n                            closeAllModals();\n                            FluxDispatcher.dispatch({ type: \"USER_SETTINGS_MODAL_SET_SECTION\", section: \"Profile Customization\" });\n                        }}\n                    >Profiles</Link> page.\n                </Forms.FormText>\n            </div>;\n        }\n    },\n    agreedToGuidelines: {\n        type: OptionType.BOOLEAN,\n        description: \"Agreed to guidelines\",\n        hidden: true,\n        default: false\n    }\n});\n"
  },
  {
    "path": "src/plugins/decor/ui/components/DecorDecorationGridDecoration.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Decoration } from \"@plugins/decor/lib/api\";\nimport { decorationToAvatarDecoration } from \"@plugins/decor/lib/utils/decoration\";\nimport { ContextMenuApi } from \"@webpack/common\";\nimport type { HTMLProps } from \"react\";\n\nimport { DecorationGridDecoration } from \".\";\nimport DecorationContextMenu from \"./DecorationContextMenu\";\n\ninterface DecorDecorationGridDecorationProps extends HTMLProps<HTMLDivElement> {\n    decoration: Decoration;\n    isSelected: boolean;\n    onSelect: () => void;\n}\n\nexport default function DecorDecorationGridDecoration(props: DecorDecorationGridDecorationProps) {\n    const { decoration } = props;\n\n    return <DecorationGridDecoration\n        {...props}\n        onContextMenu={e => {\n            ContextMenuApi.openContextMenu(e, () => (\n                <DecorationContextMenu\n                    decoration={decoration}\n                />\n            ));\n        }}\n        avatarDecoration={decorationToAvatarDecoration(decoration)}\n    />;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/DecorSection.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Button } from \"@components/Button\";\nimport { Flex } from \"@components/Flex\";\nimport { useAuthorizationStore } from \"@plugins/decor/lib/stores/AuthorizationStore\";\nimport { useCurrentUserDecorationsStore } from \"@plugins/decor/lib/stores/CurrentUserDecorationsStore\";\nimport { cl } from \"@plugins/decor/ui\";\nimport { openChangeDecorationModal } from \"@plugins/decor/ui/modals/ChangeDecorationModal\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { useEffect } from \"@webpack/common\";\n\nconst CustomizationSection = findComponentByCodeLazy(\".DESCRIPTION\", \"hasBackground:\");\n\nexport interface DecorSectionProps {\n    hideTitle?: boolean;\n    hideDivider?: boolean;\n    noMargin?: boolean;\n}\n\nexport default function DecorSection({ hideTitle = false, hideDivider = false, noMargin = false }: DecorSectionProps) {\n    const authorization = useAuthorizationStore();\n    const { selectedDecoration, select: selectDecoration, fetch: fetchDecorations } = useCurrentUserDecorationsStore();\n\n    useEffect(() => {\n        if (authorization.isAuthorized()) fetchDecorations();\n    }, [authorization.token]);\n\n    return <CustomizationSection\n        title={!hideTitle && \"Decor\"}\n        hasBackground={true}\n        hideDivider={hideDivider}\n        className={noMargin && cl(\"section-remove-margin\")}\n    >\n        <Flex gap=\"4px\">\n            <Button\n                onClick={() => {\n                    if (!authorization.isAuthorized()) {\n                        authorization.authorize().then(openChangeDecorationModal).catch(() => { });\n                    } else openChangeDecorationModal();\n                }}\n                variant=\"primary\"\n                size=\"small\"\n            >\n                Change Decoration\n            </Button>\n            {selectedDecoration && authorization.isAuthorized() && <Button\n                onClick={() => selectDecoration(null)}\n                variant=\"secondary\"\n                size={\"small\"}\n            >\n                Remove Decoration\n            </Button>}\n        </Flex>\n    </CustomizationSection>;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/DecorationContextMenu.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { CopyIcon, DeleteIcon } from \"@components/Icons\";\nimport { Decoration } from \"@plugins/decor/lib/api\";\nimport { useCurrentUserDecorationsStore } from \"@plugins/decor/lib/stores/CurrentUserDecorationsStore\";\nimport { cl } from \"@plugins/decor/ui\";\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { Alerts, ContextMenuApi, Menu, UserStore } from \"@webpack/common\";\n\nexport default function DecorationContextMenu({ decoration }: { decoration: Decoration; }) {\n    const { delete: deleteDecoration } = useCurrentUserDecorationsStore();\n\n    return <Menu.Menu\n        navId={cl(\"decoration-context-menu\")}\n        onClose={ContextMenuApi.closeContextMenu}\n        aria-label=\"Decoration Options\"\n    >\n        <Menu.MenuItem\n            id={cl(\"decoration-context-menu-copy-hash\")}\n            label=\"Copy Decoration Hash\"\n            icon={CopyIcon}\n            action={() => copyToClipboard(decoration.hash)}\n        />\n        {decoration.authorId === UserStore.getCurrentUser().id &&\n            <Menu.MenuItem\n                id={cl(\"decoration-context-menu-delete\")}\n                label=\"Delete Decoration\"\n                color=\"danger\"\n                icon={DeleteIcon}\n                action={() => Alerts.show({\n                    title: \"Delete Decoration\",\n                    body: `Are you sure you want to delete ${decoration.alt}?`,\n                    confirmText: \"Delete\",\n                    confirmColor: cl(\"danger-btn\"),\n                    cancelText: \"Cancel\",\n                    onConfirm() {\n                        deleteDecoration(decoration);\n                    }\n                })}\n            />\n        }\n    </Menu.Menu>;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/DecorationGridCreate.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { PlusIcon } from \"@components/Icons\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { Text } from \"@webpack/common\";\nimport { HTMLProps } from \"react\";\n\nimport { DecorationGridItem } from \".\";\n\ntype DecorationGridCreateProps = HTMLProps<HTMLDivElement> & {\n    onSelect: () => void;\n};\n\nexport default function DecorationGridCreate(props: DecorationGridCreateProps) {\n    return <DecorationGridItem\n        {...props}\n        isSelected={false}\n    >\n        <PlusIcon />\n        <Text\n            variant=\"text-xs/normal\"\n            color=\"text-strong\"\n        >\n            {getIntlMessage(\"CREATE\")}\n        </Text>\n    </DecorationGridItem >;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/DecorationGridNone.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { NoEntrySignIcon } from \"@components/Icons\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { Text } from \"@webpack/common\";\nimport { HTMLProps } from \"react\";\n\nimport { DecorationGridItem } from \".\";\n\ntype DecorationGridNoneProps = HTMLProps<HTMLDivElement> & {\n    isSelected: boolean;\n    onSelect: () => void;\n};\n\nexport default function DecorationGridNone(props: DecorationGridNoneProps) {\n    return <DecorationGridItem\n        {...props}\n    >\n        <NoEntrySignIcon />\n        <Text\n            variant=\"text-xs/normal\"\n            color=\"text-strong\"\n        >\n            {getIntlMessage(\"NONE\")}\n        </Text>\n    </DecorationGridItem >;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/Grid.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { cl } from \"@plugins/decor/ui\";\nimport { React } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nexport interface GridProps<ItemT> {\n    renderItem: (item: ItemT) => JSX.Element;\n    getItemKey: (item: ItemT) => string;\n    itemKeyPrefix?: string;\n    items: Array<ItemT>;\n}\n\nexport default function Grid<ItemT,>({ renderItem, getItemKey, itemKeyPrefix: ikp, items }: GridProps<ItemT>) {\n    return <div className={cl(\"sectioned-grid-list-grid\")}>\n        {items.map(item =>\n            <React.Fragment\n                key={`${ikp ? `${ikp}-` : \"\"}${getItemKey(item)}`}\n            >\n                {renderItem(item)}\n            </React.Fragment>\n        )}\n    </div>;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/SectionedGridList.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { cl } from \"@plugins/decor/ui\";\nimport { classes } from \"@utils/misc\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { React } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nimport Grid, { GridProps } from \"./Grid\";\n\nconst ScrollerClasses = findCssClassesLazy(\"managedReactiveScroller\", \"thin\");\n\ntype Section<SectionT, ItemT> = SectionT & {\n    items: Array<ItemT>;\n};\n\ninterface SectionedGridListProps<ItemT, SectionT, SectionU = Section<SectionT, ItemT>> extends Omit<GridProps<ItemT>, \"items\"> {\n    renderSectionHeader: (section: SectionU) => JSX.Element;\n    getSectionKey: (section: SectionU) => string;\n    sections: SectionU[];\n}\n\nexport default function SectionedGridList<ItemT, SectionU,>(props: SectionedGridListProps<ItemT, SectionU>) {\n    return <div className={classes(cl(\"sectioned-grid-list-container\"), ScrollerClasses.thin)}>\n        {props.sections.map(section => <div key={props.getSectionKey(section)} className={cl(\"sectioned-grid-list-section\")}>\n            {props.renderSectionHeader(section)}\n            <Grid\n                renderItem={props.renderItem}\n                getItemKey={props.getItemKey}\n                itemKeyPrefix={props.getSectionKey(section)}\n                items={section.items}\n            />\n        </div>)}\n    </div>;\n}\n"
  },
  {
    "path": "src/plugins/decor/ui/components/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { AvatarDecoration } from \"@plugins/decor\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport type { ComponentType, HTMLProps, PropsWithChildren } from \"react\";\n\ntype DecorationGridItemComponent = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement>> & {\n    onSelect: () => void,\n    isSelected: boolean,\n}>;\n\nexport let DecorationGridItem: DecorationGridItemComponent;\nexport const setDecorationGridItem = v => DecorationGridItem = v;\n\nexport const AvatarDecorationModalPreview = findComponentByCodeLazy(\"#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}\");\n\ntype DecorationGridDecorationComponent = React.ComponentType<HTMLProps<HTMLDivElement> & {\n    avatarDecoration: AvatarDecoration;\n    onSelect: () => void,\n    isSelected: boolean,\n}>;\n\nexport let DecorationGridDecoration: DecorationGridDecorationComponent;\nexport const setDecorationGridDecoration = v => DecorationGridDecoration = v;\n"
  },
  {
    "path": "src/plugins/decor/ui/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { classNameFactory } from \"@utils/css\";\nimport { extractAndLoadChunksLazy, findCssClassesLazy } from \"@webpack\";\n\nexport const cl = classNameFactory(\"vc-decor-\");\nexport const DecorationModalClasses = findCssClassesLazy(\"modalPreview\", \"modalCloseButton\", \"spinner\", \"modal\");\n\nexport const requireAvatarDecorationModal = extractAndLoadChunksLazy([\"initialSelectedDecoration:\", /initialSelectedDecoration:\\i,.{0,300}\\i\\.e\\(/]);\nexport const requireCreateStickerModal = extractAndLoadChunksLazy([\".CREATE_STICKER_MODAL,\", \"isDisplayingIndividualStickers\"]);\n"
  },
  {
    "path": "src/plugins/decor/ui/modals/ChangeDecorationModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Button as NewButton } from \"@components/Button\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Decoration, getPresets, Preset } from \"@plugins/decor/lib/api\";\nimport { GUILD_ID, INVITE_KEY } from \"@plugins/decor/lib/constants\";\nimport { useAuthorizationStore } from \"@plugins/decor/lib/stores/AuthorizationStore\";\nimport { useCurrentUserDecorationsStore } from \"@plugins/decor/lib/stores/CurrentUserDecorationsStore\";\nimport { decorationToAvatarDecoration } from \"@plugins/decor/lib/utils/decoration\";\nimport { settings } from \"@plugins/decor/settings\";\nimport { cl, DecorationModalClasses, requireAvatarDecorationModal } from \"@plugins/decor/ui\";\nimport { AvatarDecorationModalPreview } from \"@plugins/decor/ui/components\";\nimport DecorationGridCreate from \"@plugins/decor/ui/components/DecorationGridCreate\";\nimport DecorationGridNone from \"@plugins/decor/ui/components/DecorationGridNone\";\nimport DecorDecorationGridDecoration from \"@plugins/decor/ui/components/DecorDecorationGridDecoration\";\nimport SectionedGridList from \"@plugins/decor/ui/components/SectionedGridList\";\nimport { copyWithToast, openInviteModal } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { Queue } from \"@utils/Queue\";\nimport { User } from \"@vencord/discord-types\";\nimport { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from \"@webpack/common\";\n\nimport { openCreateDecorationModal } from \"./CreateDecorationModal\";\nimport { openGuidelinesModal } from \"./GuidelinesModal\";\n\nfunction usePresets() {\n    const [presets, setPresets] = useState<Preset[]>([]);\n    useEffect(() => { getPresets().then(setPresets); }, []);\n    return presets;\n}\n\ninterface Section {\n    title: string;\n    subtitle?: string;\n    sectionKey: string;\n    items: (\"none\" | \"create\" | Decoration)[];\n    authorIds?: string[];\n}\n\ninterface SectionHeaderProps {\n    section: Section;\n}\n\nconst fetchAuthorsQueue = new Queue();\n\nfunction SectionHeader({ section }: SectionHeaderProps) {\n    const hasSubtitle = typeof section.subtitle !== \"undefined\";\n    const hasAuthorIds = typeof section.authorIds !== \"undefined\";\n\n    const [authors, setAuthors] = useState<User[]>([]);\n\n    useEffect(() => {\n        fetchAuthorsQueue.push(async () => {\n            if (!section.authorIds) return;\n\n            for (const authorId of section.authorIds) {\n                const author = UserStore.getUser(authorId) ?? await UserUtils.getUser(authorId).catch(() => null);\n                if (author == null) continue;\n\n                setAuthors(authors => [...authors, author]);\n            }\n        });\n    }, [section.authorIds]);\n\n    return <div>\n        <Flex>\n            <Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle>\n            {hasAuthorIds && <UserSummaryItem\n                users={authors}\n                guildId={undefined}\n                renderIcon={false}\n                max={5}\n                showDefaultAvatarsForNullUsers\n                size={16}\n                showUserPopout\n                className={Margins.bottom8}\n            />}\n        </Flex>\n        {hasSubtitle &&\n            <Forms.FormText className={Margins.bottom8}>\n                {section.subtitle}\n            </Forms.FormText>\n        }\n    </div>;\n}\n\nfunction ChangeDecorationModal(props: ModalProps) {\n    // undefined = not trying, null = none, Decoration = selected\n    const [tryingDecoration, setTryingDecoration] = useState<Decoration | null | undefined>(undefined);\n    const isTryingDecoration = typeof tryingDecoration !== \"undefined\";\n\n    const avatarDecoration = tryingDecoration != null ? decorationToAvatarDecoration(tryingDecoration) : tryingDecoration;\n\n    const {\n        decorations,\n        selectedDecoration,\n        fetch: fetchUserDecorations,\n        select: selectDecoration\n    } = useCurrentUserDecorationsStore();\n\n    useEffect(() => {\n        fetchUserDecorations();\n    }, []);\n\n    const activeSelectedDecoration = isTryingDecoration ? tryingDecoration : selectedDecoration;\n    const activeDecorationHasAuthor = typeof activeSelectedDecoration?.authorId !== \"undefined\";\n    const hasDecorationPendingReview = decorations.some(d => d.reviewed === false);\n\n    const presets = usePresets();\n    const presetDecorations = presets.flatMap(preset => preset.decorations);\n\n    const activeDecorationPreset = presets.find(preset => preset.id === activeSelectedDecoration?.presetId);\n    const isActiveDecorationPreset = typeof activeDecorationPreset !== \"undefined\";\n\n    const ownDecorations = decorations.filter(d => !presetDecorations.some(p => p.hash === d.hash));\n\n    const data = [\n        {\n            title: \"Your Decorations\",\n            subtitle: \"You can delete your own decorations by right clicking on them.\",\n            sectionKey: \"ownDecorations\",\n            items: [\"none\", ...ownDecorations, \"create\"]\n        },\n        ...presets.map(preset => ({\n            title: preset.name,\n            subtitle: preset.description || undefined,\n            sectionKey: `preset-${preset.id}`,\n            items: preset.decorations,\n            authorIds: preset.authorIds\n        }))\n    ] as Section[];\n\n    return <ModalRoot\n        {...props}\n        size={ModalSize.DYNAMIC}\n        className={DecorationModalClasses.modal}\n    >\n        <ModalHeader separator={false} className={cl(\"modal-header\")}>\n            <Text\n                color=\"text-strong\"\n                variant=\"heading-lg/semibold\"\n                tag=\"h1\"\n                style={{ flexGrow: 1 }}\n            >\n                Change Decoration\n            </Text>\n            <ModalCloseButton onClick={props.onClose} />\n        </ModalHeader>\n        <ModalContent\n            className={cl(\"change-decoration-modal-content\")}\n            scrollbarType=\"none\"\n        >\n            <ErrorBoundary>\n                <SectionedGridList\n                    renderItem={item => {\n                        if (typeof item === \"string\") {\n                            switch (item) {\n                                case \"none\":\n                                    return <DecorationGridNone\n                                        className={cl(\"change-decoration-modal-decoration\")}\n                                        isSelected={activeSelectedDecoration === null}\n                                        onSelect={() => setTryingDecoration(null)}\n                                    />;\n                                case \"create\":\n                                    return <Tooltip text=\"You already have a decoration pending review\" shouldShow={hasDecorationPendingReview}>\n                                        {tooltipProps => <DecorationGridCreate\n                                            className={cl(\"change-decoration-modal-decoration\")}\n                                            {...tooltipProps}\n                                            onSelect={!hasDecorationPendingReview ? (settings.store.agreedToGuidelines ? openCreateDecorationModal : openGuidelinesModal) : () => { }}\n                                        />}\n                                    </Tooltip>;\n                            }\n                        } else {\n                            return <Tooltip text={\"Pending review\"} shouldShow={item.reviewed === false}>\n                                {tooltipProps => (\n                                    <DecorDecorationGridDecoration\n                                        {...tooltipProps}\n                                        className={cl(\"change-decoration-modal-decoration\")}\n                                        onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }}\n                                        isSelected={activeSelectedDecoration?.hash === item.hash}\n                                        decoration={item}\n                                    />\n                                )}\n                            </Tooltip>;\n                        }\n                    }}\n                    getItemKey={item => typeof item === \"string\" ? item : item.hash}\n                    getSectionKey={section => section.sectionKey}\n                    renderSectionHeader={section => <SectionHeader section={section} />}\n                    sections={data}\n                />\n                <div className={cl(\"change-decoration-modal-preview\")}>\n                    <AvatarDecorationModalPreview\n                        avatarDecoration={avatarDecoration}\n                        user={UserStore.getCurrentUser()}\n                    />\n                    {isActiveDecorationPreset && <Forms.FormTitle className=\"\">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}\n                    {typeof activeSelectedDecoration === \"object\" &&\n                        <Text\n                            variant=\"text-sm/semibold\"\n                            color=\"text-strong\"\n                        >\n                            {activeSelectedDecoration?.alt}\n                        </Text>\n                    }\n                    {activeDecorationHasAuthor && (\n                        <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>\n                            Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}\n                        </Text>\n                    )}\n                    {isActiveDecorationPreset && (\n                        <Button onClick={() => copyWithToast(activeDecorationPreset.id)}>\n                            Copy Preset ID\n                        </Button>\n                    )}\n                </div>\n            </ErrorBoundary>\n        </ModalContent>\n        <ModalFooter className={cl(\"change-decoration-modal-footer\", \"modal-footer\")}>\n            <div className={cl(\"modal-footer-btn-container\")}>\n                <Button\n                    onClick={props.onClose}\n                    color={Button.Colors.PRIMARY}\n                >\n                    Cancel\n                </Button>\n                <Button\n                    onClick={() => {\n                        selectDecoration(tryingDecoration!).then(props.onClose);\n                    }}\n                    disabled={!isTryingDecoration}\n                >\n                    Apply\n                </Button>\n            </div>\n            <div className={cl(\"modal-footer-btn-container\")}>\n                <Tooltip text=\"Join Decor's Discord Server for notifications on your decoration's review, and when new presets are released\">\n                    {tooltipProps => <NewButton\n                        {...tooltipProps}\n                        onClick={async () => {\n                            if (!GuildStore.getGuild(GUILD_ID)) {\n                                const inviteAccepted = await openInviteModal(INVITE_KEY);\n                                if (inviteAccepted) {\n                                    closeAllModals();\n                                    FluxDispatcher.dispatch({ type: \"LAYER_POP_ALL\" });\n                                }\n                            } else {\n                                props.onClose();\n                                FluxDispatcher.dispatch({ type: \"LAYER_POP_ALL\" });\n                                NavigationRouter.transitionToGuild(GUILD_ID);\n                            }\n                        }}\n                        variant=\"link\"\n                    >\n                        Discord Server\n                    </NewButton>}\n                </Tooltip>\n                <NewButton\n                    onClick={() => Alerts.show({\n                        title: \"Log Out\",\n                        body: \"Are you sure you want to log out of Decor?\",\n                        confirmText: \"Log Out\",\n                        confirmColor: cl(\"danger-btn\"),\n                        cancelText: \"Cancel\",\n                        onConfirm() {\n                            useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);\n                            props.onClose();\n                        }\n                    })}\n                    variant=\"dangerSecondary\"\n                >\n                    Log Out\n                </NewButton>\n            </div>\n        </ModalFooter>\n    </ModalRoot>;\n}\n\nexport const openChangeDecorationModal = () =>\n    requireAvatarDecorationModal().then(() => openModal(props => <ChangeDecorationModal {...props} />));\n"
  },
  {
    "path": "src/plugins/decor/ui/modals/CreateDecorationModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Link } from \"@components/Link\";\nimport { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from \"@plugins/decor/lib/constants\";\nimport { useCurrentUserDecorationsStore } from \"@plugins/decor/lib/stores/CurrentUserDecorationsStore\";\nimport { cl, DecorationModalClasses, requireAvatarDecorationModal, requireCreateStickerModal } from \"@plugins/decor/ui\";\nimport { AvatarDecorationModalPreview } from \"@plugins/decor/ui/components\";\nimport { openInviteModal } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { filters, findComponentByCodeLazy, mapMangledModuleLazy } from \"@webpack\";\nimport { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from \"@webpack/common\";\n\nconst FileUpload = findComponentByCodeLazy(\".currentTarget.files\", \"lineClamp:1\");\n\nconst { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE=\"positive', {\n    HelpMessageTypes: filters.byProps(\"POSITIVE\", \"WARNING\", \"INFO\"),\n    HelpMessage: filters.byCode(\"messageType:\")\n});\n\nfunction useObjectURL(object: Blob | MediaSource | null) {\n    const [url, setUrl] = useState<string | null>(null);\n\n    useEffect(() => {\n        if (!object) return;\n\n        const objectUrl = URL.createObjectURL(object);\n        setUrl(objectUrl);\n\n        return () => {\n            URL.revokeObjectURL(objectUrl);\n            setUrl(null);\n        };\n    }, [object]);\n\n    return url;\n}\n\nfunction CreateDecorationModal(props: ModalProps) {\n    const [name, setName] = useState(\"\");\n    const [file, setFile] = useState<File | null>(null);\n    const [submitting, setSubmitting] = useState(false);\n    const [error, setError] = useState<Error | null>(null);\n\n    useEffect(() => {\n        if (error) setError(null);\n    }, [file]);\n\n    const { create: createDecoration } = useCurrentUserDecorationsStore();\n\n    const fileUrl = useObjectURL(file);\n\n    const decoration = useMemo(() => fileUrl ? { asset: fileUrl, skuId: RAW_SKU_ID } : null, [fileUrl]);\n\n    return <ModalRoot\n        {...props}\n        size={ModalSize.MEDIUM}\n        className={DecorationModalClasses.modal}\n    >\n        <ModalHeader separator={false} className={cl(\"modal-header\")}>\n            <Text\n                color=\"text-strong\"\n                variant=\"heading-lg/semibold\"\n                tag=\"h1\"\n                style={{ flexGrow: 1 }}\n            >\n                Create Decoration\n            </Text>\n            <ModalCloseButton onClick={props.onClose} />\n        </ModalHeader>\n        <ModalContent\n            className={cl(\"create-decoration-modal-content\")}\n            scrollbarType=\"none\"\n        >\n            <ErrorBoundary>\n                <HelpMessage messageType={HelpMessageTypes.WARNING}>\n                    Make sure your decoration does not violate <Link\n                        href=\"https://github.com/decor-discord/.github/blob/main/GUIDELINES.md\"\n                    >\n                        the guidelines\n                    </Link> before submitting it.\n                </HelpMessage>\n                <div className={cl(\"create-decoration-modal-form-preview-container\")}>\n                    <div className={cl(\"create-decoration-modal-form\")}>\n                        {error !== null && <Text color=\"text-danger\" variant=\"text-xs/normal\">{error.message}</Text>}\n                        <section>\n                            <Forms.FormTitle tag=\"h5\">File</Forms.FormTitle>\n                            <FileUpload\n                                filename={file?.name}\n                                placeholder=\"Choose a file\"\n                                buttonText=\"Browse\"\n                                filters={[{ name: \"Decoration file\", extensions: [\"png\", \"apng\"] }]}\n                                onFileSelect={setFile}\n                            />\n                            <Forms.FormText className={Margins.top8}>\n                                File should be APNG or PNG.\n                            </Forms.FormText>\n                        </section>\n                        <section>\n                            <Forms.FormTitle tag=\"h5\">Name</Forms.FormTitle>\n                            <TextInput\n                                placeholder=\"Companion Cube\"\n                                value={name}\n                                onChange={setName}\n                            />\n                            <Forms.FormText className={Margins.top8}>\n                                This name will be used when referring to this decoration.\n                            </Forms.FormText>\n                        </section>\n                    </div>\n                    <div>\n                        <AvatarDecorationModalPreview\n                            avatarDecoration={decoration}\n                            user={UserStore.getCurrentUser()}\n                        />\n                    </div>\n                </div>\n                <HelpMessage messageType={HelpMessageTypes.INFO} className={Margins.bottom8}>\n                    To receive updates on your decoration's review, join <Link\n                        href={`https://discord.gg/${INVITE_KEY}`}\n                        onClick={async e => {\n                            e.preventDefault();\n                            if (!GuildStore.getGuild(GUILD_ID)) {\n                                const inviteAccepted = await openInviteModal(INVITE_KEY);\n                                if (inviteAccepted) {\n                                    closeAllModals();\n                                    FluxDispatcher.dispatch({ type: \"LAYER_POP_ALL\" });\n                                }\n                            } else {\n                                closeAllModals();\n                                FluxDispatcher.dispatch({ type: \"LAYER_POP_ALL\" });\n                                NavigationRouter.transitionToGuild(GUILD_ID);\n                            }\n                        }}\n                    >\n                        Decor's Discord server\n                    </Link> and allow direct messages.\n                </HelpMessage>\n            </ErrorBoundary>\n        </ModalContent>\n        <ModalFooter className={cl(\"modal-footer\")}>\n            <div className={cl(\"modal-footer-btn-container\")}>\n                <Button\n                    onClick={props.onClose}\n                    color={Button.Colors.PRIMARY}\n                >\n                    Cancel\n                </Button>\n                <Button\n                    onClick={() => {\n                        setSubmitting(true);\n                        createDecoration({ alt: name, file: file! })\n                            .then(props.onClose).catch(e => { setSubmitting(false); setError(e); });\n                    }}\n                    disabled={!file || !name || submitting}\n                >\n                    Submit for Review\n                </Button>\n            </div>\n        </ModalFooter>\n    </ModalRoot>;\n}\n\nexport const openCreateDecorationModal = () =>\n    Promise.all([requireAvatarDecorationModal(), requireCreateStickerModal()])\n        .then(() => openModal(props => <CreateDecorationModal {...props} />));\n"
  },
  {
    "path": "src/plugins/decor/ui/modals/GuidelinesModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Flex } from \"@components/Flex\";\nimport { Link } from \"@components/Link\";\nimport { settings } from \"@plugins/decor/settings\";\nimport { cl, DecorationModalClasses, requireAvatarDecorationModal } from \"@plugins/decor/ui\";\nimport { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { Button, Forms, Text } from \"@webpack/common\";\n\nimport { openCreateDecorationModal } from \"./CreateDecorationModal\";\n\nfunction GuidelinesModal(props: ModalProps) {\n    return <ModalRoot\n        {...props}\n        size={ModalSize.SMALL}\n        className={DecorationModalClasses.modal}\n    >\n        <ModalHeader separator={false} className={cl(\"modal-header\")}>\n            <Text\n                color=\"text-strong\"\n                variant=\"heading-lg/semibold\"\n                tag=\"h1\"\n                style={{ flexGrow: 1 }}\n            >\n                Hold on\n            </Text>\n            <ModalCloseButton onClick={props.onClose} />\n        </ModalHeader>\n        <ModalContent\n            scrollbarType=\"none\"\n        >\n            <Forms.FormText>\n                By submitting a decoration, you agree to <Link\n                    href=\"https://github.com/decor-discord/.github/blob/main/GUIDELINES.md\"\n                >\n                    the guidelines\n                </Link>. Not reading these guidelines may get your account suspended from creating more decorations in the future.\n            </Forms.FormText>\n        </ModalContent>\n        <ModalFooter className={cl(\"modal-footer\")}>\n            <Flex gap=\"4px\">\n                <Button\n                    onClick={() => {\n                        settings.store.agreedToGuidelines = true;\n                        props.onClose();\n                        openCreateDecorationModal();\n                    }}\n                >\n                    Continue\n                </Button>\n                <Button\n                    onClick={props.onClose}\n                    color={Button.Colors.PRIMARY}\n                    look={Button.Looks.LINK}\n                >\n                    Go Back\n                </Button>\n            </Flex>\n        </ModalFooter>\n    </ModalRoot>;\n}\n\nexport const openGuidelinesModal = () =>\n    requireAvatarDecorationModal().then(() => openModal(props => <GuidelinesModal {...props} />));\n"
  },
  {
    "path": "src/plugins/decor/ui/styles.css",
    "content": ".vc-decor-danger-btn {\n    color: var(--control-critical-primary-text-default);\n    background-color: var(--control-critical-primary-background-default);\n}\n\n.vc-decor-change-decoration-modal-content {\n    position: relative;\n    display: flex;\n    border-radius: 5px 5px 0 0;\n    padding: 0 16px;\n    gap: 4px;\n}\n\n.vc-decor-change-decoration-modal-preview {\n    display: flex;\n    flex-direction: column;\n    margin-top: 24px;\n    gap: 8px;\n    max-width: 280px;\n}\n\n.vc-decor-change-decoration-modal-decoration {\n    width: 80px;\n    height: 80px;\n}\n\n.vc-decor-change-decoration-modal-footer {\n    justify-content: space-between;\n}\n\n.vc-decor-create-decoration-modal-content {\n    display: flex;\n    flex-direction: column;\n    gap: 20px;\n    padding: 0 16px;\n}\n\n.vc-decor-create-decoration-modal-form-preview-container {\n    display: flex;\n    gap: 16px;\n}\n\n.vc-decor-modal-header {\n    padding: 16px;\n}\n\n.vc-decor-modal-footer {\n    padding: 16px;\n}\n\n.vc-decor-modal-footer-btn-container {\n    display: flex;\n    flex-direction: row;\n    gap: 0.5em;\n}\n\n.vc-decor-create-decoration-modal-form {\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    gap: 16px;\n}\n\n.vc-decor-sectioned-grid-list-container {\n    display: flex;\n    flex-direction: column;\n    overflow: hidden scroll;\n    max-height: 512px;\n    gap: 12px;\n\n    /* ((80 + 8 (grid gap)) * desired columns) (scrolled takes the extra 8 padding off conveniently) */\n    width: 352px;\n}\n\n.vc-decor-sectioned-grid-list-grid {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n}\n\n.vc-decor-section-remove-margin {\n    margin-bottom: 0;\n}"
  },
  {
    "path": "src/plugins/devCompanion.dev/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { showNotification } from \"@api/Notifications\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport { canonicalizeMatch, canonicalizeReplace } from \"@utils/patches\";\nimport definePlugin, { OptionType, ReporterTestable } from \"@utils/types\";\nimport { filters, findAll, search } from \"@webpack\";\n\nconst PORT = 8485;\n\nconst logger = new Logger(\"DevCompanion\");\n\nlet socket: WebSocket | undefined;\n\ntype Node = StringNode | RegexNode | FunctionNode;\n\ninterface StringNode {\n    type: \"string\";\n    value: string;\n}\n\ninterface RegexNode {\n    type: \"regex\";\n    value: {\n        pattern: string;\n        flags: string;\n    };\n}\n\ninterface FunctionNode {\n    type: \"function\";\n    value: string;\n}\n\ninterface PatchData {\n    find: string;\n    replacement: {\n        match: StringNode | RegexNode;\n        replace: StringNode | FunctionNode;\n    }[];\n}\n\ninterface FindData {\n    type: string;\n    args: Array<StringNode | FunctionNode>;\n}\n\nconst settings = definePluginSettings({\n    notifyOnAutoConnect: {\n        description: \"Whether to notify when Dev Companion has automatically connected.\",\n        type: OptionType.BOOLEAN,\n        default: true\n    }\n});\n\nfunction parseNode(node: Node) {\n    switch (node.type) {\n        case \"string\":\n            return node.value;\n        case \"regex\":\n            return new RegExp(node.value.pattern, node.value.flags);\n        case \"function\":\n            // We LOVE remote code execution\n            // Safety: This comes from localhost only, which actually means we have less permissions than the source,\n            // since we're running in the browser sandbox, whereas the sender has host access\n            return (0, eval)(node.value);\n        default:\n            throw new Error(\"Unknown Node Type \" + (node as any).type);\n    }\n}\n\nfunction initWs(isManual = false) {\n    let wasConnected = isManual;\n    let hasErrored = false;\n    const ws = socket = new WebSocket(`ws://127.0.0.1:${PORT}`);\n\n    ws.addEventListener(\"open\", () => {\n        wasConnected = true;\n\n        logger.info(\"Connected to WebSocket\");\n\n        (settings.store.notifyOnAutoConnect || isManual) && showNotification({\n            title: \"Dev Companion Connected\",\n            body: \"Connected to WebSocket\",\n            noPersist: true\n        });\n    });\n\n    ws.addEventListener(\"error\", e => {\n        if (!wasConnected) return;\n\n        hasErrored = true;\n\n        logger.error(\"Dev Companion Error:\", e);\n\n        showNotification({\n            title: \"Dev Companion Error\",\n            body: (e as ErrorEvent).message || \"No Error Message\",\n            color: \"var(--status-danger, red)\",\n            noPersist: true,\n        });\n    });\n\n    ws.addEventListener(\"close\", e => {\n        if (!wasConnected || hasErrored) return;\n\n        logger.info(\"Dev Companion Disconnected:\", e.code, e.reason);\n\n        showNotification({\n            title: \"Dev Companion Disconnected\",\n            body: e.reason || \"No Reason provided\",\n            color: \"var(--status-danger, red)\",\n            noPersist: true,\n        });\n    });\n\n    ws.addEventListener(\"message\", e => {\n        try {\n            var { nonce, type, data } = JSON.parse(e.data);\n        } catch (err) {\n            logger.error(\"Invalid JSON:\", err, \"\\n\" + e.data);\n            return;\n        }\n\n        function reply(error?: string) {\n            const data = { nonce, ok: !error } as Record<string, unknown>;\n            if (error) data.error = error;\n\n            ws.send(JSON.stringify(data));\n        }\n\n        logger.info(\"Received Message:\", type, \"\\n\", data);\n\n        switch (type) {\n            case \"testPatch\": {\n                const { find, replacement } = data as PatchData;\n\n                const candidates = search(find);\n                const keys = Object.keys(candidates);\n                if (keys.length !== 1)\n                    return reply(\"Expected exactly one 'find' matches, found \" + keys.length);\n\n                const mod = candidates[keys[0]];\n                let src = String(mod).replaceAll(\"\\n\", \"\");\n\n                if (src.startsWith(\"function(\")) {\n                    src = \"0,\" + src;\n                } else if (src.charCodeAt(0) >= 49 /* 1*/ && src.charCodeAt(0) <= 57 /* 9*/) {\n                    src = \"0,function\" + src.substring(src.indexOf(\"(\"));\n                }\n\n                let i = 0;\n\n                for (const { match, replace } of replacement) {\n                    i++;\n\n                    try {\n                        const matcher = canonicalizeMatch(parseNode(match));\n                        const replacement = canonicalizeReplace(parseNode(replace), 'Vencord.Plugins.plugins[\"PlaceHolderPluginName\"]');\n\n                        const newSource = src.replace(matcher, replacement as string);\n\n                        if (src === newSource) throw \"Had no effect\";\n                        Function(newSource);\n\n                        src = newSource;\n                    } catch (err) {\n                        return reply(`Replacement ${i} failed: ${err}`);\n                    }\n                }\n\n                reply();\n                break;\n            }\n            case \"testFind\": {\n                const { type, args } = data as FindData;\n                try {\n                    var parsedArgs = args.map(parseNode);\n                } catch (err) {\n                    return reply(\"Failed to parse args: \" + err);\n                }\n\n                try {\n                    let results: any[];\n                    switch (type.replace(\"find\", \"\").replace(\"Lazy\", \"\")) {\n                        case \"\":\n                            results = findAll(parsedArgs[0]);\n                            break;\n                        case \"CssClasses\":\n                            results = findAll(filters.byClassNames(...parsedArgs), { topLevelOnly: true });\n                            break;\n                        case \"ByProps\":\n                            results = findAll(filters.byProps(...parsedArgs));\n                            break;\n                        case \"Store\":\n                            results = findAll(filters.byStoreName(parsedArgs[0]));\n                            break;\n                        case \"ByCode\":\n                            results = findAll(filters.byCode(...parsedArgs));\n                            break;\n                        case \"ModuleId\":\n                            results = Object.keys(search(parsedArgs[0]));\n                            break;\n                        case \"ComponentByCode\":\n                            results = findAll(filters.componentByCode(...parsedArgs));\n                            break;\n                        default:\n                            return reply(\"Unknown Find Type \" + type);\n                    }\n\n                    const uniqueResultsCount = new Set(results).size;\n                    if (uniqueResultsCount === 0) throw \"No results\";\n                    if (uniqueResultsCount > 1) throw \"Found more than one result! Make this filter more specific\";\n                } catch (err) {\n                    return reply(\"Failed to find: \" + err);\n                }\n\n                reply();\n                break;\n            }\n            default:\n                reply(\"Unknown Type \" + type);\n                break;\n        }\n    });\n}\n\nexport default definePlugin({\n    name: \"DevCompanion\",\n    description: \"Dev Companion Plugin\",\n    authors: [Devs.Ven],\n    reporterTestable: ReporterTestable.None,\n    settings,\n\n    toolboxActions: {\n        \"Reconnect\"() {\n            socket?.close(1000, \"Reconnecting\");\n            initWs(true);\n        }\n    },\n\n    start() {\n        initWs();\n    },\n\n    stop() {\n        socket?.close(1000, \"Plugin Stopped\");\n        socket = void 0;\n    }\n});\n"
  },
  {
    "path": "src/plugins/disableCallIdle/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"DisableCallIdle\",\n    description: \"Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.\",\n    authors: [Devs.Nuckyz],\n    patches: [\n        {\n            find: \"this.idleTimeout.start(\",\n            replacement: {\n                match: /this\\.idleTimeout\\.(start|stop)/g,\n                replace: \"$self.noop\"\n            }\n        },\n        {\n            find: \"handleIdleUpdate(){\",\n            replacement: {\n                match: \"handleIdleUpdate(){\",\n                replace: \"handleIdleUpdate(){return;\"\n            }\n        }\n    ],\n\n    noop() { }\n});\n"
  },
  {
    "path": "src/plugins/dontRoundMyTimestamps/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { moment } from \"@webpack/common\";\n\nexport default definePlugin({\n    name: \"DontRoundMyTimestamps\",\n    authors: [Devs.Lexi],\n    description: \"Always rounds relative timestamps down, so 7.6y becomes 7y instead of 8y\",\n\n    start() {\n        moment.relativeTimeRounding(Math.floor);\n    },\n\n    stop() {\n        moment.relativeTimeRounding(Math.round);\n    }\n});\n"
  },
  {
    "path": "src/plugins/experiments/hideBugReport.css",
    "content": "#staff-help-popout-staff-help-bug-reporter {\n    display: none;\n}\n"
  },
  {
    "path": "src/plugins/experiments/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport { Margins } from \"@utils/margins\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByPropsLazy, findLazy } from \"@webpack\";\nimport { Forms, React } from \"@webpack/common\";\n\nimport hideBugReport from \"./hideBugReport.css?managed\";\n\nconst KbdStyles = findByPropsLazy(\"key\", \"combo\");\nconst BugReporterExperiment = findLazy(m => m?.definition?.name === \"2026-01-bug-reporter\");\n\nconst modKey = IS_MAC ? \"cmd\" : \"ctrl\";\nconst altKey = IS_MAC ? \"opt\" : \"alt\";\n\nconst settings = definePluginSettings({\n    toolbarDevMenu: {\n        type: OptionType.BOOLEAN,\n        description: \"Change the Help (?) toolbar button (top right in chat) to Discord's developer menu\",\n        default: false,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"Experiments\",\n    description: \"Enable Access to Experiments & other dev-only features in Discord!\",\n    authors: [\n        Devs.Megu,\n        Devs.Ven,\n        Devs.Nickyux,\n        Devs.BanTheNons,\n        Devs.Nuckyz,\n    ],\n\n    settings,\n\n    patches: [\n        {\n            find: \"Object.defineProperties(this,{isDeveloper\",\n            replacement: {\n                match: /(?<={isDeveloper:\\{[^}]+?,get:\\(\\)=>)\\i/,\n                replace: \"true\"\n            }\n        },\n        {\n            find: 'type:\"user\",revision',\n            replacement: {\n                match: /!(\\i)(?=&&\"CONNECTION_OPEN\")/,\n                replace: \"!($1=true)\"\n            }\n        },\n        {\n            find: 'placeholder:\"Search experiments\"',\n            replacement: {\n                match: /(?<=children:\\[)(?=\\(0,\\i\\.jsx?\\)\\(\\i\\.\\i,{placeholder:\"Search experiments\")/,\n                replace: \"$self.WarningCard(),\"\n            }\n        },\n        // Change top right toolbar button from the help one to the dev one\n        {\n            find: '?\"BACK_FORWARD_NAVIGATION\":',\n            replacement: {\n                match: /hasBugReporterAccess:(\\i)/,\n                replace: \"_hasBugReporterAccess:$1=true\"\n            },\n            predicate: () => settings.store.toolbarDevMenu\n        },\n        // Disable opening the bug report menu when clicking the top right toolbar dev button\n        {\n            find: 'navId:\"staff-help-popout\"',\n            replacement: {\n                match: /(isShown.+?)onClick:\\i/,\n                replace: (_, rest) => `${rest}onClick:()=>{}`\n            }\n        },\n        // Enable experiment embed on sent experiment links\n        {\n            find: \"Clear Treatment \",\n            replacement: [\n                {\n                    // TODO: stable compat optional chaining remove once some time has passed\n                    match: /\\i\\??\\.isStaff\\(\\)/,\n                    replace: \"true\"\n                },\n                // Fix some tricky experiments name causing a client crash\n                {\n                    match: /\\.isStaffPersonal\\(\\).+?if\\(null==(\\i)\\|\\|null==\\i(?=\\)return null;)/,\n                    replace: \"$&||({})[$1]!=null\"\n                }\n            ]\n        },\n        // Fix another function which cases crashes with tricky experiment names and the experiment embed\n        {\n            find: \"}getServerAssignment(\",\n            replacement: {\n                match: /}getServerAssignment\\((\\i),\\i,\\i\\){/,\n                replace: \"$&if($1==null)return;\"\n            }\n        }\n    ],\n\n    start: () => !BugReporterExperiment.getConfig().hasBugReporterAccess && enableStyle(hideBugReport),\n    stop: () => disableStyle(hideBugReport),\n\n    settingsAboutComponent: () => {\n        return (\n            <React.Fragment>\n                <Forms.FormTitle tag=\"h3\">More Information</Forms.FormTitle>\n                <Paragraph size=\"md\">\n                    You can open Discord's DevTools via {\" \"}\n                    <div className={KbdStyles.combo} style={{ display: \"inline-flex\" }}>\n                        <kbd className={KbdStyles.key}>{modKey}</kbd> +{\" \"}\n                        <kbd className={KbdStyles.key}>{altKey}</kbd> +{\" \"}\n                        <kbd className={KbdStyles.key}>O</kbd>{\" \"}\n                    </div>\n                </Paragraph>\n            </React.Fragment>\n        );\n    },\n\n    WarningCard: ErrorBoundary.wrap(() => (\n        <ErrorCard id=\"vc-experiments-warning-card\" className={Margins.bottom16}>\n            <Forms.FormTitle tag=\"h2\">Hold on!!</Forms.FormTitle>\n\n            <Forms.FormText>\n                Experiments are unreleased Discord features. They might not work, or even break your client or get your account disabled.\n            </Forms.FormText>\n\n            <Forms.FormText className={Margins.top8}>\n                Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments.\n\n                If you don't know what an experiment does, ignore it. Do not ask us what experiments do either, we probably don't know.\n            </Forms.FormText>\n\n            <Forms.FormText className={Margins.top8}>\n                No, you cannot use server-side features like checking the \"Send to Client\" box.\n            </Forms.FormText>\n        </ErrorCard>\n    ), { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/expressionCloner/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { CheckedTextInput } from \"@components/CheckedTextInput\";\nimport { Devs } from \"@utils/constants\";\nimport { getGuildAcronym } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport { ModalContent, ModalHeader, ModalRoot, openModalLazy } from \"@utils/modal\";\nimport definePlugin from \"@utils/types\";\nimport { Guild, GuildSticker } from \"@vencord/discord-types\";\nimport { StickerFormatType } from \"@vencord/discord-types/enums\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, IconUtils, Menu, PermissionsBits, PermissionStore, React, RestAPI, StickersStore, Toasts, Tooltip, UserStore } from \"@webpack/common\";\nimport { Promisable } from \"type-fest\";\n\nconst uploadEmoji = findByCodeLazy(\".GUILD_EMOJIS(\", \"EMOJI_UPLOAD_START\");\n\nconst getGuildMaxEmojiSlots = findByCodeLazy(\".additionalEmojiSlots\") as (guild: Guild) => number;\n\ninterface Sticker extends GuildSticker {\n    t: \"Sticker\";\n}\n\ninterface Emoji {\n    t: \"Emoji\";\n    id: string;\n    name: string;\n    isAnimated: boolean;\n}\n\ntype Data = Emoji | Sticker;\n\nconst StickerExtMap = {\n    [StickerFormatType.PNG]: \"png\",\n    [StickerFormatType.APNG]: \"png\",\n    [StickerFormatType.LOTTIE]: \"json\",\n    [StickerFormatType.GIF]: \"gif\"\n} as const;\n\nconst PremiumTierStickerLimitMap = {\n    0: 5,\n    1: 15,\n    2: 30,\n    3: 60\n} as const;\n\nconst MAX_EMOJI_SIZE_BYTES = 256 * 1024;\nconst MAX_STICKER_SIZE_BYTES = 512 * 1024;\n\nfunction getGuildMaxStickerSlots(guild: Guild) {\n    if (guild.features.has(\"MORE_STICKERS\") && guild.premiumTier === 3)\n        return 120;\n\n    return PremiumTierStickerLimitMap[guild.premiumTier] ?? PremiumTierStickerLimitMap[0];\n}\n\nfunction getUrl(data: Data, size: number) {\n    if (data.t === \"Emoji\")\n        return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.webp?size=${size}&lossless=true&animated=true`;\n\n    return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExtMap[data.format_type]}?size=${size}&lossless=true&animated=true`;\n}\n\nasync function fetchSticker(id: string) {\n    const cached = StickersStore.getStickerById(id);\n    if (cached) return cached;\n\n    const { body } = await RestAPI.get({\n        url: Constants.Endpoints.STICKER(id)\n    });\n\n    FluxDispatcher.dispatch({\n        type: \"STICKER_FETCH_SUCCESS\",\n        sticker: body\n    });\n\n    return body as Sticker;\n}\n\nasync function cloneSticker(guildId: string, sticker: Sticker) {\n    const data = new FormData();\n    data.append(\"name\", sticker.name);\n    data.append(\"tags\", sticker.tags);\n    data.append(\"description\", sticker.description);\n    data.append(\"file\", await fetchBlob(sticker));\n\n    const { body } = await RestAPI.post({\n        url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),\n        body: data,\n    });\n\n    FluxDispatcher.dispatch({\n        type: \"GUILD_STICKERS_CREATE_SUCCESS\",\n        guildId,\n        sticker: {\n            ...body,\n            user: UserStore.getCurrentUser()\n        }\n    });\n}\n\nasync function cloneEmoji(guildId: string, emoji: Emoji) {\n    const data = await fetchBlob(emoji);\n\n    const dataUrl = await new Promise<string>(resolve => {\n        const reader = new FileReader();\n        reader.onload = () => resolve(reader.result as string);\n        reader.readAsDataURL(data);\n    });\n\n    return uploadEmoji({\n        guildId,\n        name: emoji.name.split(\"~\")[0],\n        image: dataUrl\n    });\n}\n\nfunction getGuildCandidates(data: Data) {\n    const meId = UserStore.getCurrentUser().id;\n\n    return Object.values(GuildStore.getGuilds()).filter(g => {\n        const canCreate = g.ownerId === meId ||\n            (PermissionStore.getGuildPermissions({ id: g.id }) & PermissionsBits.CREATE_GUILD_EXPRESSIONS) === PermissionsBits.CREATE_GUILD_EXPRESSIONS;\n        if (!canCreate) return false;\n\n        if (data.t === \"Sticker\") {\n            const stickerSlots = getGuildMaxStickerSlots(g);\n            const stickers = StickersStore.getStickersByGuildId(g.id);\n\n            return !stickers || stickers.length < stickerSlots;\n        }\n\n        const { isAnimated } = data as Emoji;\n\n        const emojiSlots = getGuildMaxEmojiSlots(g);\n        const emojis = EmojiStore.getGuildEmoji(g.id);\n\n        let count = 0;\n        for (const emoji of emojis) {\n            if (emoji.animated === isAnimated && !emoji.managed) {\n                count++;\n            }\n        }\n\n        return count < emojiSlots;\n    }).sort((a, b) => a.name.localeCompare(b.name));\n}\n\nasync function fetchBlob(data: Data) {\n    const MAX_SIZE = data.t === \"Sticker\"\n        ? MAX_STICKER_SIZE_BYTES\n        : MAX_EMOJI_SIZE_BYTES;\n\n    for (let size = 4096; size >= 16; size /= 2) {\n        const url = getUrl(data, size);\n        const res = await fetch(url);\n        if (!res.ok)\n            throw new Error(`Failed to fetch ${url} - ${res.status}`);\n\n        const blob = await res.blob();\n        if (blob.size <= MAX_SIZE)\n            return blob;\n    }\n\n    throw new Error(`Failed to fetch ${data.t} within size limit of ${MAX_SIZE / 1000}kB`);\n}\n\nasync function doClone(guildId: string, data: Sticker | Emoji) {\n    try {\n        if (data.t === \"Sticker\")\n            await cloneSticker(guildId, data);\n        else\n            await cloneEmoji(guildId, data);\n\n        Toasts.show({\n            message: `Successfully cloned ${data.name} to ${GuildStore.getGuild(guildId)?.name ?? \"your server\"}!`,\n            type: Toasts.Type.SUCCESS,\n            id: Toasts.genId()\n        });\n    } catch (e: any) {\n        let message = \"Something went wrong (check console!)\";\n        try {\n            message = JSON.parse(e.text).message;\n        } catch { }\n\n        new Logger(\"ExpressionCloner\").error(\"Failed to clone\", data.name, \"to\", guildId, e);\n        Toasts.show({\n            message: \"Failed to clone: \" + message,\n            type: Toasts.Type.FAILURE,\n            id: Toasts.genId()\n        });\n    }\n}\n\nconst getFontSize = (s: string) => {\n    // [18, 18, 16, 16, 14, 12, 10]\n    const sizes = [20, 20, 18, 18, 16, 14, 12];\n    return sizes[s.length] ?? 4;\n};\n\nconst nameValidator = /^\\w+$/i;\n\nfunction CloneModal({ data }: { data: Sticker | Emoji; }) {\n    const [isCloning, setIsCloning] = React.useState(false);\n    const [name, setName] = React.useState(data.name);\n\n    const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);\n\n    const guilds = React.useMemo(() => getGuildCandidates(data), [data.id, x]);\n\n    return (\n        <>\n            <Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>\n            <CheckedTextInput\n                value={name}\n                onChange={v => {\n                    data.name = v;\n                    setName(v);\n                }}\n                validate={v =>\n                    (data.t === \"Emoji\" && v.length > 2 && v.length < 32 && nameValidator.test(v))\n                    || (data.t === \"Sticker\" && v.length > 2 && v.length < 30)\n                    || \"Name must be between 2 and 32 characters and only contain alphanumeric characters\"\n                }\n            />\n            <div style={{\n                display: \"flex\",\n                flexWrap: \"wrap\",\n                gap: \"1em\",\n                padding: \"1em 0.5em\",\n                justifyContent: \"center\",\n                alignItems: \"center\"\n            }}>\n                {guilds.map(g => (\n                    <Tooltip key={g.id} text={g.name}>\n                        {({ onMouseLeave, onMouseEnter }) => (\n                            <div\n                                onMouseLeave={onMouseLeave}\n                                onMouseEnter={onMouseEnter}\n                                role=\"button\"\n                                aria-label={\"Clone to \" + g.name}\n                                aria-disabled={isCloning}\n                                style={{\n                                    borderRadius: \"50%\",\n                                    backgroundColor: \"var(--background-base-lower)\",\n                                    display: \"inline-flex\",\n                                    justifyContent: \"center\",\n                                    alignItems: \"center\",\n                                    width: \"4em\",\n                                    height: \"4em\",\n                                    cursor: isCloning ? \"not-allowed\" : \"pointer\",\n                                    filter: isCloning ? \"brightness(50%)\" : \"none\"\n                                }}\n                                onClick={isCloning ? void 0 : async () => {\n                                    setIsCloning(true);\n                                    doClone(g.id, data).finally(() => {\n                                        invalidateMemo();\n                                        setIsCloning(false);\n                                    });\n                                }}\n                            >\n                                {g.icon ? (\n                                    <img\n                                        aria-hidden\n                                        style={{\n                                            borderRadius: \"50%\",\n                                            width: \"100%\",\n                                            height: \"100%\",\n                                        }}\n                                        src={IconUtils.getGuildIconURL({\n                                            id: g.id,\n                                            icon: g.icon,\n                                            canAnimate: true,\n                                            size: 512\n                                        })}\n                                        alt={g.name}\n                                    />\n                                ) : (\n                                    <Forms.FormText\n                                        style={{\n                                            fontSize: getFontSize(getGuildAcronym(g)),\n                                            width: \"100%\",\n                                            overflow: \"hidden\",\n                                            whiteSpace: \"nowrap\",\n                                            textAlign: \"center\",\n                                            cursor: isCloning ? \"not-allowed\" : \"pointer\",\n                                        }}\n                                    >\n                                        {getGuildAcronym(g)}\n                                    </Forms.FormText>\n                                )}\n                            </div>\n                        )}\n                    </Tooltip>\n                ))}\n            </div>\n        </>\n    );\n}\n\nfunction buildMenuItem(type: \"Emoji\" | \"Sticker\", fetchData: () => Promisable<Omit<Sticker | Emoji, \"t\">>) {\n    return (\n        <Menu.MenuItem\n            id=\"emote-cloner\"\n            key=\"emote-cloner\"\n            label={`Clone ${type}`}\n            action={() =>\n                openModalLazy(async () => {\n                    const res = await fetchData();\n                    const data = { t: type, ...res } as Sticker | Emoji;\n                    const url = getUrl(data, 128);\n\n                    return modalProps => (\n                        <ModalRoot {...modalProps}>\n                            <ModalHeader>\n                                <img\n                                    role=\"presentation\"\n                                    aria-hidden\n                                    src={url}\n                                    alt=\"\"\n                                    height={24}\n                                    width={24}\n                                    style={{ marginRight: \"0.5em\" }}\n                                />\n                                <Forms.FormText>Clone {data.name}</Forms.FormText>\n                            </ModalHeader>\n                            <ModalContent>\n                                <CloneModal data={data} />\n                            </ModalContent>\n                        </ModalRoot>\n                    );\n                })\n            }\n        />\n    );\n}\n\nfunction isGifUrl(url: string) {\n    const u = new URL(url);\n    return u.pathname.endsWith(\".gif\") || u.searchParams.get(\"animated\") === \"true\";\n}\n\nconst messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {\n    const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};\n\n    if (!favoriteableId) return;\n\n    const menuItem = (() => {\n        switch (favoriteableType) {\n            case \"emoji\":\n                const match = props.message.content.match(RegExp(`<a?:(\\\\w+)(?:~\\\\d+)?:${favoriteableId}>|https://cdn\\\\.discordapp\\\\.com/emojis/${favoriteableId}\\\\.`));\n                const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);\n                if (!match && !reaction) return;\n                const name = (match && match[1]) ?? reaction?.emoji.name ?? \"FakeNitroEmoji\";\n\n                return buildMenuItem(\"Emoji\", () => ({\n                    id: favoriteableId,\n                    name,\n                    isAnimated: isGifUrl(itemHref ?? itemSrc)\n                }));\n            case \"sticker\":\n                const sticker = props.message.stickerItems.find(s => s.id === favoriteableId);\n                if (sticker?.format_type === 3 /* LOTTIE */) return;\n\n                return buildMenuItem(\"Sticker\", () => fetchSticker(favoriteableId));\n        }\n    })();\n\n    if (menuItem)\n        findGroupChildrenByChildId(\"copy-link\", children)?.push(menuItem);\n};\n\nconst expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {\n    const { id, name, type } = props?.target?.dataset ?? {};\n    if (!id) return;\n\n    if (type === \"emoji\" && name) {\n        const firstChild = props.target.firstChild as HTMLImageElement;\n\n        children.push(buildMenuItem(\"Emoji\", () => ({\n            id,\n            name,\n            isAnimated: firstChild && isGifUrl(firstChild.src)\n        })));\n    } else if (type === \"sticker\" && !props.target.className?.includes(\"lottieCanvas\")) {\n        children.push(buildMenuItem(\"Sticker\", () => fetchSticker(id)));\n    }\n};\n\nmigratePluginSettings(\"ExpressionCloner\", \"EmoteCloner\");\nexport default definePlugin({\n    name: \"ExpressionCloner\",\n    description: \"Allows you to clone Emotes & Stickers to your own server (right click them)\",\n    tags: [\"StickerCloner\", \"EmoteCloner\", \"EmojiCloner\"],\n    authors: [Devs.Ven, Devs.Nuckyz],\n    contextMenus: {\n        \"message\": messageContextMenuPatch,\n        \"expression-picker\": expressionPickerPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/f8break/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"F8Break\",\n    description: \"Pause the client when you press F8 with DevTools (+ breakpoints) open.\",\n    authors: [Devs.lewisakura],\n\n    start() {\n        window.addEventListener(\"keydown\", this.event);\n    },\n\n    stop() {\n        window.removeEventListener(\"keydown\", this.event);\n    },\n\n    event(e: KeyboardEvent) {\n        if (e.code === \"F8\") {\n            // Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again.\n            // It's up to you on what to do, friend. Happy travels!\n            debugger;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/fakeNitro/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from \"@api/MessageEvents\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { ApngBlendOp, ApngDisposeOp, parseAPNG } from \"@utils/apng\";\nimport { Devs } from \"@utils/constants\";\nimport { getCurrentGuild } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType, Patch } from \"@utils/types\";\nimport type { Emoji, Message, Sticker } from \"@vencord/discord-types\";\nimport { StickerFormatType } from \"@vencord/discord-types/enums\";\nimport { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from \"@webpack\";\nimport { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from \"@webpack/common\";\nimport { applyPalette, GIFEncoder, quantize } from \"gifenc\";\nimport type { ReactElement, ReactNode } from \"react\";\n\nconst BINARY_READ_OPTIONS = findByPropsLazy(\"readerFactory\");\n\nfunction searchProtoClassField(localName: string, protoClass: any) {\n    const field = protoClass?.fields?.find((field: any) => field.localName === localName);\n    if (!field) return;\n\n    const fieldGetter = Object.values(field).find(value => typeof value === \"function\") as any;\n    return fieldGetter?.();\n}\n\nconst PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);\nconst AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField(\"appearance\", PreloadedUserSettingsActionCreators.ProtoClass));\nconst ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField(\"clientThemeSettings\", AppearanceSettingsActionCreators));\n\nconst isUnusableRoleSubscriptionEmoji = findByCodeLazy(\".getUserIsAdmin(\");\n\nconst enum EmojiIntentions {\n    REACTION,\n    STATUS,\n    COMMUNITY_CONTENT,\n    CHAT,\n    GUILD_STICKER_RELATED_EMOJI,\n    GUILD_ROLE_BENEFIT_EMOJI,\n    COMMUNITY_CONTENT_ONLY,\n    SOUNDBOARD,\n    VOICE_CHANNEL_TOPIC,\n    GIFT,\n    AUTO_SUGGESTION,\n    POLLS\n}\n\nconst IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;\n\nconst enum FakeNoticeType {\n    Sticker,\n    Emoji\n}\n\nconst fakeNitroEmojiRegex = /\\/emojis\\/(\\d+?)\\.(png|webp|gif)/;\nconst fakeNitroStickerRegex = /\\/stickers\\/(\\d+?)\\./;\nconst fakeNitroGifStickerRegex = /\\/attachments\\/\\d+?\\/\\d+?\\/(\\d+?)\\.gif/;\nconst hyperLinkRegex = /\\[.+?\\]\\((https?:\\/\\/.+?)\\)/;\n\nconst settings = definePluginSettings({\n    enableEmojiBypass: {\n        description: \"Allows sending fake emojis (also bypasses missing permission to use custom emojis)\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    emojiSize: {\n        description: \"Size of the emojis when sending\",\n        type: OptionType.SLIDER,\n        default: 48,\n        markers: [32, 48, 56, 64, 96, 128, 160, 256, 512]\n    },\n    transformEmojis: {\n        description: \"Whether to transform fake emojis into real ones\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    enableStickerBypass: {\n        description: \"Allows sending fake stickers (also bypasses missing permission to use stickers)\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    stickerSize: {\n        description: \"Size of the stickers when sending\",\n        type: OptionType.SLIDER,\n        default: 160,\n        markers: [32, 64, 128, 160, 256, 512]\n    },\n    transformStickers: {\n        description: \"Whether to transform fake stickers into real ones\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    transformCompoundSentence: {\n        description: \"Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)\",\n        type: OptionType.BOOLEAN,\n        default: false\n    },\n    enableStreamQualityBypass: {\n        description: \"Allow streaming in nitro quality\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    useHyperLinks: {\n        description: \"Whether to use hyperlinks when sending fake emojis and stickers\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    hyperLinkText: {\n        description: \"What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.\",\n        type: OptionType.STRING,\n        default: \"{{NAME}}\"\n    },\n    disableEmbedPermissionCheck: {\n        description: \"Whether to disable the embed permission check when sending fake emojis and stickers\",\n        type: OptionType.BOOLEAN,\n        default: false\n    }\n});\n\nfunction hasPermission(channelId: string, permission: bigint) {\n    const channel = ChannelStore.getChannel(channelId);\n\n    if (!channel || channel.isPrivate()) return true;\n\n    return PermissionStore.can(permission, channel);\n}\n\nconst hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS);\nconst hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS);\nconst hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);\nconst hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);\n\nfunction makeBypassPatches(): Omit<Patch, \"plugin\"> {\n    const mapping: Array<{ func: string, predicate?: () => boolean; }> = [\n        { func: \"canUseCustomStickersEverywhere\", predicate: () => settings.store.enableStickerBypass },\n        { func: \"canUseHighVideoUploadQuality\", predicate: () => settings.store.enableStreamQualityBypass },\n        { func: \"canStreamQuality\", predicate: () => settings.store.enableStreamQualityBypass },\n        { func: \"canUseClientThemes\" },\n        { func: \"canUsePremiumAppIcons\" }\n    ];\n\n    return {\n        find: \"canUseCustomStickersEverywhere:\",\n        replacement: mapping.map(({ func, predicate }) => ({\n            match: new RegExp(String.raw`(?<=${func}:)\\i`),\n            replace: \"() => true\",\n            predicate\n        }))\n    };\n}\n\nexport default definePlugin({\n    name: \"FakeNitro\",\n    authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan],\n    description: \"Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality\",\n    dependencies: [\"MessageEventsAPI\"],\n\n    settings,\n\n    patches: [\n        // General bypass patches\n        makeBypassPatches(),\n        // Patch the emoji picker in voice calls to not be bypassed by fake nitro\n        {\n            find: '.getByName(\"fork_and_knife\")',\n            predicate: () => settings.store.enableEmojiBypass,\n            replacement: {\n                match: \".CHAT\",\n                replace: \".STATUS\"\n            }\n        },\n        {\n            find: \".GUILD_SUBSCRIPTION_UNAVAILABLE;\",\n            group: true,\n            predicate: () => settings.store.enableEmojiBypass,\n            replacement: [\n                {\n                    // Create a variable for the intention of using the emoji\n                    match: /(?<=\\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\\i).+?)/,\n                    replace: (_, intention) => `const fakeNitroIntention=${intention};`\n                },\n                {\n                    // Disallow the emoji for external if the intention doesn't allow it\n                    match: /&&!\\i&&!\\i(?=\\)return \\i\\.\\i\\.DISALLOW_EXTERNAL;)/,\n                    replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`\n                },\n                {\n                    // Disallow the emoji for unavailable if the intention doesn't allow it\n                    match: /!\\i\\.available(?=\\)return \\i\\.\\i\\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,\n                    replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`\n                },\n                {\n                    // Disallow the emoji for premium locked if the intention doesn't allow it\n                    match: /!(\\i\\.\\i\\.canUseEmojisEverywhere\\(\\i\\))/,\n                    replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`\n                },\n                {\n                    // Allow animated emojis to be used if the intention allows it\n                    match: /(?<=\\|\\|)\\i\\.\\i\\.canUseAnimatedEmojis\\(\\i\\)/,\n                    replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`\n                }\n            ]\n        },\n        // Allows the usage of subscription-locked emojis\n        {\n            find: \".getUserIsAdmin(\",\n            replacement: {\n                match: /(function \\i\\(\\i,\\i)\\){(.{0,250}.getUserIsAdmin\\(.+?return!1})/,\n                replace: (_, rest1, rest2) => `${rest1},fakeNitroOriginal){if(!fakeNitroOriginal)return false;${rest2}`\n            }\n        },\n        // Make stickers always available\n        {\n            find: '\"SENDABLE\"',\n            predicate: () => settings.store.enableStickerBypass,\n            replacement: {\n                match: /\\i\\.available\\?/,\n                replace: \"true?\"\n            }\n        },\n        // Remove boost requirements to stream with high quality\n        {\n            find: \"#{intl::STREAM_FPS_OPTION}\",\n            predicate: () => settings.store.enableStreamQualityBypass,\n            replacement: {\n                match: /guildPremiumTier:\\i\\.\\i\\.TIER_\\d,?/g,\n                replace: \"\"\n            }\n        },\n        {\n            find: '\"UserSettingsProtoStore\"',\n            replacement: [\n                {\n                    // Overwrite incoming connection settings proto with our local settings\n                    match: /function (\\i)\\((\\i)\\){(?=.*CONNECTION_OPEN:\\1)/,\n                    replace: (m, funcName, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`\n                },\n                {\n                    // Overwrite non local proto changes with our local settings\n                    match: /let{settings:/,\n                    replace: \"arguments[0].local||$self.handleProtoChange(arguments[0].settings.proto);$&\"\n                }\n            ]\n        },\n        // Call our function to handle changing the gradient theme when selecting a new one\n        {\n            find: \",updateTheme(\",\n            replacement: {\n                match: /(function \\i\\(\\i\\){let{backgroundGradientPresetId:(\\i).+?)(\\i\\.\\i\\.updateAsync.+?theme=(.+?),.+?},\\i\\))/,\n                replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`\n            }\n        },\n        // Allow users to use custom client themes\n        {\n            find: \"customUserThemeSettings:{\",\n            // Discord has two separate modules for treatments 1 and 2\n            all: true,\n            replacement: {\n                match: /(?<=\\i=)\\(0,\\i\\.\\i\\)\\(\\i\\.\\i\\.TIER_2\\)(?=,|;)/g,\n                replace: \"true\"\n            }\n        },\n        {\n            find: '[\"strong\",\"em\",\"u\",\"text\",\"inlineCode\",\"s\",\"spoiler\"]',\n            replacement: [\n                {\n                    // Call our function to decide whether the emoji link should be kept or not\n                    predicate: () => settings.store.transformEmojis,\n                    match: /1!==(\\i)\\.length\\|\\|1!==\\i\\.length/,\n                    replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`\n                },\n                {\n                    // Patch the rendered message content to add fake nitro emojis or remove sticker links\n                    predicate: () => settings.store.transformEmojis || settings.store.transformStickers,\n                    match: /(?=return{hasSpoilerEmbeds:\\i,hasBailedAst:\\i,content:(\\i))/,\n                    replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`\n                }\n            ]\n        },\n        {\n            find: \"}renderStickersAccessories(\",\n            replacement: [\n                {\n                    // Call our function to decide whether the embed should be ignored or not\n                    predicate: () => settings.store.transformEmojis || settings.store.transformStickers,\n                    match: /(renderEmbeds\\((\\i)\\){)(.+?embeds\\.map\\(\\((\\i),\\i\\)?=>{)/,\n                    replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`\n                },\n                {\n                    // Patch the stickers array to add fake nitro stickers\n                    predicate: () => settings.store.transformStickers,\n                    match: /renderStickersAccessories\\((\\i)\\){let (\\i)=\\(0,\\i\\.\\i\\)\\(\\i\\).+?;/,\n                    replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`\n                },\n                {\n                    // Filter attachments to remove fake nitro stickers or emojis\n                    predicate: () => settings.store.transformStickers,\n                    match: /renderAttachments\\(\\i\\){.+?{attachments:(\\i).+?;/,\n                    replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`\n                }\n            ]\n        },\n        {\n            find: \"#{intl::STICKER_POPOUT_UNJOINED_PRIVATE_GUILD_DESCRIPTION}\",\n            predicate: () => settings.store.transformStickers,\n            replacement: [\n                {\n                    // Export the renderable sticker to be used in the fake nitro sticker notice\n                    match: /let{renderableSticker:(\\i).{0,270}sticker:\\i,channel:\\i,/,\n                    replace: (m, renderableSticker) => `${m}fakeNitroRenderableSticker:${renderableSticker},`\n                },\n                {\n                    // Add the fake nitro sticker notice\n                    match: /(let \\i,{sticker:\\i,channel:\\i,closePopout:\\i.+?}=(\\i).+?;)(.+?description:)(\\i)(?=,sticker:\\i)/,\n                    replace: (_, rest, props, rest2, reactNode) => `${rest}let{fakeNitroRenderableSticker}=${props};${rest2}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!fakeNitroRenderableSticker?.fake)`\n                }\n            ]\n        },\n        {\n            find: \".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,\",\n            predicate: () => settings.store.transformEmojis,\n            replacement: {\n                // Export the emoji node to be used in the fake nitro emoji notice\n                match: /isDiscoverable:\\i,shouldHideRoleSubscriptionCTA:\\i,(?<={node:(\\i),.+?)/,\n                replace: (m, node) => `${m}fakeNitroNode:${node},`\n            }\n        },\n        {\n            find: \"#{intl::EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION}\",\n            predicate: () => settings.store.transformEmojis,\n            replacement: {\n                // Add the fake nitro emoji notice\n                match: /(?<=emojiDescription:)(\\i)(?<=\\1=\\i\\((\\i)\\).+?)/,\n                replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)`\n            }\n        },\n        // Separate patch for allowing using custom app icons\n        {\n            find: \"getCurrentDesktopIcon(),\",\n            replacement: {\n                match: /\\i\\.\\i\\.isPremium\\(\\i\\.\\i\\.getCurrentUser\\(\\)\\)/,\n                replace: \"true\"\n            }\n        },\n        // Make all Soundboard sounds available\n        {\n            find: 'type:\"GUILD_SOUNDBOARD_SOUND_CREATE\"',\n            replacement: {\n                match: /(?<=type:\"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)\".+?available:)\\i\\.available/g,\n                replace: \"true\"\n            }\n        }\n    ],\n\n    get guildId() {\n        return getCurrentGuild()?.id;\n    },\n\n    get canUseEmotes() {\n        return (UserStore.getCurrentUser().premiumType ?? 0) > 0;\n    },\n\n    get canUseStickers() {\n        return (UserStore.getCurrentUser().premiumType ?? 0) > 1;\n    },\n\n    handleProtoChange(proto: any, user: any) {\n        try {\n            if (proto == null || typeof proto === \"string\") return;\n\n            const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;\n\n            if (premiumType !== 2) {\n                proto.appearance ??= AppearanceSettingsActionCreators.create();\n\n                const protoStoreAppearenceSettings = UserSettingsProtoStore.settings.appearance;\n\n                const appearanceSettingsOverwrite = AppearanceSettingsActionCreators.create({\n                    ...proto.appearance,\n                    theme: protoStoreAppearenceSettings?.theme,\n                    clientThemeSettings: protoStoreAppearenceSettings?.clientThemeSettings\n                });\n\n                proto.appearance = appearanceSettingsOverwrite;\n            }\n        } catch (err) {\n            new Logger(\"FakeNitro\").error(err);\n        }\n    },\n\n    handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {\n        const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;\n        if (premiumType === 2 || backgroundGradientPresetId == null) return original();\n\n        if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !BINARY_READ_OPTIONS) return;\n\n        const currentAppearanceSettings = PreloadedUserSettingsActionCreators.getCurrentValue().appearance;\n\n        const newAppearanceProto = currentAppearanceSettings != null\n            ? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), BINARY_READ_OPTIONS)\n            : AppearanceSettingsActionCreators.create();\n\n        newAppearanceProto.theme = theme;\n\n        const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({\n            backgroundGradientPresetId: {\n                value: backgroundGradientPresetId\n            }\n        });\n\n        newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummy;\n        newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;\n\n        const proto = PreloadedUserSettingsActionCreators.ProtoClass.create();\n        proto.appearance = newAppearanceProto;\n\n        FluxDispatcher.dispatch({\n            type: \"USER_SETTINGS_PROTO_UPDATE\",\n            local: true,\n            partial: true,\n            settings: {\n                type: 1,\n                proto\n            }\n        });\n    },\n\n    trimContent(content: Array<any>) {\n        const firstContent = content[0];\n        if (typeof firstContent === \"string\") {\n            content[0] = firstContent.trimStart();\n            content[0] || content.shift();\n        } else if (typeof firstContent?.props?.children === \"string\") {\n            firstContent.props.children = firstContent.props.children.trimStart();\n            firstContent.props.children || content.shift();\n        }\n\n        const lastIndex = content.length - 1;\n        const lastContent = content[lastIndex];\n        if (typeof lastContent === \"string\") {\n            content[lastIndex] = lastContent.trimEnd();\n            content[lastIndex] || content.pop();\n        } else if (typeof lastContent?.props?.children === \"string\") {\n            lastContent.props.children = lastContent.props.children.trimEnd();\n            lastContent.props.children || content.pop();\n        }\n    },\n\n    clearEmptyArrayItems(array: Array<any>) {\n        return array.filter(item => item != null);\n    },\n\n    ensureChildrenIsArray(child: ReactElement<any>) {\n        if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];\n    },\n\n    patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {\n        // If content has more than one child or it's a single ReactElement like a header, list or span\n        if ((content.length > 1 || typeof content[0]?.type === \"string\") && !settings.store.transformCompoundSentence) return content;\n\n        let nextIndex = content.length;\n\n        const transformLinkChild = (child: ReactElement<any>) => {\n            if (settings.store.transformEmojis) {\n                const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);\n                if (fakeNitroMatch) {\n                    let url: URL | null = null;\n                    try {\n                        url = new URL(child.props.href);\n                    } catch { }\n\n                    const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get(\"name\") ?? \"FakeNitroEmoji\";\n                    const isAnimated = fakeNitroMatch[2] === \"gif\" || url?.searchParams.get(\"animated\") === \"true\";\n\n                    return Parser.defaultRules.customEmoji.react({\n                        jumboable: !inline && content.length === 1 && typeof content[0].type !== \"string\",\n                        animated: isAnimated,\n                        emojiId: fakeNitroMatch[1],\n                        name: emojiName,\n                        fake: true\n                    }, void 0, { key: String(nextIndex++) });\n                }\n            }\n\n            if (settings.store.transformStickers) {\n                if (fakeNitroStickerRegex.test(child.props.href)) return null;\n\n                const gifMatch = child.props.href.match(fakeNitroGifStickerRegex);\n                if (gifMatch) {\n                    // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker\n                    if (StickersStore.getStickerById(gifMatch[1])) return null;\n                }\n            }\n\n            return child;\n        };\n\n        const transformChild = (child: ReactElement<any>) => {\n            if (child?.props?.trusted != null) return transformLinkChild(child);\n            if (child?.props?.children != null) {\n                if (!Array.isArray(child.props.children)) {\n                    child.props.children = modifyChild(child.props.children);\n                    return child;\n                }\n\n                child.props.children = modifyChildren(child.props.children);\n                if (child.props.children.length === 0) return null;\n                return child;\n            }\n\n            return child;\n        };\n\n        const modifyChild = (child: ReactElement<any>) => {\n            const newChild = transformChild(child);\n\n            if (newChild?.type === \"ul\" || newChild?.type === \"ol\") {\n                this.ensureChildrenIsArray(newChild);\n                if (newChild.props.children.length === 0) return null;\n\n                let listHasAnItem = false;\n                for (const [index, child] of newChild.props.children.entries()) {\n                    if (child == null) {\n                        delete newChild.props.children[index];\n                        continue;\n                    }\n\n                    this.ensureChildrenIsArray(child);\n                    if (child.props.children.length > 0) listHasAnItem = true;\n                    else delete newChild.props.children[index];\n                }\n\n                if (!listHasAnItem) return null;\n\n                newChild.props.children = this.clearEmptyArrayItems(newChild.props.children);\n            }\n\n            return newChild;\n        };\n\n        const modifyChildren = (children: Array<ReactElement<any>>) => {\n            for (const [index, child] of children.entries()) children[index] = modifyChild(child);\n\n            children = this.clearEmptyArrayItems(children);\n\n            return children;\n        };\n\n        try {\n            const newContent = modifyChildren(lodash.cloneDeep(content));\n            this.trimContent(newContent);\n\n            return newContent;\n        } catch (err) {\n            new Logger(\"FakeNitro\").error(err);\n            return content;\n        }\n    },\n\n    patchFakeNitroStickers(stickers: Array<any>, message: Message) {\n        const itemsToMaybePush: Array<string> = [];\n\n        const contentItems = message.content.split(/\\s/);\n        if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems);\n        else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]);\n\n        itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === \"image/gif\").map(attachment => attachment.url));\n\n        for (const item of itemsToMaybePush) {\n            if (!settings.store.transformCompoundSentence && !item.startsWith(\"http\") && !hyperLinkRegex.test(item)) continue;\n\n            const imgMatch = item.match(fakeNitroStickerRegex);\n            if (imgMatch) {\n                let url: URL | null = null;\n                try {\n                    url = new URL(item);\n                } catch { }\n\n                const stickerName = StickersStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get(\"name\") ?? \"FakeNitroSticker\";\n                stickers.push({\n                    format_type: 1,\n                    id: imgMatch[1],\n                    name: stickerName,\n                    fake: true\n                });\n\n                continue;\n            }\n\n            const gifMatch = item.match(fakeNitroGifStickerRegex);\n            if (gifMatch) {\n                if (!StickersStore.getStickerById(gifMatch[1])) continue;\n\n                const stickerName = StickersStore.getStickerById(gifMatch[1])?.name ?? \"FakeNitroSticker\";\n                stickers.push({\n                    format_type: 2,\n                    id: gifMatch[1],\n                    name: stickerName,\n                    fake: true\n                });\n            }\n        }\n\n        return stickers;\n    },\n\n    shouldIgnoreEmbed(embed: Message[\"embeds\"][number], message: Message) {\n        try {\n            const contentItems = message.content.split(/\\s/);\n            if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;\n\n            switch (embed.type) {\n                case \"image\": {\n                    const url = embed.url ?? embed.image?.url;\n                    if (!url) return false;\n                    if (\n                        !settings.store.transformCompoundSentence\n                        && !contentItems.some(item => item === url || item.match(hyperLinkRegex)?.[1] === url)\n                    ) return false;\n\n                    if (settings.store.transformEmojis) {\n                        if (fakeNitroEmojiRegex.test(url)) return true;\n                    }\n\n                    if (settings.store.transformStickers) {\n                        if (fakeNitroStickerRegex.test(url)) return true;\n\n                        const gifMatch = url.match(fakeNitroGifStickerRegex);\n                        if (gifMatch) {\n                            // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker\n                            if (StickersStore.getStickerById(gifMatch[1])) return true;\n                        }\n                    }\n\n                    break;\n                }\n            }\n        } catch (e) {\n            new Logger(\"FakeNitro\").error(\"Error in shouldIgnoreEmbed:\", e);\n        }\n\n        return false;\n    },\n\n    filterAttachments(attachments: Message[\"attachments\"]) {\n        return attachments.filter(attachment => {\n            if (attachment.content_type !== \"image/gif\") return true;\n\n            const match = attachment.url.match(fakeNitroGifStickerRegex);\n            if (match) {\n                // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker\n                if (StickersStore.getStickerById(match[1])) return false;\n            }\n\n            return true;\n        });\n    },\n\n    shouldKeepEmojiLink(link: any) {\n        return link.target && fakeNitroEmojiRegex.test(link.target);\n    },\n\n    addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {\n        if (!fake) return node;\n\n        node = Array.isArray(node) ? node : [node];\n\n        switch (type) {\n            case FakeNoticeType.Sticker: {\n                node.push(\" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.\");\n\n                return node;\n            }\n            case FakeNoticeType.Emoji: {\n                node.push(\" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.\");\n\n                return node;\n            }\n        }\n    },\n\n    getStickerLink({ format_type, id }: Sticker) {\n        const ext = format_type === StickerFormatType.GIF ? \"gif\" : \"png\";\n        return `https://media.discordapp.net/stickers/${id}.${ext}?size=${settings.store.stickerSize}`;\n    },\n\n    async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {\n\n        const { frames, width, height } = await fetch(stickerLink)\n            .then(res => res.arrayBuffer())\n            .then(parseAPNG);\n\n        const gif = GIFEncoder();\n        const resolution = settings.store.stickerSize;\n\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = resolution;\n        canvas.height = resolution;\n\n        const ctx = canvas.getContext(\"2d\", {\n            willReadFrequently: true\n        })!;\n\n        const scale = resolution / Math.max(width, height);\n        ctx.scale(scale, scale);\n\n        let previousFrameData: ImageData;\n\n        for (const frame of frames) {\n            const { left, top, width, height, img, delay, blendOp, disposeOp } = frame;\n\n            previousFrameData = ctx.getImageData(left, top, width, height);\n\n            if (blendOp === ApngBlendOp.SOURCE) {\n                ctx.clearRect(left, top, width, height);\n            }\n\n            ctx.drawImage(img, left, top, width, height);\n\n            const { data } = ctx.getImageData(0, 0, resolution, resolution);\n\n            const palette = quantize(data, 256);\n            const index = applyPalette(data, palette);\n\n            gif.writeFrame(index, resolution, resolution, {\n                transparent: true,\n                palette,\n                delay\n            });\n\n            if (disposeOp === ApngDisposeOp.BACKGROUND) {\n                ctx.clearRect(left, top, width, height);\n            } else if (disposeOp === ApngDisposeOp.PREVIOUS) {\n                ctx.putImageData(previousFrameData, left, top);\n            }\n        }\n\n        gif.finish();\n\n        const file = new File([gif.bytesView() as Uint8Array<ArrayBuffer>], `${stickerId}.gif`, { type: \"image/gif\" });\n        UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);\n    },\n\n    canUseEmote(e: Emoji, channelId: string) {\n        if (e.type === 0) return true;\n        if (e.available === false) return false;\n\n        if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;\n\n        let isUsableTwitchSubEmote = false;\n        if (e.managed && e.guildId) {\n            const myRoles = GuildMemberStore.getSelfMember(e.guildId)?.roles ?? [];\n            isUsableTwitchSubEmote = e.roles.some(r => myRoles.includes(r));\n        }\n\n        if (this.canUseEmotes || isUsableTwitchSubEmote)\n            return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);\n        else\n            return !e.animated && e.guildId === this.guildId;\n    },\n\n    start() {\n        const s = settings.store;\n\n        if (!s.enableEmojiBypass && !s.enableStickerBypass) {\n            return;\n        }\n\n        function getWordBoundary(origStr: string, offset: number) {\n            return (!origStr[offset] || /\\s/.test(origStr[offset])) ? \"\" : \" \";\n        }\n\n        function cannotEmbedNotice() {\n            return new Promise<boolean>(resolve => {\n                Alerts.show({\n                    title: \"Hold on!\",\n                    body: <div>\n                        <Forms.FormText>\n                            You are trying to send/edit a message that contains a FakeNitro emoji or sticker,\n                            however you do not have permissions to embed links in the current channel.\n                            Are you sure you want to send this message? Your FakeNitro items will appear as a link only.\n                        </Forms.FormText>\n                        <Forms.FormText>\n                            You can disable this notice in the plugin settings.\n                        </Forms.FormText>\n                    </div>,\n                    confirmText: \"Send Anyway\",\n                    cancelText: \"Cancel\",\n                    secondaryConfirmText: \"Do not show again\",\n                    onConfirm: () => resolve(true),\n                    onCloseCallback: () => setImmediate(() => resolve(false)),\n                    onConfirmSecondary() {\n                        settings.store.disableEmbedPermissionCheck = true;\n                        resolve(true);\n                    }\n                });\n            });\n        }\n\n        this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {\n            const { guildId } = this;\n\n            let hasBypass = false;\n\n            stickerBypass: {\n                if (!s.enableStickerBypass)\n                    break stickerBypass;\n\n                const sticker = StickersStore.getStickerById(extra.stickers?.[0]!);\n                if (!sticker)\n                    break stickerBypass;\n\n                // Discord Stickers are now free yayyy!! :D\n                if (\"pack_id\" in sticker)\n                    break stickerBypass;\n\n                const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId);\n                if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))\n                    break stickerBypass;\n\n                const link = this.getStickerLink(sticker);\n\n                if (sticker.format_type === StickerFormatType.APNG) {\n                    if (!hasAttachmentPerms(channelId)) {\n                        Alerts.show({\n                            title: \"Hold on!\",\n                            body: <div>\n                                <Forms.FormText>\n                                    You cannot send this message because it contains an animated FakeNitro sticker,\n                                    and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.\n                                </Forms.FormText>\n                            </div>\n                        });\n                    } else {\n                        this.sendAnimatedSticker(link, sticker.id, channelId);\n                    }\n\n                    return { cancel: true };\n                } else {\n                    hasBypass = true;\n\n                    const url = new URL(link);\n                    url.searchParams.set(\"name\", sticker.name);\n                    url.searchParams.set(\"lossless\", \"true\");\n\n                    const linkText = s.hyperLinkText.replaceAll(\"{{NAME}}\", sticker.name);\n\n                    messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;\n                    extra.stickers!.length = 0;\n                }\n            }\n\n            if (s.enableEmojiBypass) {\n                for (const emoji of messageObj.validNonShortcutEmojis) {\n                    if (this.canUseEmote(emoji, channelId)) continue;\n\n                    hasBypass = true;\n\n                    const emojiString = `<${emoji.animated ? \"a\" : \"\"}:${emoji.originalName || emoji.name}:${emoji.id}>`;\n\n                    const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));\n                    url.searchParams.set(\"size\", s.emojiSize.toString());\n                    url.searchParams.set(\"name\", emoji.name);\n                    url.searchParams.set(\"lossless\", \"true\");\n\n                    const linkText = s.hyperLinkText.replaceAll(\"{{NAME}}\", emoji.name);\n\n                    messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {\n                        return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`;\n                    });\n                }\n            }\n\n            if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {\n                if (!await cannotEmbedNotice()) {\n                    return { cancel: true };\n                }\n            }\n\n            return { cancel: false };\n        });\n\n        this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {\n            if (!s.enableEmojiBypass) return;\n\n            let hasBypass = false;\n\n            messageObj.content = messageObj.content.replace(/(?<!\\\\)<a?:(?:\\w+):(\\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {\n                const emoji = EmojiStore.getCustomEmojiById(emojiId);\n                if (emoji == null) return emojiStr;\n                if (this.canUseEmote(emoji, channelId)) return emojiStr;\n\n                hasBypass = true;\n\n                const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));\n                url.searchParams.set(\"size\", s.emojiSize.toString());\n                url.searchParams.set(\"name\", emoji.name);\n                url.searchParams.set(\"lossless\", \"true\");\n\n                const linkText = s.hyperLinkText.replaceAll(\"{{NAME}}\", emoji.name);\n\n                return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`;\n            });\n\n            if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {\n                if (!await cannotEmbedNotice()) {\n                    return { cancel: true };\n                }\n            }\n\n            return { cancel: false };\n        });\n    },\n\n    stop() {\n        removeMessagePreSendListener(this.preSend);\n        removeMessagePreEditListener(this.preEdit);\n    }\n});\n"
  },
  {
    "path": "src/plugins/fakeProfileThemes/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// This plugin is a port from Alyxia's Vendetta plugin\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Divider } from \"@components/Divider\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast, fetchUserProfile } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { useAwaiter } from \"@utils/react\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { User, UserProfile } from \"@vencord/discord-types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { Button, ColorPicker, Forms, React, Text, UserProfileStore, UserStore, useState } from \"@webpack/common\";\nimport virtualMerge from \"virtual-merge\";\n\ninterface Colors {\n    primary: number;\n    accent: number;\n}\n\nfunction encode(primary: number, accent: number): string {\n    const message = `[#${primary.toString(16).padStart(6, \"0\")},#${accent.toString(16).padStart(6, \"0\")}]`;\n    const padding = \"\";\n    const encoded = Array.from(message)\n        .map(x => x.codePointAt(0))\n        .filter(x => x! >= 0x20 && x! <= 0x7f)\n        .map(x => String.fromCodePoint(x! + 0xe0000))\n        .join(\"\");\n\n    return (padding || \"\") + \" \" + encoded;\n}\n\n// Courtesy of Cynthia.\nfunction decode(bio: string): Array<number> | null {\n    if (bio == null) return null;\n\n    const colorString = bio.match(\n        /\\u{e005b}\\u{e0023}([\\u{e0061}-\\u{e0066}\\u{e0041}-\\u{e0046}\\u{e0030}-\\u{e0039}]{1,6})\\u{e002c}\\u{e0023}([\\u{e0061}-\\u{e0066}\\u{e0041}-\\u{e0046}\\u{e0030}-\\u{e0039}]{1,6})\\u{e005d}/u,\n    );\n    if (colorString != null) {\n        const parsed = [...colorString[0]]\n            .map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))\n            .join(\"\");\n        const colors = parsed\n            .substring(1, parsed.length - 1)\n            .split(\",\")\n            .map(x => parseInt(x.replace(\"#\", \"0x\"), 16));\n\n        return colors;\n    } else {\n        return null;\n    }\n}\n\nconst settings = definePluginSettings({\n    nitroFirst: {\n        description: \"Default color source if both are present\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Nitro colors\", value: true, default: true },\n            { label: \"Fake colors\", value: false },\n        ]\n    }\n});\n\n// I can't be bothered to figure out the semantics of this component. The\n// functions surely get some event argument sent to them and they likely aren't\n// all required. If anyone who wants to use this component stumbles across this\n// code, you'll have to do the research yourself.\ninterface ProfileModalProps {\n    user: User;\n    pendingThemeColors: [number, number];\n    onAvatarChange: () => void;\n    onBannerChange: () => void;\n    canUsePremiumCustomization: boolean;\n    hideExampleButton: boolean;\n    hideFakeActivity: boolean;\n    isTryItOut: boolean;\n}\n\nconst ProfileModal = findComponentByCodeLazy<ProfileModalProps>(\"isTryItOut:\", \"pendingThemeColors:\", \"pendingAvatarDecoration:\", \"EDIT_PROFILE_BANNER\");\n\nfunction SettingsAboutComponentWrapper() {\n    const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));\n\n    return !userProfileLoading && <SettingsAboutComponent />;\n}\n\nfunction SettingsAboutComponent() {\n    const existingColors = decode(\n        UserProfileStore.getUserProfile(UserStore.getCurrentUser().id)?.bio ?? \"\"\n    ) ?? [0, 0];\n    const [color1, setColor1] = useState(existingColors[0]);\n    const [color2, setColor2] = useState(existingColors[1]);\n\n    return (\n        <section>\n            <Forms.FormTitle tag=\"h3\">Usage</Forms.FormTitle>\n            <Forms.FormText>\n                After enabling this plugin, you will see custom colors in\n                the profiles of other people using compatible plugins.{\" \"}\n            </Forms.FormText>\n            <Forms.FormText className={Margins.top8}>\n                <strong>To set your own profile theme colors:</strong>\n                <ul>\n                    <li>&mdash; use the color pickers below to choose your colors</li>\n                    <li>&mdash; click the \"Copy 3y3\" button</li>\n                    <li>&mdash; paste the invisible text anywhere in your bio</li>\n                </ul>\n                <Divider\n                    className={classes(Margins.top8, Margins.bottom8)}\n                />\n                <Forms.FormTitle tag=\"h3\">Color pickers</Forms.FormTitle>\n                <Flex gap=\"1em\">\n                    <ColorPicker\n                        color={color1}\n                        label={\n                            <Text\n                                variant={\"text-xs/normal\"}\n                                style={{ marginTop: \"4px\" }}\n                            >\n                                Primary\n                            </Text>\n                        }\n                        onChange={(color: number) => {\n                            setColor1(color);\n                        }}\n                    />\n                    <ColorPicker\n                        color={color2}\n                        label={\n                            <Text\n                                variant={\"text-xs/normal\"}\n                                style={{ marginTop: \"4px\" }}\n                            >\n                                Accent\n                            </Text>\n                        }\n                        onChange={(color: number) => {\n                            setColor2(color);\n                        }}\n                    />\n                    <Button\n                        onClick={() => {\n                            const colorString = encode(color1, color2);\n                            copyWithToast(colorString);\n                        }}\n                        color={Button.Colors.PRIMARY}\n                        size={Button.Sizes.XLARGE}\n                        style={{ marginBottom: \"auto\" }}\n                    >\n                        Copy 3y3\n                    </Button>\n                </Flex>\n                <Divider\n                    className={classes(Margins.top8, Margins.bottom8)}\n                />\n                <Forms.FormTitle tag=\"h3\">Preview</Forms.FormTitle>\n                <div className=\"vc-fpt-preview\">\n                    <ProfileModal\n                        user={UserStore.getCurrentUser()}\n                        pendingThemeColors={[color1, color2]}\n                        onAvatarChange={() => { }}\n                        onBannerChange={() => { }}\n                        canUsePremiumCustomization={true}\n                        hideExampleButton={true}\n                        hideFakeActivity={true}\n                        isTryItOut={true}\n                    />\n                </div>\n            </Forms.FormText>\n        </section>);\n}\n\nexport default definePlugin({\n    name: \"FakeProfileThemes\",\n    description: \"Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding\",\n    authors: [Devs.Alyxia, Devs.Remty],\n    patches: [\n        {\n            find: \"UserProfileStore\",\n            replacement: {\n                match: /(?<=getUserProfile\\(\\i\\){return )(.+?)(?=})/,\n                replace: \"$self.colorDecodeHook($1)\"\n            },\n        },\n        {\n            find: \"#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\",\n            replacement: {\n                match: /#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\\).+?}\\)(?=\\])(?<=color:(\\i),.{0,500}?color:(\\i),.{0,500}?)/,\n                replace: \"$&,$self.addCopy3y3Button({primary:$1,accent:$2})\"\n            }\n        }\n    ],\n\n    settingsAboutComponent: SettingsAboutComponentWrapper,\n\n    settings,\n    colorDecodeHook(user: UserProfile) {\n        if (user?.bio) {\n            // don't replace colors if already set with nitro\n            if (settings.store.nitroFirst && user.themeColors) return user;\n            const colors = decode(user.bio);\n            if (colors) {\n                return virtualMerge(user, {\n                    premiumType: 2,\n                    themeColors: colors\n                });\n            }\n        }\n        return user;\n    },\n    addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {\n        return <Button\n            onClick={() => {\n                const colorString = encode(primary, accent);\n                copyWithToast(colorString);\n            }}\n            color={Button.Colors.PRIMARY}\n            size={Button.Sizes.XLARGE}\n            className={Margins.left16}\n        >Copy 3y3\n        </Button >;\n    }, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/fakeProfileThemes/styles.css",
    "content": ".vc-fpt-preview * {\n    pointer-events: none;\n}\n"
  },
  {
    "path": "src/plugins/favEmojiFirst/README.md",
    "content": "# FavoriteEmojiFirst\n\nPuts your favorite emoji first in the emoji autocomplete.\n\n![a screenshot of the favourite emojis section](https://github.com/Vendicated/Vencord/assets/45497981/419c8c16-1afc-46e0-9cc2-20b9c3489711)\n![a comparison of the emoji picker before and after enabling this plugin](https://github.com/Vendicated/Vencord/assets/45497981/4f57626d-cfc6-4155-a47c-2eac191231bb)\n"
  },
  {
    "path": "src/plugins/favEmojiFirst/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Emoji } from \"@vencord/discord-types\";\nimport { EmojiStore } from \"@webpack/common\";\n\ninterface EmojiAutocompleteState {\n    query?: {\n        type: string;\n        typeInfo: {\n            sentinel: string;\n        };\n        results: {\n            emojis: Emoji[] & { sliceTo?: number; };\n        };\n    };\n}\n\nexport default definePlugin({\n    name: \"FavoriteEmojiFirst\",\n    authors: [Devs.Aria, Devs.Ven],\n    description: \"Puts your favorite emoji first in the emoji autocomplete.\",\n    patches: [\n        {\n            find: \"renderResults({results:\",\n            replacement: [\n                {\n                    // https://regex101.com/r/N7kpLM/1\n                    match: /let \\i=.{1,100}renderResults\\({results:(\\i)\\.query\\.results,/,\n                    replace: \"$self.sortEmojis($1);$&\"\n                },\n            ],\n        },\n\n        {\n            find: \"numEmojiResults:\",\n            replacement: [\n                // set maxCount to Infinity so our sortEmojis callback gets the entire list, not just the first 10\n                // and remove Discord's emojiResult slice, storing the endIndex on the array for us to use later\n                {\n                    // https://regex101.com/r/x2mobQ/1\n                    // searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length)\n                    match: /,maxCount:(\\i)(.{1,500}\\i)=(\\i)\\.slice\\(0,(Math\\.max\\(\\i,\\i(?:-\\i\\.length){2}\\))\\)/,\n                    // ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis)\n                    replace: \",maxCount:Infinity$2=($3.sliceTo = $4, $3)\"\n                }\n            ]\n        }\n    ],\n\n    sortEmojis({ query }: EmojiAutocompleteState) {\n        if (\n            query?.type !== \"EMOJIS_AND_STICKERS\"\n            || query.typeInfo?.sentinel !== \":\"\n            || !query.results?.emojis?.length\n        ) return;\n\n        const emojiContext = EmojiStore.getDisambiguatedEmojiContext();\n\n        query.results.emojis = query.results.emojis.sort((a, b) => {\n            const aIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(a);\n            const bIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(b);\n\n            if (aIsFavorite && !bIsFavorite) return -1;\n\n            if (!aIsFavorite && bIsFavorite) return 1;\n\n            return 0;\n        }).slice(0, query.results.emojis.sliceTo ?? Infinity);\n    }\n});\n"
  },
  {
    "path": "src/plugins/favGifSearch/README.md",
    "content": "# FavoriteGifSearch\n\nAdds a search bar to favorite gifs.\n\n![Screenshot](https://github.com/Vendicated/Vencord/assets/45497981/19552adc-d921-4153-976e-e9361dc8fdaf)\n"
  },
  {
    "path": "src/plugins/favGifSearch/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { useCallback, useEffect, useRef, useState } from \"@webpack/common\";\n\ninterface SearchBarComponentProps {\n    ref?: React.RefObject<any>;\n    autoFocus: boolean;\n    size: string;\n    onChange: (query: string) => void;\n    onClear: () => void;\n    query: string;\n    placeholder: string;\n    className?: string;\n}\n\ntype TSearchBarComponent =\n    React.FC<SearchBarComponentProps>;\n\ninterface Gif {\n    format: number;\n    src: string;\n    width: number;\n    height: number;\n    order: number;\n    url: string;\n}\n\ninterface Instance {\n    dead?: boolean;\n    state: {\n        resultType?: string;\n    };\n    props: {\n        favCopy: Gif[],\n\n        favorites: Gif[],\n    },\n    forceUpdate: () => void;\n}\n\nexport const settings = definePluginSettings({\n    searchOption: {\n        type: OptionType.SELECT,\n        description: \"The part of the url you want to search\",\n        options: [\n            {\n                label: \"Entire Url\",\n                value: \"url\"\n            },\n            {\n                label: \"Path Only (/somegif.gif)\",\n                value: \"path\"\n            },\n            {\n                label: \"Host & Path (tenor.com somgif.gif)\",\n                value: \"hostandpath\",\n                default: true\n            }\n        ] as const\n    }\n});\n\nexport default definePlugin({\n    name: \"FavoriteGifSearch\",\n    authors: [Devs.Aria],\n    description: \"Adds a search bar to favorite gifs.\",\n\n    patches: [\n        {\n            find: \"renderHeaderContent()\",\n            replacement: [\n                {\n                    // https://regex101.com/r/07gpzP/1\n                    // ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default: ... return r.jsx(($<searchComp>), {...props}))\n                    match: /(renderHeaderContent\\(\\).{1,150}FAVORITES:return)(.{1,150});(case.{1,200}default:.{0,50}?return\\(0,\\i\\.jsx\\)\\((?<searchComp>\\i\\..{1,10}),)/,\n                    replace: \"$1 this.state.resultType === 'Favorites' ? $self.renderSearchBar(this, $<searchComp>) : $2;$3\"\n                },\n                {\n                    // to persist filtered favorites when component re-renders.\n                    // when resizing the window the component rerenders and we loose the filtered favorites and have to type in the search bar to get them again\n                    match: /(,suggestions:\\i,favorites:)(\\i),/,\n                    replace: \"$1$self.getFav($2),favCopy:$2,\"\n                }\n\n            ]\n        }\n    ],\n\n    settings,\n\n    getTargetString,\n\n    instance: null as Instance | null,\n    renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {\n        this.instance = instance;\n        return (\n            <ErrorBoundary noop>\n                <SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />\n            </ErrorBoundary>\n        );\n    },\n\n    getFav(favorites: Gif[]) {\n        if (!this.instance || this.instance.dead) return favorites;\n        const { favorites: filteredFavorites } = this.instance.props;\n\n        return filteredFavorites != null && filteredFavorites?.length !== favorites.length ? filteredFavorites : favorites;\n\n    }\n});\n\n\nfunction SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) {\n    const [query, setQuery] = useState(\"\");\n    const ref = useRef<HTMLElement>(null);\n\n    const onChange = useCallback((searchQuery: string) => {\n        setQuery(searchQuery);\n        const { props } = instance;\n\n        // return early\n        if (searchQuery === \"\") {\n            props.favorites = props.favCopy;\n            instance.forceUpdate();\n            return;\n        }\n\n\n        // scroll back to top\n        ref.current\n            ?.closest(\"#gif-picker-tab-panel\")\n            ?.querySelector('[class*=\"scrollerBase\"]')\n            ?.scrollTo(0, 0);\n\n\n        const result =\n            props.favCopy\n                .map(gif => ({\n                    score: fuzzySearch(searchQuery.toLowerCase(), getTargetString(gif.url ?? gif.src).replace(/(%20|[_-])/g, \" \").toLowerCase()),\n                    gif,\n                }))\n                .filter(m => m.score != null) as { score: number; gif: Gif; }[];\n\n        result.sort((a, b) => b.score - a.score);\n        props.favorites = result.map(e => e.gif);\n\n        instance.forceUpdate();\n    }, [instance.state]);\n\n    useEffect(() => {\n        return () => {\n            instance.dead = true;\n        };\n    }, []);\n\n    return (\n        <SearchBarComponent\n            ref={ref}\n            autoFocus={true}\n            size=\"md\"\n            className=\"\"\n            onChange={onChange}\n            onClear={() => {\n                setQuery(\"\");\n                if (instance.props.favCopy != null) {\n                    instance.props.favorites = instance.props.favCopy;\n                    instance.forceUpdate();\n                }\n            }}\n            query={query}\n            placeholder=\"Search Favorite Gifs\"\n        />\n    );\n}\n\n\n\nexport function getTargetString(urlStr: string) {\n    let url: URL;\n    try {\n        url = new URL(urlStr);\n    } catch (err) {\n        // Can't resolve URL, return as-is\n        return urlStr;\n    }\n\n    switch (settings.store.searchOption) {\n        case \"url\":\n            return url.href;\n        case \"path\":\n            if (url.host === \"media.discordapp.net\" || url.host === \"tenor.com\")\n                // /attachments/899763415290097664/1095711736461537381/attachment-1.gif -> attachment-1.gif\n                // /view/some-gif-hi-24248063 -> some-gif-hi-24248063\n                return url.pathname.split(\"/\").at(-1) ?? url.pathname;\n            return url.pathname;\n        case \"hostandpath\":\n            if (url.host === \"media.discordapp.net\" || url.host === \"tenor.com\")\n                return `${url.host} ${url.pathname.split(\"/\").at(-1) ?? url.pathname}`;\n            return `${url.host} ${url.pathname}`;\n\n        default:\n            return \"\";\n    }\n}\n\nfunction fuzzySearch(searchQuery: string, searchString: string) {\n    let searchIndex = 0;\n    let score = 0;\n\n    for (let i = 0; i < searchString.length; i++) {\n        if (searchString[i] === searchQuery[searchIndex]) {\n            score++;\n            searchIndex++;\n        } else {\n            score--;\n        }\n\n        if (searchIndex === searchQuery.length) {\n            return score;\n        }\n    }\n\n    return null;\n}\n"
  },
  {
    "path": "src/plugins/fixCodeblockGap/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"FixCodeblockGap\",\n    description: \"Removes the gap between codeblocks and text below it\",\n    authors: [Devs.Grzesiek11],\n    patches: [\n        {\n            find: String.raw`/^${\"```\"}(?:([a-z0-9_+\\-.#]+?)\\n)?\\n*([^\\n][^]*?)\\n*${\"```\"}`,\n            replacement: {\n                match: String.raw`/^${\"```\"}(?:([a-z0-9_+\\-.#]+?)\\n)?\\n*([^\\n][^]*?)\\n*${\"```\"}`,\n                replace: \"$&\\\\n?\",\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/fixImagesQuality/README.md",
    "content": "# Fix Images Quality\n\nImproves quality of images by loading them at their original resolution\n\n### The default behaviour is the following\n\n- In chat, optimised but full resolution images will be loaded.\n- In the image modal, the original image will be loaded.\n\nYou can also enable original image in chat via the plugin settings, but this may cause performance issues!\n\nThis plugin does not change how others see your images!\n"
  },
  {
    "path": "src/plugins/fixImagesQuality/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Card } from \"@components/Card\";\nimport { Flex } from \"@components/Flex\";\nimport { Margins } from \"@components/margins\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    originalImagesInChat: {\n        type: OptionType.BOOLEAN,\n        description: \"Also load the original image in Chat. WARNING: Read the caveats above\",\n        default: false,\n    }\n});\n\nexport default definePlugin({\n    name: \"FixImagesQuality\",\n    description: \"Improves quality of images by loading them at their original resolution\",\n    authors: [Devs.Nuckyz, Devs.Ven],\n    settings,\n\n    patches: [\n        {\n            find: \".handleImageLoad)\",\n            replacement: {\n                match: /getSrc\\(\\i\\)\\{/,\n                replace: \"$&var _vcSrc=$self.getSrc(this.props,arguments[1]);if(_vcSrc)return _vcSrc;\"\n            }\n        }\n    ],\n\n    settingsAboutComponent() {\n        return (\n            <Card variant=\"normal\">\n                <Flex flexDirection=\"column\" gap=\"4px\">\n                    <Paragraph size=\"md\" weight=\"semibold\">The default behaviour is the following:</Paragraph>\n                    <Paragraph>\n                        <ul>\n                            <li>&mdash; In chat, optimised but full resolution images will be loaded.</li>\n                            <li>&mdash; In the image modal, the original image will be loaded.</li>\n                        </ul>\n                    </Paragraph>\n                    <Paragraph size=\"md\" weight=\"semibold\" className={Margins.top8}>You can also enable original image in chat, but beware of the following caveats:</Paragraph>\n                    <Paragraph>\n                        <ul>\n                            <li>&mdash; Animated images (GIF, WebP, etc.) in chat will always animate, regardless of if the App is focused.</li>\n                            <li>&mdash; May cause lag.</li>\n                        </ul>\n                    </Paragraph>\n                </Flex>\n            </Card>\n        );\n    },\n\n    getSrc(props: { src: string; width: number; height: number; contentType: string; mosaicStyleAlt?: boolean; trigger?: string; }, freeze?: boolean) {\n        if (!props?.src) return;\n\n        try {\n            const { contentType, height, src, width, mosaicStyleAlt, trigger } = props;\n\n            // Embed images do not have a content type set.\n            // It's difficult to differentiate between images and videos. but mosaicStyleAlt seems exclusive to images\n            const isImage = contentType?.startsWith(\"image/\") ?? (typeof mosaicStyleAlt === \"boolean\");\n            if (!isImage || src.startsWith(\"data:\")) return;\n\n            const url = new URL(src);\n            if (!url.pathname.startsWith(\"/attachments/\")) return;\n\n            url.searchParams.set(\"animated\", String(!freeze));\n            if (freeze && url.pathname.endsWith(\".gif\")) {\n                // gifs don't support animated=false, so we have no choice but to use webp\n                url.searchParams.set(\"format\", \"webp\");\n            }\n\n            const isModal = !!trigger;\n            if (!settings.store.originalImagesInChat && !isModal) {\n                // make sure the image is not too large\n                const pixels = width * height;\n                const limit = 2000 * 1200;\n\n                if (pixels <= limit)\n                    return url.toString();\n\n                const scale = Math.sqrt(pixels / limit);\n\n                url.searchParams.set(\"width\", Math.round(width / scale).toString());\n                url.searchParams.set(\"height\", Math.round(height / scale).toString());\n                return url.toString();\n            }\n\n            url.hostname = \"cdn.discordapp.com\";\n            return url.toString();\n        } catch (e) {\n            new Logger(\"FixImagesQuality\").error(\"Failed to make image src\", e);\n            return;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/fixSpotifyEmbeds.desktop/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { makeRange, OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    volume: {\n        type: OptionType.SLIDER,\n        description: \"The volume % to set for spotify embeds. Anything above 10% is veeeery loud\",\n        markers: makeRange(0, 100, 10),\n        stickToMarkers: false,\n        default: 10\n    }\n});\n\n// The entire code of this plugin can be found in ipcPlugins\nexport default definePlugin({\n    name: \"FixSpotifyEmbeds\",\n    description: \"Fixes spotify embeds being incredibly loud by letting you customise the volume\",\n    authors: [Devs.Ven],\n    settings,\n});\n"
  },
  {
    "path": "src/plugins/fixSpotifyEmbeds.desktop/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { RendererSettings } from \"@main/settings\";\nimport { app, WebFrameMain, webFrameMain } from \"electron\";\n\n// TODO: routingID is deprecated and should be replaced with frameToken, but it's too new\nconst ids = [] as Record<\"routingId\" | \"processId\", number>[];\n\nfunction cleanUpAndGetSpotifyFrames() {\n    const spotifyFrames = [] as WebFrameMain[];\n    for (let i = ids.length - 1; i >= 0; i--) {\n        const { processId, routingId } = ids[i];\n\n        const frame = webFrameMain.fromId(processId, routingId);\n        if (!frame) {\n            ids.splice(i, 1);\n            continue;\n        }\n\n        spotifyFrames.push(frame);\n    }\n\n    return spotifyFrames;\n}\n\napp.on(\"browser-window-created\", (_, win) => {\n    win.webContents.on(\"frame-created\", (_, { frame }) => {\n        frame?.once(\"dom-ready\", () => {\n            if (frame.url.startsWith(\"https://open.spotify.com/embed/\")) {\n                cleanUpAndGetSpotifyFrames(); // clean up stale frames\n\n                const { routingId, processId } = frame;\n                ids.push({ routingId, processId });\n\n                const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;\n                if (!settings?.enabled) return;\n\n                frame.executeJavaScript(`\n                    globalThis._vcVolume = ${settings.volume / 100};\n                    const original = Audio.prototype.play;\n                    Audio.prototype.play = function() {\n                        this.volume = _vcVolume;\n                        return original.apply(this, arguments);\n                    }\n                `);\n            }\n        });\n    });\n\n    RendererSettings.addChangeListener(\"plugins.FixSpotifyEmbeds.volume\", newVolume => {\n        try {\n            cleanUpAndGetSpotifyFrames().forEach(frame =>\n                frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)\n            );\n        } catch (e) {\n            console.error(\"FixSpotifyEmbeds: Failed to update volume\", e);\n        }\n    });\n});\n"
  },
  {
    "path": "src/plugins/fixYoutubeEmbeds.desktop/README.md",
    "content": "# FixYoutubeEmbeds\n\nBypasses youtube videos being blocked from display on Discord (for example by UMG)\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be)\n"
  },
  {
    "path": "src/plugins/fixYoutubeEmbeds.desktop/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"FixYoutubeEmbeds\",\n    description: \"Bypasses youtube videos being blocked from display on Discord (for example by UMG)\",\n    authors: [Devs.coolelectronics]\n});\n"
  },
  {
    "path": "src/plugins/fixYoutubeEmbeds.desktop/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { RendererSettings } from \"@main/settings\";\nimport { app } from \"electron\";\n\napp.on(\"browser-window-created\", (_, win) => {\n    win.webContents.on(\"frame-created\", (_, { frame }) => {\n        frame?.once(\"dom-ready\", () => {\n            if (frame.url.startsWith(\"https://www.youtube.com/\")) {\n                const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;\n                if (!settings?.enabled) return;\n\n                frame.executeJavaScript(`\n                new MutationObserver(() => {\n                    if(\n                        document.querySelector('div.ytp-error-content-wrap-subreason a[href*=\"www.youtube.com/watch?v=\"]')\n                    ) location.reload()\n                }).observe(document.body, { childList: true, subtree:true });\n                `);\n            }\n        });\n    });\n});\n"
  },
  {
    "path": "src/plugins/forceOwnerCrown/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Channel, User } from \"@vencord/discord-types\";\nimport { GuildStore } from \"@webpack/common\";\n\nexport default definePlugin({\n    name: \"ForceOwnerCrown\",\n    description: \"Force the owner crown next to usernames even if the server is large.\",\n    authors: [Devs.D3SOX, Devs.Nickyux],\n    patches: [\n        {\n            find: \"#{intl::GUILD_OWNER}),children:\",\n            replacement: {\n                match: /(?<=decorators:.{0,200}?isOwner:)\\i/,\n                replace: \"$self.isGuildOwner(arguments[0])\"\n            }\n        }\n    ],\n    isGuildOwner(props: { user: User, channel: Channel, isOwner: boolean, guildId?: string; }) {\n        if (!props?.user?.id) return props.isOwner;\n        if (props.channel?.type === 3 /* GROUP_DM */)\n            return props.isOwner;\n\n        // guild id is in props twice, fallback if the first is undefined\n        const guildId = props.guildId ?? props.channel?.guild_id;\n        const userId = props.user.id;\n\n        return GuildStore.getGuild(guildId)?.ownerId === userId;\n    },\n});\n"
  },
  {
    "path": "src/plugins/friendInvites/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ApplicationCommandInputType, sendBotMessage } from \"@api/Commands\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { findByPropsLazy } from \"@webpack\";\n\nconst FriendInvites = findByPropsLazy(\"createFriendInvite\");\n\nexport default definePlugin({\n    name: \"FriendInvites\",\n    description: \"Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).\",\n    authors: [Devs.afn, Devs.Dziurwa],\n    commands: [\n        {\n            name: \"create friend invite\",\n            description: \"Generates a friend invite link.\",\n            inputType: ApplicationCommandInputType.BUILT_IN,\n\n            execute: async (args, ctx) => {\n                const invite = await FriendInvites.createFriendInvite();\n\n                sendBotMessage(ctx.channel.id, {\n                    content: `\n                        discord.gg/${invite.code} ·\n                        Expires: <t:${new Date(invite.expires_at).getTime() / 1000}:R> ·\n                        Max uses: \\`${invite.max_uses}\\`\n                    `.trim().replace(/\\s+/g, \" \")\n                });\n            }\n        },\n        {\n            name: \"view friend invites\",\n            description: \"View a list of all generated friend invites.\",\n            inputType: ApplicationCommandInputType.BUILT_IN,\n            execute: async (_, ctx) => {\n                const invites = await FriendInvites.getAllFriendInvites();\n                const friendInviteList = invites.map(i =>\n                    `\n                    _discord.gg/${i.code}_ ·\n                    Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·\n                    Times used: \\`${i.uses}/${i.max_uses}\\`\n                    `.trim().replace(/\\s+/g, \" \")\n                );\n\n                sendBotMessage(ctx.channel.id, {\n                    content: friendInviteList.join(\"\\n\") || \"You have no active friend invites!\"\n                });\n            },\n        },\n        {\n            name: \"revoke friend invites\",\n            description: \"Revokes all generated friend invites.\",\n            inputType: ApplicationCommandInputType.BUILT_IN,\n            execute: async (_, ctx) => {\n                await FriendInvites.revokeFriendInvites();\n\n                sendBotMessage(ctx.channel.id, {\n                    content: \"All friend invites have been revoked.\"\n                });\n            },\n        },\n    ]\n});\n"
  },
  {
    "path": "src/plugins/friendsSince/README.md",
    "content": "# FriendsSince\n\nShows when you became friends with someone in the user popout\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/bb258188-ab48-4c4d-9858-1e90ba41e926)\n"
  },
  {
    "path": "src/plugins/friendsSince/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { getCurrentChannel } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from \"@webpack\";\nimport { RelationshipStore, Text } from \"@webpack/common\";\n\nconst WrapperClasses = findCssClassesLazy(\"memberSinceWrapper\");\nconst ContainerClasses = findCssClassesLazy(\"memberSince\");\nconst getCreatedAtDate = findByCodeLazy('month:\"short\",day:\"numeric\"');\nconst locale = findByPropsLazy(\"getLocale\");\nconst Section = findComponentByCodeLazy(\"headingVariant:\", '\"section\"', \"headingIcon:\");\n\nexport default definePlugin({\n    name: \"FriendsSince\",\n    description: \"Shows when you became friends with someone in the user popout\",\n    authors: [Devs.Elvyra, Devs.Antti],\n    patches: [\n        // DM User Sidebar\n        {\n            find: \".SIDEBAR}),nicknameIcons\",\n            replacement: {\n                match: /#{intl::USER_PROFILE_MEMBER_SINCE}\\),.{0,100}userId:(\\i\\.id)}\\)}\\)/,\n                replace: \"$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})\"\n            }\n        },\n        // User Profile Modal\n        {\n            find: \",applicationRoleConnection:\",\n            replacement: {\n                match: /#{intl::USER_PROFILE_MEMBER_SINCE}\\),.{0,100}userId:(\\i\\.id),.{0,100}}\\)}\\),/,\n                replace: \"$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),\"\n            }\n        },\n        // User Profile Modal v2\n        {\n            find: \".MODAL_V2,onClose:\",\n            replacement: {\n                match: /#{intl::USER_PROFILE_MEMBER_SINCE}\\),.{0,100}userId:(\\i\\.id),.{0,100}}\\)}\\),/,\n                replace: \"$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),\"\n            }\n        }\n    ],\n\n    FriendsSinceComponent: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {\n        if (!RelationshipStore.isFriend(userId)) return null;\n\n        const friendsSince = RelationshipStore.getSince(userId);\n        if (!friendsSince) return null;\n\n        if (isSidebar) {\n            return (\n                <Section\n                    heading=\"Friends Since\"\n                    headingVariant=\"text-xs/semibold\"\n                    headingColor=\"text-strong\"\n                >\n                    <Text variant=\"text-sm/normal\">\n                        {getCreatedAtDate(friendsSince, locale.getLocale())}\n                    </Text>\n                </Section>\n            );\n        }\n\n        return (\n            <Section\n                heading=\"Friends Since\"\n                headingVariant=\"text-xs/medium\"\n                headingColor=\"text-default\"\n                className=\"vc-friendsSince-profile-section\"\n            >\n                <div className={WrapperClasses.memberSinceWrapper}>\n                    <div className={ContainerClasses.memberSince}>\n                        {!!getCurrentChannel()?.guild_id && (\n                            <svg\n                                aria-hidden=\"true\"\n                                width=\"16\"\n                                height=\"16\"\n                                viewBox=\"0 0 24 24\"\n                                fill=\"var(--interactive-icon-default)\"\n                            >\n                                <path d=\"M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z\" />\n                                <path d=\"M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z\" />\n                            </svg>\n                        )}\n                        <Text variant=\"text-sm/normal\">\n                            {getCreatedAtDate(friendsSince, locale.getLocale())}\n                        </Text>\n                    </div>\n                </div>\n            </Section>\n        );\n    }, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/friendsSince/styles.css",
    "content": ".vc-friendsSince-profile-section {\n    gap: 4px;\n}"
  },
  {
    "path": "src/plugins/fullSearchContext/README.md",
    "content": "# FullSearchContext\n\nMakes the message context menu in message search results have all options you'd expect.\n\n![](https://github.com/user-attachments/assets/472d1327-3935-44c7-b7c4-0978b5348550)\n"
  },
  {
    "path": "src/plugins/fullSearchContext/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { NoopComponent } from \"@utils/react\";\nimport definePlugin from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { filters, findByCodeLazy, waitFor } from \"@webpack\";\nimport { ChannelStore, ContextMenuApi, UserStore } from \"@webpack/common\";\n\nconst useMessageMenu = findByCodeLazy(\".MESSAGE,commandTargetId:\");\n\ninterface CopyIdMenuItemProps {\n    id: string;\n    label: string;\n}\n\nlet CopyIdMenuItem: (props: CopyIdMenuItemProps) => React.ReactElement | null = NoopComponent;\nwaitFor(filters.componentByCode('\"cannot copy null text\"'), m => CopyIdMenuItem = m);\n\nfunction MessageMenu({ message, channel, onHeightUpdate }) {\n    const canReport = message.author &&\n        !(message.author.id === UserStore.getCurrentUser().id || message.author.system);\n\n    return useMessageMenu({\n        navId: \"message-actions\",\n        ariaLabel: getIntlMessage(\"MESSAGE_UTILITIES_A11Y_LABEL\"),\n\n        message,\n        channel,\n        canReport,\n        onHeightUpdate,\n        onClose: () => ContextMenuApi.closeContextMenu(),\n\n        textSelection: \"\",\n        favoriteableType: null,\n        favoriteableId: null,\n        favoriteableName: null,\n        itemHref: void 0,\n        itemSrc: void 0,\n        itemSafeSrc: void 0,\n        itemTextContent: void 0,\n\n        isFullSearchContextMenu: true\n    });\n}\n\ninterface MessageActionsProps {\n    message: Message;\n    isFullSearchContextMenu?: boolean;\n}\n\nconst contextMenuPatch: NavContextMenuPatchCallback = (children, props: MessageActionsProps) => {\n    if (props?.isFullSearchContextMenu == null) return;\n\n    const group = findGroupChildrenByChildId(\"devmode-copy-id\", children, true);\n    group?.push(\n        CopyIdMenuItem({ id: props.message.author.id, label: getIntlMessage(\"COPY_ID_AUTHOR\") })\n    );\n};\n\nmigratePluginSettings(\"FullSearchContext\", \"SearchReply\");\nexport default definePlugin({\n    name: \"FullSearchContext\",\n    description: \"Makes the message context menu in message search results have all options you'd expect\",\n    authors: [Devs.Ven, Devs.Aria],\n\n    patches: [{\n        find: \"onClick:this.handleMessageClick,\",\n        replacement: {\n            match: /this(?=\\.handleContextMenu\\(\\i,\\i\\))/,\n            replace: \"$self\"\n        }\n    }],\n\n    handleContextMenu(event: React.MouseEvent, message: Message) {\n        const channel = ChannelStore.getChannel(message.channel_id);\n        if (!channel) return;\n\n        event.stopPropagation();\n\n        ContextMenuApi.openContextMenu(event, contextMenuProps =>\n            <MessageMenu\n                message={message}\n                channel={channel}\n                onHeightUpdate={contextMenuProps.onHeightUpdate}\n            />\n        );\n    },\n\n    contextMenus: {\n        \"message-actions\": contextMenuPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/fullUserInChatbox/README.md",
    "content": "# Full User In Chatbox\n\nAdds the full user mention to the textbox\n\nAdds the avatar if you have mentioned avatars enabled\n\nProvides the full context menu to make it easy to access the users profile, and other common actions\n\nhttps://github.com/user-attachments/assets/cd9edb33-99c8-4c8d-b669-8cddd05f4b45\n"
  },
  {
    "path": "src/plugins/fullUserInChatbox/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { UserStore, useStateFromStores } from \"@webpack/common\";\nimport { ReactNode } from \"react\";\n\nconst UserMentionComponent = findComponentByCodeLazy(\".USER_MENTION)\");\n\ninterface UserMentionComponentProps {\n    id: string;\n    channelId: string;\n    guildId: string;\n    originalComponent: () => ReactNode;\n}\n\nexport default definePlugin({\n    name: \"FullUserInChatbox\",\n    description: \"Makes the user mention in the chatbox have more functionalities, like left/right clicking\",\n    authors: [Devs.sadan],\n\n    patches: [\n        {\n            // Same find as RoleColorEverywhere chatbox mentions\n            find: '\"text\":\"locked\"',\n            replacement: {\n                match: /(hidePersonalInformation\\).+?)(if\\(null!=\\i\\){.+?return \\i)(?=})/,\n                replace: \"$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});\"\n            }\n        }\n    ],\n\n    UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => {\n        const user = useStateFromStores([UserStore], () => UserStore.getUser(props.id));\n        if (user == null) {\n            return props.originalComponent();\n        }\n\n        return <UserMentionComponent\n            // This seems to be constant\n            className=\"mention\"\n            userId={props.id}\n            channelId={props.channelId}\n        />;\n    }, {\n        fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()\n    })\n});\n"
  },
  {
    "path": "src/plugins/gameActivityToggle/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport VencordToolboxPlugin from \"@plugins/vencordToolbox\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { Menu } from \"@webpack/common\";\n\nimport managedStyle from \"./style.css?managed\";\n\nconst Button = findComponentByCodeLazy(\".GREEN,positionKeyStemOverride:\");\n\nconst ShowCurrentGame = getUserSettingLazy<boolean>(\"status\", \"showCurrentGame\")!;\n\nconst settings = definePluginSettings({\n    oldIcon: {\n        type: OptionType.BOOLEAN,\n        description: \"Use the old icon style before Discord icon redesign\",\n        default: false\n    },\n    location: {\n        type: OptionType.SELECT,\n        description: \"Where to show the game activity toggle button\",\n        options: [\n            { label: \"Next to Mute/Deafen\", value: \"PANEL\", default: true },\n            { label: \"Vencord Toolbox\", value: \"TOOLBOX\" }\n        ],\n        get hidden() {\n            return !isPluginEnabled(VencordToolboxPlugin.name);\n        }\n    }\n});\n\nfunction Icon() {\n    const { oldIcon } = settings.use([\"oldIcon\"]);\n    const showCurrentGame = ShowCurrentGame.useSetting();\n\n\n    const redLinePath = !oldIcon\n        ? \"M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z\"\n        : \"M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z\";\n\n    const maskBlackPath = !oldIcon\n        ? \"M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z\"\n        : \"M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z\";\n\n    return (\n        <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\">\n            <path\n                fill={!showCurrentGame && !oldIcon ? \"var(--status-danger)\" : \"currentColor\"}\n                mask={!showCurrentGame ? \"url(#gameActivityMask)\" : void 0}\n                d=\"M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z\"\n            />\n            {!showCurrentGame && <>\n                <path fill=\"var(--status-danger)\" d={redLinePath} />\n                <mask id=\"gameActivityMask\">\n                    <rect fill=\"white\" x=\"0\" y=\"0\" width=\"24\" height=\"24\" />\n                    <path fill=\"black\" d={maskBlackPath} />\n                </mask>\n            </>}\n        </svg>\n    );\n}\n\nfunction GameActivityToggleButton(props: { nameplate?: any; }) {\n    const { location } = settings.use([\"location\"]);\n    const showCurrentGame = ShowCurrentGame.useSetting();\n\n    if (location !== \"PANEL\" && isPluginEnabled(VencordToolboxPlugin.name)) return null;\n\n    return (\n        <Button\n            tooltipText={showCurrentGame ? \"Disable Game Activity\" : \"Enable Game Activity\"}\n            icon={Icon}\n            role=\"switch\"\n            aria-checked={!showCurrentGame}\n            redGlow={!showCurrentGame}\n            plated={props?.nameplate != null}\n            onClick={() => ShowCurrentGame.updateSetting(old => !old)}\n        />\n    );\n}\n\nexport default definePlugin({\n    name: \"GameActivityToggle\",\n    description: \"Adds a button next to the mic and deafen button to toggle game activity.\",\n    authors: [Devs.Nuckyz, Devs.RuukuLada],\n    dependencies: [\"UserSettingsAPI\"],\n    settings,\n\n    managedStyle,\n\n    patches: [\n        {\n            find: \".DISPLAY_NAME_STYLES_COACHMARK)\",\n            replacement: {\n                match: /children:\\[(?=.{0,25}?accountContainerRef)/,\n                replace: \"children:[$self.GameActivityToggleButton(arguments[0]),\"\n            }\n        }\n    ],\n\n    toolboxActions() {\n        const { location } = settings.use([\"location\"]);\n        const showCurrentGame = ShowCurrentGame.useSetting();\n\n        if (location !== \"TOOLBOX\") return null;\n\n        return (\n            <Menu.MenuCheckboxItem\n                id=\"game-activity-toggle-toolbox\"\n                label=\"Enable Game Activity\"\n                checked={showCurrentGame}\n                action={() => ShowCurrentGame.updateSetting(old => !old)}\n            />\n        );\n    },\n\n    GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/gameActivityToggle/style.css",
    "content": "[class*=\"panels\"] [class*=\"avatarWrapper\"] {\n    min-width: 0;\n}"
  },
  {
    "path": "src/plugins/gifPaste/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport { insertTextIntoChatInputBox } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { ExpressionPickerStore } from \"@webpack/common\";\n\nexport default definePlugin({\n    name: \"GifPaste\",\n    description: \"Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it\",\n    authors: [Devs.Ven],\n\n    patches: [{\n        find: \"handleSelectGIF=\",\n        replacement: {\n            match: /handleSelectGIF=(\\i)=>\\{/,\n            replace: \"$&if (!this.props.className) return $self.handleSelect($1);\"\n        }\n    }],\n\n    handleSelect(gif?: { url: string; }) {\n        if (gif) {\n            insertTextIntoChatInputBox(gif.url + \" \");\n            ExpressionPickerStore.closeExpressionPicker();\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/greetStickerPicker/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Channel, Message } from \"@vencord/discord-types\";\nimport { findLazy } from \"@webpack\";\nimport { ContextMenuApi, FluxDispatcher, Menu, MessageActions } from \"@webpack/common\";\n\nenum GreetMode {\n    Greet = \"Greet\",\n    NormalMessage = \"Message\"\n}\n\nconst settings = definePluginSettings({\n    greetMode: {\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Greet (you can only greet 3 times)\", value: GreetMode.Greet, default: true },\n            { label: \"Normal Message (you can greet spam)\", value: GreetMode.NormalMessage }\n        ],\n        description: \"Choose the greet mode\"\n    }\n}).withPrivateSettings<{\n    multiGreetChoices?: string[];\n    unholyMultiGreetEnabled?: boolean;\n}>();\n\nconst WELCOME_STICKERS = findLazy(m => Array.isArray(m) && m[0]?.name === \"Wave\");\n\nfunction greet(channel: Channel, message: Message, stickers: string[]) {\n    const options = MessageActions.getSendMessageOptionsForReply({\n        channel,\n        message,\n        shouldMention: true,\n        showMentionToggle: true\n    });\n\n    if (settings.store.greetMode === GreetMode.NormalMessage || stickers.length > 1) {\n        options.stickerIds = stickers;\n        const msg = {\n            content: \"\",\n            tts: false,\n            invalidEmojis: [],\n            validNonShortcutEmojis: []\n        };\n\n        MessageActions._sendMessage(channel.id, msg, options);\n    } else {\n        MessageActions.sendGreetMessage(channel.id, stickers[0], options);\n    }\n}\n\n\nfunction GreetMenu({ channel, message }: { message: Message, channel: Channel; }) {\n    const s = settings.use([\"greetMode\", \"multiGreetChoices\"]);\n    const { greetMode, multiGreetChoices = [] } = s;\n\n    return (\n        <Menu.Menu\n            navId=\"greet-sticker-picker\"\n            onClose={() => FluxDispatcher.dispatch({ type: \"CONTEXT_MENU_CLOSE\" })}\n            aria-label=\"Greet Sticker Picker\"\n        >\n            <Menu.MenuGroup\n                label=\"Greet Mode\"\n            >\n                {Object.values(GreetMode).map(mode => (\n                    <Menu.MenuRadioItem\n                        key={mode}\n                        group=\"greet-mode\"\n                        id={\"greet-mode-\" + mode}\n                        label={mode}\n                        checked={mode === greetMode}\n                        action={() => s.greetMode = mode}\n                    />\n                ))}\n            </Menu.MenuGroup>\n\n            <Menu.MenuSeparator />\n\n            <Menu.MenuGroup\n                label=\"Greet Stickers\"\n            >\n                {WELCOME_STICKERS.map(sticker => (\n                    <Menu.MenuItem\n                        key={sticker.id}\n                        id={\"greet-\" + sticker.id}\n                        label={sticker.description.split(\" \")[0]}\n                        action={() => greet(channel, message, [sticker.id])}\n                    />\n                ))}\n            </Menu.MenuGroup>\n\n            {!settings.store.unholyMultiGreetEnabled ? null : (\n                <>\n                    <Menu.MenuSeparator />\n\n                    <Menu.MenuItem\n                        label=\"Unholy Multi-Greet\"\n                        id=\"unholy-multi-greet\"\n                    >\n                        {WELCOME_STICKERS.map(sticker => {\n                            const checked = multiGreetChoices.some(s => s === sticker.id);\n\n                            return (\n                                <Menu.MenuCheckboxItem\n                                    key={sticker.id}\n                                    id={\"multi-greet-\" + sticker.id}\n                                    label={sticker.description.split(\" \")[0]}\n                                    checked={checked}\n                                    disabled={!checked && multiGreetChoices.length >= 3}\n                                    action={() => {\n                                        s.multiGreetChoices = checked\n                                            ? multiGreetChoices.filter(s => s !== sticker.id)\n                                            : [...multiGreetChoices, sticker.id];\n                                    }}\n                                />\n                            );\n                        })}\n\n                        <Menu.MenuSeparator />\n                        <Menu.MenuItem\n                            id=\"multi-greet-submit\"\n                            label=\"Send Greets\"\n                            action={() => greet(channel, message, multiGreetChoices!)}\n                            disabled={multiGreetChoices.length === 0}\n                        />\n\n                    </Menu.MenuItem>\n                </>\n            )}\n        </Menu.Menu>\n    );\n}\n\nexport default definePlugin({\n    name: \"GreetStickerPicker\",\n    description: \"Allows you to use any greet sticker instead of only the random one by right-clicking the 'Wave to say hi!' button\",\n    authors: [Devs.Ven],\n\n    settings,\n\n    patches: [\n        {\n            find: \"#{intl::WELCOME_CTA_LABEL}\",\n            replacement: {\n                match: /className:\\i\\.\\i,(?=.{0,40}?\"sticker\")(?<={channel:\\i,message:\\i}=(\\i).+?)/,\n                replace: \"$&onContextMenu:(vcEvent)=>$self.pickSticker(vcEvent, $1),\"\n            }\n        }\n    ],\n\n    pickSticker(\n        event: React.UIEvent,\n        props: {\n            channel: Channel,\n            message: Message;\n        }\n    ) {\n        if (!(props.message as any).deleted)\n            ContextMenuApi.openContextMenu(event, () => <GreetMenu {...props} />);\n    }\n});\n"
  },
  {
    "path": "src/plugins/hideAttachments/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { get, set } from \"@api/DataStore\";\nimport { updateMessage } from \"@api/MessageUpdater\";\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { ImageInvisible, ImageVisible } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport { classes } from \"@utils/misc\";\nimport definePlugin from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { ChannelStore } from \"@webpack/common\";\n\nconst KEY = \"HideAttachments_HiddenIds\";\n\nlet hiddenMessages = new Set<string>();\n\nasync function getHiddenMessages() {\n    hiddenMessages = await get(KEY) ?? new Set();\n    return hiddenMessages;\n}\n\nconst saveHiddenMessages = (ids: Set<string>) => set(KEY, ids);\n\nmigratePluginSettings(\"HideMedia\", \"HideAttachments\");\n\nconst hasMedia = (msg: Message) => msg.attachments.length > 0 || msg.embeds.length > 0 || msg.stickerItems.length > 0 || msg.components.length > 0;\n\nasync function toggleHide(channelId: string, messageId: string) {\n    const ids = await getHiddenMessages();\n    if (!ids.delete(messageId))\n        ids.add(messageId);\n\n    await saveHiddenMessages(ids);\n    updateMessage(channelId, messageId);\n}\n\nexport default definePlugin({\n    name: \"HideMedia\",\n    description: \"Hide attachments and embeds for individual messages via hover button\",\n    authors: [Devs.Ven],\n    dependencies: [\"MessageUpdaterAPI\"],\n\n    patches: [{\n        find: \"this.renderAttachments(\",\n        replacement: {\n            match: /(?<=\\i=)this\\.render(?:Attachments|Embeds|StickersAccessories|ComponentAccessories)\\((\\i)\\)/g,\n            replace: \"$self.shouldHide($1?.id)?null:$&\"\n        }\n    }],\n\n    messagePopoverButton: {\n        icon: ImageInvisible,\n        render(msg) {\n            if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null;\n\n            const isHidden = hiddenMessages.has(msg.id);\n\n            return {\n                label: isHidden ? \"Show Media\" : \"Hide Media\",\n                icon: isHidden ? ImageVisible : ImageInvisible,\n                message: msg,\n                channel: ChannelStore.getChannel(msg.channel_id),\n                onClick: () => toggleHide(msg.channel_id, msg.id)\n            };\n        },\n    },\n\n    renderMessageAccessory({ message }) {\n        if (!this.shouldHide(message.id)) return null;\n\n        return (\n            <span className={classes(\"vc-hideAttachments-accessory\", !message.content && \"vc-hideAttachments-no-content\")}>\n                Media Hidden\n            </span>\n        );\n    },\n\n    async start() {\n        await getHiddenMessages();\n    },\n\n    stop() {\n        hiddenMessages.clear();\n    },\n\n    shouldHide(messageId: string) {\n        return hiddenMessages.has(messageId);\n    },\n});\n"
  },
  {
    "path": "src/plugins/hideAttachments/styles.css",
    "content": ".vc-hideAttachments-accessory {\n    color: var(--text-muted);\n    margin-top: 0.5em;\n    font-style: italic;\n    font-weight: 400;\n}\n\n.vc-hideAttachments-no-content {\n    margin-top: 0;\n}\n"
  },
  {
    "path": "src/plugins/iLoveSpam/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"iLoveSpam\",\n    description: \"Do not hide messages from 'likely spammers'\",\n    authors: [Devs.botato, Devs.Nyako],\n    patches: [\n        {\n            find: \"hasFlag:{writable\",\n            replacement: {\n                match: /if\\((\\i)<=(?:0x40000000|(?:1<<30|1073741824))\\)return/,\n                replace: \"if($1===(1<<20))return false;$&\",\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/ignoreActivities/README.md",
    "content": "# IgnoreActivities\n\nIgnore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings.\n\n![](https://github.com/user-attachments/assets/f0c19060-0ecf-4f1c-8165-a5aa40143c82)\n\n![](https://github.com/user-attachments/assets/73c3fa7a-5b90-41ee-a4d6-91fa76458b74)\n\n![](https://github.com/user-attachments/assets/1ab3fe73-3911-48d1-8a08-e976af614b41)\n\nThe activity stays showing as a detected game even if ignored, differently from the stock Toggle Detection button from Discord:\n\n![](https://github.com/user-attachments/assets/08ea60c3-3a31-42de-ae4c-7535fbf1b45a)\n"
  },
  {
    "path": "src/plugins/ignoreActivities/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings, Settings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Devs } from \"@utils/constants\";\nimport { Margins } from \"@utils/margins\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Button, Forms, RunningGameStore, showToast, TextArea, Toasts, Tooltip, useEffect, useState } from \"@webpack/common\";\n\nconst enum ActivitiesTypes {\n    Game,\n    Embedded\n}\n\ninterface IgnoredActivity {\n    id: string;\n    name: string;\n    type: ActivitiesTypes;\n}\n\nconst enum FilterMode {\n    Whitelist,\n    Blacklist\n}\n\nconst ShowCurrentGame = getUserSettingLazy(\"status\", \"showCurrentGame\")!;\n\nfunction ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {\n    return (\n        <Tooltip text={tooltipText}>\n            {tooltipProps => (\n                <button\n                    {...tooltipProps}\n                    onClick={e => handleActivityToggle(e, activity)}\n                    style={{ all: \"unset\", cursor: \"pointer\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\" }}\n                >\n                    <svg\n                        width=\"24\"\n                        height=\"24\"\n                        viewBox=\"0 -960 960 960\"\n                    >\n                        <path fill={fill} d={path} />\n                    </svg>\n                </button>\n            )}\n        </Tooltip>\n    );\n}\n\nconst ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, \"Disable activity\", \"M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z\", fill);\nconst ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, \"Enable activity\", \"m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z\", fill);\n\nfunction ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {\n    const s = settings.use([\"ignoredActivities\"]);\n    const { ignoredActivities } = s;\n\n    if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, \"var(--status-danger)\");\n    return ToggleIconOn(activity, isPlaying ? \"var(--green-300)\" : \"var(--interactive-icon-default)\");\n}\n\nfunction handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {\n    e.stopPropagation();\n\n    const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);\n    if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);\n    else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);\n}\n\nfunction recalculateActivities() {\n    ShowCurrentGame.updateSetting(old => old);\n}\n\nfunction ImportCustomRPCComponent() {\n    return (\n        <Flex flexDirection=\"column\">\n            <Forms.FormText>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>\n            <div>\n                <Button\n                    onClick={() => {\n                        const id = Settings.plugins.CustomRPC?.appID as string | undefined;\n                        if (!id) {\n                            return showToast(\"CustomRPC application ID is not set.\", Toasts.Type.FAILURE);\n                        }\n\n                        const isAlreadyAdded = idsListPushID?.(id);\n                        if (isAlreadyAdded) {\n                            showToast(\"CustomRPC application ID is already added.\", Toasts.Type.FAILURE);\n                        }\n                    }}\n                >\n                    Import CustomRPC ID\n                </Button>\n            </div>\n        </Flex>\n    );\n}\n\nlet idsListPushID: ((id: string) => boolean) | null = null;\n\nfunction IdsListComponent(props: { setValue: (value: string) => void; }) {\n    const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? \"\");\n\n    idsListPushID = (id: string) => {\n        const currentIds = new Set(idsList.split(\",\").map(id => id.trim()).filter(Boolean));\n\n        const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);\n\n        const ids = Array.from(currentIds).join(\", \");\n        setIdsList(ids);\n        props.setValue(ids);\n\n        return isAlreadyAdded;\n    };\n\n    useEffect(() => () => {\n        idsListPushID = null;\n    }, []);\n\n    function handleChange(newValue: string) {\n        setIdsList(newValue);\n        props.setValue(newValue);\n    }\n\n    return (\n        <section>\n            <Forms.FormTitle tag=\"h3\">Filter List</Forms.FormTitle>\n            <Forms.FormText className={Margins.bottom8}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>\n            <TextArea\n                type=\"text\"\n                value={idsList}\n                onChange={handleChange}\n                placeholder=\"235834946571337729, 343383572805058560\"\n            />\n        </section>\n    );\n}\n\nconst settings = definePluginSettings({\n    importCustomRPC: {\n        type: OptionType.COMPONENT,\n        component: ImportCustomRPCComponent\n    },\n    listMode: {\n        type: OptionType.SELECT,\n        description: \"Change the mode of the filter list\",\n        options: [\n            {\n                label: \"Whitelist\",\n                value: FilterMode.Whitelist,\n                default: true\n            },\n            {\n                label: \"Blacklist\",\n                value: FilterMode.Blacklist,\n            }\n        ],\n        onChange: recalculateActivities\n    },\n    idsList: {\n        type: OptionType.COMPONENT,\n        default: \"\",\n        onChange(newValue: string) {\n            const ids = new Set(newValue.split(\",\").map(id => id.trim()).filter(Boolean));\n            settings.store.idsList = Array.from(ids).join(\", \");\n            recalculateActivities();\n        },\n        component: props => <IdsListComponent setValue={props.setValue} />\n    },\n    ignorePlaying: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore all playing activities (These are usually game and RPC activities)\",\n        default: false,\n        onChange: recalculateActivities\n    },\n    ignoreStreaming: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore all streaming activities\",\n        default: false,\n        onChange: recalculateActivities\n    },\n    ignoreListening: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore all listening activities (These are usually spotify activities)\",\n        default: false,\n        onChange: recalculateActivities\n    },\n    ignoreWatching: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore all watching activities\",\n        default: false,\n        onChange: recalculateActivities\n    },\n    ignoreCompeting: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore all competing activities (These are normally special game activities)\",\n        default: false,\n        onChange: recalculateActivities\n    },\n    ignoredActivities: {\n        type: OptionType.CUSTOM,\n        default: [] as IgnoredActivity[],\n        onChange: recalculateActivities\n    }\n});\n\nfunction isActivityTypeIgnored(type: number, id?: string) {\n    if (id && settings.store.idsList.includes(id)) {\n        return settings.store.listMode === FilterMode.Blacklist;\n    }\n\n    switch (type) {\n        case 0: return settings.store.ignorePlaying;\n        case 1: return settings.store.ignoreStreaming;\n        case 2: return settings.store.ignoreListening;\n        case 3: return settings.store.ignoreWatching;\n        case 5: return settings.store.ignoreCompeting;\n    }\n\n    return false;\n}\n\nexport default definePlugin({\n    name: \"IgnoreActivities\",\n    authors: [Devs.Nuckyz, Devs.Kylie],\n    description: \"Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below\",\n    dependencies: [\"UserSettingsAPI\"],\n\n    settings,\n\n    patches: [\n        {\n            find: '\"LocalActivityStore\"',\n            replacement: [\n                {\n                    match: /\\.LISTENING.+?(?=!?\\i\\(\\)\\(\\i,\\i\\))(?<=(\\i)\\.push.+?)/,\n                    replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`\n                }\n            ]\n        },\n        {\n            find: '\"ActivityTrackingStore\"',\n            replacement: {\n                match: /getVisibleRunningGames\\(\\).+?;(?=for)(?<=(\\i)=\\i\\.\\i\\.getVisibleRunningGames.+?)/,\n                replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));`\n            }\n        },\n        {\n            find: \"#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}\",\n            replacement: {\n                match: /(\\i)&&!\\i\\|\\|\\i\\?null(?<=(\\i)\\.verified&&.+?)/,\n                replace: \"$self.renderToggleGameActivityButton($2,$1),$&\"\n            }\n        },\n\n        // Activities from the apps launcher in the bottom right of the chat bar\n        {\n            find: \"#{intl::EMBEDDED_ACTIVITIES_DEVELOPER_ACTIVITY}\",\n            replacement: {\n                match: /lineClamp:1.{0,50}?(?=!\\i&&\\i\\?.+?application:(\\i))/,\n                replace: \"$&$self.renderToggleActivityButton($1),\"\n            }\n        }\n    ],\n\n    async start() {\n        if (settings.store.ignoredActivities.length !== 0) {\n            const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];\n\n            for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) {\n                if (ignoredActivity.type !== ActivitiesTypes.Game) continue;\n\n                if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {\n                    settings.store.ignoredActivities.splice(index, 1);\n                }\n            }\n        }\n    },\n\n    isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {\n        if (isActivityTypeIgnored(props.type, props.application_id)) return false;\n\n        if (props.application_id != null) {\n            return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));\n        } else {\n            const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;\n            if (exePath) {\n                return !settings.store.ignoredActivities.some(activity => activity.id === exePath);\n            }\n        }\n\n        return true;\n    },\n\n    renderToggleGameActivityButton(props: { id?: string; name: string, exePath: string; }, nowPlaying: boolean) {\n        return (\n            <ErrorBoundary noop>\n                <div style={{ marginLeft: 12, zIndex: 0 }}>\n                    {ToggleActivityComponent({ id: props.id ?? props.exePath, name: props.name, type: ActivitiesTypes.Game }, nowPlaying)}\n                </div>\n            </ErrorBoundary>\n        );\n    },\n\n    renderToggleActivityButton(props: { id: string; name: string; }) {\n        return (\n            <ErrorBoundary noop>\n                {ToggleActivityComponent({ id: props.id, name: props.name, type: ActivitiesTypes.Embedded })}\n            </ErrorBoundary>\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/imageFilename/README.md",
    "content": "# ImageFilename\n\nDisplay the file name of images & GIFs as a tooltip when hovering over them\n\n![](https://github.com/user-attachments/assets/44583f17-506f-4913-b85c-513eee77b645)\n"
  },
  {
    "path": "src/plugins/imageFilename/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst ImageExtensionRe = /\\.(png|jpg|jpeg|gif|webp|avif)$/i;\nconst GifHostRegex = /^(.+?\\.)?(tenor|giphy|imgur)\\.com$/i;\n\nconst settings = definePluginSettings({\n    showFullUrl: {\n        description: \"Show the full URL of the image instead of just the file name. Always enabled for GIFs because they usually have no meaningful file name\",\n        type: OptionType.BOOLEAN,\n        default: false,\n    },\n});\n\nexport default definePlugin({\n    name: \"ImageFilename\",\n    authors: [Devs.Ven],\n    description: \"Display the file name of images & GIFs as a tooltip when hovering over them\",\n    settings,\n\n    patches: [\n        {\n            find: \".RESPONSIVE?\",\n            replacement: {\n                match: /(?=\"data-role\":\"img\",\"data-safe-src\":)(?<=href:(\\i).+?)/,\n                replace: \"title:$self.getTitle($1),\"\n            }\n        },\n    ],\n\n    getTitle(src: string) {\n        try {\n            const url = new URL(src);\n            const isGif = GifHostRegex.test(url.hostname);\n            if (!isGif && !ImageExtensionRe.test(url.pathname)) return undefined;\n\n            return isGif || settings.store.showFullUrl\n                ? src\n                : url.pathname.split(\"/\").pop();\n        } catch {\n            return undefined;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/imageLink/README.md",
    "content": "# ImageLink\n\nIf a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless.\n"
  },
  {
    "path": "src/plugins/imageLink/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ImageLink\",\n    description: \"Never hide image links in messages, even if it's the only content\",\n    authors: [Devs.Kyuuhachi, Devs.Sqaaakoi],\n\n    patches: [\n        {\n            find: \"unknownUserMentionPlaceholder:\",\n            replacement: {\n                // SimpleEmbedTypes.has(embed.type) && isEmbedInline(embed)\n                match: /\\i\\.has\\(\\i\\.type\\)&&\\(0,\\i\\.\\i\\)\\(\\i\\)/,\n                replace: \"false\",\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/imageZoom/README.md",
    "content": "# ImageZoom\n\nLets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size\n\n![the plugin in action](https://github.com/Vendicated/Vencord/assets/45497981/408cd77d-c5f4-40bc-8de2-f977a31b3e5f)\n![the context menu options offered by the plugin](https://github.com/Vendicated/Vencord/assets/45497981/3bede636-f1ce-493f-af46-788b920cb81c)\n"
  },
  {
    "path": "src/plugins/imageZoom/components/Magnifier.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { settings } from \"@plugins/imageZoom\";\nimport { ELEMENT_ID } from \"@plugins/imageZoom/constants\";\nimport { waitFor } from \"@plugins/imageZoom/utils/waitFor\";\nimport { classNameFactory } from \"@utils/css\";\nimport { FluxDispatcher, useLayoutEffect, useMemo, useRef, useState } from \"@webpack/common\";\n\ninterface Vec2 {\n    x: number,\n    y: number;\n}\n\nexport interface MagnifierProps {\n    zoom: number;\n    size: number,\n    instance: any;\n}\n\nconst cl = classNameFactory(\"vc-imgzoom-\");\n\nexport const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: initialSize, zoom: initalZoom }) => {\n    const [ready, setReady] = useState(false);\n\n    const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });\n    const [imagePosition, setImagePosition] = useState<Vec2>({ x: 0, y: 0 });\n    const [opacity, setOpacity] = useState(0);\n\n    const isShiftDown = useRef(false);\n\n    const zoom = useRef(initalZoom);\n    const size = useRef(initialSize);\n\n    const element = useRef<HTMLDivElement | null>(null);\n    const currentVideoElementRef = useRef<HTMLVideoElement | null>(null);\n    const originalVideoElementRef = useRef<HTMLVideoElement | null>(null);\n    const imageRef = useRef<HTMLImageElement | null>(null);\n\n    // since we accessing document im gonna use useLayoutEffect\n    useLayoutEffect(() => {\n        const onKeyDown = (e: KeyboardEvent) => {\n            if (e.key === \"Shift\") {\n                isShiftDown.current = true;\n            }\n        };\n        const onKeyUp = (e: KeyboardEvent) => {\n            if (e.key === \"Shift\") {\n                isShiftDown.current = false;\n            }\n        };\n        const syncVideos = () => {\n            if (currentVideoElementRef.current && originalVideoElementRef.current)\n                currentVideoElementRef.current.currentTime = originalVideoElementRef.current.currentTime;\n        };\n\n        const updateMousePosition = (e: MouseEvent) => {\n            if (!element.current) return;\n\n            if (instance.state.mouseOver && instance.state.mouseDown) {\n                const offset = size.current / 2;\n                const pos = { x: e.pageX, y: e.pageY };\n                const x = -((pos.x - element.current.getBoundingClientRect().left) * zoom.current - offset);\n                const y = -((pos.y - element.current.getBoundingClientRect().top) * zoom.current - offset);\n                setLensPosition({ x: e.x - offset, y: e.y - offset });\n                setImagePosition({ x, y });\n                setOpacity(1);\n            } else {\n                setOpacity(0);\n            }\n\n        };\n\n        const onMouseDown = (e: MouseEvent) => {\n            if (instance.state.mouseOver && e.button === 0 /* left click */) {\n                zoom.current = settings.store.zoom;\n                size.current = settings.store.size;\n\n                // close context menu if open\n                if (document.getElementById(\"image-context\")) {\n                    FluxDispatcher.dispatch({ type: \"CONTEXT_MENU_CLOSE\" });\n                }\n\n                updateMousePosition(e);\n                setOpacity(1);\n            }\n        };\n\n        const onMouseUp = () => {\n            setOpacity(0);\n        };\n\n        const onWheel = async (e: WheelEvent) => {\n            if (instance.state.mouseOver && instance.state.mouseDown && !isShiftDown.current) {\n                const val = zoom.current + ((e.deltaY / 100) * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;\n                zoom.current = val <= 1 ? 1 : val;\n                if (settings.store.saveZoomValues) settings.store.zoom = zoom.current;\n                updateMousePosition(e);\n            }\n            if (instance.state.mouseOver && instance.state.mouseDown && isShiftDown.current) {\n                const val = size.current + (e.deltaY * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;\n                size.current = val <= 50 ? 50 : val;\n                if (settings.store.saveZoomValues) settings.store.size = size.current;\n                updateMousePosition(e);\n            }\n        };\n\n        waitFor(() => instance.state.readyState === \"READY\", () => {\n            const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement;\n            element.current = elem;\n            elem.querySelector(\"img,video\")?.setAttribute(\"draggable\", \"false\");\n            if (instance.props.animated) {\n                originalVideoElementRef.current = elem!.querySelector(\"video\")!;\n                originalVideoElementRef.current.addEventListener(\"timeupdate\", syncVideos);\n            }\n\n            setReady(true);\n        });\n\n        document.addEventListener(\"keydown\", onKeyDown);\n        document.addEventListener(\"keyup\", onKeyUp);\n        document.addEventListener(\"mousemove\", updateMousePosition);\n        document.addEventListener(\"mousedown\", onMouseDown);\n        document.addEventListener(\"mouseup\", onMouseUp);\n        document.addEventListener(\"wheel\", onWheel);\n\n        return () => {\n            document.removeEventListener(\"keydown\", onKeyDown);\n            document.removeEventListener(\"keyup\", onKeyUp);\n            document.removeEventListener(\"mousemove\", updateMousePosition);\n            document.removeEventListener(\"mousedown\", onMouseDown);\n            document.removeEventListener(\"mouseup\", onMouseUp);\n            document.removeEventListener(\"wheel\", onWheel);\n        };\n    }, []);\n\n    const imageSrc = useMemo(() => {\n        try {\n            const imageUrl = new URL(instance.props.src);\n            if (imageUrl.pathname.startsWith(\"/attachments/\"))\n                imageUrl.hostname = \"cdn.discordapp.com\";\n\n            imageUrl.searchParams.set(\"animated\", \"true\");\n            return imageUrl.toString();\n        } catch {\n            return instance.props.src;\n        }\n    }, [instance.props.src]);\n\n    if (!ready) return null;\n\n    const box = element.current?.getBoundingClientRect();\n\n    if (!box) return null;\n\n    return (\n        <div\n            className={cl(\"lens\", { \"nearest-neighbor\": settings.store.nearestNeighbour, square: settings.store.square })}\n            style={{\n                opacity,\n                width: size.current + \"px\",\n                height: size.current + \"px\",\n                transform: `translate(${lensPosition.x}px, ${lensPosition.y}px)`,\n            }}\n        >\n            {instance.props.animated ?\n                (\n                    <video\n                        ref={currentVideoElementRef}\n                        style={{\n                            position: \"absolute\",\n                            left: `${imagePosition.x}px`,\n                            top: `${imagePosition.y}px`\n                        }}\n                        width={`${box.width * zoom.current}px`}\n                        height={`${box.height * zoom.current}px`}\n                        poster={instance.props.src}\n                        src={originalVideoElementRef.current?.src ?? instance.props.src}\n                        autoPlay\n                        loop\n                        muted\n                    />\n                ) : (\n                    <img\n                        className={cl(\"image\")}\n                        ref={imageRef}\n                        style={{\n                            position: \"absolute\",\n                            transform: `translate(${imagePosition.x}px, ${imagePosition.y}px)`\n                        }}\n                        width={`${box.width * zoom.current}px`}\n                        height={`${box.height * zoom.current}px`}\n                        src={imageSrc}\n                        alt=\"\"\n                    />\n                )}\n        </div>\n    );\n}, { noop: true });\n"
  },
  {
    "path": "src/plugins/imageZoom/constants.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const ELEMENT_ID = \"vc-imgzoom-magnify-modal\";\n"
  },
  {
    "path": "src/plugins/imageZoom/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { debounce } from \"@shared/debounce\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { createRoot, Menu } from \"@webpack/common\";\nimport { JSX } from \"react\";\nimport type { Root } from \"react-dom/client\";\n\nimport { Magnifier, MagnifierProps } from \"./components/Magnifier\";\nimport { ELEMENT_ID } from \"./constants\";\nimport managedStyle from \"./styles.css?managed\";\n\nexport const settings = definePluginSettings({\n    saveZoomValues: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to save zoom and lens size values\",\n        default: true,\n    },\n\n    invertScroll: {\n        type: OptionType.BOOLEAN,\n        description: \"Invert scroll\",\n        default: true,\n    },\n\n    nearestNeighbour: {\n        type: OptionType.BOOLEAN,\n        description: \"Use Nearest Neighbour Interpolation when scaling images\",\n        default: false,\n    },\n\n    square: {\n        type: OptionType.BOOLEAN,\n        description: \"Make the lens square\",\n        default: false,\n    },\n\n    zoom: {\n        description: \"Zoom of the lens\",\n        type: OptionType.SLIDER,\n        markers: [1, 5, 10, 20, 30, 40, 50],\n        default: 2,\n        stickToMarkers: false,\n    },\n    size: {\n        description: \"Radius / Size of the lens\",\n        type: OptionType.SLIDER,\n        markers: [50, 100, 250, 500, 750, 1000],\n        default: 100,\n        stickToMarkers: false,\n    },\n\n    zoomSpeed: {\n        description: \"How fast the zoom / lens size changes\",\n        type: OptionType.SLIDER,\n        markers: [0.1, 0.5, 1, 2, 3, 4, 5],\n        default: 0.5,\n        stickToMarkers: false,\n    },\n});\n\n\nconst imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {\n    // Discord re-uses the image context menu for links to for the copy and open buttons\n    if (\"href\" in props) return;\n    // emojis in user statuses\n    if (props.target?.classList?.contains(\"emoji\")) return;\n\n    const { square, nearestNeighbour } = settings.use([\"square\", \"nearestNeighbour\"]);\n\n    children.push(\n        <Menu.MenuGroup id=\"image-zoom\">\n            <Menu.MenuCheckboxItem\n                id=\"vc-square\"\n                label=\"Square Lens\"\n                checked={square}\n                action={() => {\n                    settings.store.square = !square;\n                }}\n            />\n            <Menu.MenuCheckboxItem\n                id=\"vc-nearest-neighbour\"\n                label=\"Nearest Neighbour\"\n                checked={nearestNeighbour}\n                action={() => {\n                    settings.store.nearestNeighbour = !nearestNeighbour;\n                }}\n            />\n            <Menu.MenuControlItem\n                id=\"vc-zoom\"\n                label=\"Zoom\"\n                control={(props, ref) => (\n                    <Menu.MenuSliderControl\n                        ref={ref}\n                        {...props}\n                        minValue={1}\n                        maxValue={50}\n                        value={settings.store.zoom}\n                        onChange={debounce((value: number) => settings.store.zoom = value, 100)}\n                    />\n                )}\n            />\n            <Menu.MenuControlItem\n                id=\"vc-size\"\n                label=\"Lens Size\"\n                control={(props, ref) => (\n                    <Menu.MenuSliderControl\n                        ref={ref}\n                        {...props}\n                        minValue={50}\n                        maxValue={1000}\n                        value={settings.store.size}\n                        onChange={debounce((value: number) => settings.store.size = value, 100)}\n                    />\n                )}\n            />\n            <Menu.MenuControlItem\n                id=\"vc-zoom-speed\"\n                label=\"Zoom Speed\"\n                control={(props, ref) => (\n                    <Menu.MenuSliderControl\n                        ref={ref}\n                        {...props}\n                        minValue={0.1}\n                        maxValue={5}\n                        value={settings.store.zoomSpeed}\n                        onChange={debounce((value: number) => settings.store.zoomSpeed = value, 100)}\n                        renderValue={(value: number) => `${value.toFixed(3)}x`}\n                    />\n                )}\n            />\n        </Menu.MenuGroup>\n    );\n};\n\nexport default definePlugin({\n    name: \"ImageZoom\",\n    description: \"Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size\",\n    authors: [Devs.Aria],\n    tags: [\"ImageUtilities\"],\n\n    managedStyle,\n\n    patches: [\n        {\n            find: \"disableArrowKeySeek:!0\",\n            replacement: [\n                {\n                    match: /useFullWidth:!0,shouldLink:/,\n                    replace: `id:\"${ELEMENT_ID}\",$&`\n                },\n                {\n                    match: /(?<=null!=(\\i)\\?.{0,20})\\i\\.\\i,{children:\\1/,\n                    replace: \"'div',{onClick:e=>e.stopPropagation(),children:$1\"\n                }\n            ]\n        },\n        // Make media viewer options not hide when zoomed in with the default Discord feature\n        {\n            find: '=\"FOCUS_SENSITIVE\",',\n            replacement: {\n                match: /(?<=\\[\\i\\.\\i]:)\\i&&!\\i&&\"PINNED\"!==\\i/,\n                replace: \"false\"\n            }\n        },\n\n        {\n            find: \".handleImageLoad)\",\n            replacement: [\n                {\n                    match: /placeholderVersion:\\i,(?=.{0,50}children:)/,\n                    replace: \"...$self.makeProps(this),$&\"\n                },\n\n                {\n                    match: /componentDidMount\\(\\){/,\n                    replace: \"$&$self.renderMagnifier(this);\",\n                },\n\n                {\n                    match: /componentWillUnmount\\(\\){/,\n                    replace: \"$&$self.unMountMagnifier();\"\n                },\n\n                {\n                    match: /componentDidUpdate\\(\\i\\){/,\n                    replace: \"$&$self.updateMagnifier(this);\"\n                }\n            ]\n        }\n    ],\n\n    settings,\n    contextMenus: {\n        \"image-context\": imageContextMenuPatch\n    },\n\n    // to stop from rendering twice /shrug\n    currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,\n    element: null as HTMLDivElement | null,\n\n    Magnifier,\n    root: null as Root | null,\n    makeProps(instance) {\n        return {\n            onMouseOver: () => this.onMouseOver(instance),\n            onMouseOut: () => this.onMouseOut(instance),\n            onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance),\n            onMouseUp: () => this.onMouseUp(instance),\n            id: instance.props.id,\n        };\n    },\n\n    renderMagnifier(instance) {\n        try {\n            if (instance.props.id === ELEMENT_ID) {\n                if (!this.currentMagnifierElement) {\n                    this.currentMagnifierElement = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;\n                    this.root = createRoot(this.element!);\n                    this.root.render(this.currentMagnifierElement);\n                }\n            }\n        } catch (error) {\n            new Logger(\"ImageZoom\").error(\"Failed to render magnifier:\", error);\n        }\n    },\n\n    updateMagnifier(instance) {\n        this.unMountMagnifier();\n        this.renderMagnifier(instance);\n    },\n\n    unMountMagnifier() {\n        this.root?.unmount();\n        this.currentMagnifierElement = null;\n        this.root = null;\n    },\n\n    onMouseOver(instance) {\n        instance.setState((state: any) => ({ ...state, mouseOver: true }));\n    },\n    onMouseOut(instance) {\n        instance.setState((state: any) => ({ ...state, mouseOver: false }));\n    },\n    onMouseDown(e: React.MouseEvent, instance) {\n        if (e.button === 0 /* left */)\n            instance.setState((state: any) => ({ ...state, mouseDown: true }));\n    },\n    onMouseUp(instance) {\n        instance.setState((state: any) => ({ ...state, mouseDown: false }));\n    },\n\n    start() {\n        this.element = document.createElement(\"div\");\n        this.element.classList.add(\"MagnifierContainer\");\n        document.body.appendChild(this.element);\n    },\n\n    stop() {\n        // so componenetWillUnMount gets called if Magnifier component is still alive\n        this.root && this.root.unmount();\n        this.element?.remove();\n    }\n});\n"
  },
  {
    "path": "src/plugins/imageZoom/styles.css",
    "content": ".vc-imgzoom-lens {\n    position: absolute;\n    inset: 0;\n    z-index: 9999;\n    border: 2px solid grey;\n    border-radius: 50%;\n    overflow: hidden;\n    cursor: none;\n    box-shadow: inset 0 0 10px 2px grey;\n    filter: drop-shadow(0 0 2px grey);\n    pointer-events: none;\n\n    /* negate the border offsetting the lens */\n    margin: -2px;\n}\n\n.vc-imgzoom-square {\n    border-radius: 0;\n}\n\n.vc-imgzoom-nearest-neighbor > .vc-imgzoom-image {\n    image-rendering: pixelated;\n\n    /* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */\n}\n"
  },
  {
    "path": "src/plugins/imageZoom/utils/waitFor.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function waitFor(condition: () => boolean, cb: () => void) {\n    if (condition()) cb();\n    else requestAnimationFrame(() => waitFor(condition, cb));\n}\n"
  },
  {
    "path": "src/plugins/implicitRelationships/README.md",
    "content": "# ImplicitRelationships\n\nShows your implicit relationships in the Friends tab.\n\nImplicit relationships on Discord are people with whom you've frecently interacted and don't have a relationship with. Even though Discord thinks you should be friends with them, you haven't added them as friends!\n\n![](https://camo.githubusercontent.com/6927161ee0c933f7ef6d61f243cca3e6ea4c8db9d1becd8cbf73c45e1bd0d127/68747470733a2f2f692e646f6c66692e65732f7055447859464662674d2e706e673f6b65793d736e3950343936416c32444c7072)\n"
  },
  {
    "path": "src/plugins/implicitRelationships/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findStoreLazy } from \"@webpack\";\nimport { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from \"@webpack/common\";\nimport { Settings } from \"Vencord\";\n\nconst UserAffinitiesStore = findStoreLazy(\"UserAffinitiesV2Store\");\n\nexport default definePlugin({\n    name: \"ImplicitRelationships\",\n    description: \"Shows your implicit relationships in the Friends tab.\",\n    authors: [Devs.Dolfies],\n    patches: [\n        // Counts header\n        {\n            find: \"#{intl::FRIENDS_ALL_HEADER}\",\n            replacement: {\n                match: /toString\\(\\)\\}\\);case (\\i\\.\\i)\\.PENDING/,\n                replace: 'toString()});case $1.IMPLICIT:return \"Implicit — \"+arguments[1];case $1.BLOCKED'\n            },\n        },\n        // No friends page\n        {\n            find: \"FriendsEmptyState: Invalid empty state\",\n            replacement: {\n                match: /case (\\i\\.\\i)\\.ONLINE:(?=return (\\i)\\.SECTION_ONLINE)/,\n                replace: \"case $1.ONLINE:case $1.IMPLICIT:\"\n            },\n        },\n        // Sections header\n        {\n            find: \"#{intl::FRIENDS_SECTION_ONLINE}),className:\",\n            replacement: {\n                match: /,{id:(\\i\\.\\i)\\.PENDING,show:.+?className:(\\i\\.\\i)(?=\\},\\{id:)/,\n                replace: (rest, relationShipTypes, className) => `,{id:${relationShipTypes}.IMPLICIT,show:true,className:${className},content:\"Implicit\"}${rest}`\n            }\n        },\n        // Sections content\n        {\n            find: '\"FriendsStore\"',\n            replacement: {\n                match: /(?<=case (\\i\\.\\i)\\.SUGGESTIONS:return \\d+===(\\i)\\.type)/,\n                replace: \";case $1.IMPLICIT:return $2.type===5\"\n            },\n        },\n        // Piggyback relationship fetch\n        {\n            find: '\"FriendsStore',\n            replacement: {\n                match: /(\\i\\.\\i)\\.fetchRelationships\\(\\)/,\n                // This relationship fetch is actually completely useless, but whatevs\n                replace: \"$1.fetchRelationships(),$self.fetchImplicitRelationships()\"\n            },\n        },\n        // Modify sort -- thanks megu for the patch (from sortFriendRequests)\n        {\n            find: \"getRelationshipCounts(){\",\n            replacement: {\n                predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,\n                match: /\\}\\)\\.sortBy\\((.+?)\\)\\.value\\(\\)/,\n                replace: \"}).sortBy(row => $self.wrapSort(($1), row)).value()\"\n            }\n        },\n\n        // Add support for the nonce parameter to Discord's shitcode\n        {\n            find: \".REQUEST_GUILD_MEMBERS,\",\n            replacement: {\n                match: /\\.send\\(\\i\\.\\i\\.REQUEST_GUILD_MEMBERS,{/,\n                replace: \"$&nonce:arguments[1].nonce,\"\n            }\n        },\n        {\n            find: \"GUILD_MEMBERS_REQUEST:\",\n            replacement: {\n                match: /presences:!!(\\i)\\.presences/,\n                replace: \"$&,nonce:$1.nonce\"\n            },\n        },\n        {\n            find: \".not_found\",\n            replacement: {\n                match: /notFound:(\\i)\\.not_found/,\n                replace: \"$&,nonce:$1.nonce\"\n            },\n        }\n    ],\n    settings: definePluginSettings(\n        {\n            sortByAffinity: {\n                type: OptionType.BOOLEAN,\n                default: true,\n                description: \"Whether to sort implicit relationships by their affinity to you.\",\n                restartNeeded: true\n            },\n        }\n    ),\n\n    wrapSort(comparator: Function, row: any) {\n        return row.type === 5\n            ? (UserAffinitiesStore.getUserAffinity(row.user.id)?.communicationRank ?? 0)\n            : comparator(row);\n    },\n\n    async fetchImplicitRelationships() {\n        // Implicit relationships are defined as users that you:\n        // 1. Have an affinity for\n        // 2. Do not have a relationship with\n        const userAffinities: Record<string, any>[] = UserAffinitiesStore.getUserAffinities();\n        const relationships = RelationshipStore.getMutableRelationships();\n        const nonFriendAffinities = userAffinities.filter(a => !RelationshipStore.getRelationshipType(a.otherUserId));\n        nonFriendAffinities.forEach(a => {\n            relationships.set(a.otherUserId, 5);\n        });\n        RelationshipStore.emitChange();\n\n        const toRequest = nonFriendAffinities.filter(a => !UserStore.getUser(a.otherUserId));\n        const allGuildIds = Object.keys(GuildStore.getGuilds());\n        const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());\n        let count = allGuildIds.length * Math.ceil(toRequest.length / 100);\n\n        // OP 8 Request Guild Members allows 100 user IDs at a time\n        // Note: As we are using OP 8 here, implicit relationships who we do not share a guild\n        // with will not be fetched; so, if they're not otherwise cached, they will not be shown\n        // This should not be a big deal as these should be rare\n        const callback = ({ chunks }) => {\n            try {\n                const chunkCount = chunks.filter(chunk => chunk.nonce === sentNonce).length;\n                if (chunkCount === 0) return;\n\n                count -= chunkCount;\n                RelationshipStore.emitChange();\n                if (count <= 0) {\n                    FluxDispatcher.unsubscribe(\"GUILD_MEMBERS_CHUNK_BATCH\", callback);\n                }\n            } catch (e) {\n                new Logger(\"ImplicitRelationships\").error(\"Error in GUILD_MEMBERS_CHUNK_BATCH handler\", e);\n            }\n        };\n\n        FluxDispatcher.subscribe(\"GUILD_MEMBERS_CHUNK_BATCH\", callback);\n        for (let i = 0; i < toRequest.length; i += 100) {\n            FluxDispatcher.dispatch({\n                type: \"GUILD_MEMBERS_REQUEST\",\n                guildIds: allGuildIds,\n                userIds: toRequest.slice(i, i + 100),\n                presences: true,\n                nonce: sentNonce,\n            });\n        }\n    },\n\n    start() {\n        Constants.FriendsSections.IMPLICIT = \"IMPLICIT\";\n    }\n});\n"
  },
  {
    "path": "src/plugins/ircColors/README.md",
    "content": "# IrcColors\n\nMakes username colors in chat unique, like in IRC clients\n\n![Chat with IrcColors and Compact++ enabled](https://github.com/Vendicated/Vencord/assets/33988779/88e05c0b-a60a-4d10-949e-8b46e1d7226c)\n\nImproves chat readability by assigning every user an unique nickname color,\nmaking distinguishing between different users easier. Inspired by the feature\nin many IRC clients, such as HexChat or WeeChat.\n\nKeep in mind this overrides role colors in chat, so if you wish to know\nsomeone's role color without checking their profile, enable the role dot: go to\n**User Settings**, **Accessibility** and switch **Role Colors** to **Show role\ncolors next to names**.\n\nCreated for use with the [Compact++](https://gitlab.com/Grzesiek11/compactplusplus-discord-theme)\ntheme.\n"
  },
  {
    "path": "src/plugins/ircColors/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { hash as h64 } from \"@intrnl/xxhash64\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { useMemo } from \"@webpack/common\";\n\n// Calculate a CSS color string based on the user ID\nfunction calculateNameColorForUser(id?: string) {\n    const { lightness } = settings.use([\"lightness\"]);\n    const idHash = useMemo(() => id ? h64(id) : null, [id]);\n\n    return idHash && `hsl(${idHash % 360n}, 100%, ${lightness}%)`;\n}\n\nconst settings = definePluginSettings({\n    lightness: {\n        description: \"Lightness, in %. Change if the colors are too light or too dark\",\n        type: OptionType.NUMBER,\n        default: 70,\n    },\n    memberListColors: {\n        description: \"Replace role colors in the member list\",\n        restartNeeded: true,\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    applyColorOnlyToUsersWithoutColor: {\n        description: \"Apply colors only to users who don't have a predefined color\",\n        restartNeeded: false,\n        type: OptionType.BOOLEAN,\n        default: false\n    },\n    applyColorOnlyInDms: {\n        description: \"Apply colors only in direct messages; do not apply colors in servers.\",\n        restartNeeded: false,\n        type: OptionType.BOOLEAN,\n        default: false\n    }\n});\n\nexport default definePlugin({\n    name: \"IrcColors\",\n    description: \"Makes username colors in chat unique, like in IRC clients\",\n    authors: [Devs.Grzesiek11, Devs.jamesbt365],\n    settings,\n\n    patches: [\n        {\n            find: '=\"SYSTEM_TAG\"',\n            replacement: {\n                // Override colorString with our custom color and disable gradients if applying the custom color.\n                match: /(?<=colorString:\\i,colorStrings:\\i,colorRoleName:\\i.*?}=)(\\i),/,\n                replace: \"$self.wrapMessageColorProps($1, arguments[0]),\"\n            }\n        },\n        {\n            find: \"#{intl::GUILD_OWNER}),children:\",\n            replacement: {\n                match: /(?<=roleName:\\i,)colorString:/,\n                replace: \"colorString:$self.calculateNameColorForListContext(arguments[0]),originalColor:\"\n            },\n            predicate: () => settings.store.memberListColors\n        }\n    ],\n\n    wrapMessageColorProps(colorProps: { colorString: string, colorStrings?: Record<\"primaryColor\" | \"secondaryColor\" | \"tertiaryColor\", string>; }, context: any) {\n        try {\n            const colorString = this.calculateNameColorForMessageContext(context);\n            if (colorString === colorProps.colorString) {\n                return colorProps;\n            }\n\n            return {\n                ...colorProps,\n                colorString,\n                colorStrings: colorProps.colorStrings && {\n                    primaryColor: colorString,\n                    secondaryColor: undefined,\n                    tertiaryColor: undefined\n                }\n            };\n        } catch (e) {\n            console.error(\"Failed to calculate message color strings:\", e);\n            return colorProps;\n        }\n    },\n\n    calculateNameColorForMessageContext(context: any) {\n        const userId: string | undefined = context?.message?.author?.id;\n        const colorString = context?.author?.colorString;\n        const color = calculateNameColorForUser(userId);\n\n        // Color preview in role settings\n        if (context?.message?.channel_id === \"1337\" && userId === \"313337\")\n            return colorString;\n\n        if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {\n            return colorString;\n        }\n\n        return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)\n            ? color\n            : colorString;\n    },\n\n    calculateNameColorForListContext(context: any) {\n        try {\n            const id = context?.user?.id;\n            const colorString = context?.colorString;\n            const color = calculateNameColorForUser(id);\n\n            if (settings.store.applyColorOnlyInDms && context?.guildId !== undefined) {\n                return colorString;\n            }\n\n            return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)\n                ? color\n                : colorString;\n        } catch (e) {\n            console.error(\"Failed to calculate name color for list context:\", e);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/keepCurrentChannel/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as DataStore from \"@api/DataStore\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { ChannelRouter, ChannelStore, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from \"@webpack/common\";\n\nexport interface LogoutEvent {\n    type: \"LOGOUT\";\n    isSwitchingAccount: boolean;\n}\n\ninterface ChannelSelectEvent {\n    type: \"CHANNEL_SELECT\";\n    channelId: string | null;\n    guildId: string | null;\n}\n\ninterface PreviousChannel {\n    guildId: string | null;\n    channelId: string | null;\n}\n\nlet isSwitchingAccount = false;\nlet previousCache: PreviousChannel | undefined;\n\nexport default definePlugin({\n    name: \"KeepCurrentChannel\",\n    description: \"Attempt to navigate to the channel you were in before switching accounts or loading Discord.\",\n    authors: [Devs.Nuckyz],\n\n    patches: [\n        {\n            find: '\"Switching accounts\"',\n            replacement: {\n                match: /goHomeAfterSwitching:\\i/,\n                replace: \"goHomeAfterSwitching:!1\"\n            }\n        }\n    ],\n\n    flux: {\n        LOGOUT(e: LogoutEvent) {\n            ({ isSwitchingAccount } = e);\n        },\n\n        CONNECTION_OPEN() {\n            if (!isSwitchingAccount) return;\n            isSwitchingAccount = false;\n\n            if (previousCache?.channelId) {\n                if (ChannelStore.hasChannel(previousCache.channelId)) {\n                    ChannelRouter.transitionToChannel(previousCache.channelId);\n                } else {\n                    NavigationRouter.transitionToGuild(\"@me\");\n                }\n            }\n        },\n\n        async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {\n            if (isSwitchingAccount) return;\n\n            previousCache = {\n                guildId,\n                channelId\n            };\n            await DataStore.set(\"KeepCurrentChannel_previousData\", previousCache);\n        }\n    },\n\n    async start() {\n        previousCache = await DataStore.get<PreviousChannel>(\"KeepCurrentChannel_previousData\");\n        if (!previousCache) {\n            previousCache = {\n                guildId: SelectedGuildStore.getGuildId(),\n                channelId: SelectedChannelStore.getChannelId() ?? null\n            };\n\n            await DataStore.set(\"KeepCurrentChannel_previousData\", previousCache);\n        } else if (previousCache.channelId) {\n            ChannelRouter.transitionToChannel(previousCache.channelId);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/lastfmRichPresence/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Sofia Lima\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { LinkButton } from \"@components/Button\";\nimport { Card } from \"@components/Card\";\nimport { Heading } from \"@components/Heading\";\nimport { Margins } from \"@components/margins\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Activity, ActivityAssets, ActivityButton } from \"@vencord/discord-types\";\nimport { ActivityFlags, ActivityStatusDisplayType, ActivityType } from \"@vencord/discord-types/enums\";\nimport { ApplicationAssetUtils, AuthenticationStore, FluxDispatcher, PresenceStore } from \"@webpack/common\";\n\ninterface TrackData {\n    name: string;\n    album: string;\n    artist: string;\n    url: string;\n    imageUrl?: string;\n}\n\nconst enum NameFormat {\n    StatusName = \"status-name\",\n    ArtistFirst = \"artist-first\",\n    SongFirst = \"song-first\",\n    ArtistOnly = \"artist\",\n    SongOnly = \"song\",\n    AlbumName = \"album\"\n}\n\n// Last.fm API keys are essentially public information and have no access to your account, so including one here is fine.\nconst API_KEY = \"790c37d90400163a5a5fe00d6ca32ef0\";\nconst DISCORD_APP_ID = \"1108588077900898414\";\nconst LASTFM_PLACEHOLDER_IMAGE_HASH = \"2a96cbd8b46e442fc41c2b86b821562f\";\n\nconst logger = new Logger(\"LastFMRichPresence\");\n\nasync function getApplicationAsset(key: string): Promise<string> {\n    return (await ApplicationAssetUtils.fetchAssetIds(DISCORD_APP_ID, [key]))[0];\n}\n\nfunction setActivity(activity: Activity | null) {\n    FluxDispatcher.dispatch({\n        type: \"LOCAL_ACTIVITY_UPDATE\",\n        activity,\n        socketId: \"LastFM\",\n    });\n}\n\nconst settings = definePluginSettings({\n    apiKey: {\n        description: \"Custom Last.fm API key. Not required but highly recommended to avoid rate limiting with our shared key\",\n        type: OptionType.STRING,\n    },\n    username: {\n        description: \"Last.fm username\",\n        type: OptionType.STRING,\n    },\n    shareUsername: {\n        description: \"Show link to Last.fm profile\",\n        type: OptionType.BOOLEAN,\n        default: false,\n    },\n    clickableLinks: {\n        description: \"Make track, artist and album names clickable links\",\n        type: OptionType.BOOLEAN,\n        default: true,\n    },\n    hideWithSpotify: {\n        description: \"Hide Last.fm presence if spotify is running\",\n        type: OptionType.BOOLEAN,\n        default: true,\n    },\n    hideWithActivity: {\n        description: \"Hide Last.fm presence if you have any other presence\",\n        type: OptionType.BOOLEAN,\n        default: false,\n    },\n    statusName: {\n        description: \"Custom status text\",\n        type: OptionType.STRING,\n        default: \"some music\",\n    },\n    statusDisplayType: {\n        description: \"Show the track / artist name in the member list\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Don't show (shows generic listening message)\",\n                value: \"off\"\n            },\n            {\n                label: \"Show artist name\",\n                value: \"artist\",\n                default: true\n            },\n            {\n                label: \"Show track name\",\n                value: \"track\"\n            }\n        ]\n    },\n    nameFormat: {\n        description: \"Show name of song and artist in status name\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Use custom status name\",\n                value: NameFormat.StatusName,\n                default: true\n            },\n            {\n                label: \"Use format 'artist - song'\",\n                value: NameFormat.ArtistFirst\n            },\n            {\n                label: \"Use format 'song - artist'\",\n                value: NameFormat.SongFirst\n            },\n            {\n                label: \"Use artist name only\",\n                value: NameFormat.ArtistOnly\n            },\n            {\n                label: \"Use song name only\",\n                value: NameFormat.SongOnly\n            },\n            {\n                label: \"Use album name (falls back to custom status text if song has no album)\",\n                value: NameFormat.AlbumName\n            }\n        ],\n    },\n    useListeningStatus: {\n        description: 'Show \"Listening to\" status instead of \"Playing\"',\n        type: OptionType.BOOLEAN,\n        default: false,\n    },\n    missingArt: {\n        description: \"When album or album art is missing\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Use large Last.fm logo\",\n                value: \"lastfmLogo\",\n                default: true\n            },\n            {\n                label: \"Use generic placeholder\",\n                value: \"placeholder\"\n            }\n        ],\n    },\n    showLastFmLogo: {\n        description: \"Show the Last.fm logo by the album cover\",\n        type: OptionType.BOOLEAN,\n        default: true,\n    },\n});\n\nexport default definePlugin({\n    name: \"LastFMRichPresence\",\n    description: \"Little plugin for Last.fm rich presence\",\n    authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],\n\n    settings,\n\n    settingsAboutComponent() {\n        return (\n            <Card>\n                <Heading tag=\"h5\">How to create an API key</Heading>\n                <Paragraph>Set <strong>Application name</strong> and <strong>Application description</strong> to anything and leave the rest blank.</Paragraph>\n                <LinkButton size=\"small\" href=\"https://www.last.fm/api/account/create\" className={Margins.top8}>Create API Key</LinkButton>\n            </Card>\n        );\n    },\n\n    start() {\n        this.updatePresence();\n        this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000);\n    },\n\n    stop() {\n        clearInterval(this.updateInterval);\n    },\n\n    async fetchTrackData(): Promise<TrackData | null> {\n        if (!settings.store.username)\n            return null;\n\n        try {\n            const params = new URLSearchParams({\n                method: \"user.getrecenttracks\",\n                api_key: settings.store.apiKey || API_KEY,\n                user: settings.store.username,\n                limit: \"1\",\n                format: \"json\"\n            });\n\n            const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);\n            if (!res.ok) throw `${res.status} ${res.statusText}`;\n\n            const json = await res.json();\n            if (json.error) {\n                logger.error(\"Error from Last.fm API\", `${json.error}: ${json.message}`);\n                return null;\n            }\n\n            const trackData = json.recenttracks?.track[0];\n\n            if (!trackData?.[\"@attr\"]?.nowplaying)\n                return null;\n\n            // why does the json api have xml structure\n            return {\n                name: trackData.name || \"Unknown\",\n                album: trackData.album[\"#text\"],\n                artist: trackData.artist[\"#text\"] || \"Unknown\",\n                url: trackData.url,\n                imageUrl: trackData.image?.find((x: any) => x.size === \"large\")?.[\"#text\"]\n            };\n        } catch (e) {\n            logger.error(\"Failed to query Last.fm API\", e);\n            // will clear the rich presence if API fails\n            return null;\n        }\n    },\n\n    async updatePresence() {\n        setActivity(await this.getActivity());\n    },\n\n    getLargeImage(track: TrackData): string | undefined {\n        if (track.imageUrl && !track.imageUrl.includes(LASTFM_PLACEHOLDER_IMAGE_HASH))\n            return track.imageUrl;\n\n        if (settings.store.missingArt === \"placeholder\")\n            return \"placeholder\";\n    },\n\n    async getActivity(): Promise<Activity | null> {\n        if (settings.store.hideWithActivity) {\n            if (PresenceStore.getActivities(AuthenticationStore.getId()).some(a => a.application_id !== DISCORD_APP_ID && a.type !== ActivityType.CUSTOM_STATUS)) {\n                return null;\n            }\n        }\n\n        if (settings.store.hideWithSpotify) {\n            if (PresenceStore.getActivities(AuthenticationStore.getId()).some(a => a.type === ActivityType.LISTENING && a.application_id !== DISCORD_APP_ID)) {\n                // there is already music status because of Spotify or richerCider (probably more)\n                return null;\n            }\n        }\n\n        const trackData = await this.fetchTrackData();\n        if (!trackData) return null;\n\n        const largeImage = this.getLargeImage(trackData);\n        const assets: ActivityAssets = largeImage ?\n            {\n                large_image: await getApplicationAsset(largeImage),\n                large_text: trackData.album || undefined,\n                ...(settings.store.showLastFmLogo && {\n                    small_image: await getApplicationAsset(\"lastfm-small\"),\n                    small_text: \"Last.fm\"\n                }),\n            } : {\n                large_image: await getApplicationAsset(\"lastfm-large\"),\n                large_text: trackData.album || undefined,\n            };\n\n        const buttons: ActivityButton[] = [];\n\n        if (settings.store.shareUsername)\n            buttons.push({\n                label: \"Last.fm Profile\",\n                url: `https://www.last.fm/user/${settings.store.username}`,\n            });\n\n        const statusName = (() => {\n            switch (settings.store.nameFormat) {\n                case NameFormat.ArtistFirst:\n                    return trackData.artist + \" - \" + trackData.name;\n                case NameFormat.SongFirst:\n                    return trackData.name + \" - \" + trackData.artist;\n                case NameFormat.ArtistOnly:\n                    return trackData.artist;\n                case NameFormat.SongOnly:\n                    return trackData.name;\n                case NameFormat.AlbumName:\n                    return trackData.album || settings.store.statusName;\n                default:\n                    return settings.store.statusName;\n            }\n        })();\n\n        const activity: Activity = {\n            application_id: DISCORD_APP_ID,\n            name: statusName,\n\n            details: trackData.name,\n            state: trackData.artist,\n            status_display_type: {\n                \"off\": ActivityStatusDisplayType.NAME,\n                \"artist\": ActivityStatusDisplayType.STATE,\n                \"track\": ActivityStatusDisplayType.DETAILS\n            }[settings.store.statusDisplayType],\n\n            assets,\n\n            buttons: buttons.length ? buttons.map(v => v.label) : undefined,\n            metadata: {\n                button_urls: buttons.map(v => v.url),\n            },\n\n            type: settings.store.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING,\n            flags: ActivityFlags.INSTANCE,\n        };\n\n        if (settings.store.clickableLinks) {\n            activity.details_url = trackData.url;\n            activity.state_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}`;\n\n            if (trackData.album) {\n                activity.assets!.large_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}/${encodeURIComponent(trackData.album)}`;\n            }\n        }\n\n        return activity;\n    }\n});\n"
  },
  {
    "path": "src/plugins/loadingQuotes/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport presetQuotesText from \"file://quotes.txt\";\n\nconst presetQuotes = presetQuotesText.split(\"\\n\").map(quote => /^\\s*[^#\\s]/.test(quote) && quote.trim()).filter(Boolean) as string[];\nconst noQuotesQuote = \"Did you really disable all loading quotes? What a buffoon you are...\";\n\nconst settings = definePluginSettings({\n    replaceEvents: {\n        description: \"Should this plugin also apply during events with special event themed quotes? (e.g. Halloween)\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    enablePluginPresetQuotes: {\n        description: \"Enable the quotes preset by this plugin\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    enableDiscordPresetQuotes: {\n        description: \"Enable Discord's preset quotes (including event quotes, during events)\",\n        type: OptionType.BOOLEAN,\n        default: false\n    },\n    additionalQuotes: {\n        description: \"Additional custom quotes to possibly appear, separated by the below delimiter\",\n        type: OptionType.STRING,\n        default: \"\",\n        multiline: true\n    },\n    additionalQuotesDelimiter: {\n        description: \"Delimiter for additional quotes\",\n        type: OptionType.STRING,\n        default: \"|\",\n    },\n});\n\nexport default definePlugin({\n    name: \"LoadingQuotes\",\n    description: \"Replace Discords loading quotes\",\n    authors: [Devs.Ven, Devs.KraXen72, Devs.UlyssesZhan],\n\n    settings,\n\n    patches: [\n        {\n            find: \"#{intl::LOADING_DID_YOU_KNOW}\",\n            replacement: [\n                {\n                    match: /_loadingText.+?(?=(\\i)\\[.{0,10}\\.random)/,\n                    replace: \"$&$self.mutateQuotes($1),\"\n                },\n                {\n                    match: /_eventLoadingText.+?(?=(\\i)\\[.{0,10}\\.random)/,\n                    replace: \"$&$self.mutateQuotes($1),\",\n                    predicate: () => settings.store.replaceEvents\n                }\n            ]\n        },\n    ],\n\n    mutateQuotes(quotes: string[]) {\n        try {\n            const { enableDiscordPresetQuotes, additionalQuotes, additionalQuotesDelimiter, enablePluginPresetQuotes } = settings.store;\n\n            if (!enableDiscordPresetQuotes)\n                quotes.length = 0;\n\n\n            if (enablePluginPresetQuotes)\n                quotes.push(...presetQuotes);\n\n            quotes.push(...additionalQuotes.split(additionalQuotesDelimiter).filter(Boolean));\n\n            if (!quotes.length)\n                quotes.push(noQuotesQuote);\n        } catch (e) {\n            new Logger(\"LoadingQuotes\").error(\"Failed to mutate quotes\", e);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/loadingQuotes/quotes.txt",
    "content": "# Blank lines and lines starting with \"#\" are ignored\n\nExplode\nRead if cute\nHave a nice day!\nStarting Lightcord...\nLoading 0BDFDB.plugin.js...\nInstalling BetterDiscord...\nh\nshhhhh did you know that you're my favourite user? But don't tell the others!!\nToday's video is sponsored by Raid Shadow Legends, one of the biggest mobile role-playing games of 2019 and it's totally free!\nNever gonna give you up, Never gonna let you down\n( ͡° ͜ʖ ͡°)\n(ﾉ◕ヮ◕)ﾉ*:･ﾟ✧\nYou look so pretty today!\nThinking of a funny quote...\n3.141592653589793\nmeow\nWelcome, friend\nIf you, or someone you love, has Ligma, please see the Ligma health line at https://bit.ly/ligma_hotline\nTrans Rights\nI’d just like to interject for a moment. What you’re refering to as Linux, is in fact, GNU/Linux, or as I’ve recently taken to calling it, GNU plus Linux.\nYou're doing good today!\nDon't worry, it's nothing 9 cups of coffee couldn't solve!\n�(repeat like 30 times)\na light amount of tomfoolery is okay\ndo you love?\nhorror\nso eepy\nSo without further ado, let's just jump right into it!\nDying is absolutely safe\nhey you! you're cute :))\nheya ~\n<:trolley:997086295010594867>\nTime is gone, space is insane. Here it comes, here again.\nsometimes it's okay to just guhhhhhhhhhhhhhh\nWelcome to nginx!\n"
  },
  {
    "path": "src/plugins/memberCount/CircleIcon.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport function CircleIcon({ className }: { className?: string; }) {\n    return (\n        <svg viewBox=\"0 0 24 24\" className={className}>\n            <circle\n                cx=\"12\"\n                cy=\"12\"\n                r=\"8\"\n            />\n        </svg>\n    );\n}\n"
  },
  {
    "path": "src/plugins/memberCount/MemberCount.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { getCurrentChannel } from \"@utils/discord\";\nimport { isObjectEmpty } from \"@utils/misc\";\nimport { ChannelStore, GuildMemberCountStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from \"@webpack/common\";\n\nimport { ChannelMemberStore, cl, numberFormat, settings, ThreadMemberListStore } from \".\";\nimport { CircleIcon } from \"./CircleIcon\";\nimport { OnlineMemberCountStore } from \"./OnlineMemberCountStore\";\nimport { VoiceIcon } from \"./VoiceIcon\";\n\nexport function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {\n    const { voiceActivity } = settings.use([\"voiceActivity\"]);\n    const includeVoice = voiceActivity && !isTooltip;\n\n    const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());\n    const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;\n\n    const voiceActivityCount = useStateFromStores(\n        [VoiceStateStore],\n        () => {\n            if (!includeVoice) return 0;\n\n            const voiceStates = VoiceStateStore.getVoiceStates(guildId);\n            if (!voiceStates) return 0;\n\n            return Object.values(voiceStates)\n                .filter(({ channelId }) => {\n                    if (!channelId) return false;\n                    const channel = ChannelStore.getChannel(channelId);\n                    return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel);\n                })\n                .length;\n        }\n    );\n\n    const totalCount = useStateFromStores(\n        [GuildMemberCountStore],\n        () => GuildMemberCountStore.getMemberCount(guildId!)\n    );\n\n    let onlineCount = useStateFromStores(\n        [OnlineMemberCountStore],\n        () => OnlineMemberCountStore.getCount(guildId)\n    );\n\n    const { groups } = useStateFromStores(\n        [ChannelMemberStore],\n        () => ChannelMemberStore.getProps(guildId, currentChannel?.id)\n    );\n\n    const threadGroups = useStateFromStores(\n        [ThreadMemberListStore],\n        () => ThreadMemberListStore.getMemberListSections(currentChannel?.id)\n    );\n\n    if (!isTooltip && (groups.length >= 1 || groups[0].id !== \"unknown\")) {\n        onlineCount = groups.reduce((total, curr) => total + (curr.id === \"offline\" ? 0 : curr.count), 0);\n    }\n\n    if (!isTooltip && threadGroups && !isObjectEmpty(threadGroups)) {\n        onlineCount = Object.values(threadGroups).reduce((total, curr) => total + (curr.sectionId === \"offline\" ? 0 : curr.userIds.length), 0);\n    }\n\n    useEffect(() => {\n        OnlineMemberCountStore.ensureCount(guildId);\n    }, [guildId]);\n\n    if (totalCount == null)\n        return null;\n\n    const formattedVoiceCount = numberFormat(voiceActivityCount ?? 0);\n    const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : \"?\";\n\n    return (\n        <div className={cl(\"widget\", { tooltip: isTooltip, \"member-list\": !isTooltip })}>\n            <Tooltip text={`${formattedOnlineCount} online in this channel`} position=\"bottom\">\n                {props => (\n                    <div {...props} className={cl(\"container\")}>\n                        <CircleIcon className={cl(\"online-count\")} />\n                        <span className={cl(\"online\")}>{formattedOnlineCount}</span>\n                    </div>\n                )}\n            </Tooltip>\n            <Tooltip text={`${numberFormat(totalCount)} total server members`} position=\"bottom\">\n                {props => (\n                    <div {...props} className={cl(\"container\")}>\n                        <CircleIcon className={cl(\"total-count\")} />\n                        <span className={cl(\"total\")}>{numberFormat(totalCount)}</span>\n                    </div>\n                )}\n            </Tooltip>\n            {includeVoice && voiceActivityCount > 0 &&\n                <Tooltip text={`${formattedVoiceCount} members in voice`} position=\"bottom\">\n                    {props => (\n                        <div {...props} className={cl(\"container\")}>\n                            <VoiceIcon className={cl(\"voice-icon\")} />\n                            <span className={cl(\"voice\")}>{formattedVoiceCount}</span>\n                        </div>\n                    )}\n                </Tooltip>\n            }\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/plugins/memberCount/OnlineMemberCountStore.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { proxyLazy } from \"@utils/lazy\";\nimport { sleep } from \"@utils/misc\";\nimport { Queue } from \"@utils/Queue\";\nimport { ChannelActionCreators, Flux, FluxDispatcher, GuildChannelStore } from \"@webpack/common\";\n\nexport const OnlineMemberCountStore = proxyLazy(() => {\n    const preloadQueue = new Queue();\n\n    const onlineMemberMap = new Map<string, number>();\n\n    class OnlineMemberCountStore extends Flux.Store {\n        getCount(guildId?: string) {\n            return onlineMemberMap.get(guildId!);\n        }\n\n        async _ensureCount(guildId: string) {\n            if (onlineMemberMap.has(guildId)) return;\n\n            await ChannelActionCreators.preload(guildId, GuildChannelStore.getDefaultChannel(guildId)!.id);\n        }\n\n        ensureCount(guildId?: string) {\n            if (!guildId || onlineMemberMap.has(guildId)) return;\n\n            preloadQueue.push(() =>\n                this._ensureCount(guildId)\n                    .then(\n                        () => sleep(200),\n                        () => sleep(200)\n                    )\n            );\n        }\n    }\n\n    return new OnlineMemberCountStore(FluxDispatcher, {\n        GUILD_MEMBER_LIST_UPDATE({ guildId, groups }: { guildId: string, groups: { count: number; id: string; }[]; }) {\n            onlineMemberMap.set(\n                guildId,\n                groups.reduce((total, curr) => total + (curr.id === \"offline\" ? 0 : curr.count), 0)\n            );\n        },\n        ONLINE_GUILD_MEMBER_COUNT_UPDATE({ guildId, count }) {\n            onlineMemberMap.set(guildId, count);\n        }\n    });\n});\n"
  },
  {
    "path": "src/plugins/memberCount/VoiceIcon.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport function VoiceIcon({ className }: { className?: string; }) {\n    return (\n        <svg viewBox=\"0 0 32 32\" fill=\"currentColor\" className={className}>\n            <path d=\"M15.6668 3C14.2523 3 12.8958 3.5619 11.8956 4.5621C10.8954 5.56229 10.3335 6.91884 10.3335 8.33333V13.6666C10.3335 15.0811 10.8954 16.4378 11.8956 17.438C12.8958 18.4381 14.2523 19 15.6668 19C17.0813 19 18.4378 18.4381 19.438 17.438C20.4382 16.4378 21.0001 15.0811 21.0001 13.6666V8.33333C21.0001 6.91884 20.4382 5.56229 19.438 4.5621C18.4378 3.5619 17.0813 3 15.6668 3Z\" />\n            <path d=\"M7.66667 13.6666C7.66667 13.313 7.52619 12.9739 7.27614 12.7238C7.02609 12.4738 6.68695 12.3333 6.33333 12.3333C5.97971 12.3333 5.64057 12.4738 5.39052 12.7238C5.14047 12.9739 5 13.313 5 13.6666C4.99911 16.2653 5.94692 18.7749 7.66545 20.7243C9.38399 22.6736 11.7551 23.9285 14.3334 24.2533V27H11.6667C11.3131 27 10.9739 27.1404 10.7239 27.3905C10.4738 27.6405 10.3334 27.9797 10.3334 28.3333C10.3334 28.6869 10.4738 29.0261 10.7239 29.2761C10.9739 29.5262 11.3131 29.6666 11.6667 29.6666H19.6667C20.0203 29.6666 20.3595 29.5262 20.6095 29.2761C20.8596 29.0261 21 28.6869 21 28.3333C21 27.9797 20.8596 27.6405 20.6095 27.3905C20.3595 27.1404 20.0203 27 19.6667 27H17V24.2533C19.5783 23.9285 21.9494 22.6736 23.6679 20.7243C25.3864 18.7749 26.3343 16.2653 26.3334 13.6666C26.3334 13.313 26.1929 12.9739 25.9428 12.7238C25.6928 12.4738 25.3536 12.3333 25 12.3333C24.6464 12.3333 24.3073 12.4738 24.0572 12.7238C23.8072 12.9739 23.6667 13.313 23.6667 13.6666C23.6667 15.7884 22.8238 17.8232 21.3235 19.3235C19.8233 20.8238 17.7884 21.6666 15.6667 21.6666C13.545 21.6666 11.5101 20.8238 10.0098 19.3235C8.50952 17.8232 7.66667 15.7884 7.66667 13.6666Z\" />\n        </svg>\n    );\n}\n"
  },
  {
    "path": "src/plugins/memberCount/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { FluxStore } from \"@vencord/discord-types\";\nimport { findStoreLazy } from \"@webpack\";\n\nimport { MemberCount } from \"./MemberCount\";\n\nexport const ChannelMemberStore = findStoreLazy(\"ChannelMemberStore\") as FluxStore & {\n    getProps(guildId?: string, channelId?: string): { groups: { count: number; id: string; }[]; };\n};\nexport const ThreadMemberListStore = findStoreLazy(\"ThreadMemberListStore\") as FluxStore & {\n    getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };\n};\n\nexport const settings = definePluginSettings({\n    toolTip: {\n        type: OptionType.BOOLEAN,\n        description: \"Show member count on the server tooltip\",\n        default: true,\n        restartNeeded: true\n    },\n    memberList: {\n        type: OptionType.BOOLEAN,\n        description: \"Show member count in the member list\",\n        default: true,\n        restartNeeded: true\n    },\n    voiceActivity: {\n        type: OptionType.BOOLEAN,\n        description: \"Show voice activity with member count in the member list\",\n        default: true\n    }\n});\n\nconst sharedIntlNumberFormat = new Intl.NumberFormat();\nexport const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);\nexport const cl = classNameFactory(\"vc-membercount-\");\n\nexport default definePlugin({\n    name: \"MemberCount\",\n    description: \"Shows the number of online members, total members, and users in voice channels on the server — in the member list and tooltip.\",\n    authors: [Devs.Ven, Devs.Commandtechno, Devs.Apexo],\n    settings,\n\n    patches: [\n        {\n            find: \"{isSidebarVisible:\",\n            replacement: [\n                {\n                    match: /children:\\[(\\i\\.useMemo[^}]+\"aria-multiselectable\")(?<=className:(\\i),.+?)/,\n                    replace: \"children:[$2?.includes('members')?$self.render():null,$1\",\n                },\n            ],\n            predicate: () => settings.store.memberList\n        },\n        {\n            find: \"GuildTooltip - \",\n            replacement: {\n                match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/,\n                replace: \"$&,$self.renderTooltip(arguments[0].guild)\"\n            },\n            predicate: () => settings.store.toolTip\n        }\n    ],\n    render: ErrorBoundary.wrap(() => <MemberCount />, { noop: true }),\n    renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/memberCount/style.css",
    "content": ".vc-membercount-widget {\n    gap: 0.85em;\n    display: flex;\n    align-content: center;\n\n    --color-online: var(--green-360);\n    --color-total: var(--primary-400);\n    --color-voice: var(--primary-400);\n}\n\n.vc-membercount-tooltip {\n    margin-top: 0.25em;\n    margin-left: 2px;\n}\n\n.vc-membercount-member-list {\n    justify-content: center;\n    flex-wrap: wrap;\n    margin-top: 1em;\n    padding-inline: 1em;\n    line-height: 1.2em;\n}\n\n.vc-membercount-container {\n    display: flex;\n    align-items: center;\n    gap: 0.5em;\n}\n\n.vc-membercount-online {\n    color: var(--color-online);\n}\n\n.vc-membercount-total {\n    color: var(--color-total);\n}\n\n.vc-membercount-voice {\n    color: var(--color-voice);\n}\n\n.vc-membercount-online-count {\n    fill: var(--status-online);\n    width: 18px;\n    height: 18px;\n}\n\n.vc-membercount-total-count {\n    fill: none;\n    stroke: var(--icon-status-offline);\n    stroke-width: 4px;\n    width: 15px;\n    height: 15px;\n}\n\n.vc-membercount-voice-icon {\n    color: var(--color-voice);\n    width: 15px;\n    height: 15px;\n}"
  },
  {
    "path": "src/plugins/mentionAvatars/README.md",
    "content": "# MentionAvatars\n\nShows user avatars and role icons inside mentions\n\n![](https://github.com/user-attachments/assets/fc76ea47-5e19-4063-a592-c57785a75cc7)\n![](https://github.com/user-attachments/assets/76c4c3d9-7cde-42db-ba84-903cbb40c163)\n"
  },
  {
    "path": "src/plugins/mentionAvatars/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { User } from \"@vencord/discord-types\";\nimport { GuildRoleStore, SelectedGuildStore, useState } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    showAtSymbol: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether the the @ symbol should be displayed on user mentions\",\n        default: true\n    }\n});\n\nfunction DefaultRoleIcon() {\n    return (\n        <svg\n            className=\"vc-mentionAvatars-icon vc-mentionAvatars-role-icon\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n        >\n            <path\n                d=\"M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006Z\"\n            />\n            <path\n                d=\"M20.0001 20.006H22.0001V19.006C22.0001 16.4433 20.2697 14.4415 17.5213 13.5352C19.0621 14.9127 20.0001 16.8059 20.0001 19.006V20.006Z\"\n            />\n            <path\n                d=\"M14.8834 11.9077C16.6657 11.5044 18.0001 9.9077 18.0001 8.00598C18.0001 5.96916 16.4693 4.28218 14.4971 4.0367C15.4322 5.09511 16.0001 6.48524 16.0001 8.00598C16.0001 9.44888 15.4889 10.7742 14.6378 11.8102C14.7203 11.8418 14.8022 11.8743 14.8834 11.9077Z\"\n            />\n        </svg>\n    );\n}\n\nexport default definePlugin({\n    name: \"MentionAvatars\",\n    description: \"Shows user avatars and role icons inside mentions\",\n    authors: [Devs.Ven, Devs.SerStars],\n\n    patches: [{\n        find: \".USER_MENTION)\",\n        replacement: {\n            match: /children:`@\\$\\{(\\i\\?\\?\\i)\\}`(?<=\\.useName\\((\\i)\\).+?)/,\n            replace: \"children:$self.renderUsername({username:$1,user:$2})\"\n        }\n    },\n    {\n        find: \".ROLE_MENTION)\",\n        replacement: {\n            match: /children:\\[\\i&&.{0,100}className:\\i.\\i,background:!1,.{0,50}?,\\i(?=\\])/,\n            replace: \"$&,$self.renderRoleIcon(arguments[0])\"\n        }\n    }],\n\n    settings,\n\n    renderUsername: ErrorBoundary.wrap((props: { user: User, username: string; }) => {\n        const { user, username } = props;\n        const [isHovering, setIsHovering] = useState(false);\n\n        if (!user) return <>{getUsernameString(username)}</>;\n\n        return (\n            <span\n                onMouseEnter={() => setIsHovering(true)}\n                onMouseLeave={() => setIsHovering(false)}\n            >\n                <img\n                    src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)}\n                    className=\"vc-mentionAvatars-icon\"\n                    style={{ borderRadius: \"50%\" }}\n                />\n                {getUsernameString(username)}\n            </span>\n        );\n    }, { noop: true }),\n\n    renderRoleIcon: ErrorBoundary.wrap(({ roleId, guildId }: { roleId: string, guildId: string; }) => {\n        // Discord uses Role Mentions for uncached users because .... idk\n        if (!roleId) return null;\n\n        const role = GuildRoleStore.getRole(guildId, roleId);\n\n        if (!role?.icon) return <DefaultRoleIcon />;\n\n        return (\n            <img\n                className=\"vc-mentionAvatars-icon vc-mentionAvatars-role-icon\"\n                src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}\n            />\n        );\n    }, { noop: true }),\n});\n\nfunction getUsernameString(username: string) {\n    return settings.store.showAtSymbol\n        ? `@${username}`\n        : username;\n}\n"
  },
  {
    "path": "src/plugins/mentionAvatars/styles.css",
    "content": ".vc-mentionAvatars-icon {\n    vertical-align: middle;\n    width: 1em !important; /* insane discord sets width: 100% in channel topic */\n    height: 1em;\n    margin: 0 4px 0.2rem 2px;\n    box-sizing: border-box;\n}\n\n.vc-mentionAvatars-role-icon {\n    margin: 0 2px 0.2rem 4px;\n}\n\n/** don't display inside the ServerInfo modal owner mention */\n.vc-gp-owner .vc-mentionAvatars-icon {\n    display: none;\n}\n"
  },
  {
    "path": "src/plugins/messageClickActions/README.md",
    "content": "# MessageClickActions\n\nAllows you to double click to edit/reply to a message or delete it if you hold the backspace key\n\n![](https://github.com/Vendicated/Vencord/assets/55940580/6885aca2-4021-4910-b636-bb40f877a816)\n"
  },
  {
    "path": "src/plugins/messageClickActions/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport NoReplyMentionPlugin from \"@plugins/noReplyMention\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { ApplicationIntegrationType, MessageFlags } from \"@vencord/discord-types/enums\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, WindowStore } from \"@webpack/common\";\n\nconst MessageActions = findByPropsLazy(\"deleteMessage\", \"startEditMessage\");\nconst EditStore = findByPropsLazy(\"isEditing\", \"isEditingAny\");\n\nlet isDeletePressed = false;\nconst keydown = (e: KeyboardEvent) => e.key === \"Backspace\" && (isDeletePressed = true);\nconst keyup = (e: KeyboardEvent) => e.key === \"Backspace\" && (isDeletePressed = false);\nconst focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);\n\nconst settings = definePluginSettings({\n    enableDeleteOnClick: {\n        type: OptionType.BOOLEAN,\n        description: \"Enable delete on click while holding backspace\",\n        default: true\n    },\n    enableDoubleClickToEdit: {\n        type: OptionType.BOOLEAN,\n        description: \"Enable double click to edit\",\n        default: true\n    },\n    enableDoubleClickToReply: {\n        type: OptionType.BOOLEAN,\n        description: \"Enable double click to reply\",\n        default: true\n    },\n    requireModifier: {\n        type: OptionType.BOOLEAN,\n        description: \"Only do double click actions when shift/ctrl is held\",\n        default: false\n    }\n});\n\nexport default definePlugin({\n    name: \"MessageClickActions\",\n    description: \"Hold Backspace and click to delete, double click to edit/reply\",\n    authors: [Devs.Ven],\n\n    settings,\n\n    start() {\n        document.addEventListener(\"keydown\", keydown);\n        document.addEventListener(\"keyup\", keyup);\n        WindowStore.addChangeListener(focusChanged);\n    },\n\n    stop() {\n        document.removeEventListener(\"keydown\", keydown);\n        document.removeEventListener(\"keyup\", keyup);\n        WindowStore.removeChangeListener(focusChanged);\n    },\n\n    onMessageClick(msg, channel, event) {\n        const myId = AuthenticationStore.getId();\n        const isMe = msg.author.id === myId;\n        const isSelfInvokedUserApp = msg.interactionMetadata?.authorizing_integration_owners[ApplicationIntegrationType.USER_INSTALL] === myId;\n        if (!isDeletePressed) {\n            if (event.detail < 2) return;\n            if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;\n            if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;\n            if (msg.deleted === true) return;\n\n            if (isMe) {\n                if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== \"SENT\") return;\n\n                MessageActions.startEditMessage(channel.id, msg.id, msg.content);\n                event.preventDefault();\n            } else {\n                if (!settings.store.enableDoubleClickToReply) return;\n\n                if (!MessageTypeSets.REPLYABLE.has(msg.type) || msg.hasFlag(MessageFlags.EPHEMERAL)) return;\n\n                const isShiftPress = event.shiftKey && !settings.store.requireModifier;\n                const shouldMention = isPluginEnabled(NoReplyMentionPlugin.name)\n                    ? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)\n                    : !isShiftPress;\n\n                FluxDispatcher.dispatch({\n                    type: \"CREATE_PENDING_REPLY\",\n                    channel,\n                    message: msg,\n                    shouldMention,\n                    showMentionToggle: channel.guild_id !== null\n                });\n            }\n        } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) {\n            if (msg.deleted) {\n                FluxDispatcher.dispatch({\n                    type: \"MESSAGE_DELETE\",\n                    channelId: channel.id,\n                    id: msg.id,\n                    mlDeleted: true\n                });\n            } else {\n                MessageActions.deleteMessage(channel.id, msg.id);\n            }\n            event.preventDefault();\n        }\n    },\n});\n"
  },
  {
    "path": "src/plugins/messageLatency/README.md",
    "content": "# MessageLatency\n\nDisplays an indicator for messages that took ≥n seconds to send.\n\n> **NOTE**\n>\n> -   This plugin only applies to messages received after opening the channel\n> -   False positives can exist if the user's system clock has drifted.\n> -   Grouped messages only display latency of the first message\n\n## Demo\n\n### Chat View\n\n![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566)\n\n### Clock -ve Drift\n\n![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8)\n\n### Clock +ve Drift\n\n![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099)\n\n### Connection Delay\n\n![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2)\n\n### Icons\n\n![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85)\n"
  },
  {
    "path": "src/plugins/messageLatency/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { isNonNullish } from \"@utils/guards\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { AuthenticationStore, SnowflakeUtils, Tooltip } from \"@webpack/common\";\n\ntype FillValue = (\"status-danger\" | \"status-warning\" | \"status-positive\" | \"text-muted\");\ntype Fill = [FillValue, FillValue, FillValue];\ntype DiffKey = keyof Diff;\n\ninterface Diff {\n    days: number,\n    hours: number,\n    minutes: number,\n    seconds: number;\n    milliseconds: number;\n}\n\nconst DISCORD_KT_DELAY = 1471228928;\n\nexport default definePlugin({\n    name: \"MessageLatency\",\n    description: \"Displays an indicator for messages that took ≥n seconds to send\",\n    authors: [Devs.arHSM],\n\n    settings: definePluginSettings({\n        latency: {\n            type: OptionType.NUMBER,\n            description: \"Threshold in seconds for latency indicator\",\n            default: 2\n        },\n        detectDiscordKotlin: {\n            type: OptionType.BOOLEAN,\n            description: \"Detect old Discord Android clients\",\n            default: true\n        },\n        showMillis: {\n            type: OptionType.BOOLEAN,\n            description: \"Show milliseconds\",\n            default: false\n        },\n        ignoreSelf: {\n            type: OptionType.BOOLEAN,\n            description: \"Don't add indicator to your own messages\",\n            default: false\n        }\n    }),\n\n    patches: [\n        {\n            find: \"showCommunicationDisabledStyles\",\n            replacement: {\n                match: /(message:(\\i),avatar:\\i,username:\\(0,\\i.jsxs\\)\\(\\i.Fragment,\\{children:\\[)(\\i&&)/,\n                replace: \"$1$self.Tooltip()({ message: $2 }),$3\"\n            }\n        }\n    ],\n\n    stringDelta(delta: number, showMillis: boolean) {\n        const diff: Diff = {\n            days: Math.floor(delta / (60 * 60 * 24 * 1000)),\n            hours: Math.floor((delta / (60 * 60 * 1000)) % 24),\n            minutes: Math.floor((delta / (60 * 1000)) % 60),\n            seconds: Math.floor(delta / 1000 % 60),\n            milliseconds: Math.floor(delta % 1000)\n        };\n\n        const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;\n        const keys = Object.keys(diff) as DiffKey[];\n\n        const ts = keys.reduce((prev, k) => {\n            const s = str(k);\n\n            return prev + (\n                isNonNullish(s)\n                    ? (prev !== \"\"\n                        ? (showMillis ? k === \"milliseconds\" : k === \"seconds\")\n                            ? \" and \"\n                            : \" \"\n                        : \"\") + s\n                    : \"\"\n            );\n        }, \"\");\n\n        return ts || \"0 seconds\";\n    },\n\n    latencyTooltipData(message: Message) {\n        const { latency, detectDiscordKotlin, showMillis, ignoreSelf } = this.settings.store;\n        const { id, nonce } = message;\n\n        // Message wasn't received through gateway\n        if (!isNonNullish(nonce)) return null;\n\n        // Bots basically never send a nonce, and if someone does do it then it's usually not a snowflake\n        if (message.author.bot) return null;\n\n        if (ignoreSelf && message.author.id === AuthenticationStore.getId()) return null;\n\n        let isDiscordKotlin = false;\n        let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds\n        if (!showMillis) {\n            delta = Math.round(delta / 1000) * 1000;\n        }\n\n        // Old Discord Android clients have a delay of around 17 days\n        // This is a workaround for that\n        if (-delta >= DISCORD_KT_DELAY - 86400000) { // One day of padding for good measure\n            isDiscordKotlin = detectDiscordKotlin;\n            delta += DISCORD_KT_DELAY;\n        }\n\n        // Thanks dziurwa (I hate you)\n        // This is when the user's clock is ahead\n        // Can't do anything if the clock is behind\n        const abs = Math.abs(delta);\n        const ahead = abs !== delta;\n        const latencyMillis = latency * 1000;\n\n        const stringDelta = abs >= latencyMillis ? this.stringDelta(abs, showMillis) : null;\n\n        // Also thanks dziurwa\n        // 2 minutes\n        const TROLL_LIMIT = 2 * 60 * 1000;\n\n        const fill: Fill = isDiscordKotlin\n            ? [\"status-positive\", \"status-positive\", \"text-muted\"]\n            : delta >= TROLL_LIMIT || ahead\n                ? [\"text-muted\", \"text-muted\", \"text-muted\"]\n                : delta >= (latencyMillis * 2)\n                    ? [\"status-danger\", \"text-muted\", \"text-muted\"]\n                    : [\"status-warning\", \"status-warning\", \"text-muted\"];\n\n        return (abs >= latencyMillis || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;\n    },\n\n    Tooltip() {\n        return ErrorBoundary.wrap(({ message }: { message: Message; }) => {\n            const d = this.latencyTooltipData(message);\n\n            if (!isNonNullish(d)) return null;\n\n            let text: string;\n            if (!d.delta) {\n                text = \"User is suspected to be on an old Discord Android client\";\n            } else {\n                text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? \" User is suspected to be on an old Discord Android client.\" : \"\");\n            }\n\n            return <Tooltip\n                text={text}\n                position=\"top\"\n            >\n                {props => <this.Icon delta={d.delta} fill={d.fill} props={props} />}\n            </Tooltip>;\n        }, { noop: true });\n    },\n\n    Icon({ delta, fill, props }: {\n        delta: string | null;\n        fill: Fill,\n        props: {\n            onClick(): void;\n            onMouseEnter(): void;\n            onMouseLeave(): void;\n            onContextMenu(): void;\n            onFocus(): void;\n            onBlur(): void;\n            \"aria-label\"?: string;\n        };\n    }) {\n        return <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 16 16\"\n            width=\"12\"\n            height=\"12\"\n            role=\"img\"\n            fill=\"none\"\n            style={{ marginRight: \"8px\", verticalAlign: -1 }}\n            aria-label={delta ?? \"Old Discord Android client\"}\n            aria-hidden=\"false\"\n            {...props}\n        >\n            <path\n                fill={`var(--${fill[0]})`}\n                d=\"M4.8001 12C4.8001 11.5576 4.51344 11.2 4.16023 11.2H2.23997C1.88676 11.2 1.6001 11.5576 1.6001 12V13.6C1.6001 14.0424 1.88676 14.4 2.23997 14.4H4.15959C4.5128 14.4 4.79946 14.0424 4.79946 13.6L4.8001 12Z\"\n            />\n            <path\n                fill={`var(--${fill[1]})`}\n                d=\"M9.6001 7.12724C9.6001 6.72504 9.31337 6.39998 8.9601 6.39998H7.0401C6.68684 6.39998 6.40011 6.72504 6.40011 7.12724V13.6727C6.40011 14.0749 6.68684 14.4 7.0401 14.4H8.9601C9.31337 14.4 9.6001 14.0749 9.6001 13.6727V7.12724Z\"\n            />\n            <path\n                fill={`var(--${fill[2]})`}\n                d=\"M14.4001 2.31109C14.4001 1.91784 14.1134 1.59998 13.7601 1.59998H11.8401C11.4868 1.59998 11.2001 1.91784 11.2001 2.31109V13.6888C11.2001 14.0821 11.4868 14.4 11.8401 14.4H13.7601C14.1134 14.4 14.4001 14.0821 14.4001 13.6888V2.31109Z\"\n            />\n        </svg>;\n    }\n});\n"
  },
  {
    "path": "src/plugins/messageLinkEmbeds/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { addMessageAccessory, removeMessageAccessory } from \"@api/MessageAccessories\";\nimport { updateMessage } from \"@api/MessageUpdater\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport { Devs } from \"@utils/constants.js\";\nimport { classes } from \"@utils/misc\";\nimport { Queue } from \"@utils/Queue\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Channel, Message } from \"@vencord/discord-types\";\nimport { findComponentByCodeLazy, findComponentLazy, findCssClassesLazy } from \"@webpack\";\nimport {\n    Button,\n    ChannelStore,\n    Constants,\n    GuildStore,\n    IconUtils,\n    MessageStore,\n    Parser,\n    PermissionsBits,\n    PermissionStore,\n    RestAPI,\n    Text,\n    UserStore\n} from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nconst messageCache = new Map<string, {\n    message?: Message;\n    fetched: boolean;\n}>();\n\nconst Embed = findComponentLazy(m => m.prototype?.renderSuppressButton);\nconst AutoModEmbed = findComponentByCodeLazy(\"withFooter\", \"childrenMessageContent:\");\nconst ChannelMessage = findComponentByCodeLazy(\"childrenExecutedCommand:\", \".hideAccessories\");\n\nconst SearchResultClasses = findCssClassesLazy(\"message\", \"searchResult\");\nconst EmbedClasses = findCssClassesLazy(\"embedAuthorIcon\", \"embedAuthor\", \"embedAuthor\", \"embedMargin\");\n\nconst MessageDisplayCompact = getUserSettingLazy(\"textAndImages\", \"messageDisplayCompact\")!;\n\nconst messageLinkRegex = /(?<!<)https?:\\/\\/(?:\\w+\\.)?discord(?:app)?\\.com\\/channels\\/(?:\\d{17,20}|@me)\\/(\\d{17,20})\\/(\\d{17,20})/g;\nconst tenorRegex = /^https:\\/\\/(?:www\\.)?tenor\\.com\\//;\n\ninterface Attachment {\n    height: number;\n    width: number;\n    url: string;\n    proxyURL?: string;\n}\n\ninterface MessageEmbedProps {\n    message: Message;\n    channel: Channel;\n}\n\nconst messageFetchQueue = new Queue();\n\nconst settings = definePluginSettings({\n    messageBackgroundColor: {\n        description: \"Background color for messages in rich embeds\",\n        type: OptionType.BOOLEAN\n    },\n    automodEmbeds: {\n        description: \"Use automod embeds instead of rich embeds (smaller but less info)\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Always use automod embeds\",\n                value: \"always\"\n            },\n            {\n                label: \"Prefer automod embeds, but use rich embeds if some content can't be shown\",\n                value: \"prefer\"\n            },\n            {\n                label: \"Never use automod embeds\",\n                value: \"never\",\n                default: true\n            }\n        ]\n    },\n    listMode: {\n        description: \"Whether to use ID list as blacklist or whitelist\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Blacklist\",\n                value: \"blacklist\",\n                default: true\n            },\n            {\n                label: \"Whitelist\",\n                value: \"whitelist\"\n            }\n        ]\n    },\n    idList: {\n        description: \"Guild/channel/user IDs to blacklist or whitelist (separate with comma)\",\n        type: OptionType.STRING,\n        default: \"\",\n        multiline: true,\n    },\n    clearMessageCache: {\n        type: OptionType.COMPONENT,\n        component: () => (\n            <Button onClick={() => messageCache.clear()}>\n                Clear the linked message cache\n            </Button>\n        )\n    }\n});\n\n\nasync function fetchMessage(channelID: string, messageID: string) {\n    const cached = messageCache.get(messageID);\n    if (cached) return cached.message;\n\n    messageCache.set(messageID, { fetched: false });\n\n    const res = await RestAPI.get({\n        url: Constants.Endpoints.MESSAGES(channelID),\n        query: {\n            limit: 1,\n            around: messageID\n        },\n        retries: 2\n    }).catch(() => null);\n\n    const msg = res?.body?.[0];\n    if (!msg) return;\n\n    const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);\n    if (!message) return;\n\n    messageCache.set(message.id, {\n        message,\n        fetched: true\n    });\n\n    return message;\n}\n\n\nfunction getImages(message: Message): Attachment[] {\n    const attachments: Attachment[] = [];\n\n    for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {\n        if (content_type?.startsWith(\"image/\"))\n            attachments.push({\n                height: height!,\n                width: width!,\n                url: url,\n                proxyURL: proxy_url!\n            });\n    }\n\n    for (const { type, image, thumbnail, url } of message.embeds ?? []) {\n        if (type === \"image\")\n            attachments.push({ ...(image ?? thumbnail!) });\n        else if (url && type === \"gifv\" && !tenorRegex.test(url))\n            attachments.push({\n                height: thumbnail!.height,\n                width: thumbnail!.width,\n                url\n            });\n    }\n\n    return attachments;\n}\n\nfunction noContent(attachments: number, embeds: number) {\n    if (!attachments && !embeds) return \"\";\n    if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? \"s\" : \"\"}]`;\n    if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? \"s\" : \"\"}]`;\n    return `[no content, ${attachments} attachment${attachments !== 1 ? \"s\" : \"\"} and ${embeds} embed${embeds !== 1 ? \"s\" : \"\"}]`;\n}\n\nfunction requiresRichEmbed(message: Message) {\n    if (message.components.length) return true;\n    if (message.attachments.some(a => !a.content_type?.startsWith(\"image/\"))) return true;\n    if (message.embeds.some(e => e.type !== \"image\" && (e.type !== \"gifv\" || tenorRegex.test(e.url!)))) return true;\n\n    return false;\n}\n\nfunction computeWidthAndHeight(width: number, height: number) {\n    const maxWidth = 400;\n    const maxHeight = 300;\n\n    if (width > height) {\n        const adjustedWidth = Math.min(width, maxWidth);\n        return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };\n    }\n\n    const adjustedHeight = Math.min(height, maxHeight);\n    return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };\n}\n\nfunction withEmbeddedBy(message: Message, embeddedBy: string[]) {\n    return new Proxy(message, {\n        get(_, prop) {\n            if (prop === \"vencordEmbeddedBy\") return embeddedBy;\n            // @ts-expect-error ts so bad\n            return Reflect.get(...arguments);\n        }\n    });\n}\n\n\nfunction MessageEmbedAccessory({ message }: { message: Message; }) {\n    // @ts-expect-error\n    const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];\n\n    const accessories = [] as (JSX.Element | null)[];\n\n    for (const [_, channelID, messageID] of message.content!.matchAll(messageLinkRegex)) {\n        if (embeddedBy.includes(messageID) || embeddedBy.length > 2) {\n            continue;\n        }\n\n        const linkedChannel = ChannelStore.getChannel(channelID);\n        if (!linkedChannel || (!linkedChannel.isPrivate() && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, linkedChannel))) {\n            continue;\n        }\n\n        const { listMode, idList } = settings.store;\n\n        const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id));\n\n        if (listMode === \"blacklist\" && isListed) continue;\n        if (listMode === \"whitelist\" && !isListed) continue;\n\n        let linkedMessage = messageCache.get(messageID)?.message;\n        if (!linkedMessage) {\n            linkedMessage ??= MessageStore.getMessage(channelID, messageID);\n            if (linkedMessage) {\n                messageCache.set(messageID, { message: linkedMessage, fetched: true });\n            } else {\n\n                messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)\n                    .then(m => m && updateMessage(message.channel_id, message.id))\n                );\n                continue;\n            }\n        }\n\n        const messageProps: MessageEmbedProps = {\n            message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),\n            channel: linkedChannel\n        };\n\n        const type = settings.store.automodEmbeds;\n        accessories.push(\n            type === \"always\" || (type === \"prefer\" && !requiresRichEmbed(linkedMessage))\n                ? <AutomodEmbedAccessory {...messageProps} />\n                : <ChannelMessageEmbedAccessory {...messageProps} />\n        );\n    }\n\n    return accessories.length ? <>{accessories}</> : null;\n}\n\nfunction getChannelLabelAndIconUrl(channel: Channel) {\n    if (channel.isDM()) return [\"Direct Message\", IconUtils.getUserAvatarURL(UserStore.getUser(channel.recipients[0]))];\n    if (channel.isGroupDM()) return [\"Group DM\", IconUtils.getChannelIconURL(channel)];\n    return [\"Server\", IconUtils.getGuildIconURL(GuildStore.getGuild(channel.guild_id))];\n}\n\nfunction ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null {\n    const compact = MessageDisplayCompact.useSetting();\n\n    const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);\n\n    const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);\n\n    return (\n        <Embed\n            embed={{\n                rawDescription: \"\",\n                color: \"var(--background-base-lower)\",\n                author: {\n                    name: <Text variant=\"text-xs/medium\" tag=\"span\">\n                        <span>{channelLabel} - </span>\n                        {Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)}\n                    </Text>,\n                    iconProxyURL: iconUrl\n                }\n            }}\n            renderDescription={() => (\n                <div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>\n                    <ChannelMessage\n                        id={`message-link-embeds-${message.id}`}\n                        message={message}\n                        channel={channel}\n                        subscribeToComponentDispatch={false}\n                        compact={compact}\n                    />\n                </div>\n            )}\n        />\n    );\n}\n\nfunction AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {\n    const { message, channel } = props;\n    const compact = MessageDisplayCompact.useSetting();\n    const images = getImages(message);\n    const { parse } = Parser;\n\n    const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);\n\n    return <AutoModEmbed\n        channel={channel}\n        childrenAccessories={\n            <Text color=\"text-muted\" variant=\"text-xs/medium\" tag=\"span\" className={`${EmbedClasses.embedAuthor} ${EmbedClasses.embedMargin}`}>\n                {iconUrl && <img src={iconUrl} className={EmbedClasses.embedAuthorIcon} alt=\"\" />}\n                <span>\n                    <span>{channelLabel} - </span>\n                    {channel.isDM()\n                        ? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)\n                        : Parser.parse(`<#${channel.id}>`)\n                    }\n                </span>\n            </Text>\n        }\n        compact={compact}\n        content={\n            <>\n                {message.content || message.attachments.length <= images.length\n                    ? parse(message.content)\n                    : [noContent(message.attachments.length, message.embeds.length)]\n                }\n                {images.map((a, idx) => {\n                    const { width, height } = computeWidthAndHeight(a.width, a.height);\n                    return (\n                        <div key={idx}>\n                            <img src={a.url} width={width} height={height} />\n                        </div>\n                    );\n                })}\n            </>\n        }\n        hideTimestamp={false}\n        message={message}\n        _messageEmbed=\"automod\"\n    />;\n}\n\nexport default definePlugin({\n    name: \"MessageLinkEmbeds\",\n    description: \"Adds a preview to messages that link another message\",\n    authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],\n    dependencies: [\"MessageAccessoriesAPI\", \"MessageUpdaterAPI\", \"UserSettingsAPI\"],\n\n    settings,\n\n    start() {\n        addMessageAccessory(\"MessageLinkEmbeds\", props => {\n            if (!messageLinkRegex.test(props.message.content))\n                return null;\n\n            // need to reset the regex because it's global\n            messageLinkRegex.lastIndex = 0;\n\n            return (\n                <MessageEmbedAccessory\n                    message={props.message}\n                />\n            );\n        }, 4 /* just above rich embeds */);\n    },\n\n    stop() {\n        removeMessageAccessory(\"MessageLinkEmbeds\");\n    }\n});\n"
  },
  {
    "path": "src/plugins/messageLogger/HistoryModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { TooltipContainer } from \"@components/TooltipContainer\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Margins } from \"@utils/margins\";\nimport { classes } from \"@utils/misc\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { TabBar, Text, Timestamp, useState } from \"@webpack/common\";\n\nimport { parseEditContent } from \".\";\n\nconst CodeContainerClasses = findCssClassesLazy(\"markup\", \"codeContainer\");\nconst MiscClasses = findCssClassesLazy(\"messageContent\", \"markupRtl\");\n\nconst cl = classNameFactory(\"vc-ml-modal-\");\n\nexport function openHistoryModal(message: any) {\n    openModal(props =>\n        <ErrorBoundary>\n            <HistoryModal\n                modalProps={props}\n                message={message}\n            />\n        </ErrorBoundary>\n    );\n}\n\nexport function HistoryModal({ modalProps, message }: { modalProps: ModalProps; message: any; }) {\n    const [currentTab, setCurrentTab] = useState(message.editHistory.length);\n    const timestamps = [message.firstEditTimestamp, ...message.editHistory.map(m => m.timestamp)];\n    const contents = [...message.editHistory.map(m => m.content), message.content];\n\n    return (\n        <ModalRoot {...modalProps} size={ModalSize.LARGE}>\n            <ModalHeader className={cl(\"head\")}>\n                <Text variant=\"heading-lg/semibold\" style={{ flexGrow: 1 }}>Message Edit History</Text>\n                <ModalCloseButton onClick={modalProps.onClose} />\n            </ModalHeader>\n\n            <ModalContent className={cl(\"contents\")}>\n                <TabBar\n                    type=\"top\"\n                    look=\"brand\"\n                    className={classes(\"vc-settings-tab-bar\", cl(\"tab-bar\"))}\n                    selectedItem={currentTab}\n                    onItemSelect={setCurrentTab}\n                >\n                    {message.firstEditTimestamp.getTime() !== message.timestamp.getTime() && (\n                        <TooltipContainer text=\"This edit state was not logged so it can't be displayed.\">\n                            <TabBar.Item\n                                className=\"vc-settings-tab-bar-item\"\n                                id={-1}\n                                disabled\n                            >\n                                <Timestamp\n                                    className={cl(\"timestamp\")}\n                                    timestamp={message.timestamp}\n                                    isEdited={true}\n                                    isInline={false}\n                                />\n                            </TabBar.Item>\n                        </TooltipContainer>\n                    )}\n\n                    {timestamps.map((timestamp, index) => (\n                        <TabBar.Item\n                            key={index}\n                            className=\"vc-settings-tab-bar-item\"\n                            id={index}\n                        >\n                            <Timestamp\n                                className={cl(\"timestamp\")}\n                                timestamp={timestamp}\n                                isEdited={true}\n                                isInline={false}\n                            />\n                        </TabBar.Item>\n                    ))}\n                </TabBar>\n\n                <div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>\n                    {parseEditContent(contents[currentTab], message)}\n                </div>\n            </ModalContent>\n        </ModalRoot>\n    );\n}\n"
  },
  {
    "path": "src/plugins/messageLogger/deleteStyleOverlay.css",
    "content": ".messagelogger-deleted {\n    background-color: hsl(var(--red-430-hsl, 0 85% 61%) / 15%) !important;\n}"
  },
  {
    "path": "src/plugins/messageLogger/deleteStyleText.css",
    "content": ".messagelogger-deleted {\n    --text-default: var(--status-danger, #f04747);\n    --interactive-icon-default: var(--status-danger, #f04747);\n    --text-muted: var(--status-danger, #f04747);\n    --embed-title: var(--red-460, #be3535);\n    --text-link: var(--red-460, #be3535);\n    --text-strong: var(--red-460, #be3535);\n}"
  },
  {
    "path": "src/plugins/messageLogger/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./messageLogger.css\";\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { updateMessage } from \"@api/MessageUpdater\";\nimport { Settings } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs, SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from \"@utils/constants\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { Logger } from \"@utils/Logger\";\nimport { classes } from \"@utils/misc\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from \"@webpack/common\";\n\nimport overlayStyle from \"./deleteStyleOverlay.css?managed\";\nimport textStyle from \"./deleteStyleText.css?managed\";\nimport { openHistoryModal } from \"./HistoryModal\";\n\ninterface MLMessage extends Message {\n    deleted?: boolean;\n    editHistory?: { timestamp: Date; content: string; }[];\n    firstEditTimestamp?: Date;\n}\n\nconst MessageClasses = findCssClassesLazy(\"edited\", \"communicationDisabled\", \"isSystemMessage\");\n\nfunction addDeleteStyle() {\n    if (Settings.plugins.MessageLogger.deleteStyle === \"text\") {\n        enableStyle(textStyle);\n        disableStyle(overlayStyle);\n    } else {\n        disableStyle(textStyle);\n        enableStyle(overlayStyle);\n    }\n}\n\nconst REMOVE_HISTORY_ID = \"ml-remove-history\";\nconst TOGGLE_DELETE_STYLE_ID = \"ml-toggle-style\";\nconst patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {\n    const { message } = props;\n    const { deleted, editHistory, id, channel_id } = message;\n\n    if (!deleted && !editHistory?.length) return;\n\n    toggle: {\n        if (!deleted) break toggle;\n\n        const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`);\n        if (!domElement) break toggle;\n\n        children.push((\n            <Menu.MenuItem\n                id={TOGGLE_DELETE_STYLE_ID}\n                key={TOGGLE_DELETE_STYLE_ID}\n                label=\"Toggle Deleted Highlight\"\n                action={() => domElement.classList.toggle(\"messagelogger-deleted\")}\n            />\n        ));\n    }\n\n    children.push((\n        <Menu.MenuItem\n            id={REMOVE_HISTORY_ID}\n            key={REMOVE_HISTORY_ID}\n            label=\"Remove Message History\"\n            color=\"danger\"\n            action={() => {\n                if (deleted) {\n                    FluxDispatcher.dispatch({\n                        type: \"MESSAGE_DELETE\",\n                        channelId: channel_id,\n                        id,\n                        mlDeleted: true\n                    });\n                } else {\n                    message.editHistory = [];\n                }\n            }}\n        />\n    ));\n};\n\nconst patchChannelContextMenu: NavContextMenuPatchCallback = (children, { channel }) => {\n    const messages = MessageStore.getMessages(channel?.id) as MLMessage[];\n    if (!messages?.some(msg => msg.deleted || msg.editHistory?.length)) return;\n\n    const group = findGroupChildrenByChildId(\"mark-channel-read\", children) ?? children;\n    group.push(\n        <Menu.MenuItem\n            id=\"vc-ml-clear-channel\"\n            label=\"Clear Message Log\"\n            color=\"danger\"\n            action={() => {\n                messages.forEach(msg => {\n                    if (msg.deleted)\n                        FluxDispatcher.dispatch({\n                            type: \"MESSAGE_DELETE\",\n                            channelId: channel.id,\n                            id: msg.id,\n                            mlDeleted: true\n                        });\n                    else\n                        updateMessage(channel.id, msg.id, {\n                            editHistory: []\n                        });\n                });\n            }}\n        />\n    );\n};\n\nexport function parseEditContent(content: string, message: Message) {\n    return Parser.parse(content, true, {\n        channelId: message.channel_id,\n        messageId: message.id,\n        allowLinks: true,\n        allowHeading: true,\n        allowList: true,\n        allowEmojiLinks: true,\n        viewingChannelId: SelectedChannelStore.getChannelId(),\n    });\n}\n\nexport default definePlugin({\n    name: \"MessageLogger\",\n    description: \"Temporarily logs deleted and edited messages.\",\n    authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi],\n    dependencies: [\"MessageUpdaterAPI\"],\n\n    contextMenus: {\n        \"message\": patchMessageContextMenu,\n        \"channel-context\": patchChannelContextMenu,\n        \"thread-context\": patchChannelContextMenu,\n        \"user-context\": patchChannelContextMenu,\n        \"gdm-context\": patchChannelContextMenu\n    },\n\n    start() {\n        addDeleteStyle();\n    },\n\n    renderEdits: ErrorBoundary.wrap(({ message: { id: messageId, channel_id: channelId } }: { message: Message; }) => {\n        const message = useStateFromStores(\n            [MessageStore],\n            () => MessageStore.getMessage(channelId, messageId) as MLMessage,\n            null,\n            (oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory\n        );\n\n        return Settings.plugins.MessageLogger.inlineEdits && (\n            <>\n                {message.editHistory?.map((edit, idx) => (\n                    <div key={idx} className=\"messagelogger-edited\">\n                        {parseEditContent(edit.content, message)}\n                        <Timestamp\n                            timestamp={edit.timestamp}\n                            isEdited={true}\n                            isInline={false}\n                        >\n                            <span className={MessageClasses.edited}>{\" \"}({getIntlMessage(\"MESSAGE_EDITED\")})</span>\n                        </Timestamp>\n                    </div>\n                ))}\n            </>\n        );\n    }, { noop: true }),\n\n    makeEdit(newMessage: any, oldMessage: any): any {\n        return {\n            timestamp: new Date(newMessage.edited_timestamp),\n            content: oldMessage.content\n        };\n    },\n\n    options: {\n        deleteStyle: {\n            type: OptionType.SELECT,\n            description: \"The style of deleted messages\",\n            default: \"text\",\n            options: [\n                { label: \"Red text\", value: \"text\", default: true },\n                { label: \"Red overlay\", value: \"overlay\" }\n            ],\n            onChange: () => addDeleteStyle()\n        },\n        logDeletes: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to log deleted messages\",\n            default: true,\n        },\n        collapseDeleted: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to collapse deleted messages, similar to blocked messages\",\n            default: false,\n            restartNeeded: true,\n        },\n        logEdits: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to log edited messages\",\n            default: true,\n        },\n        inlineEdits: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to display edit history as part of message content\",\n            default: true\n        },\n        ignoreBots: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to ignore messages by bots\",\n            default: false\n        },\n        ignoreSelf: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to ignore messages by yourself\",\n            default: false\n        },\n        ignoreUsers: {\n            type: OptionType.STRING,\n            description: \"Comma-separated list of user IDs to ignore\",\n            default: \"\",\n            multiline: true\n        },\n        ignoreChannels: {\n            type: OptionType.STRING,\n            description: \"Comma-separated list of channel IDs to ignore\",\n            default: \"\",\n            multiline: true\n        },\n        ignoreGuilds: {\n            type: OptionType.STRING,\n            description: \"Comma-separated list of guild IDs to ignore\",\n            default: \"\",\n            multiline: true\n        },\n    },\n\n    handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) {\n        try {\n            if (cache == null || (!isBulk && !cache.has(data.id))) return cache;\n\n            const mutate = (id: string) => {\n                const msg = cache.get(id);\n                if (!msg) return;\n\n                const EPHEMERAL = 64;\n                const shouldIgnore = data.mlDeleted ||\n                    (msg.flags & EPHEMERAL) === EPHEMERAL ||\n                    this.shouldIgnore(msg);\n\n                if (shouldIgnore) {\n                    cache = cache.remove(id);\n                } else {\n                    cache = cache.update(id, m => m\n                        .set(\"deleted\", true)\n                        .set(\"attachments\", m.attachments.map(a => (a.deleted = true, a))));\n                }\n            };\n\n            if (isBulk) {\n                data.ids.forEach(mutate);\n            } else {\n                mutate(data.id);\n            }\n        } catch (e) {\n            new Logger(\"MessageLogger\").error(\"Error during handleDelete\", e);\n        }\n        return cache;\n    },\n\n    shouldIgnore(message: any, isEdit = false) {\n        try {\n            const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;\n            const myId = UserStore.getCurrentUser().id;\n\n            return ignoreBots && message.author?.bot ||\n                ignoreSelf && message.author?.id === myId ||\n                ignoreUsers.includes(message.author?.id) ||\n                ignoreChannels.includes(message.channel_id) ||\n                ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||\n                (isEdit ? !logEdits : !logDeletes) ||\n                ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||\n                // Ignore Venbot in the support channels\n                (message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === SUPPORT_CATEGORY_ID);\n        } catch (e) {\n            return false;\n        }\n    },\n\n    EditMarker({ message, className, children, ...props }: any) {\n        return (\n            <span\n                {...props}\n                className={classes(\"messagelogger-edit-marker\", className)}\n                onClick={() => openHistoryModal(message)}\n                role=\"button\"\n            >\n                {children}\n            </span>\n        );\n    },\n\n    // DELETED_MESSAGE_COUNT: getMessage(\"{count, plural, =0 {No deleted messages} one {{count} deleted message} other {{count} deleted messages}}\")\n    // TODO: Find a better way to generate intl messages\n    DELETED_MESSAGE_COUNT: () => ({\n        ast: [[\n            6,\n            \"count\",\n            {\n                \"=0\": [\"No deleted messages\"],\n                one: [\n                    [\n                        1,\n                        \"count\"\n                    ],\n                    \" deleted message\"\n                ],\n                other: [\n                    [\n                        1,\n                        \"count\"\n                    ],\n                    \" deleted messages\"\n                ]\n            },\n            0,\n            \"cardinal\"\n        ]]\n    }),\n\n    patches: [\n        {\n            // MessageStore\n            find: '\"MessageStore\"',\n            replacement: [\n                {\n                    // Add deleted=true to all target messages in the MESSAGE_DELETE event\n                    match: /function (?=.+?MESSAGE_DELETE:(\\i))\\1\\((\\i)\\){let.+?((?:\\i\\.){2})getOrCreate.+?}(?=function)/,\n                    replace:\n                        \"function $1($2){\" +\n                        \"   var cache = $3getOrCreate($2.channelId);\" +\n                        \"   cache = $self.handleDelete(cache, $2, false);\" +\n                        \"   $3commit(cache);\" +\n                        \"}\"\n                },\n                {\n                    // Add deleted=true to all target messages in the MESSAGE_DELETE_BULK event\n                    match: /function (?=.+?MESSAGE_DELETE_BULK:(\\i))\\1\\((\\i)\\){let.+?((?:\\i\\.){2})getOrCreate.+?}(?=function)/,\n                    replace:\n                        \"function $1($2){\" +\n                        \"   var cache = $3getOrCreate($2.channelId);\" +\n                        \"   cache = $self.handleDelete(cache, $2, true);\" +\n                        \"   $3commit(cache);\" +\n                        \"}\"\n                },\n                {\n                    // Add current cached content + new edit time to cached message's editHistory\n                    match: /(function (\\i)\\((\\i)\\).+?)\\.update\\((\\i)(?=.*MESSAGE_UPDATE:\\2)/,\n                    replace: \"$1\" +\n                        \".update($4,m =>\" +\n                        \"   (($3.message.flags & 64) === 64 || $self.shouldIgnore($3.message, true)) ? m :\" +\n                        \"   $3.message.edited_timestamp && $3.message.content !== m.content ?\" +\n                        \"       m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($3.message, m)]) :\" +\n                        \"       m\" +\n                        \")\" +\n                        \".update($4\"\n                },\n                {\n                    // fix up key (edit last message) attempting to edit a deleted message\n                    match: /(?<=getLastEditableMessage\\(\\i\\)\\{.{0,200}\\.find\\((\\i)=>)/,\n                    replace: \"!$1.deleted &&\"\n                }\n            ]\n        },\n\n        {\n            // Message domain model\n            find: \"}addReaction(\",\n            replacement: [\n                {\n                    match: /this\\.customRenderedContent=(\\i)\\.customRenderedContent,/,\n                    replace: \"this.customRenderedContent = $1.customRenderedContent,\" +\n                        \"this.deleted = $1.deleted || false,\" +\n                        \"this.editHistory = $1.editHistory || [],\" +\n                        \"this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp,\"\n                }\n            ]\n        },\n\n        {\n            // Updated message transformer(?)\n            find: \".PREMIUM_REFERRAL&&(\",\n            replacement: [\n                {\n                    // Pass through editHistory & deleted & original attachments to the \"edited message\" transformer\n                    match: /(?<=null!=\\i\\.edited_timestamp\\)return )\\i\\(\\i,\\{reactions:(\\i)\\.reactions.{0,50}\\}\\)/,\n                    replace:\n                        \"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp })\"\n                },\n\n                {\n                    // Construct new edited message and add editHistory & deleted (ref above)\n                    // Pass in custom data to attachment parser to mark attachments deleted as well\n                    match: /attachments:(\\i)\\((\\i)\\)/,\n                    replace:\n                        \"attachments: $1((() => {\" +\n                        \"   if ($self.shouldIgnore($2)) return $2;\" +\n                        \"   let old = arguments[1]?.attachments;\" +\n                        \"   if (!old) return $2;\" +\n                        \"   let new_ = $2.attachments?.map(a => a.id) ?? [];\" +\n                        \"   let diff = old.filter(a => !new_.includes(a.id));\" +\n                        \"   old.forEach(a => a.deleted = true);\" +\n                        \"   $2.attachments = [...diff, ...$2.attachments];\" +\n                        \"   return $2;\" +\n                        \"})()),\" +\n                        \"deleted: arguments[1]?.deleted,\" +\n                        \"editHistory: arguments[1]?.editHistory,\" +\n                        \"firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)\"\n                },\n                {\n                    // Preserve deleted attribute on attachments\n                    match: /(\\((\\i)\\){return null==\\2\\.attachments.+?)spoiler:/,\n                    replace:\n                        \"$1deleted: arguments[0]?.deleted,\" +\n                        \"spoiler:\"\n                }\n            ]\n        },\n\n        {\n            // Attachment renderer\n            find: \"#{intl::REMOVE_ATTACHMENT_TOOLTIP_TEXT}\",\n            replacement: [\n                {\n                    match: /\\.SPOILER,(?=\\[\\i\\.\\i\\]:)/,\n                    replace: '$&\"messagelogger-deleted-attachment\":arguments[0]?.item?.originalItem?.deleted,'\n                }\n            ]\n        },\n\n        {\n            // Base message component renderer\n            find: \"Message must not be a thread starter message\",\n            replacement: [\n                {\n                    // Append messagelogger-deleted to classNames if deleted\n                    match: /\\)\\(\"li\",\\{(.+?),className:/,\n                    replace: \")(\\\"li\\\",{$1,className:(arguments[0].message.deleted ? \\\"messagelogger-deleted \\\" : \\\"\\\")+\"\n                }\n            ]\n        },\n\n        {\n            // Message content renderer\n            find: \".SEND_FAILED,\",\n            replacement: {\n                // Render editHistory behind the message content\n                match: /\\]:\\i.isUnsupported.{0,20}?,children:\\[/,\n                replace: \"$&arguments[0]?.message?.editHistory?.length>0&&$self.renderEdits(arguments[0]),\"\n            }\n        },\n\n        {\n            find: \"#{intl::MESSAGE_EDITED}\",\n            replacement: {\n                // Make edit marker clickable\n                match: /(isInline:!1,children:.{0,50}?)\"span\",\\{(?=className:)/,\n                replace: \"$1$self.EditMarker,{message:arguments[0].message,\"\n            }\n        },\n\n        {\n            // ReferencedMessageStore\n            find: '\"ReferencedMessageStore\"',\n            replacement: [\n                {\n                    match: /MESSAGE_DELETE:\\i,/,\n                    replace: \"MESSAGE_DELETE:()=>{},\"\n                },\n                {\n                    match: /MESSAGE_DELETE_BULK:\\i,/,\n                    replace: \"MESSAGE_DELETE_BULK:()=>{},\"\n                }\n            ]\n        },\n\n        {\n            // Message context base menu\n            find: \".MESSAGE,commandTargetId:\",\n            replacement: [\n                {\n                    // Remove the first section if message is deleted\n                    match: /children:(\\[\"\"===.+?\\])/,\n                    replace: \"children:arguments[0].message.deleted?[]:$1\"\n                }\n            ]\n        },\n        {\n            // Message grouping\n            find: \"NON_COLLAPSIBLE.has(\",\n            replacement: {\n                match: /if\\((\\i)\\.blocked\\)return \\i\\.\\i\\.MESSAGE_GROUP_BLOCKED;/,\n                replace: '$&else if($1.deleted) return\"MESSAGE_GROUP_DELETED\";',\n            },\n            predicate: () => Settings.plugins.MessageLogger.collapseDeleted\n        },\n        {\n            // Message group rendering\n            find: \"#{intl::NEW_MESSAGES_ESTIMATED_WITH_DATE}\",\n            replacement: [\n                {\n                    match: /(\\i).type===\\i\\.\\i\\.MESSAGE_GROUP_BLOCKED\\|\\|/,\n                    replace: '$&$1.type===\"MESSAGE_GROUP_DELETED\"||',\n                },\n                {\n                    match: /(\\i).type===\\i\\.\\i\\.MESSAGE_GROUP_BLOCKED\\?.*?:/,\n                    replace: '$&$1.type===\"MESSAGE_GROUP_DELETED\"?$self.DELETED_MESSAGE_COUNT:',\n                },\n            ],\n            predicate: () => Settings.plugins.MessageLogger.collapseDeleted\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/messageLogger/messageLogger.css",
    "content": ".messagelogger-deleted [class*=\"buttons\"] {\n    display: none;\n}\n\n.messagelogger-deleted\n:is(\n    .messagelogger-deleted-attachment,\n    .emoji,\n    [data-type=\"sticker\"],\n    [class*=\"embedIframe\"],\n    [class*=\"embedSpotify\"],\n    [class*=\"imageContainer\"]\n) {\n    filter: grayscale(1) !important;\n    transition: 150ms filter ease-in-out;\n\n    &[class*=\"hiddenMosaicItem\"] {\n        filter: grayscale(1) blur(var(--custom-message-attachment-spoiler-blur-radius, 44px)) !important;\n    }\n\n    &:hover {\n        filter: grayscale(0) !important;\n    }\n}\n\n.messagelogger-deleted [class*=\"spoilerWarning\"] {\n    color: var(--status-danger);\n}\n\n.theme-dark .messagelogger-edited {\n    filter: brightness(80%);\n}\n\n.theme-light .messagelogger-edited {\n    opacity: 0.5;\n}\n\n.messagelogger-edit-marker {\n    cursor: pointer;\n}\n\n.vc-ml-modal-timestamp {\n    cursor: unset;\n    height: unset;\n}\n\n.vc-ml-modal-tab-bar {\n    flex-wrap: wrap;\n    gap: 16px;\n}\n"
  },
  {
    "path": "src/plugins/moreQuickReactions/README.md",
    "content": "# MoreQuickReactions\n\nIncreases the number of reactions available in the Quick React hover menu.\n\nYou can change the number of reactions shown in the plugin settings.\n\n![](https://github.com/user-attachments/assets/957defe3-509f-4e1c-8dd8-b1bdfdf74172)\n"
  },
  {
    "path": "src/plugins/moreQuickReactions/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    reactionCount: {\n        description: \"Number of reactions (0-42)\",\n        type: OptionType.NUMBER,\n        default: 5\n    },\n});\n\nexport default definePlugin({\n    name: \"MoreQuickReactions\",\n    description: \"Increases the number of reactions available in the Quick React hover menu\",\n    authors: [Devs.iamme],\n    settings,\n\n    get reactionCount() {\n        return settings.store.reactionCount;\n    },\n\n    patches: [\n        {\n            find: \"#{intl::MESSAGE_UTILITIES_A11Y_LABEL}\",\n            replacement: {\n                match: /(?<=length>=3\\?.{0,40})\\.slice\\(0,3\\)/,\n                replace: \".slice(0,$self.reactionCount)\"\n            }\n        }\n    ],\n});\n"
  },
  {
    "path": "src/plugins/mutualGroupDMs/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { BaseText } from \"@components/BaseText\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { isNonNullish } from \"@utils/guards\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin from \"@utils/types\";\nimport { Channel, User } from \"@vencord/discord-types\";\nimport { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from \"@webpack\";\nimport { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, Text, useMemo, UserStore } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nconst SelectedChannelActionCreators = findByPropsLazy(\"selectPrivateChannel\");\nconst UserUtils = findByPropsLazy(\"getGlobalName\");\n\nconst ProfileListClasses = findCssClassesLazy(\"empty\", \"textContainer\", \"connectionIcon\");\nconst TabBarClasses = findCssClassesLazy(\"tabPanelScroller\", \"tabBarPanel\");\nconst MutualsListClasses = findCssClassesLazy(\"row\", \"icon\", \"name\", \"details\");\nconst ExpandableList = findComponentByCodeLazy('action:\"PRESS_SECTION\"', \"section\");\n\nfunction getGroupDMName(channel: Channel) {\n    return channel.name ||\n        channel.recipients\n            .map(UserStore.getUser)\n            .filter(isNonNullish)\n            .map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c))\n            .join(\", \");\n}\n\nconst getMutualGroupDms = (userId: string) =>\n    ChannelStore.getSortedPrivateChannels()\n        .filter(c => c.isGroupDM() && c.recipients.includes(userId));\n\nconst isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;\n\nfunction getMutualGDMCountText(user: User) {\n    const count = getMutualGroupDms(user.id).length;\n    return `${count === 0 ? \"No\" : count} Mutual Group${count !== 1 ? \"s\" : \"\"}`;\n}\n\nfunction renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {\n    return mutualDms.map(c => (\n        <Clickable\n            key={c.id}\n            className={MutualsListClasses.row}\n            onClick={() => {\n                onClose();\n                SelectedChannelActionCreators.selectPrivateChannel(c.id);\n            }}\n        >\n            <Avatar\n                src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}\n                size=\"SIZE_40\"\n                className={MutualsListClasses.icon}\n            >\n            </Avatar>\n            <div className={MutualsListClasses.details}>\n                <div className={MutualsListClasses.name}>{getGroupDMName(c)}</div>\n                <Text variant=\"text-xs/medium\">{c.recipients.length + 1} Members</Text>\n            </div>\n        </Clickable>\n    ));\n}\n\nconst IS_PATCHED = Symbol(\"MutualGroupDMs.Patched\");\n\nexport default definePlugin({\n    name: \"MutualGroupDMs\",\n    description: \"Shows mutual group dms in profiles\",\n    authors: [Devs.amia],\n\n    patches: [\n        // User Profile Modal\n        {\n            find: \".BOT_DATA_ACCESS?(\",\n            replacement: [\n                {\n                    match: /\\i\\.useEffect.{0,100}(\\i)\\[0\\]\\.section/,\n                    replace: \"$self.pushSection($1,arguments[0].user);$&\"\n                },\n                {\n                    match: /\\(0,\\i\\.jsx\\)\\(\\i,\\{items:\\i,section:(\\i)/,\n                    replace: \"$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&\"\n                },\n                // Discord adds spacing between each item which pushes our tab off screen.\n                // set the gap to zero to ensure ours stays on screen\n                {\n                    match: /className:\\i\\.\\i(?=,type:\"top\")/,\n                    replace: '$& + \" vc-mutual-gdms-modal-tab-bar\"'\n                }\n            ]\n        },\n        // User Profile Modal v2\n        {\n            find: \".WIDGETS?\",\n            replacement: [\n                {\n                    match: /items:(\\i),.+?(?=return\\(0,\\i\\.jsxs?\\)\\(\"div)/,\n                    replace: \"$&$self.pushSection($1,arguments[0].user);\"\n                },\n                {\n                    match: /children:(?=.{0,100}?component:.+?section:(\\i))/,\n                    replace: \"$&$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):\"\n                },\n                // Make the gap between each item smaller so our tab can fit.\n                {\n                    match: /type:\"top\",/,\n                    replace: '$&className:\"vc-mutual-gdms-modal-v2-tab-bar\",'\n                },\n            ]\n        },\n        {\n            find: 'section:\"MUTUAL_FRIENDS\"',\n            replacement: [\n                {\n                    match: /\\i\\|\\|\\i(?=\\?\\(0,\\i\\.jsxs?\\)\\(\\i\\.\\i\\.Overlay,)/,\n                    replace: \"$&||$self.getMutualGroupDms(arguments[0].user.id).length>0\"\n                },\n                {\n                    match: /\\.openUserProfileModal.+?\\)}\\)}\\)(?<=,(\\i)&&(\\i)&&(\\(0,\\i\\.jsxs?\\)\\(\\i\\.\\i,{className:(\\i)\\.\\i}\\)).{0,50}?\"MUTUAL_FRIENDS\".+?)/,\n                    replace: (m, hasMutualGuilds, hasMutualFriends, Divider, classes) => \"\" +\n                        `${m},$self.renderDMPageList({user:arguments[0].user,hasDivider:${hasMutualGuilds}||${hasMutualFriends},Divider:${Divider},listStyle:${classes}.list})`\n                }\n            ]\n        }\n    ],\n\n    getMutualGroupDms(userId: string) {\n        try {\n            return getMutualGroupDms(userId);\n        } catch (e) {\n            new Logger(\"MutualGroupDMs\").error(\"Failed to get mutual group dms:\", e);\n        }\n\n        return [];\n    },\n\n    pushSection(sections: any[], user: User) {\n        try {\n            if (isBotOrSelf(user) || sections[IS_PATCHED]) return;\n\n            sections[IS_PATCHED] = true;\n            sections.push({\n                text: getMutualGDMCountText(user),\n                section: \"MUTUAL_GDMS\",\n            });\n        } catch (e) {\n            new Logger(\"MutualGroupDMs\").error(\"Failed to push mutual group dms section:\", e);\n        }\n    },\n\n    renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {\n        const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);\n        const entries = renderClickableGDMs(mutualGDms, onClose);\n\n        return (\n            <ScrollerThin\n                className={TabBarClasses.tabPanelScroller}\n                fade={true}\n                onClose={onClose}\n            >\n                {entries.length > 0\n                    ? entries\n                    : (\n                        <div className={ProfileListClasses.empty}>\n                            <div className={ProfileListClasses.textContainer}>\n                                <BaseText tag=\"h3\" size=\"md\" weight=\"medium\" style={{ color: \"var(--text-strong)\" }}>You don't have any group chats in common</BaseText>\n                            </div>\n                        </div>\n                    )\n                }\n            </ScrollerThin>\n        );\n    }),\n\n    renderDMPageList: ErrorBoundary.wrap(({ user, hasDivider, Divider, listStyle }: { user: User, hasDivider: boolean, Divider: JSX.Element, listStyle: string; }) => {\n        const mutualGDms = getMutualGroupDms(user.id);\n        if (mutualGDms.length === 0) return null;\n\n        return (\n            <>\n                {hasDivider && Divider}\n                <ExpandableList\n                    listClassName={listStyle}\n                    header={\"Mutual Groups\"}\n                    isLoading={false}\n                    items={renderClickableGDMs(mutualGDms, () => { })}\n                />\n            </>\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/mutualGroupDMs/style.css",
    "content": ".vc-mutual-gdms-modal-tab-bar {\n    gap: 0;\n}\n\n.vc-mutual-gdms-modal-v2-tab-bar {\n    --space-xl: 16px;\n}\n"
  },
  {
    "path": "src/plugins/newGuildSettings/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport {\n    findGroupChildrenByChildId,\n    NavContextMenuPatchCallback\n} from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { CogWheel } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Guild } from \"@vencord/discord-types\";\nimport { findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from \"@webpack\";\nimport { Menu, UserStore } from \"@webpack/common\";\n\nconst { updateGuildNotificationSettings } = findByPropsLazy(\"updateGuildNotificationSettings\");\nconst { toggleShowAllChannels } = mapMangledModuleLazy(\".onboardExistingMember(\", {\n    toggleShowAllChannels: m => {\n        const s = String(m);\n        return s.length < 100 && !s.includes(\"onboardExistingMember\") && !s.includes(\"getOptedInChannels\");\n    }\n});\nconst isOptInEnabledForGuild = findByCodeLazy(\".COMMUNITY)||\", \".isOptInEnabled(\");\n\nconst settings = definePluginSettings({\n    guild: {\n        description: \"Mute Guild automatically\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    messages: {\n        description: \"Server Notification Settings\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"All messages\", value: 0 },\n            { label: \"Only @mentions\", value: 1 },\n            { label: \"Nothing\", value: 2 },\n            { label: \"Server default\", value: 3, default: true }\n        ],\n    },\n    everyone: {\n        description: \"Suppress @everyone and @here\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    role: {\n        description: \"Suppress All Role @mentions\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    highlights: {\n        description: \"Suppress Highlights automatically\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    events: {\n        description: \"Mute New Events automatically\",\n        type: OptionType.BOOLEAN,\n        default: true\n    },\n    showAllChannels: {\n        description: \"Show all channels automatically\",\n        type: OptionType.BOOLEAN,\n        default: true\n    }\n});\n\nconst makeContextMenuPatch: (shouldAddIcon: boolean) => NavContextMenuPatchCallback = (shouldAddIcon: boolean) => (children, { guild }: { guild: Guild, onClose(): void; }) => {\n    if (!guild) return;\n\n    const group = findGroupChildrenByChildId(\"privacy\", children);\n    group?.push(\n        <Menu.MenuItem\n            label=\"Apply NewGuildSettings\"\n            id=\"vc-newguildsettings-apply\"\n            icon={shouldAddIcon ? CogWheel : void 0}\n            action={() => applyDefaultSettings(guild.id)}\n        />\n    );\n};\n\nfunction applyDefaultSettings(guildId: string | null) {\n    if (guildId === \"@me\" || guildId === \"null\" || guildId == null) return;\n    updateGuildNotificationSettings(guildId,\n        {\n            muted: settings.store.guild,\n            suppress_everyone: settings.store.everyone,\n            suppress_roles: settings.store.role,\n            mute_scheduled_events: settings.store.events,\n            notify_highlights: settings.store.highlights ? 1 : 0\n        });\n    if (settings.store.messages !== 3) {\n        updateGuildNotificationSettings(guildId,\n            {\n                message_notifications: settings.store.messages,\n            });\n    }\n    if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {\n        toggleShowAllChannels(guildId);\n    }\n}\n\nexport default definePlugin({\n    name: \"NewGuildSettings\",\n    description: \"Automatically mute new servers and change various other settings upon joining\",\n    tags: [\"MuteNewGuild\", \"mute\", \"server\"],\n    authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP],\n    contextMenus: {\n        \"guild-context\": makeContextMenuPatch(false),\n        \"guild-header-popout\": makeContextMenuPatch(true)\n    },\n    patches: [\n        {\n            find: \",acceptInvite(\",\n            replacement: {\n                match: /INVITE_ACCEPT_SUCCESS.+?,(\\i)=null!=.+?;/,\n                replace: (m, guildId) => `${m}$self.applyDefaultSettings(${guildId});`\n            }\n        },\n        {\n            find: \"{joinGuild:\",\n            replacement: {\n                match: /guildId:(\\i),lurker:(\\i).{0,20}}\\)\\);/,\n                replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.applyDefaultSettings(${guildId});`\n            }\n        }\n    ],\n    settings,\n    applyDefaultSettings,\n    flux: {\n        GUILD_JOIN_REQUEST_UPDATE({ guildId, request, status }) {\n            if (status === \"APPROVED\" && request.user_id === UserStore.getCurrentUser().id)\n                applyDefaultSettings(guildId);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/noBlockedMessages/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings, migratePluginSetting } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { runtimeHashMessageKey } from \"@utils/intlHash\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { i18n, RelationshipStore } from \"@webpack/common\";\n\ninterface MessageDeleteProps {\n    // Internal intl message for BLOCKED_MESSAGE_COUNT\n    collapsedReason: () => any;\n}\n\n// Remove this migration once enough time has passed\nmigratePluginSetting(\"NoBlockedMessages\", \"ignoreBlockedMessages\", \"ignoreMessages\");\nconst settings = definePluginSettings({\n    ignoreMessages: {\n        description: \"Completely ignores incoming messages from blocked and ignored (if enabled) users\",\n        type: OptionType.BOOLEAN,\n        default: false,\n        restartNeeded: true\n    },\n    applyToIgnoredUsers: {\n        description: \"Additionally apply to 'ignored' users\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: false\n    }\n});\n\nexport default definePlugin({\n    name: \"NoBlockedMessages\",\n    description: \"Hides all blocked/ignored messages from chat completely\",\n    authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365],\n    settings,\n\n    patches: [\n        {\n            find: \".__invalid_blocked,\",\n            replacement: [\n                {\n                    match: /let{messages:\\i,[^}]*?collapsedReason[^}]*}/,\n                    replace: \"if($self.shouldHide(arguments[0]))return null;$&\"\n                }\n            ]\n        },\n        ...[\n            '\"MessageStore\"',\n            '\"ReadStateStore\"'\n        ].map(find => ({\n            find,\n            predicate: () => settings.store.ignoreMessages,\n            replacement: [\n                {\n                    match: /(?<=function (\\i)\\((\\i)\\){)(?=.*MESSAGE_CREATE:\\1)/,\n                    replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message))return;`\n                }\n            ]\n        }))\n    ],\n\n    shouldIgnoreMessage(message: Message) {\n        try {\n            if (RelationshipStore.isBlocked(message.author.id)) {\n                return true;\n            }\n            return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id);\n        } catch (e) {\n            new Logger(\"NoBlockedMessages\").error(\"Failed to check if user is blocked or ignored:\", e);\n            return false;\n        }\n    },\n\n    shouldHide(props: MessageDeleteProps): boolean {\n        try {\n            const collapsedReason = props.collapsedReason();\n            const is = (key: string) => collapsedReason === i18n.t[runtimeHashMessageKey(key)]();\n\n            return is(\"BLOCKED_MESSAGE_COUNT\") || (settings.store.applyToIgnoredUsers && is(\"IGNORED_MESSAGE_COUNT\"));\n        } catch (e) {\n            new Logger(\"NoBlockedMessages\").error(\"Failed to check if message should be hidden:\", e);\n            return false;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/noDeepLinks.web/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"DisableDeepLinks\",\n    description: \"Disables Discord's stupid deep linking feature which tries to force you to use their Desktop App\",\n    authors: [Devs.Ven],\n    required: true,\n\n    noop: () => { },\n\n    patches: [{\n        find: /\\.openNativeAppModal\\(.{0,50}?\\.DEEP_LINK/,\n        replacement: {\n            match: /\\i\\.\\i\\.openNativeAppModal/,\n            replace: \"$self.noop\",\n        }\n    }]\n});\n"
  },
  {
    "path": "src/plugins/noDefaultHangStatus/README.md",
    "content": "# NoDefaultHangStatus\n\nDisable the default hang status when joining voice channels\n\n![Visualization](https://github.com/Vendicated/Vencord/assets/24937357/329a9742-236f-48f7-94ff-c3510eca505a)\n"
  },
  {
    "path": "src/plugins/noDefaultHangStatus/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoDefaultHangStatus\",\n    description: \"Disable the default hang status when joining voice channels\",\n    authors: [Devs.D3SOX],\n\n    patches: [\n        {\n            find: \".CHILLING)\",\n            replacement: {\n                match: /{enableHangStatus:(\\i),/,\n                replace: \"{_enableHangStatus:$1=false,\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/noDevtoolsWarning/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoDevtoolsWarning\",\n    description: \"Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.\",\n    authors: [Devs.Ven],\n    patches: [{\n        find: \"setDevtoolsCallbacks\",\n        replacement: {\n            match: /if\\(null!=\\i&&\"0.0.0\"===\\i\\.app\\.getVersion\\(\\)\\)/,\n            replace: \"if(true)\"\n        }\n    }]\n});\n"
  },
  {
    "path": "src/plugins/noF1/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoF1\",\n    description: \"Disables F1 help bind.\",\n    authors: [Devs.Cyn],\n    patches: [\n        {\n            find: ',\"f1\"],comboKeysBindGlobal:',\n            replacement: {\n                match: ',\"f1\"],comboKeysBindGlobal:',\n                replace: \"],comboKeysBindGlobal:\",\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/noMaskedUrlPaste/README.md",
    "content": "# NoMaskedUrlPaste\n\nPasting a link while you have text selected will NOT paste your link as a masked link.\n"
  },
  {
    "path": "src/plugins/noMaskedUrlPaste/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants.js\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoMaskedUrlPaste\",\n    authors: [Devs.CatNoir],\n    description: \"Pasting a link while having text selected will not paste as masked URL\",\n    patches: [\n        {\n            find: \".selection,preventEmojiSurrogates:\",\n            replacement: {\n                match: /if\\(null!=\\i.selection&&\\i.\\i.isExpanded\\(\\i.selection\\)\\)/,\n                replace: \"if(false)\"\n            }\n        }\n    ],\n});\n"
  },
  {
    "path": "src/plugins/noMosaic/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    inlineVideo: {\n        description: \"Play videos without carousel modal\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"NoMosaic\",\n    authors: [Devs.AutumnVN],\n    description: \"Removes Discord image mosaic\",\n    tags: [\"image\", \"mosaic\", \"media\"],\n\n    settings,\n\n    patches: [\n        {\n            find: '\"PLAINTEXT_PREVIEW\":\"OTHER\"',\n            replacement: {\n                match: /=>\"IMAGE\"===\\i\\|\\|\"VIDEO\"===\\i(?:\\|\\|(\"VISUAL_PLACEHOLDER\"===\\i))?;/,\n                replace: (_, visualPlaceholderPred) => visualPlaceholderPred != null ? `=>${visualPlaceholderPred};` : \"=>false;\"\n            }\n        },\n        {\n            find: \"renderAttachments(\",\n            predicate: () => settings.store.inlineVideo,\n            replacement: {\n                match: /url:(\\i)\\.url\\}\\);return /,\n                replace: \"$&$1.content_type?.startsWith('image/')&&\"\n            }\n        },\n    ]\n});\n"
  },
  {
    "path": "src/plugins/noOnboardingDelay/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoOnboardingDelay\",\n    description: \"Skips the slow and annoying onboarding delay\",\n    authors: [Devs.nekohaxx],\n    patches: [\n        {\n            find: \"#{intl::ONBOARDING_COVER_WELCOME_SUBTITLE}\",\n            replacement: {\n                match: \"3e3\",\n                replace: \"0\"\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/noPendingCount/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByPropsLazy } from \"@webpack\";\n\nconst MessageRequestStore = findByPropsLazy(\"getMessageRequestsCount\");\n\nconst settings = definePluginSettings({\n    hideFriendRequestsCount: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide incoming friend requests count\",\n        default: true,\n        restartNeeded: true\n    },\n    hideMessageRequestsCount: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide message requests count\",\n        default: true,\n        restartNeeded: true\n    },\n    hidePremiumOffersCount: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide nitro offers count\",\n        default: true,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"NoPendingCount\",\n    description: \"Removes the ping count of incoming friend requests, message requests, and nitro offers.\",\n    authors: [Devs.amia],\n\n    settings: settings,\n\n    // Functions used to determine the top left count indicator can be found in the single module that calls getUnacknowledgedOffers(...)\n    // or by searching for \"showProgressBadge:\"\n    patches: [\n        {\n            find: \"getPendingCount(){\",\n            predicate: () => settings.store.hideFriendRequestsCount,\n            replacement: {\n                match: /(?<=getPendingCount\\(\\)\\{)/,\n                replace: \"return 0;\"\n            }\n        },\n        // Message requests hook\n        {\n            find: \"getMessageRequestsCount(){\",\n            predicate: () => settings.store.hideMessageRequestsCount,\n            replacement: {\n                match: /(?<=getMessageRequestsCount\\(\\)\\{)/,\n                replace: \"return 0;\"\n            }\n        },\n        // This prevents the Message Requests tab from always hiding due to the previous patch (and is compatible with spam requests)\n        // In short, only the red badge is hidden. Button visibility behavior isn't changed.\n        {\n            find: \".getSpamChannelsCount();return\",\n            predicate: () => settings.store.hideMessageRequestsCount,\n            replacement: {\n                match: /(?<=getSpamChannelsCount\\(\\);return )\\i\\.getMessageRequestsCount\\(\\)/,\n                replace: \"$self.getRealMessageRequestCount()\"\n            }\n        },\n        {\n            find: \"showProgressBadge:\",\n            predicate: () => settings.store.hidePremiumOffersCount,\n            replacement: {\n                // The two groups inside the first group grab the minified names of the variables,\n                // they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.\n                match: /(\\{unviewedTrialCount:(\\i),unviewedDiscountCount:(\\i)\\}.+?)\\2\\+\\3/,\n                replace: (_, rest) => `${rest}0`\n            }\n        }\n    ],\n\n    getRealMessageRequestCount() {\n        return MessageRequestStore.getMessageRequestChannelIds().size;\n    }\n});\n"
  },
  {
    "path": "src/plugins/noProfileThemes/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { UserStore } from \"@webpack/common\";\n\nexport default definePlugin({\n    name: \"NoProfileThemes\",\n    description: \"Completely removes Nitro profile themes from everyone but yourself\",\n    authors: [Devs.TheKodeToad],\n    patches: [\n        {\n            find: \"hasThemeColors(){\",\n            replacement: {\n                match: /get canUsePremiumProfileCustomization\\(\\){return /,\n                replace: \"$&$self.isCurrentUser(this.userId)&&\"\n            }\n        },\n    ],\n\n    isCurrentUser: (userId: string) => userId === UserStore.getCurrentUser()?.id,\n});\n"
  },
  {
    "path": "src/plugins/noReplyMention/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport type { Message } from \"@vencord/discord-types\";\nimport { ChannelStore, GuildMemberStore } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    userList: {\n        description:\n            \"List of user ids to allow or exempt pings for (separated by commas or spaces)\",\n        type: OptionType.STRING,\n        default: \"1234567890123445,1234567890123445\",\n        multiline: true\n    },\n    roleList: {\n        description:\n            \"List of role ids to allow or exempt pings for (separated by commas or spaces)\",\n        type: OptionType.STRING,\n        default: \"1234567890123445,1234567890123445\",\n        multiline: true\n    },\n    shouldPingListed: {\n        description: \"Behaviour\",\n        type: OptionType.SELECT,\n        options: [\n            {\n                label: \"Do not ping the listed users / roles\",\n                value: false,\n            },\n            {\n                label: \"Only ping the listed users / roles\",\n                value: true,\n                default: true,\n            },\n        ],\n    },\n    inverseShiftReply: {\n        description: \"Invert Discord's shift replying behaviour (enable to make shift reply mention user)\",\n        type: OptionType.BOOLEAN,\n        default: false,\n    }\n});\n\nexport default definePlugin({\n    name: \"NoReplyMention\",\n    description: \"Disables reply pings by default\",\n    authors: [Devs.DustyAngel47, Devs.rae, Devs.pylix, Devs.outfoxxed],\n    settings,\n\n    shouldMention(message: Message, isHoldingShift: boolean) {\n        let isListed = settings.store.userList.includes(message.author.id);\n\n        const channel = ChannelStore.getChannel(message.channel_id);\n        if (channel?.guild_id && !isListed) {\n            const roles = GuildMemberStore.getMember(channel.guild_id, message.author.id)?.roles;\n            isListed = !!roles && roles.some(role => settings.store.roleList.includes(role));\n        }\n\n        const isExempt = settings.store.shouldPingListed ? isListed : !isListed;\n        return settings.store.inverseShiftReply ? isHoldingShift !== isExempt : !isHoldingShift && isExempt;\n    },\n\n    patches: [\n        {\n            find: \",\\\"Message\\\")}function\",\n            replacement: {\n                match: /:(\\i),shouldMention:!(\\i)\\.shiftKey/,\n                replace: \":$1,shouldMention:$self.shouldMention($1,$2.shiftKey)\"\n            }\n        }\n    ],\n});\n"
  },
  {
    "path": "src/plugins/noServerEmojis/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport type { Channel, Emoji } from \"@vencord/discord-types\";\n\nconst settings = definePluginSettings({\n    shownEmojis: {\n        description: \"The types of emojis to show in the autocomplete menu.\",\n        type: OptionType.SELECT,\n        default: \"onlyUnicode\",\n        options: [\n            { label: \"Only unicode emojis\", value: \"onlyUnicode\" },\n            { label: \"Unicode emojis and server emojis from current server\", value: \"currentServer\" },\n            { label: \"Unicode emojis and all server emojis (Discord default)\", value: \"all\" }\n        ]\n    }\n});\n\nexport default definePlugin({\n    name: \"NoServerEmojis\",\n    authors: [Devs.UlyssesZhan],\n    description: \"Do not show server emojis in the autocomplete menu.\",\n    settings,\n\n    patches: [\n        {\n            find: \"}searchWithoutFetchingLatest(\",\n            replacement: {\n                match: /\\.nameMatchesChain\\(\\i\\)\\.reduce\\(\\((\\i),(\\i)\\)=>\\{(?<=channel:(\\i).+?)/,\n                replace: \"$&if($self.shouldSkip($3,$2))return $1;\"\n            }\n        }\n    ],\n\n    shouldSkip(channel: Channel | undefined | null, emoji: Emoji) {\n        if (emoji.type !== 1) {\n            return false;\n        }\n\n        if (settings.store.shownEmojis === \"onlyUnicode\") {\n            return true;\n        }\n\n        if (settings.store.shownEmojis === \"currentServer\") {\n            return emoji.guildId !== (channel != null ? channel.getGuildId() : null);\n        }\n\n        return false;\n    }\n});\n"
  },
  {
    "path": "src/plugins/noSystemBadge.discordDesktop/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoSystemBadge\",\n    description: \"Disables the taskbar and system tray unread count badge.\",\n    authors: [Devs.rushii],\n    patches: [\n        {\n            find: \",setSystemTrayApplications\",\n            replacement: [\n                {\n                    match: /setBadge\\(\\i\\).+?},/,\n                    replace: \"setBadge(){},\"\n                },\n                {\n                    match: /setSystemTrayIcon\\(\\i\\).+?},/,\n                    replace: \"setSystemTrayIcon(){},\"\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/noTypingAnimation/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoTypingAnimation\",\n    authors: [Devs.AutumnVN],\n    description: \"Disables the CPU-intensive typing dots animation\",\n    patches: [\n        {\n            find: \"dotCycle\",\n            replacement: {\n                match: /focused:(\\i)/g,\n                replace: (_, focused) => `_focused:${focused}=false`\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/noUnblockToJump/README.md",
    "content": "# No Unblock To Jump\n\nRemoves the popup preventing you to jump to a message from a blocked/ignored user or likely spammer (eg: in search results)\n\n![A modal popup telling you to unblock a user to jump their message](https://github.com/user-attachments/assets/0e4b859d-f3b3-4101-9a83-829afb473d1e)\n"
  },
  {
    "path": "src/plugins/noUnblockToJump/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Sofia Lima\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"NoUnblockToJump\",\n    description: \"Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them\",\n    authors: [Devs.dzshn],\n    patches: [\n        {\n            find: \"#{intl::UNIGNORE_TO_JUMP_BODY}\",\n            replacement: {\n                match: /if\\(\\i\\.\\i\\.isBlockedForMessage\\(/,\n                replace: \"return true;$&\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/notificationVolume/README.md",
    "content": "# NotificationVolume\n\nSet a separate volume for notifications and in-app sounds (e.g. messages, call sound, mute/unmute) helping your ears stay healthy for many years to come.\n"
  },
  {
    "path": "src/plugins/notificationVolume/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    notificationVolume: {\n        type: OptionType.SLIDER,\n        description: \"Notification volume\",\n        markers: [0, 25, 50, 75, 100],\n        default: 100,\n        stickToMarkers: false\n    }\n});\n\nexport default definePlugin({\n    name: \"NotificationVolume\",\n    description: \"Save your ears and set a separate volume for notifications and in-app sounds\",\n    authors: [Devs.philipbry],\n    settings,\n    patches: [\n        {\n            find: \"ensureAudio(){\",\n            replacement: {\n                match: /(?=Math\\.min\\(\\i\\.\\i\\.getOutputVolume\\(\\)\\/100)/g,\n                replace: \"$self.settings.store.notificationVolume/100*\"\n            },\n        },\n    ],\n});\n"
  },
  {
    "path": "src/plugins/onePingPerDM/README.md",
    "content": "# OnePingPerDM\nIf unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit\n\n## Purpose\n- Prevents ping audio spam in DMs\n- Be able to distinguish more than one ping as multiple users\n- Be less annoyed while gaming\n"
  },
  {
    "path": "src/plugins/onePingPerDM/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { MessageJSON } from \"@vencord/discord-types\";\nimport { ChannelType } from \"@vencord/discord-types/enums\";\nimport { ChannelStore, ReadStateStore, UserStore } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    channelToAffect: {\n        type: OptionType.SELECT,\n        description: \"Select the type of DM for the plugin to affect\",\n        options: [\n            { label: \"Both\", value: \"both_dms\", default: true },\n            { label: \"User DMs\", value: \"user_dm\" },\n            { label: \"Group DMs\", value: \"group_dm\" },\n        ]\n    },\n    allowMentions: {\n        type: OptionType.BOOLEAN,\n        description: \"Receive audio pings for @mentions\",\n        default: false,\n    },\n    allowEveryone: {\n        type: OptionType.BOOLEAN,\n        description: \"Receive audio pings for @everyone and @here in group DMs\",\n        default: false,\n    },\n});\n\nexport default definePlugin({\n    name: \"OnePingPerDM\",\n    description: \"If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit\",\n    authors: [Devs.ProffDea],\n    settings,\n    patches: [\n        {\n            find: \".getDesktopType()===\",\n            replacement: [\n                {\n                    match: /(\\i\\.\\i\\.getDesktopType\\(\\)===\\i\\.\\i\\.NEVER)\\)/,\n                    replace: \"$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else \"\n                },\n                {\n                    match: /sound:(\\i\\?\\i:void 0,volume:\\i,onClick)/,\n                    replace: \"sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1\"\n                }\n            ]\n        }\n    ],\n    isPrivateChannelRead(message: MessageJSON) {\n        const channelType = ChannelStore.getChannel(message.channel_id)?.type;\n        if (\n            (channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) ||\n            (channelType === ChannelType.DM && settings.store.channelToAffect === \"group_dm\") ||\n            (channelType === ChannelType.GROUP_DM && settings.store.channelToAffect === \"user_dm\") ||\n            (settings.store.allowMentions && message.mentions.some(m => m.id === UserStore.getCurrentUser().id)) ||\n            (settings.store.allowEveryone && message.mention_everyone)\n        ) {\n            return true;\n        }\n        return ReadStateStore.getOldestUnreadMessageId(message.channel_id) === message.id;\n    },\n});\n"
  },
  {
    "path": "src/plugins/oneko/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"oneko\",\n    description: \"cat follow mouse (real)\",\n    // Listing adryd here because this literally just evals her script\n    authors: [Devs.Ven, Devs.adryd],\n\n    start() {\n        fetch(\"https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.js\")\n            .then(x => x.text())\n            .then(s => s.replace(\"./oneko.gif\", \"https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif\")\n                .replace(\"(isReducedMotion)\", \"(false)\"))\n            .then(eval);\n    },\n\n    stop() {\n        document.getElementById(\"oneko\")?.remove();\n    }\n});\n"
  },
  {
    "path": "src/plugins/openInApp/README.md",
    "content": "# OpenInApp\n\nOpen links in their respective apps instead of your browser\n\n## Currently supports:\n\n- Spotify\n- Steam\n- EpicGames\n- Tidal\n- Apple Music (iTunes)\n"
  },
  {
    "path": "src/plugins/openInApp/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType, PluginNative, SettingsDefinition } from \"@utils/types\";\nimport { showToast, Toasts } from \"@webpack/common\";\nimport type { MouseEvent } from \"react\";\n\ninterface URLReplacementRule {\n    match: RegExp;\n    replace: (...matches: string[]) => string;\n    description: string;\n    shortlinkMatch?: RegExp;\n    accountViewReplace?: (userId: string) => string;\n}\n\n// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant\nconst UrlReplacementRules: Record<string, URLReplacementRule> = {\n    spotify: {\n        match: /^https:\\/\\/open\\.spotify\\.com\\/(?:intl-[a-z]{2}\\/)?(track|album|artist|playlist|user|episode|prerelease)\\/(.+)(?:\\?.+?)?$/,\n        replace: (_, type, id) => `spotify://${type}/${id}`,\n        description: \"Open Spotify links in the Spotify app\",\n        shortlinkMatch: /^https:\\/\\/spotify\\.link\\/.+$/,\n        accountViewReplace: userId => `spotify:user:${userId}`,\n    },\n    steam: {\n        match: /^https:\\/\\/(steamcommunity\\.com|(?:help|store)\\.steampowered\\.com)\\/.+$/,\n        replace: match => `steam://openurl/${match}`,\n        description: \"Open Steam links in the Steam app\",\n        shortlinkMatch: /^https:\\/\\/s.team\\/.+$/,\n        accountViewReplace: userId => `steam://openurl/https://steamcommunity.com/profiles/${userId}`,\n    },\n    epic: {\n        match: /^https:\\/\\/store\\.epicgames\\.com\\/(.+)$/,\n        replace: (_, id) => `com.epicgames.launcher://store/${id}`,\n        description: \"Open Epic Games links in the Epic Games Launcher\",\n    },\n    tidal: {\n        match: /^https:\\/\\/(?:listen\\.)?tidal\\.com\\/(?:browse\\/)?(track|album|artist|playlist|user|video|mix)\\/([a-f0-9-]+).*/,\n        replace: (_, type, id) => `tidal://${type}/${id}`,\n        description: \"Open Tidal links in the Tidal app\",\n    },\n    itunes: {\n        match: /^https:\\/\\/(?:geo\\.)?music\\.apple\\.com\\/([a-z]{2}\\/)?(album|artist|playlist|song|curator)\\/([^/?#]+)\\/?([^/?#]+)?(?:\\?.*)?(?:#.*)?$/,\n        replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,\n        description: \"Open Apple Music links in the iTunes app\"\n    },\n};\n\nconst pluginSettings = definePluginSettings(\n    Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {\n        acc[key] = {\n            type: OptionType.BOOLEAN,\n            description: rule.description,\n            default: true,\n        };\n        return acc;\n    }, {} as SettingsDefinition)\n);\n\n\nconst Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import(\"./native\")>;\n\nexport default definePlugin({\n    name: \"OpenInApp\",\n    description: \"Open links in their respective apps instead of your browser\",\n    authors: [Devs.Ven, Devs.surgedevs],\n    settings: pluginSettings,\n\n    patches: [\n        {\n            find: \"trackAnnouncementMessageLinkClicked({\",\n            replacement: {\n                match: /function (\\i\\(\\i,\\i\\)\\{)(?=.{0,150}trusted:)/,\n                replace: \"async function $1 if(await $self.handleLink(...arguments)) return;\"\n            }\n        },\n        {\n            find: \"no artist ids in metadata\",\n            predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,\n            replacement: [\n                {\n                    match: /\\i\\.\\i\\.isProtocolRegistered\\(\\)/g,\n                    replace: \"true\"\n                },\n                {\n                    match: /\\(0,\\i\\.isDesktop\\)\\(\\)/,\n                    replace: \"true\"\n                }\n            ]\n        },\n\n        // User Profile Modal & User Profile Modal v2\n        ...[\".__invalid_connectedAccountOpenIconContainer\", \".BLUESKY||\"].map(find => ({\n            find,\n            replacement: {\n                match: /(?<=onClick:(\\i)=>\\{)(?=.{0,100}\\.CONNECTED_ACCOUNT_VIEWED)(?<==(\\i)\\.metadata.+?)/,\n                replace: \"if($self.handleAccountView($1,$2.type,$2.id)) return;\"\n            }\n        }))\n    ],\n\n    async handleLink(data: { href: string; }, event?: MouseEvent) {\n        if (!data) return false;\n\n        let url = data.href;\n        if (!url) return false;\n\n        for (const [key, rule] of Object.entries(UrlReplacementRules)) {\n            if (!pluginSettings.store[key]) continue;\n\n            if (rule.shortlinkMatch?.test(url)) {\n                event?.preventDefault();\n                url = await Native.resolveRedirect(url);\n            }\n\n            if (rule.match.test(url)) {\n                showToast(\"Opened link in native app\", Toasts.Type.SUCCESS);\n\n                const newUrl = url.replace(rule.match, rule.replace);\n                VencordNative.native.openExternal(newUrl);\n\n                event?.preventDefault();\n                return true;\n            }\n        }\n\n        // in case short url didn't end up being something we can handle\n        if (event?.defaultPrevented) {\n            window.open(url, \"_blank\");\n            return true;\n        }\n\n        return false;\n    },\n\n    handleAccountView(e: MouseEvent, platformType: string, userId: string) {\n        const rule = UrlReplacementRules[platformType];\n        if (rule?.accountViewReplace && pluginSettings.store[platformType]) {\n            VencordNative.native.openExternal(rule.accountViewReplace(userId));\n            e.preventDefault();\n            return true;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/openInApp/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { IpcMainInvokeEvent } from \"electron\";\nimport { request } from \"https\";\n\n// These links don't support CORS, so this has to be native\nconst validRedirectUrls = /^https:\\/\\/(spotify\\.link|s\\.team)\\/.+$/;\n\nfunction getRedirect(url: string) {\n    return new Promise<string>((resolve, reject) => {\n        const req = request(new URL(url), { method: \"HEAD\" }, res => {\n            resolve(\n                res.headers.location\n                    ? getRedirect(res.headers.location)\n                    : url\n            );\n        });\n        req.on(\"error\", reject);\n        req.end();\n    });\n}\n\nexport async function resolveRedirect(_: IpcMainInvokeEvent, url: string) {\n    if (!validRedirectUrls.test(url)) return url;\n\n    return getRedirect(url);\n}\n"
  },
  {
    "path": "src/plugins/overrideForumDefaults/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    defaultLayout: {\n        type: OptionType.SELECT,\n        options: [\n            { label: \"List\", value: 1, default: true },\n            { label: \"Gallery\", value: 2 }\n        ],\n        description: \"Which layout to use as default\"\n    },\n    defaultSortOrder: {\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Recently Active\", value: 0, default: true },\n            { label: \"Date Posted\", value: 1 }\n        ],\n        description: \"Which sort order to use as default\"\n    }\n});\n\nexport default definePlugin({\n    name: \"OverrideForumDefaults\",\n    description: \"Allows you to override default forum layout/sort order. you can still change it on a per-channel basis\",\n    authors: [Devs.Inbestigator],\n    patches: [\n        {\n            find: \"getDefaultLayout(){\",\n            replacement: [\n                {\n                    match: /}getDefaultLayout\\(\\){/,\n                    replace: \"$&return $self.getLayout();\"\n                },\n                {\n                    match: /}getDefaultSortOrder\\(\\){/,\n                    replace: \"$&return $self.getSortOrder();\"\n                }\n            ]\n        }\n    ],\n\n    getLayout: () => settings.store.defaultLayout,\n    getSortOrder: () => settings.store.defaultSortOrder,\n\n    settings\n});\n"
  },
  {
    "path": "src/plugins/pauseInvitesForever/README.md",
    "content": "# PauseInvitesForever\n\nAdds a button to the Security Actions modal to to pause invites indefinitely.\n\n![](https://github.com/Vendicated/Vencord/assets/47677887/e5ba40a3-cb08-462a-8615-fb74dd54c790)\n"
  },
  {
    "path": "src/plugins/pauseInvitesForever/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { getIntlMessage, hasGuildFeature } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { Constants, GuildStore, PermissionStore, RestAPI } from \"@webpack/common\";\n\nfunction showDisableInvites(guildId: string) {\n    const guild = GuildStore.getGuild(guildId);\n    if (!guild) return false;\n\n    return (\n        !hasGuildFeature(guild, \"INVITES_DISABLED\") &&\n        PermissionStore.getGuildPermissionProps(guild).canManageRoles\n    );\n}\n\nfunction disableInvites(guildId: string) {\n    const guild = GuildStore.getGuild(guildId);\n    const features = [...guild.features, \"INVITES_DISABLED\"];\n    RestAPI.patch({\n        url: Constants.Endpoints.GUILD(guildId),\n        body: { features },\n    });\n}\n\nexport default definePlugin({\n    name: \"PauseInvitesForever\",\n    tags: [\"DisableInvitesForever\"],\n    description: \"Brings back the option to pause invites indefinitely that stupit Discord removed.\",\n    authors: [Devs.Dolfies, Devs.amia],\n\n    patches: [\n        {\n            find: \"#{intl::GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}\",\n            group: true,\n            replacement: [\n                {\n                    match: /children:\\i\\.\\i\\.string\\(\\i\\.\\i#{intl::GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}\\)/,\n                    replace: \"children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})\",\n                },\n                {\n                    match: /\\.INVITES_DISABLED\\)(?=.+?#{intl::INVITES_PERMANENTLY_DISABLED_TIP}.+?checked:(\\i)).+?\\[\\1,(\\i)\\]=\\i.useState\\(\\i\\)/,\n                    replace: \"$&,setChecked=$2\"\n                }\n            ]\n        }\n    ],\n\n    renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {\n        return (\n            <div>\n                {getIntlMessage(\"GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION\")}\n                {showDisableInvites(guildId) && <a role=\"button\" onClick={() => {\n                    setChecked(true);\n                    disableInvites(guildId);\n                }}> Pause Indefinitely.</a>}\n            </div>\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/permissionFreeWill/README.md",
    "content": "# PermissionFreeWill\n\nRemoves the client-side restrictions that prevent editing channel permissions, such as permission lockouts (\"Pretty sure\nyou don't want to do this\") and onboarding requirements (\"Making this change will make your server incompatible [...]\")\n\n## Warning\n\nThis plugin will let you create permissions in servers that **WILL** lock you out of channels until an administrator\ncan resolve it for you. Please be careful with the overwrites you are making and check carefully.\n"
  },
  {
    "path": "src/plugins/permissionFreeWill/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    lockout: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: 'Bypass the permission lockout prevention (\"Pretty sure you don\\'t want to do this\")',\n        restartNeeded: true\n    },\n    onboarding: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: 'Bypass the onboarding requirements (\"Making this change will make your server incompatible [...]\")',\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"PermissionFreeWill\",\n    description: \"Disables the client-side restrictions for channel permission management.\",\n    authors: [Devs.lewisakura],\n\n    patches: [\n        // Permission lockout, just set the check to true\n        {\n            find: \"#{intl::STAGE_CHANNEL_CANNOT_OVERWRITE_PERMISSION}\",\n            replacement: [\n                {\n                    match: /case\"DENY\":.{0,50}if\\((?=\\i\\.\\i\\.can)/,\n                    replace: \"$&true||\"\n                }\n            ],\n            predicate: () => settings.store.lockout\n        },\n        // Onboarding, same thing but we need to prevent the check\n        {\n            find: \"#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}\",\n            replacement: [\n                {\n                    // replace export getters with functions that always resolve to true\n                    match: /{(?:\\i:\\(\\)=>\\i,?){2}}/,\n                    replace: m => m.replaceAll(canonicalizeMatch(/\\(\\)=>\\i/g), \"()=>()=>Promise.resolve(true)\")\n                }\n            ],\n            predicate: () => settings.store.onboarding\n        }\n    ],\n    settings\n});\n"
  },
  {
    "path": "src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { InfoIcon, OwnerCrownIcon } from \"@components/Icons\";\nimport { cl, getGuildPermissionSpecMap } from \"@plugins/permissionsViewer/utils\";\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { getIntlMessage, getUniqueUsername } from \"@utils/discord\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { Guild, Role, UnicodeEmoji, User } from \"@vencord/discord-types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from \"@webpack/common\";\n\nimport { settings } from \"..\";\nimport { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from \"./icons\";\n\nexport const enum PermissionType {\n    Role = 0,\n    User = 1,\n    Owner = 2\n}\n\nexport interface RoleOrUserPermission {\n    type: PermissionType;\n    id?: string;\n    permissions?: bigint;\n    overwriteAllow?: bigint;\n    overwriteDeny?: bigint;\n}\n\ntype GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; };\nconst getRoleIconData: GetRoleIconData = findByCodeLazy(\"convertSurrogateToName\", \"customIconSrc\", \"unicodeEmoji\");\n\nfunction getRoleIconSrc(role: Role) {\n    const icon = getRoleIconData(role, 20);\n    if (!icon) return;\n\n    const { customIconSrc, unicodeEmoji } = icon;\n    return customIconSrc ?? unicodeEmoji?.url;\n}\n\nfunction RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {\n    const guildPermissionSpecMap = useMemo(() => getGuildPermissionSpecMap(guild), [guild.id]);\n\n    useStateFromStores(\n        [GuildMemberStore],\n        () => GuildMemberStore.getMemberIds(guild.id),\n        null,\n        (old, current) => old.length === current.length\n    );\n\n    useEffect(() => {\n        permissions.sort((a, b) => a.type - b.type);\n    }, [permissions]);\n\n    useEffect(() => {\n        const usersToRequest = permissions\n            .filter(p => p.type === PermissionType.User && !GuildMemberStore.isMember(guild.id, p.id!))\n            .map(({ id }) => id);\n\n        FluxDispatcher.dispatch({\n            type: \"GUILD_MEMBERS_REQUEST\",\n            guildIds: [guild.id],\n            userIds: usersToRequest\n        });\n    }, []);\n\n    const [selectedItemIndex, selectItem] = useState(0);\n    const selectedItem = permissions[selectedItemIndex];\n\n    const roles = GuildRoleStore.getRolesSnapshot(guild.id);\n\n    return (\n        <ModalRoot\n            {...modalProps}\n            size={ModalSize.LARGE}\n        >\n            <ModalHeader>\n                <Text className={cl(\"modal-title\")} variant=\"heading-lg/semibold\">{header} Permissions</Text>\n                <ModalCloseButton onClick={modalProps.onClose} />\n            </ModalHeader>\n\n            <ModalContent className={cl(\"modal-content\")}>\n                {!selectedItem && (\n                    <div className={cl(\"modal-no-perms\")}>\n                        <Text variant=\"heading-lg/normal\">No permissions to display!</Text>\n                    </div>\n                )}\n\n                {selectedItem && (\n                    <div className={cl(\"modal-container\")}>\n                        <ScrollerThin className={cl(\"modal-list\")} orientation=\"auto\">\n                            {permissions.map((permission, index) => {\n                                const user: User | undefined = UserStore.getUser(permission.id ?? \"\");\n                                const role: Role | undefined = roles[permission.id ?? \"\"];\n                                const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined;\n\n                                return (\n                                    <div\n                                        key={index}\n                                        className={cl(\"modal-list-item-btn\")}\n                                        onClick={() => selectItem(index)}\n                                        role=\"button\"\n                                        tabIndex={0}\n                                    >\n                                        <div\n                                            className={cl(\"modal-list-item\", { \"modal-list-item-active\": selectedItemIndex === index })}\n                                            onContextMenu={e => {\n                                                if (permission.type === PermissionType.Role)\n                                                    ContextMenuApi.openContextMenu(e, () => (\n                                                        <RoleContextMenu\n                                                            guild={guild}\n                                                            roleId={permission.id!}\n                                                            onClose={modalProps.onClose}\n                                                        />\n                                                    ));\n                                                else if (permission.type === PermissionType.User) {\n                                                    ContextMenuApi.openContextMenu(e, () => (\n                                                        <UserContextMenu\n                                                            userId={permission.id!}\n                                                        />\n                                                    ));\n                                                }\n                                            }}\n                                        >\n                                            {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (\n                                                <span\n                                                    className={cl(\"modal-role-circle\")}\n                                                    style={{ backgroundColor: role?.colorString ?? \"var(--primary-300)\" }}\n                                                />\n                                            )}\n                                            {permission.type === PermissionType.Role && roleIconSrc != null && (\n                                                <img\n                                                    className={cl(\"modal-role-image\")}\n                                                    src={roleIconSrc}\n                                                />\n                                            )}\n                                            {permission.type === PermissionType.User && user != null && (\n                                                <img\n                                                    className={cl(\"modal-user-img\")}\n                                                    src={user.getAvatarURL(void 0, void 0, false)}\n                                                />\n                                            )}\n                                            <Text variant=\"text-md/normal\" className={cl(\"modal-list-item-text\")}>\n                                                {\n                                                    permission.type === PermissionType.Role\n                                                        ? role?.name ?? \"Unknown Role\"\n                                                        : permission.type === PermissionType.User\n                                                            ? (user != null && getUniqueUsername(user)) ?? \"Unknown User\"\n                                                            : (\n                                                                <Flex gap=\"0.2em\">\n                                                                    @owner\n                                                                    <OwnerCrownIcon height={18} width={18} aria-hidden=\"true\" />\n                                                                </Flex>\n                                                            )\n                                                }\n                                            </Text>\n                                        </div>\n                                    </div>\n                                );\n                            })}\n                        </ScrollerThin>\n                        <div className={cl(\"modal-divider\")} />\n                        <ScrollerThin className={cl(\"modal-perms\")} orientation=\"auto\">\n                            {Object.values(PermissionsBits).map(bit => (\n                                <div key={bit} className={cl(\"modal-perms-item\")}>\n                                    <div className={cl(\"modal-perms-item-icon\")}>\n                                        {(() => {\n                                            const { permissions, overwriteAllow, overwriteDeny } = selectedItem;\n\n                                            if (permissions)\n                                                return (permissions & bit) === bit\n                                                    ? PermissionAllowedIcon()\n                                                    : PermissionDeniedIcon();\n\n                                            if (overwriteAllow && (overwriteAllow & bit) === bit)\n                                                return PermissionAllowedIcon();\n                                            if (overwriteDeny && (overwriteDeny & bit) === bit)\n                                                return PermissionDeniedIcon();\n\n                                            return PermissionDefaultIcon();\n                                        })()}\n                                    </div>\n                                    <Text variant=\"text-md/normal\">{guildPermissionSpecMap[String(bit)].title}</Text>\n\n                                    <Tooltip text={\n                                        (() => {\n                                            const { description } = guildPermissionSpecMap[String(bit)];\n                                            return typeof description === \"function\" ? i18n.intl.format(description, {}) : description;\n                                        })()\n                                    }>\n                                        {props => <InfoIcon {...props} />}\n                                    </Tooltip>\n                                </div>\n                            ))}\n                        </ScrollerThin>\n                    </div>\n                )}\n            </ModalContent>\n        </ModalRoot>\n    );\n}\n\nfunction RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: string; onClose: () => void; }) {\n    return (\n        <Menu.Menu\n            navId={cl(\"role-context-menu\")}\n            onClose={ContextMenuApi.closeContextMenu}\n            aria-label=\"Role Options\"\n        >\n            <Menu.MenuItem\n                id={cl(\"copy-role-id\")}\n                label={getIntlMessage(\"COPY_ID_ROLE\")}\n                action={() => {\n                    copyToClipboard(roleId);\n                }}\n            />\n\n            {(settings.store as any).unsafeViewAsRole && (\n                <Menu.MenuItem\n                    id={cl(\"view-as-role\")}\n                    label={getIntlMessage(\"VIEW_AS_ROLE\")}\n                    action={() => {\n                        const role = GuildRoleStore.getRole(guild.id, roleId);\n                        if (!role) return;\n\n                        onClose();\n                        FluxDispatcher.dispatch({\n                            type: \"IMPERSONATE_UPDATE\",\n                            guildId: guild.id,\n                            data: {\n                                type: \"ROLES\",\n                                roles: {\n                                    [roleId]: role\n                                }\n                            }\n                        });\n                    }}\n                />\n            )}\n        </Menu.Menu>\n    );\n}\n\nfunction UserContextMenu({ userId }: { userId: string; }) {\n    return (\n        <Menu.Menu\n            navId={cl(\"user-context-menu\")}\n            onClose={ContextMenuApi.closeContextMenu}\n            aria-label=\"User Options\"\n        >\n            <Menu.MenuItem\n                id={cl(\"copy-user-id\")}\n                label={getIntlMessage(\"COPY_ID_USER\")}\n                action={() => {\n                    copyToClipboard(userId);\n                }}\n            />\n        </Menu.Menu>\n    );\n}\n\nconst RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);\n\nexport default function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {\n    return openModal(modalProps => (\n        <RolesAndUsersPermissions\n            modalProps={modalProps}\n            permissions={permissions}\n            guild={guild}\n            header={header}\n        />\n    ));\n}\n"
  },
  {
    "path": "src/plugins/permissionsViewer/components/UserPermissions.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { HeadingTertiary } from \"@components/Heading\";\nimport { cl, getGuildPermissionSpecMap, getSortedRolesForMember, sortUserRoles } from \"@plugins/permissionsViewer/utils\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport type { Guild, GuildMember } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { PermissionsBits, Text, Tooltip, useMemo, UserStore } from \"@webpack/common\";\n\nimport { PermissionsSortOrder, settings } from \"..\";\nimport openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from \"./RolesAndUsersPermissions\";\n\ninterface UserPermission {\n    permission: string;\n    roleName: string;\n    roleColor: string;\n    rolePosition: number;\n}\n\ntype UserPermissions = Array<UserPermission>;\n\nconst RoleClasses = findCssClassesLazy(\"role\", \"roleName\", \"roleRemoveButton\", \"roleNameOverflow\", \"root\");\nconst RoleBorderClasses = findCssClassesLazy(\"roleCircle\", \"dot\", \"dotBorderColor\");\n\ninterface FakeRoleProps extends React.HTMLAttributes<HTMLDivElement> {\n    text: string;\n    color: string;\n}\n\nfunction FakeRole({ text, color, ...props }: FakeRoleProps) {\n    return (\n        <div {...props} className={classes(RoleClasses.role)}>\n            <div className={RoleClasses.roleRemoveButton}>\n                <span\n                    className={RoleBorderClasses.roleCircle}\n                    style={{ backgroundColor: color }}\n                />\n            </div>\n            <div className={RoleClasses.roleName}>\n                <Text\n                    className={RoleClasses.roleNameOverflow}\n                    variant=\"text-xs/medium\"\n                >\n                    {text}\n                </Text>\n            </div>\n        </div>\n    );\n}\n\ninterface GrantedByTooltipProps {\n    roleName: string;\n    roleColor: string;\n}\n\nfunction GrantedByTooltip({ roleName, roleColor }: GrantedByTooltipProps) {\n    return (\n        <>\n            <Text variant=\"text-sm/medium\">Granted By</Text>\n            <FakeRole text={roleName} color={roleColor} />\n        </>\n    );\n}\n\nfunction UserPermissionsComponent({ guild, guildMember, closePopout }: { guild: Guild; guildMember: GuildMember; closePopout: () => void; }) {\n    const { permissionsSortOrder } = settings.use([\"permissionsSortOrder\"]);\n\n    const guildPermissionSpecMap = useMemo(() => getGuildPermissionSpecMap(guild), [guild.id]);\n\n    const [rolePermissions, userPermissions] = useMemo(() => {\n        const userPermissions: UserPermissions = [];\n\n        const userRoles = getSortedRolesForMember(guild, guildMember);\n\n        const rolePermissions: Array<RoleOrUserPermission> = userRoles.map(role => ({\n            type: PermissionType.Role,\n            ...role\n        }));\n\n        if (guild.ownerId === guildMember.userId) {\n            rolePermissions.push({\n                type: PermissionType.Owner,\n                permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n)\n            });\n\n            const OWNER = getIntlMessage(\"GUILD_OWNER\") ?? \"Server Owner\";\n            userPermissions.push({\n                permission: OWNER,\n                roleName: \"Owner\",\n                roleColor: \"var(--primary-300)\",\n                rolePosition: Infinity\n            });\n        }\n\n        sortUserRoles(userRoles);\n\n        for (const bit of Object.values(PermissionsBits)) {\n            for (const { permissions, colorString, position, name } of userRoles) {\n                if ((permissions & bit) === bit) {\n                    userPermissions.push({\n                        permission: guildPermissionSpecMap[String(bit)].title,\n                        roleName: name,\n                        roleColor: colorString || \"var(--primary-300)\",\n                        rolePosition: position\n                    });\n\n                    break;\n                }\n            }\n        }\n\n        userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);\n\n        return [rolePermissions, userPermissions];\n    }, [permissionsSortOrder]);\n\n    return <div>\n        <div className={cl(\"user-header-container\")}>\n            <HeadingTertiary>Permissions</HeadingTertiary>\n            <div className={cl(\"user-header-btns\")}>\n                <Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? \"Highest Role\" : \"Lowest Role\"}`}>\n                    {tooltipProps => (\n                        <div\n                            {...tooltipProps}\n                            className={cl(\"user-header-btn\")}\n                            role=\"button\"\n                            tabIndex={0}\n                            onClick={() => {\n                                settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;\n                            }}\n                        >\n                            <svg\n                                width=\"24\"\n                                height=\"24\"\n                                viewBox=\"0 96 960 960\"\n                                transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? \"scale(1 1)\" : \"scale(1 -1)\"}\n                            >\n                                <path fill=\"var(--text-default)\" d=\"M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z\" />\n                            </svg>\n                        </div>\n                    )}\n                </Tooltip>\n                <Tooltip text=\"Role Details\">\n                    {tooltipProps => (\n                        <div\n                            {...tooltipProps}\n                            className={cl(\"user-header-btn\")}\n                            role=\"button\"\n                            tabIndex={0}\n                            onClick={() => {\n                                closePopout();\n                                openRolesAndUsersPermissionsModal(rolePermissions, guild, guildMember.nick || UserStore.getUser(guildMember.userId).username);\n                            }}\n                        >\n                            <svg\n                                width=\"24\"\n                                height=\"24\"\n                                viewBox=\"0 0 24 24\"\n                            >\n                                <path fill=\"var(--text-default)\" d=\"M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z\" />\n                            </svg>\n                        </div>\n                    )}\n                </Tooltip>\n            </div>\n        </div>\n        {userPermissions.length > 0 && (\n            <div className={classes(RoleClasses.root)}>\n                {userPermissions.map(({ permission, roleColor, roleName }) => (\n                    <Tooltip\n                        key={permission}\n                        text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}\n                        tooltipClassName={cl(\"granted-by-container\")}\n                        tooltipContentClassName={cl(\"granted-by-content\")}\n                    >\n                        {tooltipProps => (\n                            <FakeRole {...tooltipProps} text={permission} color={roleColor} />\n                        )}\n                    </Tooltip>\n                ))}\n            </div>\n        )}\n    </div>;\n}\n\nexport default ErrorBoundary.wrap(UserPermissionsComponent, { noop: true });\n"
  },
  {
    "path": "src/plugins/permissionsViewer/components/icons.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function PermissionDeniedIcon() {\n    return (\n        <svg\n            height=\"24\"\n            width=\"24\"\n            viewBox=\"0 0 24 24\"\n        >\n            <title>Denied</title>\n            <path fill=\"var(--status-danger)\" d=\"M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z\" />\n        </svg>\n    );\n}\n\nexport function PermissionAllowedIcon() {\n    return (\n        <svg\n            height=\"24\"\n            width=\"24\"\n            viewBox=\"0 0 24 24\"\n        >\n            <title>Allowed</title>\n            <path fill=\"var(--status-positive)\" d=\"M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ\" />\n        </svg>\n    );\n}\n\nexport function PermissionDefaultIcon() {\n    return (\n        <svg\n            height=\"24\"\n            width=\"24\"\n            viewBox=\"0 0 16 16\"\n        >\n            <g>\n                <title>Not overwritten</title>\n                <polygon fill=\"var(--text-default)\" points=\"12 2.32 10.513 2 4 13.68 5.487 14\" />\n            </g>\n        </svg>\n    );\n}\n"
  },
  {
    "path": "src/plugins/permissionsViewer/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { SafetyIcon } from \"@components/Icons\";\nimport { TooltipContainer } from \"@components/TooltipContainer\";\nimport { Devs } from \"@utils/constants\";\nimport { classes } from \"@utils/misc\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport type { Guild } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useRef, UserStore } from \"@webpack/common\";\n\nimport openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from \"./components/RolesAndUsersPermissions\";\nimport UserPermissions from \"./components/UserPermissions\";\nimport { getSortedRolesForMember, sortPermissionOverwrites } from \"./utils\";\n\nconst PopoutClasses = findCssClassesLazy(\"container\", \"scroller\", \"list\");\n\nexport const enum PermissionsSortOrder {\n    HighestRole,\n    LowestRole\n}\n\nconst enum MenuItemParentType {\n    User,\n    Channel,\n    Guild\n}\n\nexport const settings = definePluginSettings({\n    permissionsSortOrder: {\n        description: \"The sort method used for defining which role grants an user a certain permission\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Highest Role\", value: PermissionsSortOrder.HighestRole, default: true },\n            { label: \"Lowest Role\", value: PermissionsSortOrder.LowestRole }\n        ]\n    },\n});\n\nfunction MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {\n    if (type === MenuItemParentType.User && !GuildMemberStore.isMember(guildId, id!)) return null;\n\n    return (\n        <Menu.MenuItem\n            id=\"perm-viewer-permissions\"\n            label=\"Permissions\"\n            action={() => {\n                const guild = GuildStore.getGuild(guildId);\n\n                const { permissions, header } = match(type)\n                    .returnType<{ permissions: RoleOrUserPermission[], header: string; }>()\n                    .with(MenuItemParentType.User, () => {\n                        const member = GuildMemberStore.getMember(guildId, id!)!;\n\n                        const permissions: RoleOrUserPermission[] = getSortedRolesForMember(guild, member)\n                            .map(role => ({\n                                type: PermissionType.Role,\n                                ...role\n                            }));\n\n                        if (guild.ownerId === id) {\n                            permissions.push({\n                                type: PermissionType.Owner,\n                                permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n)\n                            });\n                        }\n\n                        return {\n                            permissions,\n                            header: member.nick ?? UserStore.getUser(member.userId).username\n                        };\n                    })\n                    .with(MenuItemParentType.Channel, () => {\n                        const channel = ChannelStore.getChannel(id!);\n\n                        const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({\n                            type: type as PermissionType,\n                            id,\n                            overwriteAllow: allow,\n                            overwriteDeny: deny\n                        })), guildId);\n\n                        return {\n                            permissions,\n                            header: channel.name\n                        };\n                    })\n                    .otherwise(() => {\n                        const permissions = GuildRoleStore.getSortedRoles(guild.id).map(role => ({\n                            type: PermissionType.Role,\n                            ...role\n                        }));\n\n                        return {\n                            permissions,\n                            header: guild.name\n                        };\n                    });\n\n                openRolesAndUsersPermissionsModal(permissions, guild, header);\n            }}\n        />\n    );\n}\n\nfunction makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {\n    return (children, props) => {\n        if (\n            !props ||\n            (type === MenuItemParentType.User && !props.user) ||\n            (type === MenuItemParentType.Guild && !props.guild) ||\n            (type === MenuItemParentType.Channel && (!props.channel || !props.guild))\n        ) {\n            return;\n        }\n\n        const group = findGroupChildrenByChildId(childId, children);\n\n        const item = match(type)\n            .with(MenuItemParentType.User, () => MenuItem(props.guildId, props.user.id, type))\n            .with(MenuItemParentType.Channel, () => MenuItem(props.guild.id, props.channel.id, type))\n            .with(MenuItemParentType.Guild, () => MenuItem(props.guild.id))\n            .otherwise(() => null);\n\n\n        if (item == null) return;\n\n        if (group) {\n            return group.push(item);\n        }\n\n        // \"roles\" may not be present due to the member not having any roles. In that case, add it above \"Copy ID\"\n        if (childId === \"roles\" && props.guildId) {\n            children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>);\n        }\n    };\n}\n\nexport default definePlugin({\n    name: \"PermissionsViewer\",\n    description: \"View the permissions a user or channel has, and the roles of a server\",\n    authors: [Devs.Nuckyz, Devs.Ven],\n    settings,\n\n    patches: [\n        {\n            find: \"#{intl::COLLAPSE_ROLES}\",\n            replacement: {\n                match: /(?<=\\i\\.id\\)\\),\\i\\(\\))(?=,\\i\\?)/,\n                replace: \",$self.ViewPermissionsButton(arguments[0])\"\n            }\n        }\n    ],\n\n    ViewPermissionsButton: ErrorBoundary.wrap(({ className, guild, userId }: { className: string; guild: Guild; userId: string; }) => {\n        const guildMember = GuildMemberStore.getMember(guild.id, userId);\n        if (!guildMember) return null;\n\n        const buttonRef = useRef(null);\n\n        return (\n            <Popout\n                position=\"bottom\"\n                align=\"center\"\n                targetElementRef={buttonRef}\n                renderPopout={({ closePopout }) => (\n                    <Dialog className={PopoutClasses.container} style={{ width: \"500px\" }}>\n                        <UserPermissions guild={guild} guildMember={guildMember} closePopout={closePopout} />\n                    </Dialog>\n                )}\n            >\n                {popoutProps => (\n                    <TooltipContainer text=\"View Permissions\">\n                        <Button\n                            {...popoutProps}\n                            ref={buttonRef}\n                            color={Button.Colors.CUSTOM}\n                            look={Button.Looks.FILLED}\n                            size={Button.Sizes.NONE}\n                            className={classes(className, \"vc-permviewer-role-button\")}\n                        >\n                            <SafetyIcon height=\"16\" width=\"16\" />\n                        </Button>\n                    </TooltipContainer>\n                )}\n            </Popout>\n        );\n    }, { noop: true }),\n\n    contextMenus: {\n        \"user-context\": makeContextMenuPatch(\"roles\", MenuItemParentType.User),\n        \"channel-context\": makeContextMenuPatch([\"mute-channel\", \"unmute-channel\"], MenuItemParentType.Channel),\n        \"guild-context\": makeContextMenuPatch(\"privacy\", MenuItemParentType.Guild),\n        \"guild-header-popout\": makeContextMenuPatch(\"privacy\", MenuItemParentType.Guild)\n    }\n});\n"
  },
  {
    "path": "src/plugins/permissionsViewer/styles.css",
    "content": "/* User Permissions Component */\n\n.vc-permviewer-user-header-container {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 8px;\n}\n\n.vc-permviewer-user-header-btns {\n    display: flex;\n    gap: 8px;\n}\n\n.vc-permviewer-user-header-btn {\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n/*  RolesAndUsersPermissions Component */\n\n.vc-permviewer-modal-content {\n    padding: 16px 4px 16px 16px;\n}\n\n.vc-permviewer-modal-title {\n    flex-grow: 1;\n}\n\n.vc-permviewer-modal-no-perms {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n}\n\n.vc-permviewer-modal-container {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    gap: 8px;\n}\n\n.vc-permviewer-modal-list {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n    padding-right: 8px;\n    width: 200px;\n}\n\n.vc-permviewer-modal-list-item-btn {\n    cursor: pointer;\n}\n\n.vc-permviewer-modal-list-item {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 8px;\n    border-radius: 5px;\n}\n\n.vc-permviewer-modal-list-item:hover {\n    background-color: var(--background-mod-subtle);\n}\n\n.vc-permviewer-modal-list-item-active {\n    background-color: var(--background-mod-strong);\n}\n\n.vc-permviewer-modal-list-item-text {\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n}\n\n.vc-permviewer-modal-role-circle {\n    border-radius: 50%;\n    width: 12px;\n    height: 12px;\n    flex-shrink: 0;\n}\n\n.vc-permviewer-modal-role-image {\n    width: 20px;\n    height: 20px;\n    object-fit: contain;\n}\n\n.vc-permviewer-modal-user-img {\n    border-radius: 50%;\n    width: 20px;\n    height: 20px;\n}\n\n.vc-permviewer-modal-divider {\n    width: 2px;\n    background-color: var(--background-mod-normal);\n}\n\n.vc-permviewer-modal-perms {\n    display: flex;\n    flex-direction: column;\n    padding-right: 8px;\n}\n\n.vc-permviewer-modal-perms-item {\n    display: flex;\n    align-items: center;\n    gap: 5px;\n    padding: 10px 2px 10px 10px;\n    border-bottom: 2px solid var(--background-mod-normal);\n}\n\n.vc-permviewer-modal-perms-item:last-child {\n    border: 0;\n}\n\n.vc-permviewer-modal-perms-item-icon {\n    border: 1px solid var(--background-mod-strong);\n    width: 24px;\n    height: 24px;\n}\n\n.vc-permviewer-modal-perms-item .vc-info-icon {\n    color: var(--interactive-muted);\n    margin-left: auto;\n    cursor: pointer;\n    transition: color ease-in 0.1s;\n}\n\n.vc-permviewer-modal-perms-item .vc-info-icon:hover {\n    color: var(--interactive-icon-active);\n}\n\n/* copy pasted from discord cause impossible to webpack find */\n.vc-permviewer-role-button {\n    border-radius: var(--radius-sm);\n    color: var(--interactive-icon-default);\n    border: 1px solid var(--user-profile-border);\n    /* stylelint-disable-next-line value-no-vendor-prefix */\n    width: -moz-fit-content;\n    width: fit-content;\n    height: 24px;\n    padding: 4px\n}\n\n.vc-permviewer-role-button:hover {\n    background-color: var(--user-profile-background-hover);\n}\n\n.vc-permviewer-granted-by-container {\n    max-width: 300px;\n    width: auto;\n}\n\n.vc-permviewer-granted-by-content {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n}"
  },
  {
    "path": "src/plugins/permissionsViewer/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { classNameFactory } from \"@utils/css\";\nimport { Guild, GuildMember, Role } from \"@vencord/discord-types\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { GuildRoleStore } from \"@webpack/common\";\n\nimport { PermissionsSortOrder, settings } from \".\";\nimport { PermissionType } from \"./components/RolesAndUsersPermissions\";\n\nexport const { getGuildPermissionSpecMap } = findByPropsLazy(\"getGuildPermissionSpecMap\");\n\nexport const cl = classNameFactory(\"vc-permviewer-\");\n\nexport function getSortedRolesForMember({ id: guildId }: Guild, member: GuildMember) {\n    // The guild id is the @everyone role\n    return GuildRoleStore\n        .getSortedRoles(guildId)\n        .filter(role => role.id === guildId || member.roles.includes(role.id));\n}\n\nexport function sortUserRoles(roles: Role[]) {\n    switch (settings.store.permissionsSortOrder) {\n        case PermissionsSortOrder.HighestRole:\n            return roles.sort((a, b) => b.position - a.position);\n        case PermissionsSortOrder.LowestRole:\n            return roles.sort((a, b) => a.position - b.position);\n        default:\n            return roles;\n    }\n}\n\nexport function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {\n    const roles = GuildRoleStore.getRolesSnapshot(guildId);\n\n    return overwrites.sort((a, b) => {\n        if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;\n\n        const roleA = roles[a.id];\n        const roleB = roles[b.id];\n\n        return roleB.position - roleA.position;\n    });\n}\n"
  },
  {
    "path": "src/plugins/petpet/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from \"@api/Commands\";\nimport { Devs } from \"@utils/constants\";\nimport { makeLazy } from \"@utils/lazy\";\nimport definePlugin from \"@utils/types\";\nimport { CommandArgument, CommandContext } from \"@vencord/discord-types\";\nimport { DraftType, UploadAttachmentStore, UploadHandler, UploadManager, UserUtils } from \"@webpack/common\";\nimport { GIFEncoder, nearestColorIndex, quantize } from \"gifenc\";\n\nconst DEFAULT_DELAY = 20;\nconst DEFAULT_RESOLUTION = 128;\nconst FRAMES = 10;\n\nconst getFrames = makeLazy(() => Promise.all(\n    Array.from(\n        { length: FRAMES },\n        (_, i) => loadImage(`https://raw.githubusercontent.com/VenPlugs/petpet/main/frames/pet${i}.gif`)\n    ))\n);\n\nfunction loadImage(source: File | string) {\n    const isFile = source instanceof File;\n    const url = isFile ? URL.createObjectURL(source) : source;\n\n    return new Promise<HTMLImageElement>((resolve, reject) => {\n        const img = new Image();\n        img.onload = () => {\n            if (isFile)\n                URL.revokeObjectURL(url);\n            resolve(img);\n        };\n        img.onerror = _event => reject(Error(`An error occurred while loading ${url}. Check the console for more info.`));\n        img.crossOrigin = \"Anonymous\";\n        img.src = url;\n    });\n}\n\nasync function resolveImage(options: CommandArgument[], ctx: CommandContext, noServerPfp: boolean): Promise<File | string | null> {\n    for (const opt of options) {\n        switch (opt.name) {\n            case \"image\":\n                const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);\n                if (upload) {\n                    if (!upload.isImage) {\n                        UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);\n                        throw \"Upload is not an image\";\n                    }\n                    return upload.item.file;\n                }\n                break;\n            case \"url\":\n                return opt.value;\n            case \"user\":\n                try {\n                    const user = await UserUtils.getUser(opt.value);\n                    return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\\?size=\\d+$/, \"?size=2048\");\n                } catch (err) {\n                    console.error(\"[petpet] Failed to fetch user\\n\", err);\n                    UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);\n                    throw \"Failed to fetch user. Check the console for more info.\";\n                }\n        }\n    }\n    UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);\n    return null;\n}\n\nfunction rgb888_to_rgb565(r: number, g: number, b: number): number {\n    return ((r << 8) & 0xf800) | ((g << 3) & 0x07e0) | (b >> 3);\n}\n\nfunction applyPaletteTransparent(data: Uint8Array | Uint8ClampedArray, palette: number[][], cache: number[], threshold: number): Uint8Array {\n    const index = new Uint8Array(Math.floor(data.length / 4));\n\n    for (let i = 0; i < index.length; i += 1) {\n        const r = data[4 * i];\n        const g = data[4 * i + 1];\n        const b = data[4 * i + 2];\n        const a = data[4 * i + 3];\n\n        if (a < threshold) {\n            index[i] = 255;\n        } else {\n            const key = rgb888_to_rgb565(r, g, b);\n            index[i] = key in cache ? cache[key] : (cache[key] = nearestColorIndex(palette, [r, g, b]));\n        }\n    }\n    return index;\n}\n\nexport default definePlugin({\n    name: \"petpet\",\n    description: \"Adds a /petpet slash command to create headpet gifs from any image\",\n    authors: [Devs.Ven, Devs.u32],\n    commands: [\n        {\n            inputType: ApplicationCommandInputType.BUILT_IN,\n            name: \"petpet\",\n            description: \"Create a petpet gif. You can only specify one of the image options\",\n            options: [\n                {\n                    name: \"delay\",\n                    description: \"The delay between each frame in ms. Rounded to nearest 10ms. Defaults to the minimum value of 20.\",\n                    type: ApplicationCommandOptionType.INTEGER\n                },\n                {\n                    name: \"resolution\",\n                    description: \"Resolution for the gif. Defaults to 120. If you enter an insane number and it freezes Discord that's your fault.\",\n                    type: ApplicationCommandOptionType.INTEGER\n                },\n                {\n                    name: \"image\",\n                    description: \"Image attachment to use\",\n                    type: ApplicationCommandOptionType.ATTACHMENT\n                },\n                {\n                    name: \"url\",\n                    description: \"URL to fetch image from\",\n                    type: ApplicationCommandOptionType.STRING\n                },\n                {\n                    name: \"user\",\n                    description: \"User whose avatar to use as image\",\n                    type: ApplicationCommandOptionType.USER\n                },\n                {\n                    name: \"no-server-pfp\",\n                    description: \"Use the normal avatar instead of the server specific one when using the 'user' option\",\n                    type: ApplicationCommandOptionType.BOOLEAN\n                }\n            ],\n            execute: async (opts, cmdCtx) => {\n                const frames = await getFrames();\n\n                const noServerPfp = findOption(opts, \"no-server-pfp\", false);\n                try {\n                    var url = await resolveImage(opts, cmdCtx, noServerPfp);\n                    if (!url) throw \"No Image specified!\";\n                } catch (err) {\n                    UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);\n                    sendBotMessage(cmdCtx.channel.id, {\n                        content: String(err),\n                    });\n                    return;\n                }\n\n                const avatar = await loadImage(url);\n\n                const delay = findOption(opts, \"delay\", DEFAULT_DELAY);\n                // Frame delays < 20ms don't function correctly on chromium and firefox\n                if (delay < 20) return sendBotMessage(cmdCtx.channel.id, { content: \"Delay must be at least 20.\" });\n\n                const resolution = findOption(opts, \"resolution\", DEFAULT_RESOLUTION);\n\n                const gif = GIFEncoder();\n\n                const paletteImageSize = Math.min(120, resolution);\n\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = resolution;\n                // Ensure there is sufficient space for the palette generation image\n                canvas.height = Math.max(resolution, 2 * paletteImageSize);\n\n                const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n                UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);\n\n                // Generate palette from an image where hand and avatar are fully visible\n                ctx.drawImage(avatar, 0, paletteImageSize, 0.8 * paletteImageSize, 0.8 * paletteImageSize);\n                ctx.drawImage(frames[0], 0, 0, paletteImageSize, paletteImageSize);\n                const { data } = ctx.getImageData(0, 0, paletteImageSize, 2 * paletteImageSize);\n                const palette = quantize(data, 255);\n\n                const cache = new Array(2 ** 16);\n\n                for (let i = 0; i < FRAMES; i++) {\n                    ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n                    const j = i < FRAMES / 2 ? i : FRAMES - i;\n                    const width = 0.8 + j * 0.02;\n                    const height = 0.8 - j * 0.05;\n                    const offsetX = (1 - width) * 0.5 + 0.1;\n                    const offsetY = 1 - height - 0.08;\n\n                    ctx.drawImage(avatar, offsetX * resolution, offsetY * resolution, width * resolution, height * resolution);\n                    ctx.drawImage(frames[i], 0, 0, resolution, resolution);\n\n                    const { data } = ctx.getImageData(0, 0, resolution, resolution);\n                    const index = applyPaletteTransparent(data, palette, cache, 1);\n\n                    gif.writeFrame(index, resolution, resolution, {\n                        transparent: true,\n                        transparentIndex: 255,\n                        delay,\n                        palette: i === 0 ? palette : undefined,\n                    });\n                }\n\n                gif.finish();\n                // @ts-ignore This causes a type error on *only some* typescript versions.\n                // usage adheres to mdn https://developer.mozilla.org/en-US/docs/Web/API/File/File#parameters\n                const file = new File([gif.bytesView()], \"petpet.gif\", { type: \"image/gif\" });\n                // Immediately after the command finishes, Discord clears all input, including pending attachments.\n                // Thus, setTimeout is needed to make this execute after Discord cleared the input\n                setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);\n            },\n        },\n    ]\n});\n"
  },
  {
    "path": "src/plugins/pictureInPicture/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Tooltip } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    loop: {\n        description: \"Whether to make the PiP video loop or not\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: false\n    }\n});\n\nexport default definePlugin({\n    name: \"PictureInPicture\",\n    description: \"Adds picture in picture to videos (next to the Download button)\",\n    authors: [Devs.Lumap],\n    settings,\n    patches: [\n        {\n            find: '[\"VIDEO\",\"CLIP\",\"AUDIO\"]',\n            replacement: {\n                match: /(\\[\\i>0&&\\i\\.length>0.{0,150}?children:)(\\i.slice\\(\\i\\))(?<=showDownload:(\\i).+?isVisualMediaType:(\\i).+?)/,\n                replace: (_, rest, origChildren, showDownload, isVisualMediaType) => `${rest}[${showDownload}&&${isVisualMediaType}&&$self.PictureInPictureButton(),...${origChildren}]`\n            }\n        }\n    ],\n\n    PictureInPictureButton: ErrorBoundary.wrap(() => {\n        return (\n            <Tooltip text=\"Toggle Picture in Picture\">\n                {tooltipProps => (\n                    <div\n                        {...tooltipProps}\n                        className=\"vc-pip-button\"\n                        role=\"button\"\n                        style={{\n                            cursor: \"pointer\",\n                            paddingTop: \"4px\",\n                            paddingLeft: \"4px\",\n                            paddingRight: \"4px\",\n                        }}\n                        onClick={e => {\n                            const video = e.currentTarget.parentNode!.parentNode!.querySelector(\"video\")!;\n                            const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;\n\n                            videoClone.loop = settings.store.loop;\n                            videoClone.style.display = \"none\";\n                            videoClone.onleavepictureinpicture = () => videoClone.remove();\n\n                            function launchPiP() {\n                                videoClone.currentTime = video.currentTime;\n                                videoClone.requestPictureInPicture();\n                                video.pause();\n                                videoClone.play();\n                            }\n\n                            if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)\n                                launchPiP();\n                            else\n                                videoClone.onloadedmetadata = launchPiP;\n                        }}\n                    >\n                        <svg width=\"24px\" height=\"24px\" viewBox=\"0 0 24 24\">\n                            <path\n                                fill=\"currentColor\"\n                                d=\"M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z\"\n                            />\n                        </svg>\n                    </div>\n                )}\n            </Tooltip>\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/pictureInPicture/styles.css",
    "content": ".vc-pip-button {\n    color: var(--interactive-icon-default);\n}\n\n.vc-pip-button:hover {\n    background-color: var(--background-mod-subtle);\n    color: var(--interactive-icon-hover);\n}"
  },
  {
    "path": "src/plugins/pinDms/components/CreateCategoryModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Divider } from \"@components/Divider\";\nimport { DEFAULT_COLOR, SWATCHES } from \"@plugins/pinDms/constants\";\nimport { categoryLen, createCategory, getCategory } from \"@plugins/pinDms/data\";\nimport { classNameFactory } from \"@utils/css\";\nimport { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from \"@utils/modal\";\nimport { extractAndLoadChunksLazy, findComponentByCodeLazy } from \"@webpack\";\nimport { Button, ColorPicker, Forms, Text, TextInput, Toasts, useMemo, useState } from \"@webpack/common\";\n\ninterface ColorPickerWithSwatchesProps {\n    defaultColor: number;\n    colors: number[];\n    value: number;\n    disabled?: boolean;\n    onChange(value: number | null): void;\n    renderDefaultButton?: () => React.ReactNode;\n    renderCustomButton?: () => React.ReactNode;\n}\n\nconst ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:\"color-picker\"');\n\nexport const requireSettingsModal = extractAndLoadChunksLazy(['type:\"USER_SETTINGS_MODAL_OPEN\"']);\n\nconst cl = classNameFactory(\"vc-pindms-modal-\");\n\ninterface Props {\n    categoryId: string | null;\n    initialChannelId: string | null;\n    modalProps: ModalProps;\n}\n\nfunction useCategory(categoryId: string | null, initalChannelId: string | null) {\n    const category = useMemo(() => {\n        if (categoryId) {\n            return getCategory(categoryId);\n        } else if (initalChannelId) {\n            return {\n                id: Toasts.genId(),\n                name: `Pin Category ${categoryLen() + 1}`,\n                color: DEFAULT_COLOR,\n                collapsed: false,\n                channels: [initalChannelId]\n            };\n        }\n    }, [categoryId, initalChannelId]);\n\n    return category;\n}\n\nexport function NewCategoryModal({ categoryId, modalProps, initialChannelId }: Props) {\n    const category = useCategory(categoryId, initialChannelId);\n    if (!category) return null;\n\n    const [name, setName] = useState(category.name);\n    const [color, setColor] = useState(category.color);\n\n    const onSave = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {\n        e.preventDefault();\n\n        category.name = name;\n        category.color = color;\n\n        if (!categoryId) {\n            createCategory(category);\n        }\n\n        modalProps.onClose();\n    };\n\n    return (\n        <ModalRoot {...modalProps}>\n            <ModalHeader>\n                <Text variant=\"heading-lg/semibold\" style={{ flexGrow: 1 }}>{categoryId ? \"Edit\" : \"New\"} Category</Text>\n            </ModalHeader>\n\n            {/* form is here so when you press enter while in the text input it submits */}\n            <form onSubmit={onSave}>\n                <ModalContent className={cl(\"content\")}>\n                    <section>\n                        <Forms.FormTitle>Name</Forms.FormTitle>\n                        <TextInput\n                            value={name}\n                            onChange={e => setName(e)}\n                        />\n                    </section>\n                    <Divider />\n                    <section>\n                        <Forms.FormTitle>Color</Forms.FormTitle>\n                        <ColorPickerWithSwatches\n                            key={category.id}\n                            defaultColor={DEFAULT_COLOR}\n                            colors={SWATCHES}\n                            onChange={c => setColor(c!)}\n                            value={color}\n                            renderDefaultButton={() => null}\n                            renderCustomButton={() => (\n                                <ColorPicker\n                                    color={color}\n                                    onChange={c => setColor(c!)}\n                                    key={category.id}\n                                    showEyeDropper={false}\n                                />\n                            )}\n                        />\n                    </section>\n                </ModalContent>\n                <ModalFooter>\n                    <Button type=\"submit\" onClick={onSave} disabled={!name}>{categoryId ? \"Save\" : \"Create\"}</Button>\n                </ModalFooter>\n            </form>\n        </ModalRoot>\n    );\n}\n\nexport const openCategoryModal = (categoryId: string | null, channelId: string | null) =>\n    openModalLazy(async () => {\n        await requireSettingsModal();\n        return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initialChannelId={channelId} />;\n    });\n\n"
  },
  {
    "path": "src/plugins/pinDms/components/contextMenu.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { PinOrder, settings } from \"@plugins/pinDms\";\nimport { addChannelToCategory, canMoveChannelInDirection, currentUserCategories, isPinned, moveChannel, removeChannelFromCategory } from \"@plugins/pinDms/data\";\nimport { Menu } from \"@webpack/common\";\n\nimport { openCategoryModal } from \"./CreateCategoryModal\";\n\nfunction createPinMenuItem(channelId: string) {\n    const pinned = isPinned(channelId);\n\n    return (\n        <Menu.MenuItem\n            id=\"pin-dm\"\n            label=\"Pin DMs\"\n        >\n\n            {!pinned && (\n                <>\n                    <Menu.MenuItem\n                        id=\"vc-add-category\"\n                        label=\"Add Category\"\n                        color=\"brand\"\n                        action={() => openCategoryModal(null, channelId)}\n                    />\n                    <Menu.MenuSeparator />\n\n                    {\n                        currentUserCategories.map(category => (\n                            <Menu.MenuItem\n                                key={category.id}\n                                id={`pin-category-${category.id}`}\n                                label={category.name}\n                                action={() => addChannelToCategory(channelId, category.id)}\n                            />\n                        ))\n                    }\n                </>\n            )}\n\n            {pinned && (\n                <>\n                    <Menu.MenuItem\n                        id=\"unpin-dm\"\n                        label=\"Unpin DM\"\n                        color=\"danger\"\n                        action={() => removeChannelFromCategory(channelId)}\n                    />\n\n                    {\n                        settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, -1) && (\n                            <Menu.MenuItem\n                                id=\"move-up\"\n                                label=\"Move Up\"\n                                action={() => moveChannel(channelId, -1)}\n                            />\n                        )\n                    }\n\n                    {\n                        settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, 1) && (\n                            <Menu.MenuItem\n                                id=\"move-down\"\n                                label=\"Move Down\"\n                                action={() => moveChannel(channelId, 1)}\n                            />\n                        )\n                    }\n                </>\n            )}\n\n        </Menu.MenuItem>\n    );\n}\n\nconst GroupDMContext: NavContextMenuPatchCallback = (children, props) => {\n    const container = findGroupChildrenByChildId(\"leave-channel\", children);\n    container?.unshift(createPinMenuItem(props.channel.id));\n};\n\nconst UserContext: NavContextMenuPatchCallback = (children, props) => {\n    const container = findGroupChildrenByChildId(\"close-dm\", children);\n    if (container) {\n        const idx = container.findIndex(c => c?.props?.id === \"close-dm\");\n        container.splice(idx, 0, createPinMenuItem(props.channel.id));\n    }\n};\n\nexport const contextMenus = {\n    \"gdm-context\": GroupDMContext,\n    \"user-context\": UserContext\n};\n"
  },
  {
    "path": "src/plugins/pinDms/constants.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport const DEFAULT_CHUNK_SIZE = 256;\nexport const DEFAULT_COLOR = 10070709;\n\nexport const SWATCHES = [\n    1752220,\n    3066993,\n    3447003,\n    10181046,\n    15277667,\n    15844367,\n    15105570,\n    15158332,\n    9807270,\n    6323595,\n\n    1146986,\n    2067276,\n    2123412,\n    7419530,\n    11342935,\n    12745742,\n    11027200,\n    10038562,\n    9936031,\n    5533306\n];\n"
  },
  {
    "path": "src/plugins/pinDms/data.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { PinOrder, PrivateChannelSortStore, settings } from \"@plugins/pinDms\";\nimport { useForceUpdater } from \"@utils/react\";\nimport { UserStore } from \"@webpack/common\";\n\nexport interface Category {\n    id: string;\n    name: string;\n    color: number;\n    channels: string[];\n    collapsed?: boolean;\n}\n\nlet forceUpdateDms: (() => void) | undefined = undefined;\nexport let currentUserCategories: Category[] = [];\n\nexport async function init() {\n    const userId = UserStore.getCurrentUser()?.id;\n    if (userId == null) return;\n\n    currentUserCategories = settings.store.userBasedCategoryList[userId] ??= [];\n    forceUpdateDms?.();\n}\n\nexport function usePinnedDms() {\n    forceUpdateDms = useForceUpdater();\n    settings.use([\"pinOrder\", \"canCollapseDmSection\", \"dmSectionCollapsed\", \"userBasedCategoryList\"]);\n}\n\nexport function getCategory(id: string) {\n    return currentUserCategories.find(c => c.id === id);\n}\n\nexport function getCategoryByIndex(index: number) {\n    return currentUserCategories[index];\n}\n\nexport function createCategory(category: Category) {\n    currentUserCategories.push(category);\n}\n\nexport function addChannelToCategory(channelId: string, categoryId: string) {\n    const category = currentUserCategories.find(c => c.id === categoryId);\n    if (category == null) return;\n\n    if (category.channels.includes(channelId)) return;\n\n    category.channels.push(channelId);\n}\n\nexport function removeChannelFromCategory(channelId: string) {\n    const category = currentUserCategories.find(c => c.channels.includes(channelId));\n    if (category == null) return;\n\n    category.channels = category.channels.filter(c => c !== channelId);\n}\n\nexport function removeCategory(categoryId: string) {\n    const categoryIndex = currentUserCategories.findIndex(c => c.id === categoryId);\n    if (categoryIndex === -1) return;\n\n    currentUserCategories.splice(categoryIndex, 1);\n}\n\nexport function collapseCategory(id: string, value = true) {\n    const category = currentUserCategories.find(c => c.id === id);\n    if (category == null) return;\n\n    category.collapsed = value;\n}\n\n// Utils\nexport function isPinned(id: string) {\n    return currentUserCategories.some(c => c.channels.includes(id));\n}\n\nexport function categoryLen() {\n    return currentUserCategories.length;\n}\n\nexport function getAllUncollapsedChannels() {\n    if (settings.store.pinOrder === PinOrder.LastMessage) {\n        const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();\n        return currentUserCategories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));\n    }\n\n    return currentUserCategories.filter(c => !c.collapsed).flatMap(c => c.channels);\n}\n\nexport function getSections() {\n    return currentUserCategories.reduce((acc, category) => {\n        acc.push(category.channels.length === 0 ? 1 : category.channels.length);\n        return acc;\n    }, [] as number[]);\n}\n\n// Move categories\nexport const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {\n    const a = array[index];\n    const b = array[index + direction];\n\n    return a && b;\n};\n\nexport const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {\n    const categoryIndex = currentUserCategories.findIndex(m => m.id === id);\n    return canMoveArrayInDirection(currentUserCategories, categoryIndex, direction);\n};\n\nexport const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);\n\nexport const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {\n    const category = currentUserCategories.find(c => c.channels.includes(channelId));\n    if (category == null) return false;\n\n    const channelIndex = category.channels.indexOf(channelId);\n    return canMoveArrayInDirection(category.channels, channelIndex, direction);\n};\n\n\nfunction swapElementsInArray(array: any[], index1: number, index2: number) {\n    if (!array[index1] || !array[index2]) return;\n    [array[index1], array[index2]] = [array[index2], array[index1]];\n}\n\nexport function moveCategory(id: string, direction: -1 | 1) {\n    const a = currentUserCategories.findIndex(m => m.id === id);\n    const b = a + direction;\n\n    swapElementsInArray(currentUserCategories, a, b);\n}\n\nexport function moveChannel(channelId: string, direction: -1 | 1) {\n    const category = currentUserCategories.find(c => c.channels.includes(channelId));\n    if (category == null) return;\n\n    const a = category.channels.indexOf(channelId);\n    const b = a + direction;\n\n    swapElementsInArray(category.channels, a, b);\n}\n"
  },
  {
    "path": "src/plugins/pinDms/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { classes } from \"@utils/misc\";\nimport definePlugin, { OptionType, StartAt } from \"@utils/types\";\nimport { Channel } from \"@vencord/discord-types\";\nimport { findCssClassesLazy, findStoreLazy } from \"@webpack\";\nimport { Clickable, ContextMenuApi, FluxDispatcher, Menu, React } from \"@webpack/common\";\n\nimport { contextMenus } from \"./components/contextMenu\";\nimport { openCategoryModal, requireSettingsModal } from \"./components/CreateCategoryModal\";\nimport { DEFAULT_CHUNK_SIZE } from \"./constants\";\nimport { canMoveCategory, canMoveCategoryInDirection, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getCategoryByIndex, getSections, init, isPinned, moveCategory, removeCategory, usePinnedDms } from \"./data\";\n\ninterface ChannelComponentProps {\n    children: React.ReactNode,\n    channel: Channel,\n    selected: boolean;\n}\n\nconst headerClasses = findCssClassesLazy(\"privateChannelsHeaderContainer\", \"headerText\");\n\nexport const PrivateChannelSortStore = findStoreLazy(\"PrivateChannelSortStore\") as { getPrivateChannelIds: () => string[]; };\n\nexport let instance: any;\n\nexport const enum PinOrder {\n    LastMessage,\n    Custom\n}\n\nexport const settings = definePluginSettings({\n    pinOrder: {\n        type: OptionType.SELECT,\n        description: \"Which order should pinned DMs be displayed in?\",\n        options: [\n            { label: \"Most recent message\", value: PinOrder.LastMessage, default: true },\n            { label: \"Custom (right click channels to reorder)\", value: PinOrder.Custom }\n        ]\n    },\n    canCollapseDmSection: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow uncategorised DMs section to be collapsable\",\n        default: false\n    },\n    dmSectionCollapsed: {\n        type: OptionType.BOOLEAN,\n        description: \"Collapse DM section\",\n        default: false,\n        hidden: true\n    },\n    userBasedCategoryList: {\n        type: OptionType.CUSTOM,\n        default: {} as Record<string, Category[]>\n    }\n});\n\nexport default definePlugin({\n    name: \"PinDMs\",\n    description: \"Allows you to pin private channels to the top of your DM list. To pin/unpin or re-order pins, right click DMs\",\n    authors: [Devs.Ven, Devs.Aria],\n    settings,\n    contextMenus,\n\n    patches: [\n        {\n            find: '\"dm-quick-launcher\"===',\n            replacement: [\n                {\n                    // Filter out pinned channels from the private channel list\n                    match: /(?<=channels:\\i,)privateChannelIds:(\\i)(?=,listRef:)/,\n                    replace: \"privateChannelIds:$1.filter(c=>!$self.isPinned(c))\"\n                },\n                {\n                    // Insert the pinned channels to sections\n                    match: /(?<=renderRow:this\\.renderRow,)sections:\\[.+?1\\)]/,\n                    replace: \"...$self.makeProps(this,{$&})\"\n                },\n\n                // Rendering\n                {\n                    match: /renderRow(?:\",|=)(\\i)=>{(?<=renderDM(?:\",|=).+?(\\i\\.\\i),\\{channel:.+?)/,\n                    replace: \"$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();\"\n                },\n                {\n                    match: /renderSection(?:\",|=)(\\i)=>{/,\n                    replace: \"$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);\"\n                },\n                {\n                    match: /renderSection(?:\",|=).{0,300}?\"span\",{/,\n                    replace: \"$&...$self.makeSpanProps(),\"\n                },\n\n                // Fix Row Height\n                {\n                    match: /(\\.startsWith\\(\"section-divider\"\\).+?return 1===)(\\i)/,\n                    replace: \"$1($2-$self.categoryLen())\"\n                },\n                {\n                    match: /getRowHeight(?:\",|=)\\((\\i),(\\i)\\)=>{/,\n                    replace: \"$&if($self.isChannelHidden($1,$2))return 0;\"\n                },\n\n                // Fix ScrollTo\n                {\n                    // Override scrollToChannel to properly account for pinned channels\n                    match: /(?<=scrollTo\\(\\{to:\\i\\}\\):\\(\\i\\+=)(\\d+)\\*\\(.+?(?=,)/,\n                    replace: \"$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)\"\n                },\n                {\n                    match: /(scrollToChannel\\(\\i\\){.{1,300})(this\\.props\\.privateChannelIds)/,\n                    replace: \"$1[...$2,...$self.getAllUncollapsedChannels()]\"\n                },\n\n            ]\n        },\n\n\n        // forceUpdate moment\n        // https://regex101.com/r/kDN9fO/1\n        {\n            find: \".FRIENDS},\\\"friends\\\"\",\n            replacement: {\n                match: /let{showLibrary:\\i,/,\n                replace: \"$self.usePinnedDms();$&\"\n            }\n        },\n\n        // Fix Alt Up/Down navigation\n        {\n            find: \".APPLICATION_STORE&&\",\n            replacement: {\n                // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]\n                match: /(?<=\\i=__OVERLAY__\\?\\i:\\[\\.\\.\\.\\i\\(\\),\\.\\.\\.)\\i/,\n                // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))\n                replace: \"$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))\"\n            }\n        },\n\n        // fix alt+shift+up/down\n        {\n            find: \"=()=>!1,ensureChatIsVisible:\",\n            replacement: {\n                match: /(?<=\\i===\\i\\.ME\\?)\\i\\.\\i\\.getPrivateChannelIds\\(\\)/,\n                replace: \"$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))\"\n            }\n        },\n    ],\n\n    sections: null as number[] | null,\n\n    set _instance(i: any) {\n        this.instance = i;\n        instance = i;\n    },\n\n    startAt: StartAt.WebpackReady,\n    start: init,\n    flux: {\n        CONNECTION_OPEN: init,\n    },\n\n    usePinnedDms,\n    isPinned,\n    categoryLen,\n    getSections,\n    getAllUncollapsedChannels,\n    requireSettingsMenu: requireSettingsModal,\n\n    makeProps(instance, { sections }: { sections: number[]; }) {\n        this._instance = instance;\n        this.sections = sections;\n\n        this.sections.splice(1, 0, ...this.getSections());\n\n        if (this.instance?.props?.privateChannelIds?.length === 0) {\n            // dont render direct messages header\n            this.sections[this.sections.length - 1] = 0;\n        }\n\n        return {\n            sections: this.sections,\n            chunkSize: this.getChunkSize(),\n        };\n    },\n\n    makeSpanProps() {\n        return settings.store.canCollapseDmSection ? {\n            onClick: () => this.collapseDMList(),\n            role: \"button\",\n            style: { cursor: \"pointer\" }\n        } : undefined;\n    },\n\n    getChunkSize() {\n        // the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably)\n        // the higher the chunk size, the more rows are rendered at once\n        // also if the chunk size is 0 it will render everything at once\n\n        const sections = this.getSections();\n        const sectionHeaderSizePx = sections.length * 40;\n        // (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5\n        // we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen\n        return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5;\n    },\n\n    isCategoryIndex(sectionIndex: number) {\n        return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;\n    },\n\n    isChannelIndex(sectionIndex: number, channelIndex: number) {\n        if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && sectionIndex !== 0) {\n            return true;\n        }\n\n        const category = getCategoryByIndex(sectionIndex - 1);\n        return this.isCategoryIndex(sectionIndex) && (category?.channels?.length === 0 || category?.channels[channelIndex]);\n    },\n\n    collapseDMList() {\n        settings.store.dmSectionCollapsed = !settings.store.dmSectionCollapsed;\n    },\n\n    isChannelHidden(categoryIndex: number, channelIndex: number) {\n        if (categoryIndex === 0) return false;\n\n        if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && this.getSections().length + 1 === categoryIndex)\n            return true;\n\n        if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;\n\n        const category = getCategoryByIndex(categoryIndex - 1);\n        if (!category) return false;\n\n        return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];\n    },\n\n    getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {\n        if (!isPinned(channelId))\n            return (\n                (rowHeight + padding) * 2 // header\n                + rowHeight * this.getAllUncollapsedChannels().length // pins\n                + originalOffset // original pin offset minus pins\n            );\n\n        return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;\n    },\n\n    renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {\n        const category = getCategoryByIndex(section - 1);\n        if (!category) return null;\n\n        return (\n            <Clickable\n                onClick={() => collapseCategory(category.id, !category.collapsed)}\n                onContextMenu={e => {\n                    ContextMenuApi.openContextMenu(e, () => (\n                        <Menu.Menu\n                            navId=\"vc-pindms-header-menu\"\n                            onClose={() => FluxDispatcher.dispatch({ type: \"CONTEXT_MENU_CLOSE\" })}\n                            color=\"danger\"\n                            aria-label=\"Pin DMs Category Menu\"\n                        >\n                            <Menu.MenuItem\n                                id=\"vc-pindms-edit-category\"\n                                label=\"Edit Category\"\n                                action={() => openCategoryModal(category.id, null)}\n                            />\n\n                            {\n                                canMoveCategory(category.id) && (\n                                    <>\n                                        {\n                                            canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem\n                                                id=\"vc-pindms-move-category-up\"\n                                                label=\"Move Up\"\n                                                action={() => moveCategory(category.id, -1)}\n                                            />\n                                        }\n                                        {\n                                            canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem\n                                                id=\"vc-pindms-move-category-down\"\n                                                label=\"Move Down\"\n                                                action={() => moveCategory(category.id, 1)}\n                                            />\n                                        }\n                                    </>\n\n                                )\n                            }\n\n                            <Menu.MenuSeparator />\n                            <Menu.MenuItem\n                                id=\"vc-pindms-delete-category\"\n                                color=\"danger\"\n                                label=\"Delete Category\"\n                                action={() => removeCategory(category.id)}\n                            />\n\n\n                        </Menu.Menu>\n                    ));\n                }}\n            >\n                <h2\n                    className={classes(headerClasses.privateChannelsHeaderContainer, \"vc-pindms-section-container\", category.collapsed ? \"vc-pindms-collapsed\" : \"\")}\n                    style={{ color: `#${category.color.toString(16).padStart(6, \"0\")}` }}\n                >\n                    <span className={headerClasses.headerText}>\n                        {category?.name ?? \"uh oh\"}\n                    </span>\n                    <svg className=\"vc-pindms-collapse-icon\" aria-hidden=\"true\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\" viewBox=\"0 0 24 24\">\n                        <path fill=\"currentColor\" d=\"M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z\"></path>\n                    </svg>\n                </h2>\n            </Clickable>\n        );\n    }, { noop: true }),\n\n    renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {\n        return ErrorBoundary.wrap(() => {\n            const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);\n\n            if (!channel || !category) return null;\n            if (this.isChannelHidden(sectionIndex, index)) return null;\n\n            return (\n                <ChannelComponent\n                    channel={channel}\n                    selected={this.instance.props.selectedChannelId === channel.id}\n                >\n                    {channel.id}\n                </ChannelComponent>\n            );\n        }, { noop: true });\n    },\n\n    getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {\n        const category = getCategoryByIndex(sectionIndex - 1);\n        if (!category) return { channel: null, category: null };\n\n        const channelId = this.getCategoryChannels(category)[index];\n\n        return { channel: channels[channelId], category };\n    },\n\n    getCategoryChannels(category: Category) {\n        if (category.channels.length === 0) return [];\n\n        if (settings.store.pinOrder === PinOrder.LastMessage) {\n            return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));\n        }\n\n        return category?.channels ?? [];\n    }\n});\n"
  },
  {
    "path": "src/plugins/pinDms/styles.css",
    "content": ".vc-pindms-section-container {\n    box-sizing: border-box;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    text-transform: uppercase;\n    font-size: 12px;\n    line-height: 16px;\n    letter-spacing: .02em;\n    font-family: var(--font-display);\n    font-weight: 600;\n    flex: 1 1 auto;\n    color: var(--channels-default);\n    cursor: pointer;\n}\n\n.vc-pindms-modal-content {\n    display: grid;\n    justify-content: center;\n    padding: 1rem;\n    gap: 1.5rem;\n}\n\n.vc-pindms-modal-content [class*=\"defaultContainer\"] {\n    display: none;\n}\n\n.vc-pindms-collapse-icon {\n    width: 16px;\n    height: 16px;\n    color: var(--interactive-icon-default);\n    transform: rotate(90deg)\n}\n\n.vc-pindms-collapsed .vc-pindms-collapse-icon {\n    transform: rotate(0deg);\n}"
  },
  {
    "path": "src/plugins/plainFolderIcon/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"PlainFolderIcon\",\n    description: \"Dont show the small guild icons in folders\",\n    authors: [Devs.botato],\n\n    patches: [\n        {\n            find: \"#{intl::GUILD_FOLDER_TOOLTIP_A11Y_LABEL}\",\n            replacement: [\n                {\n                    // Discord always renders both plain and guild icons folders and uses a css transtion to switch between them\n                    match: /\\.slice\\(0,4\\).+?\\]:(\\i),\\[\\i\\.\\i\\]:!\\1/,\n                    replace: (m, hasFolderButtonContent) => `${m},\"vc-plainFolderIcon-plain\":!${hasFolderButtonContent}`\n                }\n\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/plainFolderIcon/style.css",
    "content": ".vc-plainFolderIcon-plain {\n    /* Without this, they are a bit laggier */\n    transition: none !important;\n\n    /* Don't show the mini guild icons */\n    transform: translateZ(0);\n\n    /* The new icons are fully transparent. Add a sane default to match the old behavior */\n    background-color: color-mix(in oklab, var(--custom-folder-color, var(--badge-background-default)) 40%, transparent);\n}"
  },
  {
    "path": "src/plugins/platformIndicators/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from \"@api/Badges\";\nimport { addMemberListDecorator, removeMemberListDecorator } from \"@api/MemberListDecorators\";\nimport { addMessageDecoration, removeMessageDecoration } from \"@api/MessageDecorations\";\nimport { Settings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { DiscordPlatform, OnlineStatus, User } from \"@vencord/discord-types\";\nimport { filters, findStoreLazy, mapMangledModuleLazy } from \"@webpack\";\nimport { AuthenticationStore, PresenceStore, Tooltip, UserStore, useStateFromStores } from \"@webpack/common\";\n\nexport interface Session {\n    sessionId: string;\n    status: string;\n    active: boolean;\n    clientInfo: {\n        version: number;\n        os: string;\n        client: string;\n    };\n}\n\nconst SessionsStore = findStoreLazy(\"SessionsStore\") as {\n    getSessions(): Record<string, Session>;\n};\nconst { useStatusFillColor } = mapMangledModuleLazy([\".5625*\", \"translate\"], {\n    useStatusFillColor: filters.byCode(\".hex\")\n});\n\nconst platformMap = {\n    embedded: \"Console\",\n    vr: \"VR\"\n};\n\nfunction Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {\n    return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (\n        <Tooltip text={tooltip}>\n            {tooltipProps => (\n                <svg\n                    {...tooltipProps}\n                    height={(opts?.height ?? 20) - (small ? 3 : 0)}\n                    width={(opts?.width ?? 20) - (small ? 3 : 0)}\n                    viewBox={opts?.viewBox ?? \"0 0 24 24\"}\n                    fill={color}\n                >\n                    <path d={path} />\n                </svg>\n            )}\n        </Tooltip>\n    );\n}\n\nconst Icons = {\n    desktop: Icon(\"M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z\"),\n    web: Icon(\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z\"),\n    mobile: Icon(\"M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z\", { viewBox: \"0 0 1000 1500\", height: 17, width: 17 }),\n    embedded: Icon(\"M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z\", { viewBox: \"0 0 50 50\" }),\n    vr: Icon(\"M8.46 8.64a1 1 0 0 1 1 1c0 .44-.3.8-.72.92l-.11.07c-.08.06-.2.19-.2.41a.99.99 0 0 1-.98.86h-.06a1 1 0 0 1-.94-1.05l.02-.32c.05-1.06.92-1.9 1.99-1.9ZM15.55 5a5.5 5.5 0 0 1 5.15 3.67h.3a2 2 0 0 1 2 2v3.18a2 2 0 0 1-2 1.99h-.2A4.54 4.54 0 0 1 16.55 19a4.45 4.45 0 0 1-3.6-1.83 1.2 1.2 0 0 0-1.9 0 4.44 4.44 0 0 1-3.9 1.82 4.54 4.54 0 0 1-3.94-3.15H3a2 2 0 0 1-2-2v-3.18c0-1.1.9-1.99 2-1.99h.3A5.5 5.5 0 0 1 8.46 5h7.09Zm-7.1 2C6.6 7 5.06 8.5 4.97 10.41l-.02.66v3.18c0 1.43 1.05 2.66 2.34 2.74.85.06 1.63-.32 2.14-1.01a3.2 3.2 0 0 1 2.57-1.3c1 0 1.97.48 2.57 1.3.5.69 1.3 1.08 2.14 1.01 1.3-.08 2.34-1.31 2.34-2.74l-.02-3.84a3.54 3.54 0 0 0-3.49-3.43H8.45Z\", { viewBox: \"0 4 24 16\", height: 20, width: 20 }),\n} satisfies Record<DiscordPlatform, any>;\n\nfunction getPlatformTooltip(platform: DiscordPlatform): string {\n    return platformMap[platform] ?? platform.charAt(0).toUpperCase() + platform.slice(1);\n}\n\nconst PlatformIcon = ({ platform, status, small }: { platform: DiscordPlatform, status: OnlineStatus; small: boolean; }) => {\n    const tooltip = getPlatformTooltip(platform as DiscordPlatform);\n\n    const Icon = Icons[platform] ?? Icons.desktop;\n\n    return <Icon color={useStatusFillColor(status)} tooltip={tooltip} small={small} />;\n};\n\nfunction ensureOwnStatus(user: User) {\n    if (user.id === AuthenticationStore.getId()) {\n        const sessions = SessionsStore.getSessions();\n        if (typeof sessions !== \"object\") return null;\n        const sortedSessions = Object.values(sessions).sort(({ status: a }, { status: b }) => {\n            if (a === b) return 0;\n            if (a === \"online\") return 1;\n            if (b === \"online\") return -1;\n            if (a === \"idle\") return 1;\n            if (b === \"idle\") return -1;\n            return 0;\n        });\n\n        const ownStatus = Object.values(sortedSessions).reduce((acc, curr) => {\n            if (curr.clientInfo.client !== \"unknown\")\n                acc[curr.clientInfo.client] = curr.status;\n            return acc;\n        }, {});\n\n        const { clientStatuses } = PresenceStore.getState();\n        clientStatuses[AuthenticationStore.getId()] = ownStatus;\n    }\n}\n\nfunction getBadges({ userId }: BadgeUserArgs): ProfileBadge[] {\n    const user = UserStore.getUser(userId);\n\n    if (!user || user.bot) return [];\n\n    ensureOwnStatus(user);\n\n    const status = PresenceStore.getClientStatus(user.id);\n    if (!status) return [];\n\n    return Object.entries(status).map(([platform, status]) => ({\n        key: `vc-platform-indicator-${platform}`,\n        component: () => (\n            <span className=\"vc-platform-indicator\">\n                <PlatformIcon\n                    key={platform}\n                    platform={platform as DiscordPlatform}\n                    status={status}\n                    small={false}\n                />\n            </span>\n        ),\n    }));\n}\n\nconst PlatformIndicator = ({ user, small = false }: { user: User; small?: boolean; }) => {\n    ensureOwnStatus(user);\n\n    const status = useStateFromStores([PresenceStore], () => PresenceStore.getClientStatus(user.id));\n    if (!status) return null;\n\n    const icons = Object.entries(status).map(([platform, status]) => (\n        <PlatformIcon\n            key={platform}\n            platform={platform as DiscordPlatform}\n            status={status}\n            small={small}\n        />\n    ));\n\n    if (!icons.length) return null;\n\n    return (\n        <span\n            className=\"vc-platform-indicator\"\n            style={{ gap: \"2px\" }}\n        >\n            {icons}\n        </span>\n    );\n};\n\nconst badge: ProfileBadge = {\n    getBadges,\n    position: BadgePosition.START,\n};\n\nconst indicatorLocations = {\n    list: {\n        description: \"In the member list\",\n        onEnable: () => addMemberListDecorator(\"platform-indicator\", ({ user }) =>\n            user && !user.bot ? <PlatformIndicator user={user} small={true} /> : null\n        ),\n        onDisable: () => removeMemberListDecorator(\"platform-indicator\")\n    },\n    badges: {\n        description: \"In user profiles, as badges\",\n        onEnable: () => addProfileBadge(badge),\n        onDisable: () => removeProfileBadge(badge)\n    },\n    messages: {\n        description: \"Inside messages\",\n        onEnable: () => addMessageDecoration(\"platform-indicator\", props => {\n            const user = props.message?.author;\n            return user && !user.bot ? <PlatformIndicator user={props.message?.author} /> : null;\n        }),\n        onDisable: () => removeMessageDecoration(\"platform-indicator\")\n    }\n};\n\nexport default definePlugin({\n    name: \"PlatformIndicators\",\n    description: \"Adds platform indicators (Desktop, Mobile, Web...) to users\",\n    authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven],\n    dependencies: [\"MessageDecorationsAPI\", \"MemberListDecoratorsAPI\"],\n\n    start() {\n        const settings = Settings.plugins.PlatformIndicators;\n        Object.entries(indicatorLocations).forEach(([key, value]) => {\n            if (settings[key]) value.onEnable();\n        });\n    },\n\n    stop() {\n        Object.entries(indicatorLocations).forEach(([_, value]) => {\n            value.onDisable();\n        });\n    },\n\n    patches: [\n        {\n            find: \".Masks.STATUS_ONLINE_MOBILE\",\n            predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,\n            replacement: [\n                {\n                    // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status\n                    match: /\\.STATUS_TYPING;switch(?=.+?(if\\(\\i\\)return \\i\\.\\i\\.Masks\\.STATUS_ONLINE_MOBILE))/,\n                    replace: \".STATUS_TYPING;$1;switch\"\n                },\n                {\n                    // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status\n                    match: /switch\\(\\i\\)\\{case \\i\\.\\i\\.ONLINE:(if\\(\\i\\)return\\{[^}]+\\})/,\n                    replace: \"$1;$&\"\n                }\n            ]\n        },\n        {\n            find: \".AVATAR_STATUS_MOBILE_16;\",\n            predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,\n            replacement: [\n                {\n                    // Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status\n                    match: /\\i===\\i\\.\\i\\.ONLINE&&(?=.{0,70}\\.AVATAR_STATUS_MOBILE_16;)/,\n                    replace: \"\"\n                },\n                {\n                    // Fix sizes for mobile indicators which aren't online\n                    match: /(?<=\\(\\i\\.status,)(\\i)(?=,\\{.{0,15}isMobile:(\\i))/,\n                    replace: '$2?\"online\":$1'\n                },\n                {\n                    // Make isMobile true no matter the status\n                    match: /(?<=\\i&&!\\i)&&\\i===\\i\\.\\i\\.ONLINE/,\n                    replace: \"\"\n                }\n            ]\n        },\n        {\n            find: \"}isMobileOnline(\",\n            predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,\n            replacement: {\n                // Make isMobileOnline return true no matter what is the user status\n                match: /(?<=\\i\\[\\i\\.\\i\\.MOBILE\\])===\\i\\.\\i\\.ONLINE/,\n                replace: \"!= null\"\n            }\n        }\n    ],\n\n    options: {\n        ...Object.fromEntries(\n            Object.entries(indicatorLocations).map(([key, value]) => {\n                return [key, {\n                    type: OptionType.BOOLEAN,\n                    description: `Show indicators ${value.description.toLowerCase()}`,\n                    // onChange doesn't give any way to know which setting was changed, so restart required\n                    restartNeeded: true,\n                    default: true\n                }];\n            })\n        ),\n        colorMobileIndicator: {\n            type: OptionType.BOOLEAN,\n            description: \"Whether to make the mobile indicator match the color of the user status.\",\n            default: true,\n            restartNeeded: true\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/platformIndicators/style.css",
    "content": ".vc-platform-indicator {\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    vertical-align: top;\n    position: relative;\n}\n"
  },
  {
    "path": "src/plugins/previewMessage/README.md",
    "content": "# PreviewMessage\n\nLets you preview your message before sending it.\n\n![the plugin in action](https://github.com/Vendicated/Vencord/assets/45497981/3ce32860-e5cd-4ea2-bdab-e121f1703579)\n\n"
  },
  {
    "path": "src/plugins/previewMessage/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ChatBarButton, ChatBarButtonFactory } from \"@api/ChatButtons\";\nimport { generateId, sendBotMessage } from \"@api/Commands\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { IconComponent, StartAt } from \"@utils/types\";\nimport { CloudUpload, MessageAttachment } from \"@vencord/discord-types\";\nimport { DraftStore, DraftType, UploadAttachmentStore, UserStore, useStateFromStores } from \"@webpack/common\";\n\nconst getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);\n\n\nconst getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>\n    new Promise(res => {\n        const img = new Image();\n        img.onload = () =>\n            res({ width: img.width, height: img.height });\n\n        img.onerror = () =>\n            res(null);\n\n        img.src = url;\n    });\n\n\nconst getAttachments = async (channelId: string) =>\n    await Promise.all(\n        UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage)\n            .map(async (upload: CloudUpload) => {\n                const { isImage, filename, spoiler, item: { file } } = upload;\n                const url = URL.createObjectURL(file);\n                const attachment: MessageAttachment = {\n                    id: generateId(),\n                    filename: spoiler ? \"SPOILER_\" + filename : filename,\n                    // weird eh? if i give it the normal content type the preview doenst work\n                    content_type: undefined,\n                    size: upload.getSize(),\n                    spoiler,\n                    // discord adds query params to the url, so we need to add a hash to prevent that\n                    url: url + \"#\",\n                    proxy_url: url + \"#\",\n                };\n\n                if (isImage) {\n                    const box = await getImageBox(url);\n                    if (!box) return attachment;\n\n                    attachment.width = box.width;\n                    attachment.height = box.height;\n                }\n\n                return attachment;\n            })\n    );\n\n\nconst PreviewIcon: IconComponent = ({ height = 20, width = 20, className }) => {\n    return (\n        <svg\n            fill=\"currentColor\"\n            fillRule=\"evenodd\"\n            width={width}\n            height={height}\n            className={className}\n            viewBox=\"0 0 24 24\"\n            style={{ scale: \"1.096\", translate: \"0 -1px\" }}\n        >\n            <path d=\"M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z\" />\n        </svg>\n    );\n};\n\nconst PreviewButton: ChatBarButtonFactory = ({ isAnyChat, isEmpty, type: { attachments }, channel: { id: channelId } }) => {\n    const draft = useStateFromStores([DraftStore], () => getDraft(channelId));\n\n    if (!isAnyChat) return null;\n\n    const hasAttachments = attachments && UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage).length > 0;\n    const hasContent = !isEmpty && draft?.length > 0;\n\n    if (!hasContent && !hasAttachments) return null;\n\n    return (\n        <ChatBarButton\n            tooltip=\"Preview Message\"\n            onClick={async () =>\n                sendBotMessage(\n                    channelId,\n                    {\n                        content: getDraft(channelId),\n                        author: UserStore.getCurrentUser(),\n                        attachments: hasAttachments ? await getAttachments(channelId) : undefined,\n                    }\n                )}\n            buttonProps={{\n                style: {\n                    translate: \"0 2px\"\n                }\n            }}\n        >\n            <PreviewIcon />\n        </ChatBarButton>\n    );\n\n};\n\nexport default definePlugin({\n    name: \"PreviewMessage\",\n    description: \"Lets you preview your message before sending it.\",\n    authors: [Devs.Aria],\n    // start early to ensure we're the first plugin to add our button\n    // This makes the popping in less awkward\n    startAt: StartAt.Init,\n\n    chatBarButton: {\n        icon: PreviewIcon,\n        render: PreviewButton\n    }\n});\n"
  },
  {
    "path": "src/plugins/quickMention/README.md",
    "content": "# QuickMention\n\nAdds a mention icon to the messages action bar\n\n![](https://github.com/Vendicated/Vencord/assets/55940580/82d3fec7-4196-4917-b3c2-6e652b2aff9e)\n"
  },
  {
    "path": "src/plugins/quickMention/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport { insertTextIntoChatInputBox } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { ChannelStore, PermissionsBits, PermissionStore } from \"@webpack/common\";\n\nfunction Icon({ height = 24, width = 24, className = \"icon\" }: { height?: number | string; width?: number | string; className?: string; }) {\n    return (\n        <svg\n            className={className}\n            height={height}\n            width={width}\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n        >\n            <path\n                d=\"M12 2C6.486 2 2 6.486 2 12C2 17.515 6.486 22 12 22C14.039 22 15.993 21.398 17.652 20.259L16.521 18.611C15.195 19.519 13.633 20 12 20C7.589 20 4 16.411 4 12C4 7.589 7.589 4 12 4C16.411 4 20 7.589 20 12V12.782C20 14.17 19.402 15 18.4 15L18.398 15.018C18.338 15.005 18.273 15 18.209 15H18C17.437 15 16.6 14.182 16.6 13.631V12C16.6 9.464 14.537 7.4 12 7.4C9.463 7.4 7.4 9.463 7.4 12C7.4 14.537 9.463 16.6 12 16.6C13.234 16.6 14.35 16.106 15.177 15.313C15.826 16.269 16.93 17 18 17L18.002 16.981C18.064 16.994 18.129 17 18.195 17H18.4C20.552 17 22 15.306 22 12.782V12C22 6.486 17.514 2 12 2ZM12 14.599C10.566 14.599 9.4 13.433 9.4 11.999C9.4 10.565 10.566 9.399 12 9.399C13.434 9.399 14.6 10.565 14.6 11.999C14.6 13.433 13.434 14.599 12 14.599Z\"\n            />\n        </svg>\n    );\n}\n\nexport default definePlugin({\n    name: \"QuickMention\",\n    authors: [Devs.kemo],\n    description: \"Adds a quick mention button to the message actions bar\",\n\n    messagePopoverButton: {\n        icon: Icon,\n        render(msg) {\n            const channel = ChannelStore.getChannel(msg.channel_id);\n            if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;\n\n            return {\n                label: \"Quick Mention\",\n                icon: Icon,\n                message: msg,\n                channel,\n                onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)\n            };\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/quickReply/README.md",
    "content": "# QuickReply\n\nReply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds\n\n![](https://github.com/Vendicated/Vencord/assets/55940580/df79a27a-6529-4c70-8870-3c17d3637e4f)\n\n"
  },
  {
    "path": "src/plugins/quickReply/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport NoBlockedMessagesPlugin from \"@plugins/noBlockedMessages\";\nimport NoReplyMentionPlugin from \"@plugins/noReplyMention\";\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { MessageFlags } from \"@vencord/discord-types/enums\";\nimport { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, MessageTypeSets, PermissionsBits, PermissionStore, RelationshipStore, SelectedChannelStore, UserStore } from \"@webpack/common\";\n\nlet currentlyReplyingId: string | null = null;\nlet currentlyEditingId: string | null = null;\n\nconst enum MentionOptions {\n    DISABLED,\n    ENABLED,\n    NO_REPLY_MENTION_PLUGIN\n}\n\nconst settings = definePluginSettings({\n    shouldMention: {\n        type: OptionType.SELECT,\n        description: \"Ping reply by default\",\n        options: [\n            {\n                label: \"Follow NoReplyMention plugin (if enabled)\",\n                value: MentionOptions.NO_REPLY_MENTION_PLUGIN,\n                default: true\n            },\n            { label: \"Enabled\", value: MentionOptions.ENABLED },\n            { label: \"Disabled\", value: MentionOptions.DISABLED },\n        ]\n    },\n    ignoreBlockedAndIgnored: {\n        type: OptionType.BOOLEAN,\n        description: \"Ignore messages by blocked/ignored users when navigating\",\n        default: true\n    }\n});\n\nexport default definePlugin({\n    name: \"QuickReply\",\n    authors: [Devs.fawn, Devs.Ven, Devs.pylix],\n    description: \"Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds\",\n    settings,\n\n    start() {\n        document.addEventListener(\"keydown\", onKeydown);\n    },\n\n    stop() {\n        document.removeEventListener(\"keydown\", onKeydown);\n    },\n\n    flux: {\n        DELETE_PENDING_REPLY() {\n            currentlyReplyingId = null;\n        },\n        MESSAGE_END_EDIT() {\n            currentlyEditingId = null;\n        },\n        CHANNEL_SELECT() {\n            currentlyReplyingId = null;\n            currentlyEditingId = null;\n        },\n        MESSAGE_START_EDIT: onStartEdit,\n        CREATE_PENDING_REPLY: onCreatePendingReply\n    }\n});\n\nfunction onStartEdit({ messageId, _isQuickEdit }: any) {\n    if (_isQuickEdit) return;\n    currentlyEditingId = messageId;\n}\n\nfunction onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {\n    if (_isQuickReply) return;\n\n    currentlyReplyingId = message.id;\n}\n\nconst isCtrl = (e: KeyboardEvent) => IS_MAC ? e.metaKey : e.ctrlKey;\nconst isAltOrMeta = (e: KeyboardEvent) => e.altKey || (!IS_MAC && e.metaKey);\n\nfunction onKeydown(e: KeyboardEvent) {\n    const isUp = e.key === \"ArrowUp\";\n    if (!isUp && e.key !== \"ArrowDown\") return;\n    if (!isCtrl(e) || isAltOrMeta(e)) return;\n\n    e.preventDefault();\n\n    if (e.shiftKey)\n        nextEdit(isUp);\n    else\n        nextReply(isUp);\n}\n\nfunction jumpIfOffScreen(channelId: string, messageId: string) {\n    const element = document.getElementById(\"message-content-\" + messageId);\n    if (!element) return;\n\n    const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);\n    const rect = element.getBoundingClientRect();\n    const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;\n\n    if (isOffscreen) {\n        MessageActions.jumpToMessage({\n            channelId,\n            messageId,\n            flash: false,\n            jumpType: \"INSTANT\"\n        });\n    }\n}\n\nfunction getNextMessage(isUp: boolean, isReply: boolean) {\n    let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;\n\n    const meId = UserStore.getCurrentUser().id;\n    const hasNoBlockedMessages = isPluginEnabled(NoBlockedMessagesPlugin.name);\n\n    messages = messages.filter(m => {\n        if (m.deleted) return false;\n        if (!isReply && m.author.id !== meId) return false; // editing only own messages\n        if (!MessageTypeSets.REPLYABLE.has(m.type) || m.hasFlag(MessageFlags.EPHEMERAL)) return false;\n        if (settings.store.ignoreBlockedAndIgnored && RelationshipStore.isBlockedOrIgnored(m.author.id)) return false;\n        if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;\n\n        return true;\n    });\n\n    const findNextNonDeleted = (id: string | null) => {\n        if (id === null) return messages[messages.length - 1];\n\n        const idx = messages.findIndex(m => m.id === id);\n        if (idx === -1) return messages[messages.length - 1];\n\n        const i = isUp ? idx - 1 : idx + 1;\n        return messages[i] ?? null;\n    };\n\n    if (isReply) {\n        const msg = findNextNonDeleted(currentlyReplyingId);\n        currentlyReplyingId = msg?.id ?? null;\n        return msg;\n    } else {\n        const msg = findNextNonDeleted(currentlyEditingId);\n        currentlyEditingId = msg?.id ?? null;\n        return msg;\n    }\n}\n\nfunction shouldMention(message: Message) {\n    switch (settings.store.shouldMention) {\n        case MentionOptions.NO_REPLY_MENTION_PLUGIN:\n            if (!isPluginEnabled(NoReplyMentionPlugin.name)) return true;\n            return NoReplyMentionPlugin.shouldMention(message, false);\n        case MentionOptions.DISABLED:\n            return false;\n        default:\n            return true;\n    }\n}\n\n// handle next/prev reply\nfunction nextReply(isUp: boolean) {\n    const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());\n    if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;\n\n    const message = getNextMessage(isUp, true);\n\n    if (!message) {\n        return void Dispatcher.dispatch({\n            type: \"DELETE_PENDING_REPLY\",\n            channelId: SelectedChannelStore.getChannelId(),\n        });\n    }\n\n    const channel = ChannelStore.getChannel(message.channel_id);\n    const meId = UserStore.getCurrentUser().id;\n\n    Dispatcher.dispatch({\n        type: \"CREATE_PENDING_REPLY\",\n        channel,\n        message,\n        shouldMention: shouldMention(message),\n        showMentionToggle: !channel.isPrivate() && message.author.id !== meId,\n        _isQuickReply: true\n    });\n\n    ComponentDispatch.dispatchToLastSubscribed(\"TEXTAREA_FOCUS\");\n    jumpIfOffScreen(channel.id, message.id);\n}\n\n// handle next/prev edit\nfunction nextEdit(isUp: boolean) {\n    const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());\n    if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;\n    const message = getNextMessage(isUp, false);\n\n    if (!message) {\n        return Dispatcher.dispatch({\n            type: \"MESSAGE_END_EDIT\",\n            channelId: SelectedChannelStore.getChannelId()\n        });\n    }\n\n    Dispatcher.dispatch({\n        type: \"MESSAGE_START_EDIT\",\n        channelId: message.channel_id,\n        messageId: message.id,\n        content: message.content,\n        _isQuickEdit: true\n    });\n\n    jumpIfOffScreen(message.channel_id, message.id);\n}\n"
  },
  {
    "path": "src/plugins/reactErrorDecoder/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { React } from \"@webpack/common\";\n\nlet ERROR_CODES: Record<string, string> | undefined;\n\nexport default definePlugin({\n    name: \"ReactErrorDecoder\",\n    description: 'Replaces \"Minifed React Error\" with the actual error.',\n    authors: [Devs.Cyn, Devs.maisymoe],\n    patches: [\n        {\n            find: \"React has blocked a javascript: URL as a security precaution.\",\n            replacement: {\n                match: /\"https:\\/\\/react.dev\\/errors\\/\"\\+\\i;/,\n                replace: \"$&const vcDecodedError=$self.decodeError(...arguments);if(vcDecodedError)return vcDecodedError;\"\n            }\n        }\n    ],\n\n    async start() {\n        const CODES_URL = `https://raw.githubusercontent.com/facebook/react/v${React.version}/scripts/error-codes/codes.json`;\n\n        ERROR_CODES = await fetch(CODES_URL)\n            .then(res => res.json())\n            .catch(e => console.error(\"[ReactErrorDecoder] Failed to fetch React error codes\\n\", e));\n    },\n\n    stop() {\n        ERROR_CODES = undefined;\n    },\n\n    decodeError(code: number, ...args: any) {\n        let index = 0;\n        return ERROR_CODES?.[code]?.replace(/%s/g, () => {\n            const arg = args[index];\n            index++;\n            return arg;\n        });\n    }\n});\n"
  },
  {
    "path": "src/plugins/readAllNotificationsButton/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { addServerListElement, removeServerListElement, ServerListRenderPosition } from \"@api/ServerList\";\nimport { TextButton } from \"@components/Button\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { ActiveJoinedThreadsStore, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from \"@webpack/common\";\n\nfunction onClick() {\n    const channels: Array<any> = [];\n\n    Object.values(GuildStore.getGuilds()).forEach(guild => {\n        GuildChannelStore.getChannels(guild.id).SELECTABLE\n            .concat(GuildChannelStore.getChannels(guild.id).VOCAL)\n            .concat(\n                Object.values(ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id))\n                    .flatMap(threadChannels => Object.values(threadChannels))\n            )\n            .forEach((c: { channel: { id: string; }; }) => {\n                if (!ReadStateStore.hasUnread(c.channel.id)) return;\n\n                channels.push({\n                    channelId: c.channel.id,\n                    messageId: ReadStateStore.lastMessageId(c.channel.id),\n                    readStateType: 0\n                });\n            });\n    });\n\n    FluxDispatcher.dispatch({\n        type: \"BULK_ACK\",\n        context: \"APP\",\n        channels: channels\n    });\n}\n\nconst ReadAllButton = () => (\n    <TextButton\n        variant=\"secondary\"\n        onClick={onClick}\n        className=\"vc-ranb-button\"\n    >\n        Read All\n    </TextButton>\n);\n\nexport default definePlugin({\n    name: \"ReadAllNotificationsButton\",\n    description: \"Read all server notifications with a single button click!\",\n    authors: [Devs.kemo],\n    dependencies: [\"ServerListAPI\"],\n\n    renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),\n\n    start() {\n        addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);\n    },\n\n    stop() {\n        removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);\n    }\n});\n"
  },
  {
    "path": "src/plugins/readAllNotificationsButton/style.css",
    "content": ".vc-ranb-button {\n    color: var(--interactive-icon-default);\n    padding: 0 0.5em;\n    width: 100%;\n    font-size: 14px;\n    white-space: nowrap;\n    box-sizing: border-box;\n}\n\n.vc-ranb-button:hover {\n    color: var(--interactive-icon-active);\n}"
  },
  {
    "path": "src/plugins/relationshipNotifier/functions.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { getUniqueUsername, openUserProfile } from \"@utils/discord\";\nimport { ChannelType, RelationshipType } from \"@vencord/discord-types/enums\";\nimport { UserUtils } from \"@webpack/common\";\n\nimport settings from \"./settings\";\nimport { ChannelDelete, GuildDelete, RelationshipRemove } from \"./types\";\nimport { deleteGroup, deleteGuild, getGroup, getGuild, GuildAvailabilityStore, notify } from \"./utils\";\n\nlet manuallyRemovedFriend: string | undefined;\nlet manuallyRemovedGuild: string | undefined;\nlet manuallyRemovedGroup: string | undefined;\n\nexport const removeFriend = (id: string) => manuallyRemovedFriend = id;\nexport const removeGuild = (id: string) => manuallyRemovedGuild = id;\nexport const removeGroup = (id: string) => manuallyRemovedGroup = id;\n\nexport async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) {\n    if (manuallyRemovedFriend === id) {\n        manuallyRemovedFriend = undefined;\n        return;\n    }\n\n    const user = await UserUtils.getUser(id)\n        .catch(() => null);\n    if (!user) return;\n\n    switch (type) {\n        case RelationshipType.FRIEND:\n            if (settings.store.friends)\n                notify(\n                    `${getUniqueUsername(user)} removed you as a friend.`,\n                    user.getAvatarURL(undefined, undefined, false),\n                    () => openUserProfile(user.id)\n                );\n            break;\n        case RelationshipType.INCOMING_REQUEST:\n            if (settings.store.friendRequestCancels)\n                notify(\n                    `A friend request from ${getUniqueUsername(user)} has been removed.`,\n                    user.getAvatarURL(undefined, undefined, false),\n                    () => openUserProfile(user.id)\n                );\n            break;\n    }\n}\n\nexport function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) {\n    if (!settings.store.servers) return;\n    if (unavailable || GuildAvailabilityStore.isUnavailable(id)) return;\n\n    if (manuallyRemovedGuild === id) {\n        deleteGuild(id);\n        manuallyRemovedGuild = undefined;\n        return;\n    }\n\n    const guild = getGuild(id);\n    if (guild) {\n        deleteGuild(id);\n        notify(`You were removed from the server ${guild.name}.`, guild.iconURL);\n    }\n}\n\nexport function onChannelDelete({ channel: { id, type } }: ChannelDelete) {\n    if (!settings.store.groups) return;\n    if (type !== ChannelType.GROUP_DM) return;\n\n    if (manuallyRemovedGroup === id) {\n        deleteGroup(id);\n        manuallyRemovedGroup = undefined;\n        return;\n    }\n\n    const group = getGroup(id);\n    if (group) {\n        deleteGroup(id);\n        notify(`You were removed from the group ${group.name}.`, group.iconURL);\n    }\n}\n"
  },
  {
    "path": "src/plugins/relationshipNotifier/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nimport { onChannelDelete, onGuildDelete, onRelationshipRemove, removeFriend, removeGroup, removeGuild } from \"./functions\";\nimport settings from \"./settings\";\nimport { syncAndRunChecks, syncFriends, syncGroups, syncGuilds } from \"./utils\";\n\nexport default definePlugin({\n    name: \"RelationshipNotifier\",\n    description: \"Notifies you when a friend, group chat, or server removes you.\",\n    authors: [Devs.nick],\n    settings,\n\n    patches: [\n        {\n            find: \"removeRelationship:(\",\n            replacement: {\n                match: /(removeRelationship:\\((\\i),\\i,\\i\\)=>)/,\n                replace: \"$1($self.removeFriend($2),0)||\"\n            }\n        },\n        {\n            find: \"async leaveGuild(\",\n            replacement: {\n                match: /(leaveGuild\\((\\i)\\){)/,\n                replace: \"$1$self.removeGuild($2);\"\n            }\n        },\n        {\n            find: \"},closePrivateChannel(\",\n            replacement: {\n                match: /(closePrivateChannel\\((\\i)\\){)/,\n                replace: \"$1$self.removeGroup($2);\"\n            }\n        }\n    ],\n\n    flux: {\n        GUILD_CREATE: syncGuilds,\n        GUILD_DELETE: onGuildDelete,\n        CHANNEL_CREATE: syncGroups,\n        CHANNEL_DELETE: onChannelDelete,\n        RELATIONSHIP_ADD: syncFriends,\n        RELATIONSHIP_UPDATE: syncFriends,\n        RELATIONSHIP_REMOVE(e) {\n            onRelationshipRemove(e);\n            syncFriends();\n        },\n        CONNECTION_OPEN: syncAndRunChecks\n    },\n\n    async start() {\n        setTimeout(() => {\n            syncAndRunChecks();\n        }, 5000);\n    },\n\n    removeFriend,\n    removeGroup,\n    removeGuild\n});\n"
  },
  {
    "path": "src/plugins/relationshipNotifier/settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { OptionType } from \"@utils/types\";\n\nexport default definePluginSettings({\n    notices: {\n        type: OptionType.BOOLEAN,\n        description: \"Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).\",\n        default: false\n    },\n    offlineRemovals: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify you when starting discord if you were removed while offline.\",\n        default: true\n    },\n    friends: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify when a friend removes you\",\n        default: true\n    },\n    friendRequestCancels: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify when a friend request is cancelled\",\n        default: true\n    },\n    servers: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify when removed from a server\",\n        default: true\n    },\n    groups: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify when removed from a group chat\",\n        default: true\n    }\n});\n"
  },
  {
    "path": "src/plugins/relationshipNotifier/types.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Channel } from \"@vencord/discord-types\";\n\nexport interface ChannelDelete {\n    type: \"CHANNEL_DELETE\";\n    channel: Channel;\n}\n\nexport interface GuildDelete {\n    type: \"GUILD_DELETE\";\n    guild: {\n        id: string;\n        unavailable?: boolean;\n    };\n}\n\nexport interface RelationshipRemove {\n    type: \"RELATIONSHIP_REMOVE\";\n    relationship: {\n        id: string;\n        nickname: string;\n        type: number;\n    };\n}\n\nexport interface SimpleGroupChannel {\n    id: string;\n    name: string;\n    iconURL?: string;\n}\n\nexport interface SimpleGuild {\n    id: string;\n    name: string;\n    iconURL?: string;\n}\n"
  },
  {
    "path": "src/plugins/relationshipNotifier/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as DataStore from \"@api/DataStore\";\nimport { popNotice, showNotice } from \"@api/Notices\";\nimport { showNotification } from \"@api/Notifications\";\nimport { getUniqueUsername, openUserProfile } from \"@utils/discord\";\nimport { FluxStore } from \"@vencord/discord-types\";\nimport { ChannelType, RelationshipType } from \"@vencord/discord-types/enums\";\nimport { findStoreLazy } from \"@webpack\";\nimport { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from \"@webpack/common\";\n\nimport settings from \"./settings\";\nimport { SimpleGroupChannel, SimpleGuild } from \"./types\";\n\nexport const GuildAvailabilityStore = findStoreLazy(\"GuildAvailabilityStore\") as FluxStore & {\n    totalGuilds: number;\n    totalUnavailableGuilds: number;\n    unavailableGuilds: string[];\n    isUnavailable(guildId: string): boolean;\n};\n\nconst guilds = new Map<string, SimpleGuild>();\nconst groups = new Map<string, SimpleGroupChannel>();\nconst friends = {\n    friends: [] as string[],\n    requests: [] as string[]\n};\n\nconst guildsKey = () => `relationship-notifier-guilds-${UserStore.getCurrentUser().id}`;\nconst groupsKey = () => `relationship-notifier-groups-${UserStore.getCurrentUser().id}`;\nconst friendsKey = () => `relationship-notifier-friends-${UserStore.getCurrentUser().id}`;\n\nasync function runMigrations() {\n    DataStore.delMany([\"relationship-notifier-guilds\", \"relationship-notifier-groups\", \"relationship-notifier-friends\"]);\n}\n\nexport async function syncAndRunChecks() {\n    await runMigrations();\n    if (UserStore.getCurrentUser() == null) return;\n\n    const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([\n        guildsKey(),\n        groupsKey(),\n        friendsKey()\n    ]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<\"friends\" | \"requests\", string[]> | undefined];\n\n    await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);\n\n    if (settings.store.offlineRemovals) {\n        if (settings.store.groups && oldGroups?.size) {\n            for (const [id, group] of oldGroups) {\n                if (!groups.has(id))\n                    notify(`You are no longer in the group ${group.name}.`, group.iconURL);\n            }\n        }\n\n        if (settings.store.servers && oldGuilds?.size) {\n            for (const [id, guild] of oldGuilds) {\n                if (!guilds.has(id) && !GuildAvailabilityStore.isUnavailable(id))\n                    notify(`You are no longer in the server ${guild.name}.`, guild.iconURL);\n            }\n        }\n\n        if (settings.store.friends && oldFriends?.friends.length) {\n            for (const id of oldFriends.friends) {\n                if (friends.friends.includes(id)) continue;\n\n                const user = await UserUtils.getUser(id).catch(() => void 0);\n                if (user)\n                    notify(\n                        `You are no longer friends with ${getUniqueUsername(user)}.`,\n                        user.getAvatarURL(undefined, undefined, false),\n                        () => openUserProfile(user.id)\n                    );\n            }\n        }\n\n        if (settings.store.friendRequestCancels && oldFriends?.requests?.length) {\n            for (const id of oldFriends.requests) {\n                if (\n                    friends.requests.includes(id) ||\n                    [RelationshipType.FRIEND, RelationshipType.BLOCKED, RelationshipType.OUTGOING_REQUEST].includes(RelationshipStore.getRelationshipType(id))\n                ) continue;\n\n                const user = await UserUtils.getUser(id).catch(() => void 0);\n                if (user)\n                    notify(\n                        `Friend request from ${getUniqueUsername(user)} has been revoked.`,\n                        user.getAvatarURL(undefined, undefined, false),\n                        () => openUserProfile(user.id)\n                    );\n            }\n        }\n    }\n}\n\nexport function notify(text: string, icon?: string, onClick?: () => void) {\n    if (settings.store.notices)\n        showNotice(text, \"OK\", () => popNotice());\n\n    showNotification({\n        title: \"Relationship Notifier\",\n        body: text,\n        icon,\n        onClick\n    });\n}\n\nexport function getGuild(id: string) {\n    return guilds.get(id);\n}\n\nexport function deleteGuild(id: string) {\n    guilds.delete(id);\n    syncGuilds();\n}\n\nexport async function syncGuilds() {\n    guilds.clear();\n\n    const me = UserStore.getCurrentUser().id;\n    for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) {\n        if (GuildMemberStore.isMember(id, me))\n            guilds.set(id, {\n                id,\n                name,\n                iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`\n            });\n    }\n    await DataStore.set(guildsKey(), guilds);\n}\n\nexport function getGroup(id: string) {\n    return groups.get(id);\n}\n\nexport function deleteGroup(id: string) {\n    groups.delete(id);\n    syncGroups();\n}\n\nexport async function syncGroups() {\n    groups.clear();\n\n    for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) {\n        if (type === ChannelType.GROUP_DM)\n            groups.set(id, {\n                id,\n                name: name || rawRecipients.map(r => r.username).join(\", \"),\n                iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png`\n            });\n    }\n\n    await DataStore.set(groupsKey(), groups);\n}\n\nexport async function syncFriends() {\n    friends.friends = [];\n    friends.requests = [];\n\n    const relationShips = RelationshipStore.getMutableRelationships();\n    for (const [id, type] of relationShips) {\n        switch (type) {\n            case RelationshipType.FRIEND:\n                friends.friends.push(id);\n                break;\n            case RelationshipType.INCOMING_REQUEST:\n                friends.requests.push(id);\n                break;\n        }\n    }\n\n    await DataStore.set(friendsKey(), friends);\n}\n"
  },
  {
    "path": "src/plugins/replaceGoogleSearch/README.md",
    "content": "# ReplaceGoogleSearch\n\nReplaces the Google search with different Engines\n\n![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/8b8158d2-0407-4d7b-9dff-a8b9bdc1a122)\n"
  },
  {
    "path": "src/plugins/replaceGoogleSearch/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Flex } from \"@components/Flex\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Menu } from \"@webpack/common\";\n\nconst DefaultEngines = {\n    Google: \"https://www.google.com/search?q=\",\n    DuckDuckGo: \"https://duckduckgo.com/?q=\",\n    Brave: \"https://search.brave.com/search?q=\",\n    Bing: \"https://www.bing.com/search?q=\",\n    Yahoo: \"https://search.yahoo.com/search?p=\",\n    Yandex: \"https://yandex.com/search/?text=\",\n    GitHub: \"https://github.com/search?q=\",\n    Reddit: \"https://www.reddit.com/search?q=\",\n    Wikipedia: \"https://wikipedia.org/w/index.php?search=\",\n    Startpage: \"https://www.startpage.com/sp/search?query=\"\n} as const;\n\nconst enum ReplacementEngineValue {\n    OFF = \"off\",\n    CUSTOM = \"custom\",\n}\n\nconst settings = definePluginSettings({\n    customEngineName: {\n        description: \"Name of the custom search engine\",\n        type: OptionType.STRING,\n        placeholder: \"Google\"\n    },\n    customEngineURL: {\n        description: \"The URL of your Engine\",\n        type: OptionType.STRING,\n        placeholder: \"https://google.com/search?q=\"\n    },\n    replacementEngine: {\n        description: \"Replace with a specific search engine instead of adding a menu\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Off\", value: ReplacementEngineValue.OFF, default: true },\n            { label: \"Custom Engine\", value: ReplacementEngineValue.CUSTOM },\n            ...Object.keys(DefaultEngines).map(engine => ({ label: engine, value: engine }))\n        ]\n    }\n});\n\nfunction search(src: string, engine: string) {\n    open(engine + encodeURIComponent(src.trim()), \"_blank\");\n}\n\nfunction makeSearchItem(src: string) {\n    const { customEngineName, customEngineURL, replacementEngine } = settings.store;\n\n    const hasCustomEngine = Boolean(customEngineName && customEngineURL);\n    const hasValidReplacementEngine = replacementEngine !== ReplacementEngineValue.OFF && !(replacementEngine === ReplacementEngineValue.CUSTOM && !hasCustomEngine);\n\n    const Engines = { ...DefaultEngines };\n\n    if (hasCustomEngine) {\n        Engines[customEngineName!] = customEngineURL;\n    }\n\n    if (hasValidReplacementEngine) {\n        const name = replacementEngine === ReplacementEngineValue.CUSTOM && hasCustomEngine\n            ? customEngineName\n            : replacementEngine;\n\n        return (\n            <Menu.MenuItem\n                label={`Search with ${name}`}\n                key=\"search-custom-engine\"\n                id=\"vc-search-custom-engine\"\n                action={() => search(src, Engines[name!])}\n            />\n        );\n    }\n\n    return (\n        <Menu.MenuItem\n            label=\"Search Text\"\n            key=\"search-text\"\n            id=\"vc-search-text\"\n        >\n            {Object.keys(Engines).map(engine => {\n                const key = \"vc-search-content-\" + engine;\n                return (\n                    <Menu.MenuItem\n                        key={key}\n                        id={key}\n                        label={\n                            <Flex gap=\"0.5em\" alignItems=\"center\">\n                                <img\n                                    style={{\n                                        borderRadius: \"50%\"\n                                    }}\n                                    aria-hidden=\"true\"\n                                    height={16}\n                                    width={16}\n                                    src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).hostname}.ico`}\n                                />\n                                {engine}\n                            </Flex>\n                        }\n                        action={() => search(src, Engines[engine])}\n                    />\n                );\n            })}\n        </Menu.MenuItem>\n    );\n}\n\nconst messageContextMenuPatch: NavContextMenuPatchCallback = (children, _props) => {\n    const selection = document.getSelection()?.toString();\n    if (!selection) return;\n\n    const group = findGroupChildrenByChildId(\"search-google\", children);\n    if (group) {\n        const idx = group.findIndex(c => c?.props?.id === \"search-google\");\n        if (idx !== -1) group[idx] = makeSearchItem(selection);\n    }\n};\n\nexport default definePlugin({\n    name: \"ReplaceGoogleSearch\",\n    description: \"Replaces the Google search with different Engine(s)\",\n    authors: [Devs.Moxxie, Devs.Ethan],\n\n    settings,\n\n    contextMenus: {\n        \"message\": messageContextMenuPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/replyTimestamp/README.md",
    "content": "# ReplyTimestamp\n\nShows timestamps on the previews of replied-to messages. Pretty simple.\n\n![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)\n"
  },
  {
    "path": "src/plugins/replyTimestamp/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./style.css\";\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport type { Message } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { DateUtils, Timestamp } from \"@webpack/common\";\nimport type { HTMLAttributes } from \"react\";\n\nconst MessageClasses = findCssClassesLazy(\"separator\", \"latin24CompactTimeStamp\");\n\nfunction Sep(props: HTMLAttributes<HTMLElement>) {\n    return <i className={MessageClasses.separator} aria-hidden={true} {...props} />;\n}\n\nconst enum ReferencedMessageState {\n    LOADED = 0,\n    NOT_LOADED = 1,\n    DELETED = 2,\n}\n\ntype ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };\n\nfunction ReplyTimestamp({\n    referencedMessage,\n    baseMessage,\n}: {\n    referencedMessage: ReferencedMessage,\n    baseMessage: Message;\n}) {\n    if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;\n    const refTimestamp = referencedMessage.message.timestamp as any;\n    const baseTimestamp = baseMessage.timestamp as any;\n    return (\n        <Timestamp\n            className=\"vc-reply-timestamp\"\n            compact={DateUtils.isSameDay(refTimestamp, baseTimestamp)}\n            timestamp={refTimestamp}\n            isInline={false}\n        >\n            <Sep>[</Sep>\n            {DateUtils.isSameDay(refTimestamp, baseTimestamp)\n                ? DateUtils.dateFormat(refTimestamp, \"LT\")\n                : DateUtils.calendarFormat(refTimestamp)\n            }\n            <Sep>]</Sep>\n        </Timestamp>\n    );\n}\n\nexport default definePlugin({\n    name: \"ReplyTimestamp\",\n    description: \"Shows a timestamp on replied-message previews\",\n    authors: [Devs.Kyuuhachi],\n\n    patches: [\n        {\n            // Same find as in ValidReply\n            find: \"#{intl::REPLY_QUOTE_MESSAGE_NOT_LOADED}\",\n            replacement: {\n                match: /\\.onClickReply,.+?}\\),(?=\\i,\\i,\\i\\])/,\n                replace: \"$&$self.ReplyTimestamp(arguments[0]),\"\n            }\n        }\n    ],\n\n    ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/replyTimestamp/style.css",
    "content": ".vc-reply-timestamp {\n    margin-right: 0.25em;\n}\n"
  },
  {
    "path": "src/plugins/revealAllSpoilers/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { findCssClassesLazy } from \"@webpack\";\n\nconst SpoilerClasses = findCssClassesLazy(\"spoilerContent\", \"hidden\");\nconst MessagesClasses = findCssClassesLazy(\"messagesWrapper\", \"navigationDescription\");\n\nexport default definePlugin({\n    name: \"RevealAllSpoilers\",\n    description: \"Reveal all spoilers in a message by Ctrl-clicking a spoiler, or in the chat with Ctrl+Shift-click\",\n    authors: [Devs.whqwert],\n\n    patches: [\n        {\n            find: \".removeObscurity,\",\n            replacement: {\n                match: /(?<=removeObscurity(?:\",|=)(\\i)=>{)/,\n                replace: (_, event) => `$self.reveal(${event});`\n            }\n        }\n    ],\n\n    reveal(event: MouseEvent) {\n        const { ctrlKey, metaKey, shiftKey, target } = event;\n\n        if (!(IS_MAC ? metaKey : ctrlKey)) { return; }\n\n        const { spoilerContent, hidden } = SpoilerClasses;\n        const { messagesWrapper } = MessagesClasses;\n\n        const parent = shiftKey\n            ? document.querySelector(`div.${messagesWrapper}`)\n            : (target as HTMLSpanElement).parentElement;\n\n        for (const spoiler of parent!.querySelectorAll(`span.${spoilerContent}.${hidden}`)) {\n            (spoiler as HTMLSpanElement).click();\n        }\n    }\n\n});\n"
  },
  {
    "path": "src/plugins/reverseImageSearch/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { Flex } from \"@components/Flex\";\nimport { OpenExternalIcon } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Menu } from \"@webpack/common\";\n\nconst Engines = {\n    Google: \"https://lens.google.com/uploadbyurl?url=\",\n    Yandex: \"https://yandex.com/images/search?rpt=imageview&url=\",\n    SauceNAO: \"https://saucenao.com/search.php?url=\",\n    IQDB: \"https://iqdb.org/?url=\",\n    Bing: \"https://www.bing.com/images/search?view=detailv2&iss=sbi&q=imgurl:\",\n    TinEye: \"https://www.tineye.com/search?url=\",\n    ImgOps: \"https://imgops.com/start?url=\"\n} as const;\n\nfunction search(src: string, engine: string) {\n    open(engine + encodeURIComponent(src), \"_blank\");\n}\n\nfunction makeSearchItem(src: string) {\n    return (\n        <Menu.MenuItem\n            label=\"Search Image\"\n            key=\"search-image\"\n            id=\"search-image\"\n        >\n            {Object.keys(Engines).map((engine, i) => {\n                const key = \"search-image-\" + engine;\n                return (\n                    <Menu.MenuItem\n                        key={key}\n                        id={key}\n                        label={\n                            <Flex alignItems=\"center\" gap=\"0.5em\">\n                                <img\n                                    style={{\n                                        borderRadius: \"50%\",\n                                    }}\n                                    aria-hidden=\"true\"\n                                    height={16}\n                                    width={16}\n                                    src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).host}.ico`}\n                                />\n                                {engine}\n                            </Flex>\n                        }\n                        action={() => search(src, Engines[engine])}\n                    />\n                );\n            })}\n            <Menu.MenuItem\n                key=\"search-image-all\"\n                id=\"search-image-all\"\n                label={\n                    <Flex alignItems=\"center\" gap=\"0.5em\">\n                        <OpenExternalIcon height={16} width={16} />\n                        All\n                    </Flex>\n                }\n                action={() => Object.values(Engines).forEach(e => search(src, e))}\n            />\n        </Menu.MenuItem>\n    );\n}\n\nconst messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {\n    if (props?.reverseImageSearchType !== \"img\") return;\n\n    const src = props.itemHref ?? props.itemSrc;\n\n    const group = findGroupChildrenByChildId(\"copy-link\", children);\n    group?.push(makeSearchItem(src));\n};\n\nconst imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {\n    if (!props?.src) return;\n\n    const group = findGroupChildrenByChildId(\"copy-native-link\", children) ?? children;\n    group.push(makeSearchItem(props.src));\n};\n\nexport default definePlugin({\n    name: \"ReverseImageSearch\",\n    description: \"Adds ImageSearch to image context menus\",\n    authors: [Devs.Ven, Devs.Nuckyz],\n    tags: [\"ImageUtilities\"],\n\n    patches: [\n        {\n            find: \"#{intl::MESSAGE_ACTIONS_MENU_LABEL}),shouldHideMediaOptions:\",\n            replacement: {\n                match: /favoriteableType:\\i,(?<=(\\i)\\.getAttribute\\(\"data-type\"\\).+?)/,\n                replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute(\"data-role\"),`\n            }\n        }\n    ],\n    contextMenus: {\n        \"message\": messageContextMenuPatch,\n        \"image-context\": imageContextMenuPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/reviewDB/auth.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport * as DataStore from \"@api/DataStore\";\nimport { Logger } from \"@utils/Logger\";\nimport { openModal } from \"@utils/modal\";\nimport { OAuth2AuthorizeModal, showToast, Toasts, UserStore } from \"@webpack/common\";\n\nimport { ReviewDBAuth } from \"./entities\";\n\nconst DATA_STORE_KEY = \"rdb-auth\";\n\nexport let Auth: ReviewDBAuth = {};\n\nexport async function initAuth() {\n    Auth = await getAuth() ?? {};\n}\n\nexport async function getAuth(): Promise<ReviewDBAuth | undefined> {\n    const auth = await DataStore.get(DATA_STORE_KEY);\n    return auth?.[UserStore.getCurrentUser()?.id];\n}\n\nexport async function getToken() {\n    const auth = await getAuth();\n    return auth?.token;\n}\n\nexport async function updateAuth(newAuth: ReviewDBAuth) {\n    return DataStore.update(DATA_STORE_KEY, auth => {\n        auth ??= {};\n        Auth = auth[UserStore.getCurrentUser().id] ??= {};\n\n        if (newAuth.token) Auth.token = newAuth.token;\n        if (newAuth.user) Auth.user = newAuth.user;\n\n        return auth;\n    });\n}\n\nexport function authorize(callback?: any) {\n    openModal(props =>\n        <OAuth2AuthorizeModal\n            {...props}\n            scopes={[\"identify\"]}\n            responseType=\"code\"\n            redirectUri=\"https://manti.vendicated.dev/api/reviewdb/auth\"\n            permissions={0n}\n            clientId=\"915703782174752809\"\n            cancelCompletesFlow={false}\n            callback={async (response: any) => {\n                try {\n                    const url = new URL(response.location);\n                    url.searchParams.append(\"clientMod\", \"vencord\");\n                    const res = await fetch(url, {\n                        headers: { Accept: \"application/json\" }\n                    });\n\n                    if (!res.ok) {\n                        const { message } = await res.json();\n                        showToast(message || \"An error occured while authorizing\", Toasts.Type.FAILURE);\n                        return;\n                    }\n\n                    const { token } = await res.json();\n                    updateAuth({ token });\n                    showToast(\"Successfully logged in!\", Toasts.Type.SUCCESS);\n                    callback?.();\n                } catch (e) {\n                    new Logger(\"ReviewDB\").error(\"Failed to authorize\", e);\n                }\n            }}\n        />\n    );\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/BlockedUserModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Auth } from \"@plugins/reviewDB/auth\";\nimport { ReviewDBUser } from \"@plugins/reviewDB/entities\";\nimport { fetchBlocks, unblockUser } from \"@plugins/reviewDB/reviewDbApi\";\nimport { cl } from \"@plugins/reviewDB/utils\";\nimport { Logger } from \"@utils/Logger\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from \"@utils/modal\";\nimport { useAwaiter } from \"@utils/react\";\nimport { Forms, Tooltip, useState } from \"@webpack/common\";\n\nfunction UnblockButton(props: { onClick?(): void; }) {\n    return (\n        <Tooltip text=\"Unblock user\">\n            {tooltipProps => (\n                <div\n                    {...tooltipProps}\n                    role=\"button\"\n                    onClick={props.onClick}\n                    className={cl(\"block-modal-unblock\")}\n                >\n                    <svg height=\"20\" viewBox=\"0 -960 960 960\" width=\"20\" fill=\"var(--status-danger)\">\n                        <path d=\"M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z\" />\n                    </svg>\n                </div>\n            )}\n        </Tooltip>\n    );\n}\n\nfunction BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy: boolean; setIsBusy(v: boolean): void; }) {\n    const [gone, setGone] = useState(false);\n    if (gone) return null;\n\n    return (\n        <div className={cl(\"block-modal-row\")}>\n            <img className={cl(\"block-modal-avatar\")} src={user.profilePhoto} alt=\"\" />\n            <Forms.FormText className={cl(\"block-modal-username\")}>{user.username}</Forms.FormText>\n            <UnblockButton\n                onClick={isBusy ? undefined : async () => {\n                    setIsBusy(true);\n                    try {\n                        await unblockUser(user.discordID);\n                        setGone(true);\n                    } finally {\n                        setIsBusy(false);\n                    }\n                }}\n            />\n        </div>\n    );\n}\n\nfunction Modal() {\n    const [isBusy, setIsBusy] = useState(false);\n    const [blocks, error, pending] = useAwaiter(fetchBlocks, {\n        onError: e => new Logger(\"ReviewDB\").error(\"Failed to fetch blocks\", e),\n        fallbackValue: [],\n    });\n\n    if (pending)\n        return null;\n    if (error)\n        return <Forms.FormText>Failed to fetch blocks: ${String(error)}</Forms.FormText>;\n    if (!blocks.length)\n        return <Forms.FormText>No blocked users.</Forms.FormText>;\n\n    return (\n        <>\n            {blocks.map(b => (\n                <BlockedUser\n                    key={b.discordID}\n                    user={b}\n                    isBusy={isBusy}\n                    setIsBusy={setIsBusy}\n                />\n            ))}\n        </>\n    );\n}\n\nexport function openBlockModal() {\n    openModal(modalProps => (\n        <ModalRoot {...modalProps}>\n            <ModalHeader className={cl(\"block-modal-header\")}>\n                <Forms.FormTitle style={{ margin: 0 }}>Blocked Users</Forms.FormTitle>\n                <ModalCloseButton onClick={modalProps.onClose} />\n            </ModalHeader>\n            <ModalContent className={cl(\"block-modal\")}>\n                {Auth.token ? <Modal /> : <Forms.FormText>You are not logged into ReviewDB!</Forms.FormText>}\n            </ModalContent>\n        </ModalRoot>\n    ));\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/MessageButton.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { DeleteIcon } from \"@components/Icons\";\nimport { classes } from \"@utils/misc\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Tooltip } from \"@webpack/common\";\n\nconst iconClasses = findCssClassesLazy(\"button\", \"wrapper\", \"disabled\", \"separator\", \"dangerous\");\n\nexport function DeleteButton({ onClick }: { onClick(): void; }) {\n    return (\n        <Tooltip text=\"Delete Review\">\n            {props => (\n                <div\n                    {...props}\n                    className={classes(iconClasses.button, iconClasses.dangerous)}\n                    onClick={onClick}\n                    role=\"button\"\n                >\n                    <DeleteIcon width=\"20\" height=\"20\" />\n                </div>\n            )}\n        </Tooltip>\n    );\n}\n\nexport function ReportButton({ onClick }: { onClick(): void; }) {\n    return (\n        <Tooltip text=\"Report Review\">\n            {props => (\n                <div\n                    {...props}\n                    className={iconClasses.button}\n                    onClick={onClick}\n                    role=\"button\"\n                >\n                    <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\">\n                        <path\n                            fill=\"currentColor\"\n                            d=\"M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z\"\n                        />\n                    </svg>\n                </div>\n            )}\n        </Tooltip>\n    );\n}\n\nexport function BlockButton({ onClick, isBlocked }: { onClick(): void; isBlocked: boolean; }) {\n    return (\n        <Tooltip text={`${isBlocked ? \"Unblock\" : \"Block\"} user`}>\n            {props => (\n                <div\n                    {...props}\n                    className={iconClasses.button}\n                    onClick={onClick}\n                    role=\"button\"\n                >\n                    <svg height=\"20\" viewBox=\"0 -960 960 960\" width=\"20\" fill=\"currentColor\">\n                        {isBlocked\n                            ? <path d=\"M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\" />\n                            : <path d=\"M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z\" />\n                        }\n                    </svg>\n                </div>\n            )}\n        </Tooltip>\n    );\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/ReviewBadge.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Badge } from \"@plugins/reviewDB/entities\";\nimport { cl } from \"@plugins/reviewDB/utils\";\nimport { MaskedLink, React, Tooltip } from \"@webpack/common\";\nimport { HTMLAttributes } from \"react\";\n\nexport default function ReviewBadge(badge: Badge & { onClick?(): void; }) {\n    const Wrapper = badge.redirectURL\n        ? MaskedLink\n        : (props: HTMLAttributes<HTMLDivElement>) => (\n            <span {...props} role=\"button\">{props.children}</span>\n        );\n\n    return (\n        <Tooltip\n            text={badge.name}>\n            {({ onMouseEnter, onMouseLeave }) => (\n                <Wrapper className={cl(\"blocked-badge\")} href={badge.redirectURL!} onClick={badge.onClick}>\n                    <img\n                        className={cl(\"badge\")}\n                        width=\"22px\"\n                        height=\"22px\"\n                        onMouseEnter={onMouseEnter}\n                        onMouseLeave={onMouseLeave}\n                        src={badge.icon}\n                        alt={badge.description}\n                    />\n                </Wrapper>\n            )}\n        </Tooltip>\n    );\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/ReviewComponent.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Auth, getToken } from \"@plugins/reviewDB/auth\";\nimport { Review, ReviewType } from \"@plugins/reviewDB/entities\";\nimport { blockUser, deleteReview, reportReview, unblockUser } from \"@plugins/reviewDB/reviewDbApi\";\nimport { settings } from \"@plugins/reviewDB/settings\";\nimport { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from \"@plugins/reviewDB/utils\";\nimport { openUserProfile } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Alerts, IconUtils, Parser, Timestamp, useState } from \"@webpack/common\";\n\nimport { openBlockModal } from \"./BlockedUserModal\";\nimport { BlockButton, DeleteButton, ReportButton } from \"./MessageButton\";\nimport ReviewBadge from \"./ReviewBadge\";\n\nconst MessageClasses = findCssClassesLazy(\"cozyMessage\", \"message\", \"groupStart\", \"buttons\", \"buttonsInner\");\nconst ContainerClasses = findCssClassesLazy(\"container\", \"isHeader\");\nconst AvatarClasses = findCssClassesLazy(\"avatar\", \"wrapper\", \"cozy\", \"clickable\", \"username\");\nconst ButtonClasses = findCssClassesLazy(\"button\", \"wrapper\", \"selected\");\nconst BotTagClasses = findCssClassesLazy(\"botTagVerified\", \"botTagRegular\", \"botText\", \"px\", \"rem\");\n\nconst dateFormat = new Intl.DateTimeFormat();\n\nexport default function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) {\n    const [showAll, setShowAll] = useState(false);\n\n    function openModal() {\n        openUserProfile(review.sender.discordID);\n    }\n\n    function delReview() {\n        Alerts.show({\n            title: \"Are you sure?\",\n            body: \"Do you really want to delete this review?\",\n            confirmText: \"Delete\",\n            cancelText: \"Nevermind\",\n            onConfirm: async () => {\n                if (!(await getToken())) {\n                    return showToast(\"You must be logged in to delete reviews.\");\n                } else {\n                    deleteReview(review.id).then(res => {\n                        if (res) {\n                            refetch();\n                        }\n                    });\n                }\n            }\n        });\n    }\n\n    function reportRev() {\n        Alerts.show({\n            title: \"Are you sure?\",\n            body: \"Do you really you want to report this review?\",\n            confirmText: \"Report\",\n            cancelText: \"Nevermind\",\n            // confirmColor: \"red\", this just adds a class name and breaks the submit button guh\n            onConfirm: async () => {\n                if (!(await getToken())) {\n                    return showToast(\"You must be logged in to report reviews.\");\n                } else {\n                    reportReview(review.id);\n                }\n            }\n        });\n    }\n\n    const isAuthorBlocked = Auth?.user?.blockedUsers?.includes(review.sender.discordID) ?? false;\n\n    function blockReviewSender() {\n        if (isAuthorBlocked)\n            return unblockUser(review.sender.discordID);\n\n        Alerts.show({\n            title: \"Are you sure?\",\n            body: \"Do you really you want to block this user? They will be unable to leave further reviews on your profile. You can unblock users in the plugin settings.\",\n            confirmText: \"Block\",\n            cancelText: \"Nevermind\",\n            // confirmColor: \"red\", this just adds a class name and breaks the submit button guh\n            onConfirm: async () => {\n                if (!(await getToken())) {\n                    return showToast(\"You must be logged in to block users.\");\n                } else {\n                    blockUser(review.sender.discordID);\n                }\n            }\n        });\n    }\n\n    return (\n        <div className={classes(cl(\"review\"), MessageClasses.cozyMessage, AvatarClasses.wrapper, MessageClasses.message, MessageClasses.groupStart, AvatarClasses.cozy)} style={\n            {\n                marginLeft: \"0px\",\n                paddingLeft: \"52px\", // wth is this\n                // nobody knows anymore\n            }\n        }>\n\n            <img\n                className={classes(AvatarClasses.avatar, AvatarClasses.clickable)}\n                onClick={openModal}\n                src={review.sender.profilePhoto || IconUtils.getDefaultAvatarURL(review.sender.discordID)}\n                style={{ left: \"0px\", zIndex: 0 }}\n                onError={e => e.currentTarget.src = IconUtils.getDefaultAvatarURL(review.sender.discordID)}\n            />\n            <div style={{ display: \"inline-flex\", justifyContent: \"center\", alignItems: \"center\" }}>\n                <span\n                    className={classes(AvatarClasses.clickable, AvatarClasses.username)}\n                    style={{ color: \"var(--channels-default)\", fontSize: \"14px\" }}\n                    onClick={() => openModal()}\n                >\n                    {review.sender.username}\n                </span>\n\n                {review.type === ReviewType.System && (\n                    <span\n                        className={classes(BotTagClasses.botTagVerified, BotTagClasses.botTagRegular, BotTagClasses.px, BotTagClasses.rem)}\n                        style={{ marginLeft: \"4px\" }}>\n                        <span className={BotTagClasses.botText}>\n                            System\n                        </span>\n                    </span>\n                )}\n            </div>\n            {isAuthorBlocked && (\n                <ReviewBadge\n                    name=\"You have blocked this user\"\n                    description=\"You have blocked this user\"\n                    icon=\"/assets/aaee57e0090991557b66.svg\"\n                    type={0}\n                    onClick={() => openBlockModal()}\n                />\n            )}\n            {review.sender.badges.map((badge, idx) => <ReviewBadge key={idx} {...badge} />)}\n\n            {\n                !settings.store.hideTimestamps && review.type !== ReviewType.System && (\n                    <Timestamp timestamp={new Date(review.timestamp * 1000)} >\n                        {dateFormat.format(review.timestamp * 1000)}\n                    </Timestamp>)\n            }\n\n            <div className={cl(\"review-comment\")}>\n                {(review.comment.length > 200 && !showAll)\n                    ? (\n                        <>\n                            {Parser.parseGuildEventDescription(review.comment.substring(0, 200))}...\n                            <br />\n                            <a onClick={() => setShowAll(true)}>Read more</a>]\n                        </>\n                    )\n                    : Parser.parseGuildEventDescription(review.comment)}\n            </div>\n\n            {review.id !== 0 && (\n                <div className={classes(ContainerClasses.container, ContainerClasses.isHeader, MessageClasses.buttons)} style={{\n                    padding: \"0px\",\n                }}>\n                    <div className={classes(ButtonClasses.wrapper, MessageClasses.buttonsInner)} >\n                        {canReportReview(review) && <ReportButton onClick={reportRev} />}\n                        {canBlockReviewAuthor(profileId, review) && <BlockButton isBlocked={isAuthorBlocked} onClick={blockReviewSender} />}\n                        {canDeleteReview(profileId, review) && <DeleteButton onClick={delReview} />}\n                    </div>\n                </div>\n            )}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/ReviewModal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Auth } from \"@plugins/reviewDB/auth\";\nimport { ReviewType } from \"@plugins/reviewDB/entities\";\nimport { REVIEWS_PER_PAGE, UserReviewsData } from \"@plugins/reviewDB/reviewDbApi\";\nimport { cl } from \"@plugins/reviewDB/utils\";\nimport { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { useForceUpdater } from \"@utils/react\";\nimport { Paginator, Text, useRef, useState } from \"@webpack/common\";\n\nimport ReviewComponent from \"./ReviewComponent\";\nimport ReviewsView, { ReviewsInputComponent } from \"./ReviewsView\";\n\nfunction Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; name: string; type: ReviewType; }) {\n    const [data, setData] = useState<UserReviewsData>();\n    const [signal, refetch] = useForceUpdater(true);\n    const [page, setPage] = useState(1);\n\n    const ref = useRef<HTMLDivElement>(null);\n\n    const reviewCount = data?.reviewCount;\n    const ownReview = data?.reviews.find(r => r.sender.discordID === Auth.user?.discordID);\n\n    return (\n        <ErrorBoundary>\n            <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>\n                <ModalHeader>\n                    <Text variant=\"heading-lg/semibold\" className={cl(\"modal-header\")}>\n                        {name}'s Reviews\n                        {!!reviewCount && <span> ({reviewCount} Reviews)</span>}\n                    </Text>\n                    <ModalCloseButton onClick={modalProps.onClose} />\n                </ModalHeader>\n\n                <ModalContent scrollerRef={ref}>\n                    <div className={cl(\"modal-reviews\")}>\n                        <ReviewsView\n                            discordId={discordId}\n                            name={name}\n                            page={page}\n                            refetchSignal={signal}\n                            onFetchReviews={setData}\n                            scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: \"smooth\" })}\n                            hideOwnReview\n                            type={type}\n                        />\n                    </div>\n                </ModalContent>\n\n                <ModalFooter className={cl(\"modal-footer\")}>\n                    <div className={cl(\"modal-footer-wrapper\")}>\n                        {ownReview && (\n                            <ReviewComponent\n                                refetch={refetch}\n                                review={ownReview}\n                                profileId={discordId}\n                            />\n                        )}\n                        <ReviewsInputComponent\n                            isAuthor={ownReview != null}\n                            discordId={discordId}\n                            name={name}\n                            refetch={refetch}\n                            modalKey={modalKey}\n                        />\n\n                        {!!reviewCount && (\n                            <Paginator\n                                currentPage={page}\n                                maxVisiblePages={5}\n                                pageSize={REVIEWS_PER_PAGE}\n                                totalCount={reviewCount}\n                                onPageChange={setPage}\n                            />\n                        )}\n                    </div>\n                </ModalFooter>\n            </ModalRoot>\n        </ErrorBoundary>\n    );\n}\n\nexport function openReviewsModal(discordId: string, name: string, type: ReviewType) {\n    const modalKey = \"vc-rdb-modal-\" + Date.now();\n\n    openModal(props => (\n        <Modal\n            modalKey={modalKey}\n            modalProps={props}\n            discordId={discordId}\n            name={name}\n            type={type}\n        />\n    ), { modalKey });\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/components/ReviewsView.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Auth, authorize } from \"@plugins/reviewDB/auth\";\nimport { Review, ReviewType } from \"@plugins/reviewDB/entities\";\nimport { addReview, getReviews, REVIEWS_PER_PAGE, UserReviewsData } from \"@plugins/reviewDB/reviewDbApi\";\nimport { settings } from \"@plugins/reviewDB/settings\";\nimport { cl, showToast } from \"@plugins/reviewDB/utils\";\nimport { useAwaiter, useForceUpdater } from \"@utils/react\";\nimport { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from \"@webpack\";\nimport { Forms, React, RelationshipStore, useRef, UserStore } from \"@webpack/common\";\n\nimport ReviewComponent from \"./ReviewComponent\";\n\nconst Transforms = findByPropsLazy(\"insertNodes\", \"textToText\");\nconst Editor = findByPropsLazy(\"start\", \"end\", \"toSlateRange\");\nconst ChatInputTypes = findByPropsLazy(\"FORM\", \"USER_PROFILE\");\nconst InputComponent = findComponentByCodeLazy(\"editorClassName\", \"CHANNEL_TEXT_AREA\");\nconst createChannelRecordFromServer = findByCodeLazy(\".GUILD_TEXT]\", \"fromServer)\");\n\ninterface UserProps {\n    discordId: string;\n    name: string;\n}\n\ninterface Props extends UserProps {\n    onFetchReviews(data: UserReviewsData): void;\n    refetchSignal?: unknown;\n    showInput?: boolean;\n    page?: number;\n    scrollToTop?(): void;\n    hideOwnReview?: boolean;\n    type: ReviewType;\n}\n\nexport default function ReviewsView({\n    discordId,\n    name,\n    onFetchReviews,\n    refetchSignal,\n    scrollToTop,\n    page = 1,\n    showInput = false,\n    hideOwnReview = false,\n    type,\n}: Props) {\n    const [signal, refetch] = useForceUpdater(true);\n\n    const [reviewData] = useAwaiter(() => getReviews(discordId, { offset: (page - 1) * REVIEWS_PER_PAGE }), {\n        fallbackValue: null,\n        deps: [refetchSignal, signal, page],\n        onSuccess: data => {\n            if (settings.store.hideBlockedUsers)\n                data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID));\n\n            data!.reviews.reverse();\n            scrollToTop?.();\n            onFetchReviews(data!);\n        }\n    });\n\n    if (!reviewData) return null;\n\n    return (\n        <>\n            <ReviewList\n                refetch={refetch}\n                reviews={reviewData!.reviews}\n                hideOwnReview={hideOwnReview}\n                profileId={discordId}\n                type={type}\n            />\n\n            {showInput && (\n                <ReviewsInputComponent\n                    name={name}\n                    discordId={discordId}\n                    refetch={refetch}\n                    isAuthor={reviewData!.reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)}\n                />\n            )}\n        </>\n    );\n}\n\nfunction ReviewList({ refetch, reviews, hideOwnReview, profileId, type }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; type: ReviewType; }) {\n    const myId = UserStore.getCurrentUser().id;\n\n    return (\n        <div className={cl(\"view\")}>\n            {reviews?.map(review =>\n                (review.sender.discordID !== myId || !hideOwnReview) &&\n                <ReviewComponent\n                    key={review.id}\n                    review={review}\n                    refetch={refetch}\n                    profileId={profileId}\n                />\n            )}\n\n            {reviews?.length === 0 && (\n                <Forms.FormText className={cl(\"placeholder\")}>\n                    Looks like nobody reviewed this {type === ReviewType.User ? \"user\" : \"server\"} yet. You could be the first!\n                </Forms.FormText>\n            )}\n        </div>\n    );\n}\n\n\nexport function ReviewsInputComponent(\n    { discordId, isAuthor, refetch, name, modalKey }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; modalKey?: string; }\n) {\n    const { token } = Auth;\n    const editorRef = useRef<any>(null);\n    const inputType = ChatInputTypes.USER_PROFILE_REPLY;\n    inputType.disableAutoFocus = true;\n\n    const channel = createChannelRecordFromServer({ id: \"0\", type: 1 });\n\n    return (\n        <>\n            <div onClick={() => {\n                if (!token) {\n                    showToast(\"Opening authorization window...\");\n                    authorize();\n                }\n            }}>\n                <InputComponent\n                    className={cl(\"input\")}\n                    channel={channel}\n                    placeholder={\n                        !token\n                            ? \"You need to authorize to review users!\"\n                            : isAuthor\n                                ? `Update review for @${name}`\n                                : `Review @${name}`\n                    }\n                    type={inputType}\n                    disableThemedBackground={true}\n                    setEditorRef={ref => editorRef.current = ref}\n                    parentModalKey={modalKey}\n                    textValue=\"\"\n                    onSubmit={\n                        async res => {\n                            const response = await addReview({\n                                userid: discordId,\n                                comment: res.value,\n                            });\n\n                            if (response) {\n                                refetch();\n\n                                const slateEditor = editorRef.current.ref.current.getSlateEditor();\n\n                                // clear editor\n                                Transforms.delete(slateEditor, {\n                                    at: {\n                                        anchor: Editor.start(slateEditor, []),\n                                        focus: Editor.end(slateEditor, []),\n                                    }\n                                });\n                            }\n\n                            // even tho we need to return this, it doesnt do anything\n                            return {\n                                shouldClear: false,\n                                shouldRefocus: true,\n                            };\n                        }\n                    }\n                />\n            </div>\n\n        </>\n    );\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/entities.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const enum UserType {\n    Banned = -1,\n    Normal = 0,\n    Admin = 1\n}\n\nexport const enum ReviewType {\n    User = 0,\n    Server = 1,\n    Support = 2,\n    System = 3\n}\n\nexport const enum NotificationType {\n    Info = 0,\n    Ban = 1,\n    Unban = 2,\n    Warning = 3\n}\n\nexport interface ReviewDBAuth {\n    token?: string;\n    user?: ReviewDBCurrentUser;\n}\n\nexport interface Badge {\n    name: string;\n    description: string;\n    icon: string;\n    redirectURL?: string;\n    type: number;\n}\n\nexport interface BanInfo {\n    id: string;\n    discordID: string;\n    reviewID: number;\n    reviewContent: string;\n    banEndDate: number;\n}\n\nexport interface Notification {\n    id: number;\n    title: string;\n    content: string;\n    type: NotificationType;\n}\n\nexport interface ReviewDBUser {\n    ID: number;\n    discordID: string;\n    username: string;\n    type: UserType;\n    profilePhoto: string;\n    badges: any[];\n}\n\nexport interface ReviewDBCurrentUser extends ReviewDBUser {\n    warningCount: number;\n    clientMod: string;\n    banInfo: BanInfo | null;\n    notification: Notification | null;\n    lastReviewID: number;\n    blockedUsers?: string[];\n}\n\nexport interface ReviewAuthor {\n    id: number,\n    discordID: string,\n    username: string,\n    profilePhoto: string,\n    badges: Badge[];\n}\n\nexport interface Review {\n    comment: string,\n    id: number,\n    star: number,\n    sender: ReviewAuthor,\n    timestamp: number;\n    type?: ReviewType;\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { OpenExternalIcon } from \"@components/Icons\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Span } from \"@components/Span\";\nimport { Devs } from \"@utils/constants\";\nimport { classes } from \"@utils/misc\";\nimport { useAwaiter } from \"@utils/react\";\nimport definePlugin from \"@utils/types\";\nimport { Guild, User } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Alerts, Clickable, IconUtils, Menu, Parser } from \"@webpack/common\";\n\nimport { Auth, initAuth, updateAuth } from \"./auth\";\nimport { openReviewsModal } from \"./components/ReviewModal\";\nimport { NotificationType, ReviewType } from \"./entities\";\nimport { getCurrentUserInfo, getReviews, readNotification } from \"./reviewDbApi\";\nimport { settings } from \"./settings\";\nimport { cl, showToast } from \"./utils\";\n\nconst DMSideBarClasses = findCssClassesLazy(\"widgetPreviews\");\nconst ProfileCardClasses = findCssClassesLazy(\"cardsList\", \"firstCardContainer\", \"card\", \"container\");\nconst ProfileCardContainerClasses = findCssClassesLazy(\"innerContainer\", \"icons\", \"icon\", \"displayCount\", \"displayCountText\", \"displayCountTextColor\", \"breadcrumb\");\nconst ProfileCardOverlayClasses = findCssClassesLazy(\"overlay\", \"isPrivate\", \"outer\");\n\nconst guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {\n    if (!guild) return;\n    children.push(\n        <Menu.MenuItem\n            label=\"View Reviews\"\n            id=\"vc-rdb-server-reviews\"\n            icon={OpenExternalIcon}\n            action={() => openReviewsModal(guild.id, guild.name, ReviewType.Server)}\n        />\n    );\n};\n\nconst userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {\n    if (!user) return;\n    children.push(\n        <Menu.MenuItem\n            label=\"View Reviews\"\n            id=\"vc-rdb-user-reviews\"\n            icon={OpenExternalIcon}\n            action={() => openReviewsModal(user.id, user.username, ReviewType.User)}\n        />\n    );\n};\n\nexport default definePlugin({\n    name: \"ReviewDB\",\n    description: \"Review other users (Adds a new settings to profiles)\",\n    authors: [Devs.mantikafasi, Devs.Ven],\n\n    settings,\n    contextMenus: {\n        \"guild-header-popout\": guildPopoutPatch,\n        \"guild-context\": guildPopoutPatch,\n        \"user-context\": userContextPatch,\n        \"user-profile-actions\": userContextPatch,\n        \"user-profile-overflow-menu\": userContextPatch\n    },\n\n    patches: [\n        {\n            // DM profile sidebar\n            find: \".SIDEBAR,disableToolbar:\",\n            replacement: {\n                match: /user:(\\i),widgets:.{0,100}?\\}\\),/,\n                replace: \"$&$self.renderProfileComponent({user:$1,isSideBar:true}),\"\n            }\n        },\n        {\n            // User popout\n            find: /\\.POPOUT,onClose:\\i}\\),nicknameIcons:.+?\\.isProvisional/,\n            replacement: {\n                match: /user:(\\i),widgets:.{0,100}?\\}\\),/,\n                replace: \"$&$self.renderProfileComponent({user:$1}),\"\n            }\n        }\n    ],\n\n    flux: {\n        CONNECTION_OPEN: initAuth,\n    },\n\n    async start() {\n        const s = settings.store;\n        const { lastReviewId, notifyReviews } = s;\n\n        await initAuth();\n\n        setTimeout(async () => {\n            if (!Auth.token) return;\n\n            const user = await getCurrentUserInfo(Auth.token);\n            updateAuth({ user });\n\n            if (notifyReviews) {\n                if (lastReviewId && lastReviewId < user.lastReviewID) {\n                    s.lastReviewId = user.lastReviewID;\n                    if (user.lastReviewID !== 0)\n                        showToast(\"You have new reviews on your profile!\");\n                }\n            }\n\n            if (user.notification) {\n                const props = user.notification.type === NotificationType.Ban ? {\n                    cancelText: \"Appeal\",\n                    confirmText: \"Ok\",\n                    onCancel: async () =>\n                        VencordNative.native.openExternal(\n                            \"https://reviewdb.mantikafasi.dev/api/redirect?\"\n                            + new URLSearchParams({\n                                token: Auth.token!,\n                                page: \"dashboard/appeal\"\n                            })\n                        )\n                } : {};\n\n                Alerts.show({\n                    title: user.notification.title,\n                    body: (\n                        Parser.parse(\n                            user.notification.content,\n                            false\n                        )\n                    ),\n                    ...props\n                });\n\n                readNotification(user.notification.id);\n            }\n        }, 4000);\n    },\n\n    renderProfileComponent: ErrorBoundary.wrap(({ user, isSideBar = false }: { user: User; isSideBar?: boolean; }) => {\n        const [reviewData] = useAwaiter(() => getReviews(user.id, { limit: 4 }), { deps: [user.id], fallbackValue: null });\n\n        // Discord are masters at using a crap ton of html elements and css classes to create a simple ui that could have\n        // been made with less than half of the number of elements, so we have to do this insanity to replicate their ui\n        const reviewsSection = (\n            <section className={ProfileCardClasses.container}>\n                <ul className={ProfileCardClasses.cardsList} tabIndex={-1}>\n                    <li className={ProfileCardClasses.firstCardContainer}>\n                        <Clickable\n                            className={classes(ProfileCardContainerClasses.breadcrumb, reviewData?.hasOptedOut && cl(\"profile-popout-disabled\"))}\n                            onClick={() => !reviewData?.hasOptedOut && openReviewsModal(user.id, user.username, ReviewType.User)}\n                        >\n                            <div className={classes(ProfileCardOverlayClasses.overlay, ProfileCardContainerClasses.innerContainer, ProfileCardClasses.card)}>\n                                <Paragraph size={isSideBar ? \"sm\" : \"xs\"} weight=\"medium\">User Reviews</Paragraph>\n                                {!!reviewData?.reviewCount\n                                    ? (\n                                        <div className={ProfileCardContainerClasses.icons}>\n                                            {reviewData.reviews\n                                                .filter(review => review.id !== 0)\n                                                .slice(0, 4)\n                                                .reverse()\n                                                .map((review, idx) => {\n                                                    const showCount = idx === 3 && reviewData.reviewCount > 4;\n\n                                                    return (\n                                                        <div className={ProfileCardContainerClasses.icon} key={review.id}>\n                                                            <img\n                                                                src={review.sender.profilePhoto}\n                                                                alt={review.sender.username}\n                                                                className={showCount ? ProfileCardContainerClasses.displayCount : undefined}\n                                                                onError={e => e.currentTarget.src = IconUtils.getDefaultAvatarURL(review.sender.discordID)}\n                                                            />\n                                                            {showCount && (\n                                                                <div className={ProfileCardContainerClasses.displayCountText}>\n                                                                    <Span className={ProfileCardContainerClasses.displayCountTextColor} size=\"xs\" weight=\"medium\" defaultColor={false}>\n                                                                        +{reviewData.reviewCount - 3}\n                                                                    </Span>\n                                                                </div>\n                                                            )}\n                                                        </div>\n                                                    );\n                                                })}\n                                        </div>\n                                    )\n                                    : <Paragraph size={isSideBar ? \"sm\" : \"xs\"}>{reviewData?.hasOptedOut ? \"User opted out\" : \"No reviews yet\"}</Paragraph>\n                                }\n                            </div>\n                        </Clickable>\n                    </li>\n                </ul>\n            </section>\n        );\n\n        return isSideBar\n            ? <div className={DMSideBarClasses.widgetPreviews}>{reviewsSection}</div>\n            : reviewsSection;\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/reviewDB/reviewDbApi.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Toasts } from \"@webpack/common\";\n\nimport { Auth, authorize, getToken, updateAuth } from \"./auth\";\nimport { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from \"./entities\";\nimport { settings } from \"./settings\";\nimport { showToast } from \"./utils\";\n\nconst API_URL = \"https://manti.vendicated.dev/api/reviewdb\";\n\nexport const REVIEWS_PER_PAGE = 50;\n\nexport interface UserReviewsData {\n    message: string;\n    reviews: Review[];\n    updated: boolean;\n    hasNextPage: boolean;\n    reviewCount: number;\n    hasOptedOut: boolean;\n}\n\nconst WarningFlag = 0b00000010;\n\nasync function rdbRequest(path: string, options: RequestInit = {}) {\n    return fetch(API_URL + path, {\n        ...options,\n        headers: {\n            ...options.headers,\n            Authorization: await getToken() || \"\",\n        }\n    });\n}\n\nexport async function getReviews(id: string, { limit, offset = 0 }: { limit?: number; offset?: number; } = {}): Promise<UserReviewsData> {\n    let flags = 0;\n    if (!settings.store.showWarning) flags |= WarningFlag;\n\n    const params = new URLSearchParams();\n    if (flags) params.append(\"flags\", String(flags));\n    if (offset) params.append(\"offset\", String(offset));\n    if (limit) params.append(\"limit\", String(limit));\n\n    const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);\n\n    const res = (req.ok)\n        ? await req.json() as UserReviewsData\n        : {\n            message: req.status === 429 ? \"You are sending requests too fast. Wait a few seconds and try again.\" : \"An Error occured while fetching reviews. Please try again later.\",\n            reviews: [],\n            updated: false,\n            hasNextPage: false,\n            reviewCount: 0,\n            hasOptedOut: false,\n        };\n\n    if (!req.ok) {\n        showToast(res.message, Toasts.Type.FAILURE);\n        return {\n            ...res,\n            reviews: [\n                {\n                    id: 0,\n                    comment: res.message,\n                    star: 0,\n                    timestamp: 0,\n                    type: ReviewType.System,\n                    sender: {\n                        id: 0,\n                        username: \"ReviewDB\",\n                        profilePhoto: \"https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless\",\n                        discordID: \"1134864775000629298\",\n                        badges: []\n                    }\n                }\n            ]\n        };\n    }\n\n    return res;\n}\n\nexport async function addReview(review: any): Promise<UserReviewsData | null> {\n\n    const token = await getToken();\n    if (!token) {\n        showToast(\"Please authorize to add a review.\");\n        authorize();\n        return null;\n    }\n\n    return await rdbRequest(`/users/${review.userid}/reviews`, {\n        method: \"PUT\",\n        body: JSON.stringify(review),\n        headers: {\n            \"Content-Type\": \"application/json\",\n        }\n    }).then(async r => {\n        const data = await r.json() as UserReviewsData;\n        showToast(data.message);\n        return r.ok ? data : null;\n    });\n}\n\nexport async function deleteReview(id: number): Promise<UserReviewsData | null> {\n    return await rdbRequest(`/users/${id}/reviews`, {\n        method: \"DELETE\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            Accept: \"application/json\",\n        },\n        body: JSON.stringify({\n            reviewid: id\n        })\n    }).then(async r => {\n        const data = await r.json() as UserReviewsData;\n        showToast(data.message);\n        return r.ok ? data : null;\n    });\n}\n\nexport async function reportReview(id: number) {\n    const res = await rdbRequest(\"/reports\", {\n        method: \"PUT\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            Accept: \"application/json\",\n        },\n        body: JSON.stringify({\n            reviewid: id,\n        })\n    }).then(r => r.json()) as UserReviewsData;\n\n    showToast(res.message);\n}\n\nasync function patchBlock(action: \"block\" | \"unblock\", userId: string) {\n    const res = await rdbRequest(\"/blocks\", {\n        method: \"PATCH\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            Accept: \"application/json\",\n        },\n        body: JSON.stringify({\n            action: action,\n            discordId: userId\n        })\n    });\n\n    if (!res.ok) {\n        showToast(`Failed to ${action} user`, Toasts.Type.FAILURE);\n    } else {\n        showToast(`Successfully ${action}ed user`, Toasts.Type.SUCCESS);\n\n        if (Auth?.user?.blockedUsers) {\n            const newBlockedUsers = action === \"block\"\n                ? [...Auth.user.blockedUsers, userId]\n                : Auth.user.blockedUsers.filter(id => id !== userId);\n            updateAuth({ user: { ...Auth.user, blockedUsers: newBlockedUsers } });\n        }\n    }\n}\n\nexport const blockUser = (userId: string) => patchBlock(\"block\", userId);\nexport const unblockUser = (userId: string) => patchBlock(\"unblock\", userId);\n\nexport async function fetchBlocks(): Promise<ReviewDBUser[]> {\n    const res = await rdbRequest(\"/blocks\", {\n        method: \"GET\",\n        headers: {\n            Accept: \"application/json\",\n        }\n    });\n\n    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);\n    return res.json();\n}\n\nexport function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> {\n    return rdbRequest(\"/users\", {\n        method: \"POST\",\n    }).then(r => r.json());\n}\n\nexport async function readNotification(id: number) {\n    return rdbRequest(`/notifications?id=${id}`, {\n        method: \"PATCH\"\n    });\n}\n"
  },
  {
    "path": "src/plugins/reviewDB/settings.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Button } from \"@components/Button\";\nimport { openInviteModal } from \"@utils/discord\";\nimport { OptionType } from \"@utils/types\";\n\nimport { authorize, getToken } from \"./auth\";\nimport { openBlockModal } from \"./components/BlockedUserModal\";\nimport { cl } from \"./utils\";\n\nexport const settings = definePluginSettings({\n    authorize: {\n        type: OptionType.COMPONENT,\n        component: () => (\n            <Button onClick={() => authorize()}>\n                Authorize with ReviewDB\n            </Button>\n        )\n    },\n    notifyReviews: {\n        type: OptionType.BOOLEAN,\n        description: \"Notify about new reviews on startup\",\n        default: true,\n    },\n    showWarning: {\n        type: OptionType.BOOLEAN,\n        description: \"Display warning to be respectful at the top of the reviews list\",\n        default: true,\n    },\n    hideTimestamps: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide timestamps on reviews\",\n        default: false,\n    },\n    hideBlockedUsers: {\n        type: OptionType.BOOLEAN,\n        description: \"Hide reviews from blocked users\",\n        default: true,\n    },\n    buttons: {\n        type: OptionType.COMPONENT,\n        component: () => (\n            <div className={cl(\"button-grid\")} >\n                <Button onClick={openBlockModal}>Manage Blocked Users</Button>\n\n                <Button\n                    variant=\"positive\"\n                    onClick={() => {\n                        VencordNative.native.openExternal(\"https://github.com/sponsors/mantikafasi\");\n                    }}\n                >\n                    Support ReviewDB development\n                </Button>\n\n                <Button variant=\"link\" onClick={async () => {\n                    let url = \"https://reviewdb.mantikafasi.dev\";\n                    const token = await getToken();\n                    if (token)\n                        url += \"/api/redirect?token=\" + encodeURIComponent(token);\n\n                    VencordNative.native.openExternal(url);\n                }}>\n                    ReviewDB website\n                </Button>\n\n\n                <Button variant=\"link\" onClick={() => openInviteModal(\"eWPBSbvznt\")}>\n                    ReviewDB Support Server\n                </Button>\n            </div >\n        )\n    }\n}).withPrivateSettings<{\n    lastReviewId?: number;\n    reviewsDropdownState?: boolean;\n}>();\n"
  },
  {
    "path": "src/plugins/reviewDB/style.css",
    "content": ".vc-rdb-badge {\n    vertical-align: middle;\n    margin-left: 4px;\n}\n\n.vc-rdb-input {\n    padding-inline: 12px;\n    margin-top: 6px;\n    margin-bottom: 12px;\n    resize: none;\n    overflow: hidden;\n    background: transparent;\n    border: 1px solid var(--input-border-default, var(--border-muted));\n    box-sizing: border-box;\n}\n\n.vc-rdb-modal-footer-wrapper {\n    width: 100%;\n    margin: 6px 16px;\n}\n\n.vc-rdb-placeholder {\n    margin-bottom: 4px;\n    font-weight: bold;\n    font-style: italic;\n    color: var(--text-muted);\n}\n\n.vc-rdb-input * {\n    font-size: 14px;\n}\n\n.vc-rdb-modal-footer {\n    padding: 0;\n}\n\n.vc-rdb-modal-footer .vc-rdb-input {\n    margin-bottom: 0;\n    background: var(--input-background-default);\n}\n\n.vc-rdb-modal-footer [class*=\"pageControlContainer\"] {\n    margin-top: 0;\n}\n\n.vc-rdb-modal-header {\n    flex-grow: 1;\n}\n\n.vc-rdb-modal-reviews {\n    margin-top: 16px;\n}\n\n.vc-rdb-review {\n    padding-top: 8px !important;\n    padding-bottom: 8px !important;\n    padding-right: 32px !important;\n}\n\n.vc-rdb-review:hover {\n    background: var(--message-background-hover) !important;\n    border-radius: 8px;\n}\n\n.vc-rdb-review-comment [class*=\"avatar\"] {\n    vertical-align: text-top;\n}\n\n.vc-rdb-review-comment {\n    overflow-y: hidden;\n    margin-top: 1px;\n    margin-bottom: 8px;\n    color: var(--text-default);\n    font-size: 15px;\n}\n\n.vc-rdb-blocked-badge {\n    cursor: pointer;\n}\n\n.vc-rdb-block-modal-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n}\n\n.vc-rdb-block-modal {\n    padding: 1em;\n    display: grid;\n    gap: 0.75em;\n}\n\n.vc-rdb-button-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 10px;\n}\n\n/* stylelint-disable-next-line media-feature-range-notation */\n@media (max-width: 600px) {\n    .vc-rdb-button-grid {\n        grid-template-columns: 1fr;\n    }\n}\n\n.vc-rdb-block-modal-row {\n    display: flex;\n    height: 2em;\n    gap: 0.5em;\n    align-items: center;\n}\n\n.vc-rdb-block-modal-avatar {\n    border-radius: 50%;\n    height: 2em;\n    width: 2em;\n}\n\n.vc-rdb-block-modal-avatar::before {\n    content: \"\";\n    display: block;\n    width: 100%;\n    height: 100%;\n    background-color: var(--border-subtle);\n}\n\n.vc-rdb-block-modal-username {\n    flex-grow: 1;\n}\n\n.vc-rdb-block-modal-unblock {\n    cursor: pointer;\n}\n\n.vc-rdb-profile-popout-disabled {\n    opacity: 0.4;\n    cursor: not-allowed;\n}"
  },
  {
    "path": "src/plugins/reviewDB/utils.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { classNameFactory } from \"@utils/css\";\nimport { Toasts, UserStore } from \"@webpack/common\";\n\nimport { Auth } from \"./auth\";\nimport { Review, UserType } from \"./entities\";\n\nexport const cl = classNameFactory(\"vc-rdb-\");\n\nexport function canDeleteReview(profileId: string, review: Review) {\n    const myId = UserStore.getCurrentUser().id;\n    return (\n        myId === profileId\n        || review.sender.discordID === myId\n        || Auth.user?.type === UserType.Admin\n    );\n}\n\nexport function canBlockReviewAuthor(profileId: string, review: Review) {\n    const myId = UserStore.getCurrentUser().id;\n    return profileId === myId && review.sender.discordID !== myId;\n}\n\nexport function canReportReview(review: Review) {\n    return review.sender.discordID !== UserStore.getCurrentUser().id;\n}\n\nexport function showToast(message: string, type = Toasts.Type.MESSAGE) {\n    Toasts.show({\n        id: Toasts.genId(),\n        message,\n        type,\n        options: {\n            position: Toasts.Position.BOTTOM, // NOBODY LIKES TOASTS AT THE TOP\n        },\n    });\n}\n"
  },
  {
    "path": "src/plugins/roleColorEverywhere/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { makeRange, OptionType } from \"@utils/types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { ChannelStore, GuildMemberStore, GuildRoleStore, GuildStore } from \"@webpack/common\";\n\nconst useMessageAuthor = findByCodeLazy('\"Result cannot be null because the message is not null\"');\n\nconst settings = definePluginSettings({\n    chatMentions: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in chat mentions (including in the message box)\",\n        restartNeeded: true\n    },\n    memberList: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in member list role headers\",\n        restartNeeded: true\n    },\n    voiceUsers: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in the voice chat user list\",\n        restartNeeded: true\n    },\n    reactorsList: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in the reactors list\",\n        restartNeeded: true\n    },\n    pollResults: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in the poll results\",\n        restartNeeded: true\n    },\n    colorChatMessages: {\n        type: OptionType.BOOLEAN,\n        default: false,\n        description: \"Color chat messages based on the author's role color\",\n        restartNeeded: true,\n    },\n    messageSaturation: {\n        type: OptionType.SLIDER,\n        description: \"Intensity of message coloring.\",\n        markers: makeRange(0, 100, 10),\n        default: 30\n    }\n});\n\nexport default definePlugin({\n    name: \"RoleColorEverywhere\",\n    authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN, Devs.Kyuuhachi, Devs.jamesbt365],\n    description: \"Adds the top role color anywhere possible\",\n    settings,\n\n    patches: [\n        // Chat Mentions\n        {\n            find: \".USER_MENTION)\",\n            replacement: [\n                {\n                    match: /(?<=user:(\\i),guildId:([^,]+?),.{0,100}?children:\\i=>\\i)\\((\\i)\\)/,\n                    replace: \"({...$3,color:$self.getColorInt($1?.id,$2)})\",\n                }\n            ],\n            predicate: () => settings.store.chatMentions\n        },\n        // Slate\n        {\n            // Same find as FullUserInChatbox\n            find: '\"text\":\"locked\"',\n            replacement: [\n                {\n                    match: /let\\{id:(\\i),guildId:\\i,channelId:(\\i)[^}]*\\}.*?\\.\\i,{(?=children)/,\n                    replace: \"$&color:$self.getColorInt($1,$2),\"\n                }\n            ],\n            predicate: () => settings.store.chatMentions\n        },\n        // Member List Role Headers\n        {\n            find: 'tutorialId:\"whos-online',\n            replacement: [\n                {\n                    match: /(#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.+}\\):null,).{0,100}?— \",\\i\\]\\}\\)\\]/,\n                    replace: (_, rest) => `${rest}$self.RoleGroupColor(arguments[0])]`\n                },\n            ],\n            predicate: () => settings.store.memberList\n        },\n        {\n            find: \"#{intl::THREAD_BROWSER_PRIVATE}\",\n            replacement: [\n                {\n                    match: /children:\\[\\i,\" — \",\\i\\]/,\n                    replace: \"children:[$self.RoleGroupColor(arguments[0])]\"\n                },\n            ],\n            predicate: () => settings.store.memberList\n        },\n        // Voice Users\n        {\n            find: \"#{intl::GUEST_NAME_SUFFIX})]\",\n            replacement: [\n                {\n                    match: /#{intl::GUEST_NAME_SUFFIX}.{0,50}?\"\"\\](?<=guildId:(\\i),.+?user:(\\i).+?)/,\n                    replace: \"$&,style:$self.getColorStyle($2.id,$1),\"\n                }\n            ],\n            predicate: () => settings.store.voiceUsers\n        },\n        // Reaction List\n        {\n            find: \"MessageReactions.render:\",\n            replacement: {\n                // FIXME: (?:medium|normal) is for stable compat\n                match: /tag:\"strong\",variant:\"text-md\\/(?:medium|normal)\"(?<=onContextMenu:.{0,15}\\((\\i),(\\i),\\i\\).+?)/,\n                replace: \"$&,style:$self.getColorStyle($2?.id,$1?.channel?.id)\"\n            },\n            predicate: () => settings.store.reactorsList,\n        },\n        // Poll Results\n        {\n            find: \",reactionVoteCounts\",\n            replacement: {\n                match: /\\.SIZE_32.+?variant:\"text-md\\/normal\",className:\\i\\.\\i,(?=\"aria-label\":)/,\n                replace: \"$&style:$self.getColorStyle(arguments[0]?.user?.id,arguments[0]?.channel?.id),\"\n            },\n            predicate: () => settings.store.pollResults\n        },\n        // Messages\n        {\n            find: \".SEND_FAILED,\",\n            replacement: {\n                match: /(?<=\\]:(\\i)\\.isUnsupported.{0,50}?,)(?=children:\\[)/,\n                replace: \"style:$self.useMessageColorsStyle($1),\"\n            },\n            predicate: () => settings.store.colorChatMessages\n        }\n    ],\n\n    getColorString(userId: string, channelOrGuildId: string) {\n        try {\n            const guildId = ChannelStore.getChannel(channelOrGuildId)?.guild_id ?? GuildStore.getGuild(channelOrGuildId)?.id;\n            if (guildId == null) return null;\n\n            return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null;\n        } catch (e) {\n            new Logger(\"RoleColorEverywhere\").error(\"Failed to get color string\", e);\n        }\n\n        return null;\n    },\n\n    getColorInt(userId: string, channelOrGuildId: string) {\n        const colorString = this.getColorString(userId, channelOrGuildId);\n        return colorString && parseInt(colorString.slice(1), 16);\n    },\n\n    getColorStyle(userId: string, channelOrGuildId: string) {\n        const colorString = this.getColorString(userId, channelOrGuildId);\n\n        return colorString && {\n            color: colorString\n        };\n    },\n\n    useMessageColorsStyle(message: any) {\n        try {\n            const { messageSaturation } = settings.use([\"messageSaturation\"]);\n            const author = useMessageAuthor(message);\n\n            // Do not apply role color if the send fails, otherwise it becomes indistinguishable\n            if (message.state === \"SEND_FAILED\") return;\n\n            if (author.colorString != null && messageSaturation !== 0) {\n                const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;\n\n                return {\n                    color: value.replace(\"{DEFAULT}\", \"--text-default\"),\n                    \"--text-strong\": value.replace(\"{DEFAULT}\", \"--text-strong\"),\n                    \"--text-muted\": value.replace(\"{DEFAULT}\", \"--text-muted\")\n                };\n            }\n        } catch (e) {\n            new Logger(\"RoleColorEverywhere\").error(\"Failed to get message color\", e);\n        }\n\n        return null;\n    },\n\n    RoleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {\n        const role = GuildRoleStore.getRole(guildId, id);\n\n        return (\n            <span style={{\n                color: role?.colorString,\n                fontWeight: \"unset\",\n                letterSpacing: \".05em\"\n            }}>\n                {title ?? label} &mdash; {count}\n            </span>\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/secretRingTone/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    onlySnow: {\n        type: OptionType.BOOLEAN,\n        description: \"Only play the Snow Halation Theme\",\n        default: false,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"SecretRingToneEnabler\",\n    description: \"Always play the secret version of the discord ringtone (except during special ringtone events)\",\n    authors: [Devs.AndrewDLO, Devs.FieryFlames, Devs.RamziAH],\n    settings,\n    patches: [\n        {\n            find: '\"call_ringing_beat\"',\n            replacement: [\n                {\n                    match: /500!==\\i\\(\\)\\.random\\(1,1e3\\)/,\n                    replace: \"false\"\n                },\n                {\n                    predicate: () => settings.store.onlySnow,\n                    match: /\"call_ringing_beat\",/,\n                    replace: \"\"\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/seeSummaries/README.md",
    "content": "# Summaries\n\nEnables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations.\n\nRead more about summaries in the [official Discord help article](https://support.discord.com/hc/en-us/articles/12926016807575-In-Channel-Conversation-Summaries)!\n\nNote that this plugin can't fetch old summaries, it can only display ones created while your Discord is running with the plugin enabled.\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/bd931b0c-2e85-4c10-9f7c-8ba01eb55745)\n"
  },
  {
    "path": "src/plugins/seeSummaries/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport * as DataStore from \"@api/DataStore\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { hasGuildFeature } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findByCodeLazy, findByPropsLazy } from \"@webpack\";\nimport { ChannelStore, GuildStore } from \"@webpack/common\";\n\nconst SummaryStore = findByPropsLazy(\"allSummaries\", \"findSummary\");\nconst createSummaryFromServer = findByCodeLazy(\".people)),startId:\", \".type}\");\n\nconst settings = definePluginSettings({\n    summaryExpiryThresholdDays: {\n        type: OptionType.SLIDER,\n        description: \"The time in days before a summary is removed. Note that only up to 50 summaries are kept per channel\",\n        markers: [1, 3, 5, 7, 10, 15, 20, 25, 30],\n        stickToMarkers: false,\n        default: 3,\n    }\n});\n\ninterface Summary {\n    count: number;\n    end_id: string;\n    id: string;\n    message_ids: string[];\n    people: string[];\n    source: number;\n    start_id: string;\n    summ_short: string;\n    topic: string;\n    type: number;\n    unsafe: boolean;\n}\n\ninterface ChannelSummaries {\n    type: string;\n    channel_id: string;\n    guild_id: string;\n    summaries: Summary[];\n\n    // custom property\n    time?: number;\n}\n\nexport default definePlugin({\n    name: \"Summaries\",\n    description: \"Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations\",\n    authors: [Devs.mantikafasi],\n    settings,\n    patches: [\n        {\n            find: \"SUMMARIZEABLE.has\",\n            replacement: {\n                match: /\\i\\.features\\.has\\(\\i\\.\\i\\.SUMMARIES_ENABLED\\w+?\\)/g,\n                replace: \"true\"\n            }\n        },\n        {\n            find: \"RECEIVE_CHANNEL_SUMMARY(\",\n            replacement: {\n                match: /shouldFetch\\((\\i),\\i\\){/,\n                replace: \"$& if(!$self.shouldFetch($1)) return false;\"\n            }\n        }\n    ],\n    flux: {\n        CONVERSATION_SUMMARY_UPDATE(data) {\n            const incomingSummaries: ChannelSummaries[] = data.summaries.map((summary: any) => ({ ...createSummaryFromServer(summary), time: Date.now() }));\n\n            // idk if this is good for performance but it doesnt seem to be a problem in my experience\n            DataStore.update(\"summaries-data\", summaries => {\n                summaries ??= {};\n                summaries[data.channel_id] ? summaries[data.channel_id].unshift(...incomingSummaries) : (summaries[data.channel_id] = incomingSummaries);\n                if (summaries[data.channel_id].length > 50)\n                    summaries[data.channel_id] = summaries[data.channel_id].slice(0, 50);\n\n                return summaries;\n            });\n        }\n    },\n\n    async start() {\n        await DataStore.update(\"summaries-data\", summaries => {\n            summaries ??= {};\n            for (const key of Object.keys(summaries)) {\n                for (let i = summaries[key].length - 1; i >= 0; i--) {\n                    if (summaries[key][i].time < Date.now() - 1000 * 60 * 60 * 24 * settings.store.summaryExpiryThresholdDays) {\n                        summaries[key].splice(i, 1);\n                    }\n                }\n\n                if (summaries[key].length === 0) {\n                    delete summaries[key];\n                }\n            }\n\n            Object.assign(SummaryStore.allSummaries(), summaries);\n            return summaries;\n        });\n    },\n\n    shouldFetch(channelId: string) {\n        const channel = ChannelStore.getChannel(channelId);\n        // SUMMARIES_ENABLED feature is not in discord-types\n        const guild = GuildStore.getGuild(channel.guild_id);\n\n        return hasGuildFeature(guild, \"SUMMARIES_ENABLED_GA\");\n    }\n});\n"
  },
  {
    "path": "src/plugins/sendTimestamps/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { ChatBarButton, ChatBarButtonFactory } from \"@api/ChatButtons\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { getTheme, insertTextIntoChatInputBox, Theme } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from \"@utils/modal\";\nimport definePlugin, { IconComponent, OptionType } from \"@utils/types\";\nimport { Button, Forms, Parser, Select, useMemo, useState } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    replaceMessageContents: {\n        description: \"Replace timestamps in message contents\",\n        type: OptionType.BOOLEAN,\n        default: true,\n    },\n});\n\nfunction parseTime(time: string) {\n    const cleanTime = time.slice(1, -1).replace(/(\\d)(AM|PM)$/i, \"$1 $2\");\n\n    let ms = new Date(`${new Date().toDateString()} ${cleanTime}`).getTime() / 1000;\n    if (isNaN(ms)) return time;\n\n    // add 24h if time is in the past\n    if (Date.now() / 1000 > ms) ms += 86400;\n\n    return `<t:${Math.round(ms)}:t>`;\n}\n\nconst Formats = [\"\", \"t\", \"T\", \"d\", \"D\", \"f\", \"F\", \"s\", \"S\", \"R\"] as const;\ntype Format = typeof Formats[number];\n\nconst cl = classNameFactory(\"vc-st-\");\n\nfunction PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): void; }) {\n    const [value, setValue] = useState<string>();\n    const [format, setFormat] = useState<Format>(\"\");\n    const time = Math.round((new Date(value!).getTime() || Date.now()) / 1000);\n\n    const formatTimestamp = (time: number, format: Format) => `<t:${time}${format && `:${format}`}>`;\n\n    const [formatted, rendered] = useMemo(() => {\n        const formatted = formatTimestamp(time, format);\n        return [formatted, Parser.parse(formatted)];\n    }, [time, format]);\n\n    return (\n        <ModalRoot {...rootProps}>\n            <ModalHeader className={cl(\"modal-header\")}>\n                <Forms.FormTitle tag=\"h2\" className={cl(\"modal-title\")}>\n                    Timestamp Picker\n                </Forms.FormTitle>\n\n                <ModalCloseButton onClick={close} className={cl(\"modal-close-button\")} />\n            </ModalHeader>\n\n            <ModalContent className={cl(\"modal-content\")}>\n                <input\n                    className={cl(\"date-picker\")}\n                    type=\"datetime-local\"\n                    value={value}\n                    onChange={e => setValue(e.currentTarget.value)}\n                    style={{\n                        colorScheme: getTheme() === Theme.Light ? \"light\" : \"dark\",\n                    }}\n                />\n\n                <Forms.FormTitle>Timestamp Format</Forms.FormTitle>\n                <div className={cl(\"format-select\")}>\n                    <Select\n                        options={\n                            Formats.map(m => ({\n                                label: m,\n                                value: m\n                            }))\n                        }\n                        isSelected={v => v === format}\n                        select={v => setFormat(v)}\n                        serialize={v => v}\n                        renderOptionLabel={o => (\n                            <div className={cl(\"format-label\")}>\n                                {Parser.parse(formatTimestamp(time, o.value))}\n                            </div>\n                        )}\n                        renderOptionValue={() => rendered}\n                    />\n                </div>\n\n                <Forms.FormTitle className={Margins.bottom8}>Preview</Forms.FormTitle>\n                <Forms.FormText className={cl(\"preview-text\")}>\n                    {rendered} ({formatted})\n                </Forms.FormText>\n            </ModalContent>\n\n            <ModalFooter>\n                <Button\n                    onClick={() => {\n                        insertTextIntoChatInputBox(formatted + \" \");\n                        close();\n                    }}\n                >Insert</Button>\n            </ModalFooter>\n        </ModalRoot>\n    );\n}\n\nconst SendTimestampIcon: IconComponent = ({ height = 20, width = 20, className }) => {\n    return (\n        <svg\n            aria-hidden=\"true\"\n            role=\"img\"\n            width={width}\n            height={height}\n            className={className}\n            viewBox=\"0 0 24 24\"\n            style={{ scale: \"1.2\" }}\n        >\n            <g fill=\"none\" fillRule=\"evenodd\">\n                <path fill=\"currentColor\" d=\"M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z\" />\n                <rect width=\"24\" height=\"24\" />\n            </g>\n        </svg>\n    );\n};\n\nconst SendTimestampButton: ChatBarButtonFactory = ({ isAnyChat }) => {\n    if (!isAnyChat) return null;\n\n    return (\n        <ChatBarButton\n            tooltip=\"Insert Timestamp\"\n            onClick={() => {\n                const key = openModal(props => (\n                    <PickerModal\n                        rootProps={props}\n                        close={() => closeModal(key)}\n                    />\n                ));\n            }}\n            buttonProps={{ \"aria-haspopup\": \"dialog\" }}\n        >\n            <SendTimestampIcon />\n        </ChatBarButton>\n    );\n};\n\nexport default definePlugin({\n    name: \"SendTimestamps\",\n    description: \"Send timestamps easily via chat box button & text shortcuts. Read the extended description!\",\n    authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],\n    settings,\n\n    chatBarButton: {\n        icon: SendTimestampIcon,\n        render: SendTimestampButton\n    },\n\n    onBeforeMessageSend(_, msg) {\n        if (settings.store.replaceMessageContents) {\n            msg.content = msg.content.replace(/`\\d{1,2}:\\d{2} ?(?:AM|PM)?`/gi, parseTime);\n        }\n    },\n\n    settingsAboutComponent() {\n        const samples = [\n            \"12:00\",\n            \"3:51\",\n            \"17:59\",\n            \"24:00\",\n            \"12:00 AM\",\n            \"0:13PM\"\n        ].map(s => `\\`${s}\\``);\n\n        return (\n            <>\n                <Forms.FormText>\n                    To quickly send send time only timestamps, include timestamps formatted as `HH:MM` (including the backticks!) in your message\n                </Forms.FormText>\n                <Forms.FormText>\n                    See below for examples.\n                    If you need anything more specific, use the Date button in the chat bar!\n                </Forms.FormText>\n                <Forms.FormText>\n                    Examples:\n                    <ul>\n                        {samples.map(s => (\n                            <li key={s}>\n                                <code>{s}</code> {\"->\"} {Parser.parse(parseTime(s))}\n                            </li>\n                        ))}\n                    </ul>\n                </Forms.FormText>\n            </>\n        );\n    },\n});\n"
  },
  {
    "path": "src/plugins/sendTimestamps/styles.css",
    "content": ".vc-st-date-picker {\n    background-color: var(--input-background-default);\n    color: var(--text-default);\n    width: 95%;\n    padding: 8px 8px 8px 12px;\n    margin: 1em 0;\n    outline: none;\n    border: 1px solid var(--input-background-default);\n    border-radius: 4px;\n    font-weight: 500;\n    font-style: inherit;\n    font-size: 100%;\n}\n\n.vc-st-format-select {\n    margin-bottom: 1em;\n\n    --border-subtle: transparent;\n}\n\n.vc-st-format-label {\n    --border-subtle: transparent;\n}\n\n.vc-st-modal-header {\n    place-content: center space-between;\n}\n\n.vc-st-modal-title {\n    margin: 0;\n}\n\n.vc-st-modal-close-button {\n    padding: 0;\n}\n\n.vc-st-preview-text {\n    margin-bottom: 1em;\n}"
  },
  {
    "path": "src/plugins/serverInfo/GuildInfoModal.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { classNameFactory } from \"@utils/css\";\nimport { getGuildAcronym, openImageModal, openUserProfile } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport { ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport { useAwaiter } from \"@utils/react\";\nimport { Guild, User } from \"@vencord/discord-types\";\nimport { findComponentByCodeLazy, findCssClassesLazy } from \"@webpack\";\nimport { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildRoleStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from \"@webpack/common\";\n\nconst IconClasses = findCssClassesLazy(\"icon\", \"acronym\", \"childWrapper\");\nconst FriendRow = findComponentByCodeLazy(\"discriminatorClass:\", \".isMobileOnline\", \"getAvatarURL\");\n\nconst cl = classNameFactory(\"vc-gp-\");\n\nexport function openGuildInfoModal(guild: Guild) {\n    openModal(props =>\n        <ModalRoot {...props} size={ModalSize.MEDIUM}>\n            <GuildInfoModal guild={guild} />\n        </ModalRoot>\n    );\n}\n\nconst enum Tabs {\n    ServerInfo,\n    Friends,\n    BlockedUsers,\n    IgnoredUsers\n}\n\ninterface GuildProps {\n    guild: Guild;\n}\n\ninterface RelationshipProps extends GuildProps {\n    setCount(count: number): void;\n}\n\nconst fetched = {\n    friends: false,\n    blocked: false,\n    ignored: false\n};\n\nfunction renderTimestamp(timestamp: number) {\n    return (\n        <Timestamp timestamp={new Date(timestamp)} />\n    );\n}\n\nfunction GuildInfoModal({ guild }: GuildProps) {\n    const [friendCount, setFriendCount] = useState<number>();\n    const [blockedCount, setBlockedCount] = useState<number>();\n    const [ignoredCount, setIgnoredCount] = useState<number>();\n\n    useEffect(() => {\n        fetched.friends = false;\n        fetched.blocked = false;\n        fetched.ignored = false;\n    }, []);\n\n    const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);\n\n    const bannerUrl = guild.banner && IconUtils.getGuildBannerURL(guild, true)!.replace(/\\?size=\\d+$/, \"?size=1024\");\n\n    const iconUrl = guild.icon && IconUtils.getGuildIconURL({\n        id: guild.id,\n        icon: guild.icon,\n        canAnimate: true,\n        size: 512\n    });\n\n    return (\n        <div className={cl(\"root\")}>\n            {bannerUrl && currentTab === Tabs.ServerInfo && (\n                <img\n                    className={cl(\"banner\")}\n                    src={bannerUrl}\n                    alt=\"\"\n                    onClick={() => openImageModal({\n                        url: bannerUrl,\n                        width: 1024\n                    })}\n                />\n            )}\n\n            <div className={cl(\"header\")}>\n                {iconUrl\n                    ? <img\n                        className={cl(\"icon\")}\n                        src={iconUrl}\n                        alt=\"\"\n                        onClick={() => openImageModal({\n                            url: iconUrl,\n                            height: 512,\n                            width: 512,\n                        })}\n                    />\n                    : <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{getGuildAcronym(guild)}</div>\n                }\n\n                <div className={cl(\"name-and-description\")}>\n                    <Forms.FormTitle tag=\"h5\" className={cl(\"name\")}>{guild.name}</Forms.FormTitle>\n                    {guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}\n                </div>\n            </div>\n\n            <TabBar\n                type=\"top\"\n                look=\"brand\"\n                className={cl(\"tab-bar\")}\n                selectedItem={currentTab}\n                onItemSelect={setCurrentTab}\n            >\n                <TabBar.Item\n                    className={cl(\"tab\", { selected: currentTab === Tabs.ServerInfo })}\n                    id={Tabs.ServerInfo}\n                >\n                    Server Info\n                </TabBar.Item>\n                <TabBar.Item\n                    className={cl(\"tab\", { selected: currentTab === Tabs.Friends })}\n                    id={Tabs.Friends}\n                >\n                    Friends{friendCount !== undefined ? ` (${friendCount})` : \"\"}\n                </TabBar.Item>\n                <TabBar.Item\n                    className={cl(\"tab\", { selected: currentTab === Tabs.BlockedUsers })}\n                    id={Tabs.BlockedUsers}\n                >\n                    Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : \"\"}\n                </TabBar.Item>\n                <TabBar.Item\n                    className={cl(\"tab\", { selected: currentTab === Tabs.IgnoredUsers })}\n                    id={Tabs.IgnoredUsers}\n                >\n                    Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : \"\"}\n                </TabBar.Item>\n            </TabBar>\n\n            <div className={cl(\"tab-content\")}>\n                {currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}\n                {currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}\n                {currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}\n                {currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}\n            </div>\n        </div>\n    );\n}\n\n\nfunction Owner(guildId: string, owner: User) {\n    const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar;\n    const ownerAvatarUrl =\n        guildAvatar\n            ? IconUtils.getGuildMemberAvatarURLSimple({\n                userId: owner!.id,\n                avatar: guildAvatar,\n                guildId,\n                canAnimate: true\n            })\n            : IconUtils.getUserAvatarURL(owner, true);\n\n    return (\n        <div className={cl(\"owner\")}>\n            <img\n                className={cl(\"owner-avatar\")}\n                src={ownerAvatarUrl}\n                alt=\"\"\n                onClick={() => openImageModal({\n                    url: ownerAvatarUrl,\n                    height: 512,\n                    width: 512\n                })}\n            />\n            {Parser.parse(`<@${owner.id}>`)}\n        </div>\n    );\n}\n\nfunction ServerInfoTab({ guild }: GuildProps) {\n    const [owner] = useAwaiter(() => UserUtils.getUser(guild.ownerId), {\n        deps: [guild.ownerId],\n        fallbackValue: null\n    });\n\n    const Fields = {\n        \"Server Owner\": owner ? Owner(guild.id, owner) : \"Loading...\",\n        \"Created At\": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),\n        \"Joined At\": guild.joinedAt ? renderTimestamp(guild.joinedAt.getTime()) : \"-\", // Not available in lurked guild\n        \"Vanity Link\": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : \"-\", // Making the anchor href valid would cause Discord to reload\n        \"Preferred Locale\": guild.preferredLocale || \"-\",\n        \"Verification Level\": [\"None\", \"Low\", \"Medium\", \"High\", \"Highest\"][guild.verificationLevel] || \"?\",\n        \"Server Boosts\": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,\n        \"Channels\": GuildChannelStore.getChannels(guild.id)?.count - 1 || \"?\", // - null category\n        \"Roles\": GuildRoleStore.getSortedRoles(guild.id).length - 1, // - @everyone\n    };\n\n    return (\n        <div className={cl(\"info\")}>\n            {Object.entries(Fields).map(([name, node]) =>\n                <div className={cl(\"server-info-pair\")} key={name}>\n                    <Forms.FormTitle tag=\"h5\">{name}</Forms.FormTitle>\n                    {typeof node === \"string\" ? <span>{node}</span> : node}\n                </div>\n            )}\n        </div>\n    );\n}\n\nfunction FriendsTab({ guild, setCount }: RelationshipProps) {\n    return UserList(\"friends\", guild, RelationshipStore.getFriendIDs(), setCount);\n}\n\nfunction BlockedUsersTab({ guild, setCount }: RelationshipProps) {\n    const blockedIds = RelationshipStore.getBlockedIDs();\n    return UserList(\"blocked\", guild, blockedIds, setCount);\n}\n\nfunction IgnoredUserTab({ guild, setCount }: RelationshipProps) {\n    const ignoredIds = RelationshipStore.getIgnoredIDs();\n    return UserList(\"ignored\", guild, ignoredIds, setCount);\n}\n\n\nfunction UserList(type: \"friends\" | \"blocked\" | \"ignored\", guild: Guild, ids: string[], setCount: (count: number) => void) {\n    const missing = [] as string[];\n    const members = [] as string[];\n\n    for (const id of ids) {\n        if (GuildMemberStore.isMember(guild.id, id))\n            members.push(id);\n        else\n            missing.push(id);\n    }\n\n    // Used for side effects (rerender on member request success)\n    useStateFromStores(\n        [GuildMemberStore],\n        () => GuildMemberStore.getMemberIds(guild.id),\n        null,\n        (old, curr) => old.length === curr.length\n    );\n\n    useEffect(() => {\n        if (!fetched[type] && missing.length) {\n            fetched[type] = true;\n            FluxDispatcher.dispatch({\n                type: \"GUILD_MEMBERS_REQUEST\",\n                guildIds: [guild.id],\n                userIds: missing\n            });\n        }\n    }, []);\n\n    useEffect(() => setCount(members.length), [members.length]);\n\n    return (\n        <ScrollerThin fade className={cl(\"scroller\")}>\n            {members.map(id =>\n                <FriendRow\n                    key={id}\n                    user={UserStore.getUser(id)}\n                    status={PresenceStore.getStatus(id) || \"offline\"}\n                    onSelect={() => openUserProfile(id)}\n                    onContextMenu={() => { }}\n                />\n            )}\n        </ScrollerThin>\n    );\n}\n"
  },
  {
    "path": "src/plugins/serverInfo/README.md",
    "content": "# ServerInfo\n\nAllows you to view info about servers and see friends and blocked users\n\n![](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)\n![](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)\n![Available as \"Server Profile\" option in the server context menu](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)\n"
  },
  {
    "path": "src/plugins/serverInfo/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Guild } from \"@vencord/discord-types\";\nimport { Menu } from \"@webpack/common\";\n\nimport { openGuildInfoModal } from \"./GuildInfoModal\";\n\nconst Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {\n    const group = findGroupChildrenByChildId(\"privacy\", children);\n\n    group?.push(\n        <Menu.MenuItem\n            id=\"vc-server-info\"\n            label=\"Server Info\"\n            action={() => openGuildInfoModal(guild)}\n        />\n    );\n};\n\nexport default definePlugin({\n    name: \"ServerInfo\",\n    description: \"Allows you to view info about a server\",\n    authors: [Devs.Ven, Devs.Nuckyz],\n    dependencies: [\"DynamicImageModalAPI\"],\n    tags: [\"guild\", \"info\", \"ServerProfile\"],\n\n    contextMenus: {\n        \"guild-context\": Patch,\n        \"guild-header-popout\": Patch\n    }\n});\n"
  },
  {
    "path": "src/plugins/serverInfo/styles.css",
    "content": ".vc-gp-root {\n    height: 100%;\n    user-select: text;\n}\n\n.vc-gp-banner {\n    cursor: pointer;\n    aspect-ratio: auto 240 / 135;\n    height: 334px;\n    width: 100%;\n    object-fit: cover;\n    overflow: clip;\n    overflow-clip-margin: content-box;\n}\n\n.vc-gp-header {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 0.5em;\n    margin: 0.5em;\n}\n\n.vc-gp-icon {\n    width: 48px;\n    height: 48px;\n    cursor: pointer;\n}\n\n.vc-gp-name-and-description {\n    display: flex;\n    flex-direction: column;\n    gap: 0.2em;\n}\n\n.vc-gp-name {\n    margin: 0;\n}\n\n.vc-gp-tab-bar {\n    border-bottom: 1px solid var(--border-subtle);\n    margin: 20px 12px 0;\n    display: flex;\n    gap: 40px;\n    align-items: stretch;\n    flex-direction: row;\n}\n\n.vc-gp-tab {\n    border-bottom: 2px solid transparent;\n    color: var(--interactive-icon-default);\n    cursor: pointer;\n    line-height: 14px;\n}\n\n.vc-gp-tab-content {\n    margin: 1em;\n}\n\n.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) {\n    border-bottom-color: var(--interactive-icon-active);\n}\n\n.vc-gp-info {\n    display: grid;\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    gap: 1em;\n}\n\n.vc-gp-server-info-pair {\n    color: var(--text-default);\n}\n\n.vc-gp-server-info-pair [class*=\"timestamp\"] {\n    margin-left: 0;\n}\n\n.vc-gp-owner {\n    display: flex;\n    align-items: center;\n    gap: 0.2em;\n}\n\n.vc-gp-owner-avatar {\n    height: 20px;\n    border-radius: 50%;\n    cursor: pointer;\n}\n\n.vc-gp-scroller {\n    width: 100%;\n    max-height: 500px;\n}\n\n.vc-gp-scroller [class*=\"listRow\"] {\n    margin: 1px 0;\n}\n\n.vc-gp-scroller [class*=\"listRow\"]:hover {\n    background-color: var(--background-mod-subtle);\n}"
  },
  {
    "path": "src/plugins/serverListIndicators/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Sofia Lima\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { addServerListElement, removeServerListElement, ServerListRenderPosition } from \"@api/ServerList\";\nimport { Settings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findStoreLazy } from \"@webpack\";\nimport { GuildStore, PresenceStore, RelationshipStore, useStateFromStores } from \"@webpack/common\";\n\nconst enum IndicatorType {\n    SERVER = 1 << 0,\n    FRIEND = 1 << 1,\n    BOTH = SERVER | FRIEND,\n}\n\nconst UserGuildJoinRequestStore = findStoreLazy(\"UserGuildJoinRequestStore\");\n\nfunction FriendsIndicator() {\n    const onlineFriendsCount = useStateFromStores([RelationshipStore, PresenceStore], () => {\n        let count = 0;\n\n        const friendIds = RelationshipStore.getFriendIDs();\n        for (const id of friendIds) {\n            const status = PresenceStore.getStatus(id) ?? \"offline\";\n            if (status === \"offline\") {\n                continue;\n            }\n\n            count++;\n        }\n\n        return count;\n    });\n\n    return (\n        <span id=\"vc-friendcount\" style={{\n            display: \"inline-block\",\n            width: \"100%\",\n            fontSize: \"12px\",\n            fontWeight: \"600\",\n            color: \"var(--text-default)\",\n            textTransform: \"uppercase\",\n            textAlign: \"center\",\n        }}>\n            {onlineFriendsCount} online\n        </span>\n    );\n}\n\nfunction ServersIndicator() {\n    const guildCount = useStateFromStores([GuildStore, UserGuildJoinRequestStore], () => {\n        const guildJoinRequests: string[] = UserGuildJoinRequestStore.computeGuildIds();\n        const guilds = GuildStore.getGuilds();\n\n        // Filter only pending guild join requests\n        return GuildStore.getGuildCount() + guildJoinRequests.filter(id => guilds[id] == null).length;\n    });\n\n    return (\n        <span id=\"vc-guildcount\" style={{\n            display: \"inline-block\",\n            width: \"100%\",\n            fontSize: \"12px\",\n            fontWeight: \"600\",\n            color: \"var(--text-default)\",\n            textTransform: \"uppercase\",\n            textAlign: \"center\",\n        }}>\n            {guildCount} servers\n        </span>\n    );\n}\n\nexport default definePlugin({\n    name: \"ServerListIndicators\",\n    description: \"Add online friend count or server count in the server list\",\n    authors: [Devs.dzshn],\n    dependencies: [\"ServerListAPI\"],\n\n    options: {\n        mode: {\n            description: \"mode\",\n            type: OptionType.SELECT,\n            options: [\n                { label: \"Only online friend count\", value: IndicatorType.FRIEND, default: true },\n                { label: \"Only server count\", value: IndicatorType.SERVER },\n                { label: \"Both server and online friend counts\", value: IndicatorType.BOTH },\n            ]\n        }\n    },\n\n    renderIndicator: () => {\n        const { mode } = Settings.plugins.ServerListIndicators;\n        return <ErrorBoundary noop>\n            <div style={{ marginBottom: \"4px\" }}>\n                {!!(mode & IndicatorType.FRIEND) && <FriendsIndicator />}\n                {!!(mode & IndicatorType.SERVER) && <ServersIndicator />}\n            </div>\n        </ErrorBoundary>;\n    },\n\n    start() {\n        addServerListElement(ServerListRenderPosition.Above, this.renderIndicator);\n    },\n\n    stop() {\n        removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator);\n    }\n});\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/api/languages.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ILanguageRegistration } from \"@vap/shiki\";\n\nimport { SHIKI_REPO, SHIKI_REPO_COMMIT } from \"./themes\";\n\nexport const JSON_REPO = \"Vencord/ShikiPluginAssets\";\nexport const JSON_REPO_COMMIT = \"75d69df9fdf596a31eef8b7f6f891231a6feab44\";\nexport const JSON_URL = `https://cdn.jsdelivr.net/gh/${JSON_REPO}@${JSON_REPO_COMMIT}/grammars.json`;\nexport const shikiRepoGrammar = (name: string) => `https://cdn.jsdelivr.net/gh/${SHIKI_REPO}@${SHIKI_REPO_COMMIT}/packages/tm-grammars/grammars/${name}.json`;\n\nexport interface Language {\n    name: string;\n    id: string;\n    devicon?: string;\n    grammarUrl: string,\n    grammar?: ILanguageRegistration[\"grammar\"];\n    scopeName: string;\n    aliases?: string[];\n    custom?: boolean;\n}\nexport interface LanguageJson {\n    name: string;\n    displayName: string;\n    scopeName: string;\n    devicon?: string;\n    aliases?: string[];\n}\n\nexport const languages: Record<string, Language> = {};\n\nexport const loadLanguages = async () => {\n    const langsJson: LanguageJson[] = await fetch(JSON_URL).then(res => res.ok ? res.json() : []);\n    const loadedLanguages = Object.fromEntries(\n        langsJson.map(lang => {\n            const { name, displayName, ...rest } = lang;\n            return [name, {\n                ...rest,\n                id: name,\n                name: displayName,\n                grammarUrl: shikiRepoGrammar(name),\n            }];\n        })\n    );\n    Object.assign(languages, loadedLanguages);\n};\n\nexport const getGrammar = (lang: Language): Promise<NonNullable<ILanguageRegistration[\"grammar\"]>> => {\n    if (lang.grammar) return Promise.resolve(lang.grammar);\n    return fetch(lang.grammarUrl).then(res => res.json());\n};\n\nconst aliasCache = new Map<string, Language>();\nexport function resolveLang(idOrAlias: string) {\n    if (Object.prototype.hasOwnProperty.call(languages, idOrAlias)) return languages[idOrAlias];\n\n    const lang = Object.values(languages).find(lang => lang.aliases?.includes(idOrAlias));\n\n    if (!lang) return null;\n\n    aliasCache.set(idOrAlias, lang);\n    return lang;\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/api/shiki.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { dispatchTheme } from \"@plugins/shikiCodeblocks.desktop/hooks/useTheme\";\nimport type { ShikiSpec } from \"@plugins/shikiCodeblocks.desktop/types\";\nimport { shikiOnigasmSrc, shikiWorkerSrc } from \"@utils/dependencies\";\nimport { WorkerClient } from \"@vap/core/ipc\";\nimport type { IShikiTheme, IThemedToken } from \"@vap/shiki\";\n\nimport { getGrammar, languages, loadLanguages, resolveLang } from \"./languages\";\nimport { themes } from \"./themes\";\n\nconst themeUrls = Object.values(themes);\n\nlet resolveClient: (client: WorkerClient<ShikiSpec>) => void;\n\nexport const shiki = {\n    client: null as WorkerClient<ShikiSpec> | null,\n    currentTheme: null as IShikiTheme | null,\n    currentThemeUrl: null as string | null,\n    timeoutMs: 10000,\n    languages,\n    themes,\n    loadedThemes: new Set<string>(),\n    loadedLangs: new Set<string>(),\n    clientPromise: new Promise<WorkerClient<ShikiSpec>>(resolve => resolveClient = resolve),\n\n    init: async (initThemeUrl: string | undefined) => {\n        /** https://stackoverflow.com/q/58098143 */\n        const workerBlob = await fetch(shikiWorkerSrc).then(res => res.blob());\n\n        const client = shiki.client = new WorkerClient<ShikiSpec>(\n            \"shiki-client\",\n            \"shiki-host\",\n            workerBlob,\n            { name: \"ShikiWorker\" },\n        );\n        await client.init();\n\n        const themeUrl = initThemeUrl || themeUrls[0];\n\n        await loadLanguages();\n        await client.run(\"setOnigasm\", { wasm: shikiOnigasmSrc });\n        await client.run(\"setHighlighter\", { theme: themeUrl, langs: [] });\n        shiki.loadedThemes.add(themeUrl);\n        await shiki._setTheme(themeUrl);\n        resolveClient(client);\n    },\n    _setTheme: async (themeUrl: string) => {\n        shiki.currentThemeUrl = themeUrl;\n        const { themeData } = await shiki.client!.run(\"getTheme\", { theme: themeUrl });\n        shiki.currentTheme = JSON.parse(themeData);\n        dispatchTheme({ id: themeUrl, theme: shiki.currentTheme });\n    },\n    loadTheme: async (themeUrl: string) => {\n        const client = await shiki.clientPromise;\n        if (shiki.loadedThemes.has(themeUrl)) return;\n\n        await client.run(\"loadTheme\", { theme: themeUrl });\n\n        shiki.loadedThemes.add(themeUrl);\n    },\n    setTheme: async (themeUrl: string) => {\n        await shiki.clientPromise;\n        themeUrl ||= themeUrls[0];\n        if (!shiki.loadedThemes.has(themeUrl)) await shiki.loadTheme(themeUrl);\n\n        await shiki._setTheme(themeUrl);\n    },\n    loadLang: async (langId: string) => {\n        const client = await shiki.clientPromise;\n        const lang = resolveLang(langId);\n\n        if (!lang || shiki.loadedLangs.has(lang.id)) return;\n\n        await client.run(\"loadLanguage\", {\n            lang: {\n                ...lang,\n                grammar: lang.grammar ?? await getGrammar(lang),\n            }\n        });\n        shiki.loadedLangs.add(lang.id);\n    },\n    tokenizeCode: async (code: string, langId: string): Promise<IThemedToken[][]> => {\n        const client = await shiki.clientPromise;\n        const lang = resolveLang(langId);\n        if (!lang) return [];\n\n        if (!shiki.loadedLangs.has(lang.id)) await shiki.loadLang(lang.id);\n\n        return await client.run(\"codeToThemedTokens\", {\n            code,\n            lang: langId,\n            theme: shiki.currentThemeUrl ?? themeUrls[0],\n        });\n    },\n    destroy() {\n        shiki.currentTheme = null;\n        shiki.currentThemeUrl = null;\n        dispatchTheme({ id: null, theme: null });\n        shiki.client?.destroy();\n    }\n};\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/api/themes.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2024 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { IShikiTheme } from \"@vap/shiki\";\n\nexport const SHIKI_REPO = \"shikijs/textmate-grammars-themes\";\nexport const SHIKI_REPO_COMMIT = \"bc5436518111d87ea58eb56d97b3f9bec30e6b83\";\nexport const shikiRepoTheme = (name: string) => `https://cdn.jsdelivr.net/gh/${SHIKI_REPO}@${SHIKI_REPO_COMMIT}/packages/tm-themes/themes/${name}.json`;\n\nexport const themes = {\n    // Default\n    DarkPlus: shikiRepoTheme(\"dark-plus\"),\n\n    // Dev Choices\n    MaterialCandy: \"https://raw.githubusercontent.com/millsp/material-candy/master/material-candy.json\",\n\n    // More from Shiki repo\n    Andromeeda: shikiRepoTheme(\"andromeeda\"),\n    AuroraX: shikiRepoTheme(\"aurora-x\"),\n    AyuDark: shikiRepoTheme(\"ayu-dark\"),\n    CatppuccinLatte: shikiRepoTheme(\"catppuccin-latte\"),\n    CatppuccinFrappe: shikiRepoTheme(\"catppuccin-frappe\"),\n    CatppuccinMacchiato: shikiRepoTheme(\"catppuccin-macchiato\"),\n    CatppuccinMocha: shikiRepoTheme(\"catppuccin-mocha\"),\n    DraculaSoft: shikiRepoTheme(\"dracula-soft\"),\n    Dracula: shikiRepoTheme(\"dracula\"),\n    EverforestDark: shikiRepoTheme(\"everforest-dark\"),\n    EverforestLight: shikiRepoTheme(\"everforest-light\"),\n    GithubDarkDefault: shikiRepoTheme(\"github-dark-default\"),\n    GithubDarkDimmed: shikiRepoTheme(\"github-dark-dimmed\"),\n    GithubDarkHighContrast: shikiRepoTheme(\"github-dark-high-contrast\"),\n    GithubDark: shikiRepoTheme(\"github-dark\"),\n    GithubLightDefault: shikiRepoTheme(\"github-light-default\"),\n    GithubLightHighContrast: shikiRepoTheme(\"github-light-high-contrast\"),\n    GithubLight: shikiRepoTheme(\"github-light\"),\n    GruvBoxDarkHard: shikiRepoTheme(\"gruvbox-dark-hard\"),\n    GruvBoxDarkMedium: shikiRepoTheme(\"gruvbox-dark-medium\"),\n    GruvBoxDarkSoft: shikiRepoTheme(\"gruvbox-dark-soft\"),\n    GruvBoxLightHard: shikiRepoTheme(\"gruvbox-light-hard\"),\n    GruvBoxLightMedium: shikiRepoTheme(\"gruvbox-light-medium\"),\n    GruvBoxLightSoft: shikiRepoTheme(\"gruvbox-light-soft\"),\n    Houston: shikiRepoTheme(\"houston\"),\n    KanagawaDragon: shikiRepoTheme(\"kanagawa-dragon\"),\n    KanagawaLotus: shikiRepoTheme(\"kanagawa-lotus\"),\n    KanagawaWave: shikiRepoTheme(\"kanagawa-wave\"),\n    LaserWave: shikiRepoTheme(\"laserwave\"),\n    LightPlus: shikiRepoTheme(\"light-plus\"),\n    MaterialDarker: shikiRepoTheme(\"material-theme-darker\"),\n    MaterialDefault: shikiRepoTheme(\"material-theme\"),\n    MaterialLighter: shikiRepoTheme(\"material-theme-lighter\"),\n    MaterialOcean: shikiRepoTheme(\"material-theme-ocean\"),\n    MaterialPalenight: shikiRepoTheme(\"material-theme-palenight\"),\n    MinDark: shikiRepoTheme(\"min-dark\"),\n    MinLight: shikiRepoTheme(\"min-light\"),\n    Monokai: shikiRepoTheme(\"monokai\"),\n    NightOwl: shikiRepoTheme(\"night-owl\"),\n    Nord: shikiRepoTheme(\"nord\"),\n    OneDarkPro: shikiRepoTheme(\"one-dark-pro\"),\n    OneLight: shikiRepoTheme(\"one-light\"),\n    Plastic: shikiRepoTheme(\"plastic\"),\n    Poimandres: shikiRepoTheme(\"poimandres\"),\n    Red: shikiRepoTheme(\"red\"),\n    RosePineDawn: shikiRepoTheme(\"rose-pine-dawn\"),\n    RosePineMoon: shikiRepoTheme(\"rose-pine-moon\"),\n    RosePine: shikiRepoTheme(\"rose-pine\"),\n    SlackDark: shikiRepoTheme(\"slack-dark\"),\n    SlackOchin: shikiRepoTheme(\"slack-ochin\"),\n    SnazzyLight: shikiRepoTheme(\"snazzy-light\"),\n    SolarizedDark: shikiRepoTheme(\"solarized-dark\"),\n    SolarizedLight: shikiRepoTheme(\"solarized-light\"),\n    Synthwave84: shikiRepoTheme(\"synthwave-84\"),\n    TokyoNight: shikiRepoTheme(\"tokyo-night\"),\n    Vesper: shikiRepoTheme(\"vesper\"),\n    VitesseBlack: shikiRepoTheme(\"vitesse-black\"),\n    VitesseDark: shikiRepoTheme(\"vitesse-dark\"),\n    VitesseLight: shikiRepoTheme(\"vitesse-light\"),\n};\n\nexport const themeCache = new Map<string, IShikiTheme>();\n\nexport const getTheme = (url: string): Promise<IShikiTheme> => {\n    if (themeCache.has(url)) return Promise.resolve(themeCache.get(url)!);\n    return fetch(url).then(res => res.json());\n};\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/components/ButtonRow.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { cl } from \"@plugins/shikiCodeblocks.desktop/utils/misc\";\n\nimport { CopyButton } from \"./CopyButton\";\n\nexport interface ButtonRowProps {\n    theme: import(\"./Highlighter\").ThemeBase;\n    content: string;\n}\n\nexport function ButtonRow({ content, theme }: ButtonRowProps) {\n    return <div className={cl(\"btns\")}>\n        <CopyButton\n            content={content}\n            className={cl(\"btn\")}\n            style={{\n                backgroundColor: theme.accentBgColor,\n                color: theme.accentFgColor,\n            }}\n        />\n    </div>;\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/components/Code.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { cl } from \"@plugins/shikiCodeblocks.desktop/utils/misc\";\nimport type { IThemedToken } from \"@vap/shiki\";\nimport { hljs } from \"@webpack/common\";\nimport { JSX } from \"react\";\n\nimport { ThemeBase } from \"./Highlighter\";\n\nexport interface CodeProps {\n    theme: ThemeBase;\n    useHljs: boolean;\n    lang?: string;\n    content: string;\n    tokens: IThemedToken[][] | null;\n}\n\nexport const Code = ({\n    theme,\n    useHljs,\n    lang,\n    content,\n    tokens,\n}: CodeProps) => {\n    let lines!: JSX.Element[];\n\n    if (useHljs) {\n        try {\n            const { value: hljsHtml } = hljs.highlight(content, { language: lang!, ignoreIllegals: true });\n            lines = hljsHtml\n                .split(\"\\n\")\n                .map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />);\n        } catch {\n            lines = content.split(\"\\n\").map((line, idx) => <span key={idx}>{line}</span>);\n        }\n    } else {\n        const renderTokens =\n            tokens ??\n            content\n                .split(\"\\n\")\n                .map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);\n\n        lines = renderTokens.map((line, idx) => {\n            // [Cynthia] this makes it so when you highlight the codeblock\n            // empty lines are also selected and copied when you Ctrl+C.\n            if (line.length === 0) {\n                return <span key={idx}>{\"\\n\"}</span>;\n            }\n\n            return (\n                <>\n                    {line.map(({ content, color, fontStyle }, i) => (\n                        <span\n                            key={i}\n                            style={{\n                                color,\n                                fontStyle: (fontStyle ?? 0) & 1 ? \"italic\" : undefined,\n                                fontWeight: (fontStyle ?? 0) & 2 ? \"bold\" : undefined,\n                                textDecoration: (fontStyle ?? 0) & 4 ? \"underline\" : undefined,\n                            }}\n                        >\n                            {content}\n                        </span>\n                    ))}\n                </>\n            );\n        });\n    }\n\n    const codeTableRows = lines.map((line, i) => (\n        <tr className={cl(\"table-row\")} key={i}>\n            <td className={cl(\"table-cell\")} style={{ color: theme.plainColor }}>{i + 1}</td>\n            <td className={cl(\"table-cell\")}>{line}</td>\n        </tr>\n    ));\n\n    return <table className={cl(\"table\")}>{...codeTableRows}</table>;\n};\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/components/CopyButton.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { useCopyCooldown } from \"@plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown\";\n\nexport interface CopyButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {\n    content: string;\n}\n\nexport function CopyButton({ content, ...props }: CopyButtonProps) {\n    const [copyCooldown, copy] = useCopyCooldown(1000);\n\n    return (\n        <button\n            {...props}\n            style={{\n                ...props.style,\n                cursor: copyCooldown ? \"default\" : undefined,\n            }}\n            onClick={() => copy(content)}\n        >\n            {copyCooldown ? \"Copied!\" : \"Copy\"}\n        </button>\n\n    );\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/components/Header.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Language } from \"@plugins/shikiCodeblocks.desktop/api/languages\";\nimport { DeviconSetting } from \"@plugins/shikiCodeblocks.desktop/types\";\nimport { cl } from \"@plugins/shikiCodeblocks.desktop/utils/misc\";\n\nexport interface HeaderProps {\n    langName?: string;\n    useDevIcon: DeviconSetting;\n    shikiLang: Language | null;\n}\n\nexport function Header({ langName, useDevIcon, shikiLang }: HeaderProps) {\n    if (!langName) return <></>;\n\n    return (\n        <div className={cl(\"lang\")}>\n            {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && (\n                <i\n                    className={`${cl(\"devicon\")} devicon-${shikiLang.devicon}${useDevIcon === DeviconSetting.Color ? \" colored\" : \"\"}`}\n                />\n            )}\n            {langName}\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/components/Highlighter.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { resolveLang } from \"@plugins/shikiCodeblocks.desktop/api/languages\";\nimport { shiki } from \"@plugins/shikiCodeblocks.desktop/api/shiki\";\nimport { useShikiSettings } from \"@plugins/shikiCodeblocks.desktop/hooks/useShikiSettings\";\nimport { useTheme } from \"@plugins/shikiCodeblocks.desktop/hooks/useTheme\";\nimport { hex2Rgb } from \"@plugins/shikiCodeblocks.desktop/utils/color\";\nimport { cl, shouldUseHljs } from \"@plugins/shikiCodeblocks.desktop/utils/misc\";\nimport { useAwaiter, useIntersection } from \"@utils/react\";\nimport { hljs, React } from \"@webpack/common\";\n\nimport { ButtonRow } from \"./ButtonRow\";\nimport { Code } from \"./Code\";\nimport { Header } from \"./Header\";\n\nexport interface ThemeBase {\n    plainColor: string;\n    accentBgColor: string;\n    accentFgColor: string;\n    backgroundColor: string;\n}\n\nexport interface HighlighterProps {\n    lang?: string;\n    content: string;\n    isPreview: boolean;\n}\n\nexport const createHighlighter = (props: HighlighterProps) => (\n    <pre className={cl(\"container\")}>\n        <ErrorBoundary>\n            <Highlighter {...props} />\n        </ErrorBoundary>\n    </pre>\n);\nexport const Highlighter = ({\n    lang,\n    content,\n    isPreview,\n}: HighlighterProps) => {\n    const {\n        tryHljs,\n        useDevIcon,\n        bgOpacity,\n    } = useShikiSettings([\"tryHljs\", \"useDevIcon\", \"bgOpacity\"]);\n    const { id: currentThemeId, theme: currentTheme } = useTheme();\n\n    const shikiLang = lang ? resolveLang(lang) : null;\n    const useHljs = shouldUseHljs({ lang, tryHljs });\n\n    const [rootRef, isIntersecting] = useIntersection(true);\n\n    const [tokens] = useAwaiter(async () => {\n        if (!shikiLang || useHljs || !isIntersecting) return null;\n        return await shiki.tokenizeCode(content, lang!);\n    }, {\n        fallbackValue: null,\n        deps: [lang, content, currentThemeId, isIntersecting],\n    });\n\n    const themeBase: ThemeBase = {\n        plainColor: currentTheme?.fg || \"var(--text-default)\",\n        accentBgColor:\n            currentTheme?.colors?.[\"statusBar.background\"] || (useHljs ? \"#7289da\" : \"#007BC8\"),\n        accentFgColor: currentTheme?.colors?.[\"statusBar.foreground\"] || \"#FFF\",\n        backgroundColor:\n            currentTheme?.colors?.[\"editor.background\"] || \"var(--background-base-lower)\",\n    };\n\n    let langName;\n    if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name;\n\n    return (\n        <div\n            ref={rootRef}\n            className={cl(\"root\", { plain: !langName, preview: isPreview })}\n            style={{\n                backgroundColor: useHljs\n                    ? themeBase.backgroundColor\n                    : `rgba(${hex2Rgb(themeBase.backgroundColor)\n                        .concat(bgOpacity / 100)\n                        .join(\", \")})`,\n                color: themeBase.plainColor,\n            }}\n        >\n            <code className={cl(\"code\")}>\n                <Header\n                    langName={langName}\n                    useDevIcon={useDevIcon}\n                    shikiLang={shikiLang}\n                />\n                <Code\n                    theme={themeBase}\n                    useHljs={useHljs}\n                    lang={lang}\n                    content={content}\n                    tokens={tokens}\n                />\n                {!isPreview && <ButtonRow\n                    content={content}\n                    theme={themeBase}\n                />}\n            </code>\n        </div>\n    );\n};\n\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/devicon.css",
    "content": "@import url(\"https://cdn.jsdelivr.net/gh/devicons/devicon@v2.17.0/devicon.min.css\");"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { React } from \"@webpack/common\";\n\nexport function useCopyCooldown(cooldown: number) {\n    const [copyCooldown, setCopyCooldown] = React.useState(false);\n\n    function copy(text: string) {\n        copyToClipboard(text);\n        setCopyCooldown(true);\n\n        setTimeout(() => {\n            setCopyCooldown(false);\n        }, cooldown);\n    }\n\n    return [copyCooldown, copy] as const;\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/hooks/useShikiSettings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { shiki } from \"@plugins/shikiCodeblocks.desktop/api/shiki\";\nimport { settings as pluginSettings, ShikiSettings } from \"@plugins/shikiCodeblocks.desktop/settings\";\nimport { React } from \"@webpack/common\";\n\nexport function useShikiSettings<F extends keyof ShikiSettings>(settingKeys: F[]) {\n    const settings = pluginSettings.use([...settingKeys, \"customTheme\", \"theme\"]);\n    const [isLoading, setLoading] = React.useState(false);\n\n    const themeUrl = settings.customTheme || settings.theme;\n\n    const willChangeTheme = shiki.currentThemeUrl && themeUrl && themeUrl !== shiki.currentThemeUrl;\n\n    if (isLoading && (!willChangeTheme)) setLoading(false);\n    if (!isLoading && willChangeTheme) {\n        setLoading(true);\n        shiki.setTheme(themeUrl);\n    }\n\n    return {\n        ...settings,\n        isThemeLoading: isLoading,\n    };\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/hooks/useTheme.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { React } from \"@webpack/common\";\n\ntype Shiki = typeof import(\"../api/shiki\").shiki;\ninterface ThemeState {\n    id: Shiki[\"currentThemeUrl\"],\n    theme: Shiki[\"currentTheme\"],\n}\n\nconst currentTheme: ThemeState = {\n    id: null,\n    theme: null,\n};\n\nconst themeSetters = new Set<React.Dispatch<React.SetStateAction<ThemeState>>>();\n\nexport const useTheme = (): ThemeState => {\n    const [, setTheme] = React.useState<ThemeState>(currentTheme);\n\n    React.useEffect(() => {\n        themeSetters.add(setTheme);\n        return () => void themeSetters.delete(setTheme);\n    }, []);\n\n    return currentTheme;\n};\n\nexport function dispatchTheme(state: ThemeState) {\n    if (currentTheme.id === state.id) return;\n    Object.assign(currentTheme, state);\n    themeSetters.forEach(setTheme => setTheme(state));\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./shiki.css\";\n\nimport { enableStyle } from \"@api/Styles\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { ReporterTestable } from \"@utils/types\";\nimport previewExampleText from \"file://previewExample.tsx\";\n\nimport { shiki } from \"./api/shiki\";\nimport { createHighlighter } from \"./components/Highlighter\";\nimport deviconStyle from \"./devicon.css?managed\";\nimport { settings } from \"./settings\";\nimport { DeviconSetting } from \"./types\";\nimport { clearStyles } from \"./utils/createStyle\";\n\nexport default definePlugin({\n    name: \"ShikiCodeblocks\",\n    description: \"Brings vscode-style codeblocks into Discord, powered by Shiki\",\n    authors: [Devs.Vap],\n    reporterTestable: ReporterTestable.Patches,\n    settings,\n\n    patches: [\n        {\n            find: \"codeBlock:{react(\",\n            replacement: {\n                match: /codeBlock:\\{react\\((\\i),(\\i),(\\i)\\)\\{/,\n                replace: \"$&return $self.renderHighlighter($1,$2,$3);\"\n            }\n        },\n        {\n            find: \"#{intl::PREVIEW_NUM_LINES}\",\n            replacement: {\n                match: /(?<=function \\i\\((\\i)\\)\\{)(?=let\\{text:\\i,language:)/,\n                replace: \"return $self.renderHighlighter({lang:$1.language,content:$1.text});\"\n            }\n        }\n    ],\n    start: async () => {\n        if (settings.store.useDevIcon !== DeviconSetting.Disabled)\n            enableStyle(deviconStyle);\n\n        await shiki.init(settings.store.customTheme || settings.store.theme);\n    },\n    stop: () => {\n        shiki.destroy();\n        clearStyles();\n    },\n    settingsAboutComponent: () => createHighlighter({\n        lang: \"tsx\",\n        content: previewExampleText,\n        isPreview: true\n    }),\n\n    // exports\n    shiki,\n    createHighlighter,\n    renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {\n        return createHighlighter({\n            lang: lang?.toLowerCase(),\n            content,\n            isPreview: false,\n        });\n    },\n});\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/previewExample.tsx",
    "content": "/* eslint-disable simple-header/header */\nimport React from \"react\";\n\nconst handleClick = async () =>\n    console.log((await import(\"@utils/clipboard\")).copyToClipboard(\"\\u200b\"));\n\nexport const Example: React.FC<{\n    real: boolean,\n    shigged?: number,\n}> = ({ real, shigged }) => <>\n    <p>{`Shigg${real ? `ies${shigged === 0x1B ? \"t\" : \"\"}` : \"y\"}`}</p>\n    <button onClick={handleClick}>Click Me</button>\n</>;\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport { parseUrl } from \"@utils/misc\";\nimport { wordsFromPascal, wordsToTitle } from \"@utils/text\";\nimport { OptionType } from \"@utils/types\";\n\nimport { shiki } from \"./api/shiki\";\nimport { themes } from \"./api/themes\";\nimport deviconStyle from \"./devicon.css?managed\";\nimport { DeviconSetting, HljsSetting } from \"./types\";\n\nconst themeNames = Object.keys(themes) as (keyof typeof themes)[];\n\nexport type ShikiSettings = typeof settings.store;\nexport const settings = definePluginSettings({\n    theme: {\n        type: OptionType.SELECT,\n        description: \"Default themes\",\n        options: themeNames.map(themeName => ({\n            label: wordsToTitle(wordsFromPascal(themeName)),\n            value: themes[themeName],\n            default: themes[themeName] === themes.DarkPlus,\n        })),\n        onChange: shiki.setTheme,\n    },\n    customTheme: {\n        type: OptionType.STRING,\n        description: \"A link to a custom vscode theme\",\n        placeholder: themes.MaterialCandy,\n        onChange: value => {\n            shiki.setTheme(value || settings.store.theme);\n        },\n    },\n    tryHljs: {\n        type: OptionType.SELECT,\n        description: \"Use the more lightweight default Discord highlighter and theme.\",\n        options: [\n            {\n                label: \"Never\",\n                value: HljsSetting.Never,\n            },\n            {\n                label: \"Prefer Shiki instead of Highlight.js\",\n                value: HljsSetting.Secondary,\n                default: true,\n            },\n            {\n                label: \"Prefer Highlight.js instead of Shiki\",\n                value: HljsSetting.Primary,\n            },\n            {\n                label: \"Always\",\n                value: HljsSetting.Always,\n            },\n        ],\n    },\n    useDevIcon: {\n        type: OptionType.SELECT,\n        description: \"How to show language icons on codeblocks\",\n        options: [\n            {\n                label: \"Disabled\",\n                value: DeviconSetting.Disabled,\n            },\n            {\n                label: \"Colorless\",\n                value: DeviconSetting.Greyscale,\n                default: true,\n            },\n            {\n                label: \"Colored\",\n                value: DeviconSetting.Color,\n            },\n        ],\n        onChange: (newValue: DeviconSetting) => {\n            if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle);\n            else enableStyle(deviconStyle);\n        },\n    },\n    bgOpacity: {\n        type: OptionType.SLIDER,\n        description: \"Background opacity\",\n        markers: [0, 20, 40, 60, 80, 100],\n        default: 100,\n        stickToMarkers: false,\n        componentProps: {\n            onValueRender: null, // Defaults to percentage\n        },\n    },\n}, {\n    theme: {\n        disabled() { return !!this.store.customTheme; },\n    },\n    customTheme: {\n        isValid(value) {\n            if (!value) return true;\n            const url = parseUrl(value);\n            if (!url) return \"Must be a valid URL\";\n\n            if (!url.pathname.endsWith(\".json\")) return \"Must be a json file\";\n\n            return true;\n        },\n    }\n});\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/shiki.css",
    "content": ".vc-shiki-container {\n    border: 4px;\n    background-color: var(--background-base-lower);\n}\n\n.vc-shiki-root {\n    border-radius: 4px;\n}\n\n.vc-shiki-root .vc-shiki-code {\n    display: block;\n    overflow-x: auto;\n    padding: 0.5em;\n    position: relative;\n    font-size: 0.875rem;\n    line-height: 1.125rem;\n    text-indent: 0;\n    white-space: pre-wrap;\n    background: transparent;\n    border: none;\n}\n\n.vc-shiki-devicon {\n    margin-right: 8px;\n    user-select: none;\n}\n\n.vc-shiki-plain .vc-shiki-code {\n    padding-top: 8px;\n}\n\n.vc-shiki-btns {\n    font-size: 1em;\n    position: absolute;\n    right: 0;\n    bottom: 0;\n    opacity: 0;\n}\n\n.vc-shiki-root:hover .vc-shiki-btns {\n    opacity: 1;\n}\n\n.vc-shiki-btn {\n    border-radius: 4px 4px 0 0;\n    padding: 4px 8px;\n    user-select: none;\n}\n\n.vc-shiki-btn ~ .vc-shiki-btn {\n    margin-left: 4px;\n}\n\n.vc-shiki-btn:last-child {\n    border-radius: 4px 0;\n}\n\n.vc-shiki-spinner-container {\n    align-items: center;\n    background-color: rgb(0 0 0 / 60%);\n    display: flex;\n    position: absolute;\n    justify-content: center;\n    inset: 0;\n}\n\n.vc-shiki-preview {\n    margin-bottom: 2em;\n}\n\n.vc-shiki-lang {\n    padding: 0 5px;\n    margin-bottom: 6px;\n    font-weight: bold;\n    text-transform: capitalize;\n    display: flex;\n    align-items: center;\n}\n\n.vc-shiki-table {\n    border-collapse: collapse;\n    width: 100%;\n}\n\n.vc-shiki-table-row {\n    height: 19px;\n    width: 100%;\n}\n\n.vc-shiki-root .vc-shiki-table-cell:first-child {\n    border-right: 1px solid transparent;\n    padding-left: 5px;\n    padding-right: 8px;\n    user-select: none;\n}\n\n.vc-shiki-root .vc-shiki-table-cell:last-child {\n    padding-left: 8px;\n    overflow-wrap: anywhere;\n    width: 100%;\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/types.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type {\n    ILanguageRegistration,\n    IShikiTheme,\n    IThemedToken,\n    IThemeRegistration,\n} from \"@vap/shiki\";\n\n/** This must be atleast a subset of the `@vap/shiki-worker` spec */\nexport type ShikiSpec = {\n    setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;\n    setHighlighter: ({ theme, langs }: {\n        theme: IThemeRegistration | void;\n        langs: ILanguageRegistration[];\n    }) => Promise<void>;\n    loadTheme: ({ theme }: {\n        theme: string | IShikiTheme;\n    }) => Promise<void>;\n    getTheme: ({ theme }: { theme: string; }) => Promise<{ themeData: string; }>;\n    loadLanguage: ({ lang }: { lang: ILanguageRegistration; }) => Promise<void>;\n    codeToThemedTokens: ({\n        code,\n        lang,\n        theme,\n    }: {\n        code: string;\n        lang?: string;\n        theme?: string;\n    }) => Promise<IThemedToken[][]>;\n};\n\nexport const enum StyleSheets {\n    Main = \"MAIN\",\n    DevIcons = \"DEVICONS\",\n}\n\nexport const enum HljsSetting {\n    Never = \"NEVER\",\n    Secondary = \"SECONDARY\",\n    Primary = \"PRIMARY\",\n    Always = \"ALWAYS\",\n}\nexport const enum DeviconSetting {\n    Disabled = \"DISABLED\",\n    Greyscale = \"GREYSCALE\",\n    Color = \"COLOR\"\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/utils/color.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function hex2Rgb(hex: string) {\n    hex = hex.slice(1);\n    if (hex.length < 6)\n        hex = hex\n            .split(\"\")\n            .map(c => c + c)\n            .join(\"\");\n    if (hex.length === 6) hex += \"ff\";\n    if (hex.length > 6) hex = hex.slice(0, 6);\n    return hex\n        .split(/(..)/)\n        .filter(Boolean)\n        .map(c => parseInt(c, 16));\n}\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/utils/createStyle.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nconst styles = new Map<string, HTMLStyleElement>();\n\nexport function setStyle(css: string, id: string) {\n    const style = document.createElement(\"style\");\n    style.innerText = css;\n    document.head.appendChild(style);\n    styles.set(id, style);\n}\n\nexport function removeStyle(id: string) {\n    styles.get(id)?.remove();\n    return styles.delete(id);\n}\n\nexport const clearStyles = () => {\n    styles.forEach(style => style.remove());\n    styles.clear();\n};\n"
  },
  {
    "path": "src/plugins/shikiCodeblocks.desktop/utils/misc.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { resolveLang } from \"@plugins/shikiCodeblocks.desktop/api/languages\";\nimport { HighlighterProps } from \"@plugins/shikiCodeblocks.desktop/components/Highlighter\";\nimport { HljsSetting } from \"@plugins/shikiCodeblocks.desktop/types\";\nimport { classNameFactory } from \"@utils/css\";\nimport { hljs } from \"@webpack/common\";\n\nexport const cl = classNameFactory(\"vc-shiki-\");\n\nexport const shouldUseHljs = ({\n    lang,\n    tryHljs,\n}: {\n    lang: HighlighterProps[\"lang\"],\n    tryHljs: HljsSetting,\n}) => {\n    const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;\n    const shikiLang = lang ? resolveLang(lang) : null;\n    const langName = shikiLang?.name;\n\n    switch (tryHljs) {\n        case HljsSetting.Always:\n            return true;\n        case HljsSetting.Primary:\n            return !!hljsLang || lang === \"\";\n        case HljsSetting.Secondary:\n            return !langName && !!hljsLang;\n        case HljsSetting.Never:\n            return false;\n        default: return false;\n    }\n};\n"
  },
  {
    "path": "src/plugins/showAllMessageButtons/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"ShowAllMessageButtons\",\n    description: \"Always show all message buttons no matter if you are holding the shift key or not.\",\n    authors: [Devs.Nuckyz],\n\n    patches: [\n        {\n            find: \"#{intl::MESSAGE_UTILITIES_A11Y_LABEL}\",\n            replacement: {\n                // isExpanded: isShiftPressed && other conditions...\n                match: /isExpanded:\\i&&(.+?),/,\n                replace: \"isExpanded:$1,\"\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/showConnections/VerifiedIcon.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { getIntlMessage } from \"@utils/discord\";\nimport { findComponentByCodeLazy, findLazy } from \"@webpack\";\nimport { useToken } from \"@webpack/common\";\n\nconst ColorMap = findLazy(m => m.colors?.INTERACTIVE_MUTED?.css);\nconst VerifiedIconComponent = findComponentByCodeLazy(\"#{intl::CONNECTIONS_ROLE_OFFICIAL_ICON_TOOLTIP}\");\n\nexport function VerifiedIcon() {\n    const color = useToken(ColorMap.colors.INTERACTIVE_MUTED).hex();\n    const forcedIconColor = useToken(ColorMap.colors.INTERACTIVE_ICON_ACTIVE ?? ColorMap.colors.INTERACTIVE_ACTIVE).hex();\n\n    return (\n        <VerifiedIconComponent\n            color={color}\n            forcedIconColor={forcedIconColor}\n            size={16}\n            tooltipText={getIntlMessage(\"CONNECTION_VERIFIED\")}\n            className=\"vc-sc-tooltip-icon\"\n        />\n    );\n}\n"
  },
  {
    "path": "src/plugins/showConnections/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { CopyIcon, LinkIcon } from \"@components/Icons\";\nimport OpenInAppPlugin from \"@plugins/openInApp\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { ConnectedAccount, User } from \"@vencord/discord-types\";\nimport { findByCodeLazy, findByPropsLazy } from \"@webpack\";\nimport { Tooltip, UserProfileStore } from \"@webpack/common\";\n\nimport { VerifiedIcon } from \"./VerifiedIcon\";\n\nconst useLegacyPlatformType: (platform: string) => string = findByCodeLazy(\".TWITTER_LEGACY:\");\nconst platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy(\"isSupported\", \"getByUrl\");\nconst getProfileThemeProps = findByCodeLazy(\".getPreviewThemeColors\", \"primaryColor:\");\n\nconst enum Spacing {\n    COMPACT,\n    COZY,\n    ROOMY\n}\nconst getSpacingPx = (spacing: Spacing | undefined) => (spacing ?? Spacing.COMPACT) * 2 + 4;\n\nconst settings = definePluginSettings({\n    iconSize: {\n        type: OptionType.NUMBER,\n        description: \"Icon size (px)\",\n        default: 32\n    },\n    iconSpacing: {\n        type: OptionType.SELECT,\n        description: \"Icon margin\",\n        default: Spacing.COZY,\n        options: [\n            { label: \"Compact\", value: Spacing.COMPACT },\n            { label: \"Cozy\", value: Spacing.COZY }, // US Spelling :/\n            { label: \"Roomy\", value: Spacing.ROOMY }\n        ]\n    }\n});\n\ninterface ConnectionPlatform {\n    getPlatformUserUrl(connection: ConnectedAccount): string;\n    icon: { lightSVG: string, darkSVG: string; };\n}\n\nconst profilePopoutComponent = ErrorBoundary.wrap(\n    (props: { user: User; displayProfile?: any; }) => (\n        <ConnectionsComponent\n            {...props}\n            id={props.user.id}\n            theme={getProfileThemeProps(props).theme}\n        />\n    ),\n    { noop: true }\n);\n\nfunction ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {\n    const profile = UserProfileStore.getUserProfile(id);\n    if (!profile)\n        return null;\n\n    const connections = profile.connectedAccounts;\n    if (!connections?.length)\n        return null;\n\n    return (\n        <Flex gap={getSpacingPx(settings.store.iconSpacing)} flexWrap=\"wrap\">\n            {connections.map(connection => <CompactConnectionComponent connection={connection} theme={theme} key={connection.id} />)}\n        </Flex>\n    );\n}\n\nfunction CompactConnectionComponent({ connection, theme }: { connection: ConnectedAccount, theme: string; }) {\n    const platform = platforms.get(useLegacyPlatformType(connection.type));\n    const url = platform.getPlatformUserUrl?.(connection);\n\n    const img = (\n        <img\n            aria-label={connection.name}\n            src={theme === \"light\" ? platform.icon.lightSVG : platform.icon.darkSVG}\n            style={{\n                width: settings.store.iconSize,\n                height: settings.store.iconSize\n            }}\n        />\n    );\n\n    const TooltipIcon = url ? LinkIcon : CopyIcon;\n\n    return (\n        <Tooltip\n            text={\n                <span className=\"vc-sc-tooltip\">\n                    <span className=\"vc-sc-connection-name\">{connection.name}</span>\n                    {connection.verified && <VerifiedIcon />}\n                    <TooltipIcon height={16} width={16} className=\"vc-sc-tooltip-icon\" />\n                </span>\n            }\n            key={connection.id}\n        >\n            {tooltipProps =>\n                url\n                    ? <a\n                        {...tooltipProps}\n                        className=\"vc-user-connection\"\n                        href={url}\n                        target=\"_blank\"\n                        rel=\"noreferrer\"\n                        onClick={e => {\n                            if (isPluginEnabled(OpenInAppPlugin.name)) {\n                                // handleLink will .preventDefault() if applicable\n                                OpenInAppPlugin.handleLink(e.currentTarget, e);\n                            }\n                        }}\n                    >\n                        {img}\n                    </a>\n                    : <button\n                        {...tooltipProps}\n                        className=\"vc-user-connection\"\n                        onClick={() => copyWithToast(connection.name)}\n                    >\n                        {img}\n                    </button>\n\n            }\n        </Tooltip>\n    );\n}\n\nexport default definePlugin({\n    name: \"ShowConnections\",\n    description: \"Show connected accounts in user popouts\",\n    authors: [Devs.TheKodeToad],\n    settings,\n\n    patches: [\n        {\n            find: /\\.POPOUT,onClose:\\i}\\),nicknameIcons:.+?\\.isProvisional/,\n            replacement: {\n                match: /userId:\\i\\.id,guild:\\i\\}\\)(?=])/,\n                replace: \"$&,$self.profilePopoutComponent(arguments[0])\"\n            }\n        }\n    ],\n\n    profilePopoutComponent,\n});\n"
  },
  {
    "path": "src/plugins/showConnections/styles.css",
    "content": ".vc-user-connection {\n    all: unset;\n    display: inline-block;\n    cursor: pointer;\n}\n\n.vc-sc-tooltip {\n    display: inline-flex;\n    gap: 0.25em;\n    align-items: center;\n}\n\n.vc-sc-connection-name {\n    word-break: break-all;\n}\n\n.vc-sc-tooltip-icon {\n    min-width: 16px;\n}\n"
  },
  {
    "path": "src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { Settings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport PermissionsViewerPlugin from \"@plugins/permissionsViewer\";\nimport openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from \"@plugins/permissionsViewer/components/RolesAndUsersPermissions\";\nimport { sortPermissionOverwrites } from \"@plugins/permissionsViewer/utils\";\nimport { classes } from \"@utils/misc\";\nimport { formatDuration } from \"@utils/text\";\nimport type { Channel } from \"@vencord/discord-types\";\nimport { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from \"@webpack\";\nimport { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from \"@webpack/common\";\n\nimport { cl, settings } from \"..\";\n\nconst enum SortOrderTypes {\n    LATEST_ACTIVITY = 0,\n    CREATION_DATE = 1\n}\n\nconst enum ForumLayoutTypes {\n    DEFAULT = 0,\n    LIST = 1,\n    GRID = 2\n}\n\ninterface DefaultReaction {\n    emojiId: string | null;\n    emojiName: string | null;\n}\n\ninterface Tag {\n    id: string;\n    name: string;\n    emojiId: string | null;\n    emojiName: string | null;\n    moderated: boolean;\n}\n\ninterface ExtendedChannel extends Channel {\n    defaultThreadRateLimitPerUser?: number;\n    defaultSortOrder?: SortOrderTypes | null;\n    defaultForumLayout?: ForumLayoutTypes;\n    defaultReactionEmoji?: DefaultReaction | null;\n    availableTags?: Array<Tag>;\n}\n\nconst enum ChannelTypes {\n    GUILD_TEXT = 0,\n    GUILD_VOICE = 2,\n    GUILD_ANNOUNCEMENT = 5,\n    GUILD_STAGE_VOICE = 13,\n    GUILD_FORUM = 15\n}\n\nconst enum VideoQualityModes {\n    AUTO = 1,\n    FULL = 2\n}\n\nconst enum ChannelFlags {\n    PINNED = 1 << 1,\n    REQUIRE_TAG = 1 << 4\n}\n\n\nconst ChatScrollClasses = findCssClassesLazy(\"auto\", \"managedReactiveScroller\", \"customTheme\");\nconst ChannelBeginHeader = findComponentByCodeLazy(\"#{intl::ROLE_REQUIRED_SINGLE_USER_MESSAGE}\");\nconst TagComponent = findComponentByCodeLazy(\"#{intl::FORUM_TAG_A11Y_FILTER_BY_TAG}\");\n\nconst EmojiParser = findByPropsLazy(\"convertSurrogateToName\");\nconst EmojiUtils = findByPropsLazy(\"getURL\", \"getEmojiColors\");\n\nconst ChannelTypesToChannelNames = {\n    [ChannelTypes.GUILD_TEXT]: \"text\",\n    [ChannelTypes.GUILD_ANNOUNCEMENT]: \"announcement\",\n    [ChannelTypes.GUILD_FORUM]: \"forum\",\n    [ChannelTypes.GUILD_VOICE]: \"voice\",\n    [ChannelTypes.GUILD_STAGE_VOICE]: \"stage\"\n};\n\nconst SortOrderTypesToNames = {\n    [SortOrderTypes.LATEST_ACTIVITY]: \"Latest activity\",\n    [SortOrderTypes.CREATION_DATE]: \"Creation date\"\n};\n\nconst ForumLayoutTypesToNames = {\n    [ForumLayoutTypes.DEFAULT]: \"Not set\",\n    [ForumLayoutTypes.LIST]: \"List view\",\n    [ForumLayoutTypes.GRID]: \"Gallery view\"\n};\n\nconst VideoQualityModesToNames = {\n    [VideoQualityModes.AUTO]: \"Automatic\",\n    [VideoQualityModes.FULL]: \"720p\"\n};\n\n// Icon from the modal when clicking a message link you don't have access to view\nconst HiddenChannelLogo = \"/assets/433e3ec4319a9d11b0cbe39342614982.svg\";\n\nfunction HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {\n    const { defaultAllowedUsersAndRolesDropdownState } = settings.use([\"defaultAllowedUsersAndRolesDropdownState\"]);\n    const [permissions, setPermissions] = useState<RoleOrUserPermission[]>([]);\n\n    const {\n        type,\n        topic,\n        lastMessageId,\n        defaultForumLayout,\n        lastPinTimestamp,\n        defaultAutoArchiveDuration,\n        availableTags,\n        id: channelId,\n        rateLimitPerUser,\n        defaultThreadRateLimitPerUser,\n        defaultSortOrder,\n        defaultReactionEmoji,\n        bitrate,\n        rtcRegion,\n        videoQualityMode,\n        permissionOverwrites,\n        guild_id\n    } = channel;\n\n    useEffect(() => {\n        const membersToFetch: Array<string> = [];\n\n        const guildOwnerId = GuildStore.getGuild(guild_id).ownerId;\n        if (!GuildMemberStore.getMember(guild_id, guildOwnerId)) membersToFetch.push(guildOwnerId);\n\n        Object.values(permissionOverwrites).forEach(({ type, id: userId }) => {\n            if (type === 1 && !GuildMemberStore.getMember(guild_id, userId)) {\n                membersToFetch.push(userId);\n            }\n        });\n\n        if (membersToFetch.length > 0) {\n            FluxDispatcher.dispatch({\n                type: \"GUILD_MEMBERS_REQUEST\",\n                guildIds: [guild_id],\n                userIds: membersToFetch\n            });\n        }\n\n        if (Settings.plugins.PermissionsViewer.enabled) {\n            setPermissions(sortPermissionOverwrites(Object.values(permissionOverwrites).map(overwrite => ({\n                type: overwrite.type as PermissionType,\n                id: overwrite.id,\n                overwriteAllow: overwrite.allow,\n                overwriteDeny: overwrite.deny\n            })), guild_id));\n        }\n    }, [channelId]);\n\n    return (\n        <div className={classes(ChatScrollClasses.auto, ChatScrollClasses.customTheme, ChatScrollClasses.managedReactiveScroller)}>\n            <div className={cl(\"container\")}>\n                <img className={cl(\"logo\")} src={HiddenChannelLogo} />\n\n                <div className={cl(\"heading-container\")}>\n                    <Text variant=\"heading-xxl/bold\">This is a {!PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) ? \"hidden\" : \"locked\"} {ChannelTypesToChannelNames[type]} channel</Text>\n                    {channel.isNSFW() &&\n                        <Tooltip text=\"NSFW\">\n                            {({ onMouseLeave, onMouseEnter }) => (\n                                <svg\n                                    onMouseLeave={onMouseLeave}\n                                    onMouseEnter={onMouseEnter}\n                                    className={cl(\"heading-nsfw-icon\")}\n                                    width=\"32\"\n                                    height=\"32\"\n                                    viewBox=\"0 0 48 48\"\n                                    aria-hidden={true}\n                                    role=\"img\"\n                                >\n                                    <path fill=\"currentColor\" d=\"M.7 43.05 24 2.85l23.3 40.2Zm23.55-6.25q.75 0 1.275-.525.525-.525.525-1.275 0-.75-.525-1.3t-1.275-.55q-.8 0-1.325.55-.525.55-.525 1.3t.55 1.275q.55.525 1.3.525Zm-1.85-6.1h3.65V19.4H22.4Z\" />\n                                </svg>\n                            )}\n                        </Tooltip>\n                    }\n                </div>\n\n                {(!channel.isGuildVoice() && !channel.isGuildStageVoice()) && (\n                    <Text variant=\"text-lg/normal\">\n                        You can not see the {channel.isForumChannel() ? \"posts\" : \"messages\"} of this channel.\n                        {channel.isForumChannel() && topic && topic.length > 0 && \" However you may see its guidelines:\"}\n                    </Text >\n                )}\n\n                {channel.isForumChannel() && topic && topic.length > 0 && (\n                    <div className={cl(\"topic-container\")}>\n                        {Parser.parseTopic(topic, false, { channelId })}\n                    </div>\n                )}\n\n                {lastMessageId &&\n                    <Text variant=\"text-md/normal\">\n                        Last {channel.isForumChannel() ? \"post\" : \"message\"} created:\n                        <Timestamp timestamp={new Date(SnowflakeUtils.extractTimestamp(lastMessageId))} />\n                    </Text>\n                }\n                {lastPinTimestamp &&\n                    <Text variant=\"text-md/normal\">Last message pin: <Timestamp timestamp={new Date(lastPinTimestamp)} /></Text>\n                }\n                {(rateLimitPerUser ?? 0) > 0 &&\n                    <Text variant=\"text-md/normal\">Slowmode: {formatDuration(rateLimitPerUser!, \"seconds\")}</Text>\n                }\n                {(defaultThreadRateLimitPerUser ?? 0) > 0 &&\n                    <Text variant=\"text-md/normal\">\n                        Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser!, \"seconds\")}\n                    </Text>\n                }\n                {((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) &&\n                    <Text variant=\"text-md/normal\">Bitrate: {bitrate} bits</Text>\n                }\n                {rtcRegion !== undefined &&\n                    <Text variant=\"text-md/normal\">Region: {rtcRegion ?? \"Automatic\"}</Text>\n                }\n                {(channel.isGuildVoice() || channel.isGuildStageVoice()) &&\n                    <Text variant=\"text-md/normal\">Video quality mode: {VideoQualityModesToNames[videoQualityMode ?? VideoQualityModes.AUTO]}</Text>\n                }\n                {(defaultAutoArchiveDuration ?? 0) > 0 &&\n                    <Text variant=\"text-md/normal\">\n                        Default inactivity duration before archiving {channel.isForumChannel() ? \"posts\" : \"threads\"}:\n                        {\" \" + formatDuration(defaultAutoArchiveDuration!, \"minutes\")}\n                    </Text>\n                }\n                {defaultForumLayout != null &&\n                    <Text variant=\"text-md/normal\">Default layout: {ForumLayoutTypesToNames[defaultForumLayout]}</Text>\n                }\n                {defaultSortOrder != null &&\n                    <Text variant=\"text-md/normal\">Default sort order: {SortOrderTypesToNames[defaultSortOrder]}</Text>\n                }\n                {defaultReactionEmoji != null &&\n                    <div className={cl(\"default-emoji-container\")}>\n                        <Text variant=\"text-md/normal\">Default reaction emoji:</Text>\n                        {Parser.defaultRules[defaultReactionEmoji.emojiName ? \"emoji\" : \"customEmoji\"].react({\n                            name: defaultReactionEmoji.emojiName\n                                ? EmojiParser.convertSurrogateToName(defaultReactionEmoji.emojiName)\n                                : EmojiStore.getCustomEmojiById(defaultReactionEmoji.emojiId)?.name ?? \"\",\n                            emojiId: defaultReactionEmoji.emojiId ?? void 0,\n                            surrogate: defaultReactionEmoji.emojiName ?? void 0,\n                            src: defaultReactionEmoji.emojiName\n                                ? EmojiUtils.getURL(defaultReactionEmoji.emojiName)\n                                : void 0\n                        }, void 0, { key: 0 })}\n                    </div>\n                }\n                {channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&\n                    <Text variant=\"text-md/normal\">Posts on this forum require a tag to be set.</Text>\n                }\n                {availableTags && availableTags.length > 0 &&\n                    <div className={cl(\"tags-container\")}>\n                        <Text variant=\"text-lg/bold\">Available tags:</Text>\n                        <div className={cl(\"tags\")}>\n                            {availableTags.map(tag => <TagComponent tag={tag} key={tag.id} />)}\n                        </div>\n                    </div>\n                }\n                <div className={cl(\"allowed-users-and-roles-container\")}>\n                    <div className={cl(\"allowed-users-and-roles-container-title\")}>\n                        {isPluginEnabled(PermissionsViewerPlugin.name) && (\n                            <Tooltip text=\"Permission Details\">\n                                {({ onMouseLeave, onMouseEnter }) => (\n                                    <button\n                                        onMouseLeave={onMouseLeave}\n                                        onMouseEnter={onMouseEnter}\n                                        className={cl(\"allowed-users-and-roles-container-permdetails-btn\")}\n                                        onClick={() => openRolesAndUsersPermissionsModal(permissions, GuildStore.getGuild(channel.guild_id), channel.name)}\n                                    >\n                                        <svg\n                                            width=\"24\"\n                                            height=\"24\"\n                                            viewBox=\"0 0 24 24\"\n                                        >\n                                            <path fill=\"currentColor\" d=\"M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z\" />\n                                        </svg>\n                                    </button>\n                                )}\n                            </Tooltip>\n                        )}\n                        <Text variant=\"text-lg/bold\">Allowed users and roles:</Text>\n                        <Tooltip text={defaultAllowedUsersAndRolesDropdownState ? \"Hide Allowed Users and Roles\" : \"View Allowed Users and Roles\"}>\n                            {({ onMouseLeave, onMouseEnter }) => (\n                                <button\n                                    onMouseLeave={onMouseLeave}\n                                    onMouseEnter={onMouseEnter}\n                                    className={cl(\"allowed-users-and-roles-container-toggle-btn\")}\n                                    onClick={() => settings.store.defaultAllowedUsersAndRolesDropdownState = !defaultAllowedUsersAndRolesDropdownState}\n                                >\n                                    <svg\n                                        width=\"24\"\n                                        height=\"24\"\n                                        viewBox=\"0 0 24 24\"\n                                        transform={defaultAllowedUsersAndRolesDropdownState ? \"scale(1 -1)\" : \"scale(1 1)\"}\n                                    >\n                                        <path fill=\"currentColor\" d=\"M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z\" />\n                                    </svg>\n                                </button>\n                            )}\n                        </Tooltip>\n                    </div>\n                    {defaultAllowedUsersAndRolesDropdownState && <ChannelBeginHeader channel={channel} />}\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport default ErrorBoundary.wrap(HiddenChannelLockScreen);\n"
  },
  {
    "path": "src/plugins/showHiddenChannels/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport type { Channel, Role } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { ChannelStore, PermissionsBits, PermissionStore, Tooltip } from \"@webpack/common\";\n\nimport HiddenChannelLockScreen from \"./components/HiddenChannelLockScreen\";\n\nexport const cl = classNameFactory(\"vc-shc-\");\n\nconst ChannelListClasses = findCssClassesLazy(\"modeSelected\", \"modeMuted\", \"unread\", \"icon\");\n\nconst enum ShowMode {\n    LockIcon,\n    HiddenIconWithMutedStyle\n}\n\nconst CONNECT = 1n << 20n;\n\nexport const settings = definePluginSettings({\n    hideUnreads: {\n        description: \"Hide Unreads\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    showMode: {\n        description: \"The mode used to display hidden channels.\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Plain style with Lock Icon instead\", value: ShowMode.LockIcon, default: true },\n            { label: \"Muted style with hidden eye icon on the right\", value: ShowMode.HiddenIconWithMutedStyle },\n        ],\n        restartNeeded: true\n    },\n    defaultAllowedUsersAndRolesDropdownState: {\n        description: \"Whether the allowed users and roles dropdown on hidden channels should be open by default\",\n        type: OptionType.BOOLEAN,\n        default: true\n    }\n});\n\nfunction isUncategorized(objChannel: { channel: Channel; comparator: number; }) {\n    return objChannel.channel.id === \"null\" && objChannel.channel.name === \"Uncategorized\" && objChannel.comparator === -1;\n}\n\nexport default definePlugin({\n    name: \"ShowHiddenChannels\",\n    description: \"Show channels that you do not have access to view.\",\n    authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn],\n    settings,\n\n    patches: [\n        {\n            // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc\n            find: '\"placeholder-channel-id\"',\n            replacement: [\n                // Remove the special logic for channels we don't have access to\n                {\n                    match: /if\\(!\\i\\.\\i\\.can\\(\\i\\.\\i\\.VIEW_CHANNEL.+?{if\\(this\\.id===\\i\\).+?threadIds:\\[\\]}}/,\n                    replace: \"\"\n                },\n                // Do not check for unreads when selecting the render level if the channel is hidden\n                {\n                    match: /(?<=&&)(?=!\\i\\.\\i\\.hasUnread\\(this\\.record\\.id\\))/,\n                    replace: \"$self.isHiddenChannel(this.record)||\"\n                },\n                // Make channels we dont have access to be the same level as normal ones\n                {\n                    match: /(this\\.record\\)\\?{renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,\n                    replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`\n                },\n                // Remove permission checking for getRenderLevel function\n                {\n                    match: /(getRenderLevel\\(\\i\\){.+?return)!\\i\\.\\i\\.can\\(\\i\\.\\i\\.VIEW_CHANNEL,this\\.record\\)\\|\\|/,\n                    replace: (_, rest) => `${rest} `\n                }\n            ]\n        },\n        {\n            find: \"VoiceChannel, transitionTo: Channel does not have a guildId\",\n            replacement: [\n                {\n                    // Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel\n                    match: /(?<=getIgnoredUsersForVoiceChannel\\((\\i)\\.id\\)[^;]+?;return\\()/,\n                    replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`\n                },\n                {\n                    // Prevent Discord from trying to connect to hidden voice channels\n                    match: /(?=\\|\\|\\i\\.\\i\\.selectVoiceChannel\\((\\i)\\.id\\))/,\n                    replace: (_, channel) => `||$self.isHiddenChannel(${channel})`\n                },\n                {\n                    // Make Discord show inside the channel if clicking on a hidden or locked channel\n                    match: /!__OVERLAY__&&\\((?<=selectVoiceChannel\\((\\i)\\.id\\).+?)/,\n                    replace: (m, channel) => `${m}$self.isHiddenChannel(${channel},true)||`\n                }\n            ]\n        },\n        // Prevent Discord from trying to connect to hidden stage channels\n        {\n            find: \".AUDIENCE),{isSubscriptionGated\",\n            replacement: {\n                match: /(\\i)\\.isRoleSubscriptionTemplatePreviewChannel\\(\\)/,\n                replace: (m, channel) => `${m}||$self.isHiddenChannel(${channel})`\n            }\n        },\n        {\n            find: 'tutorialId:\"instant-invite\"',\n            replacement: [\n                // Render null instead of the buttons if the channel is hidden\n                ...[\n                    \"renderEditButton\",\n                    \"renderInviteButton\",\n                ].map(func => ({\n                    match: new RegExp(`(?<=${func}\\\\(\\\\){)`, \"g\"), // Global because Discord has multiple declarations of the same functions\n                    replace: \"if($self.isHiddenChannel(this.props.channel))return null;\"\n                }))\n            ]\n        },\n        {\n            find: \"VoiceChannel.renderPopout: There must always be something to render\",\n            all: true,\n            // Render null instead of the buttons if the channel is hidden\n            replacement: {\n                match: /(?<=renderOpenChatButton(?:\",|=)\\(\\)=>{)/,\n                replace: \"if($self.isHiddenChannel(this.props.channel))return null;\"\n            }\n        },\n        {\n            find: \"#{intl::CHANNEL_TOOLTIP_DIRECTORY}\",\n            predicate: () => settings.store.showMode === ShowMode.LockIcon,\n            replacement: {\n                // Lock Icon\n                match: /(?=switch\\((\\i)\\.type\\).{0,30}\\.GUILD_ANNOUNCEMENT.{0,70}\\(0,\\i\\.\\i\\))/,\n                replace: (_, channel) => `if($self.isHiddenChannel(${channel}))return $self.LockIcon;`\n            }\n        },\n        {\n            find: \"UNREAD_IMPORTANT:\",\n            predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,\n            replacement: [\n                // Make the channel appear as muted if it's hidden\n                {\n                    match: /Children\\.count.+?;(?=return\\(0,\\i\\.jsxs?\\)\\(\\i\\.\\i,{focusTarget:)(?<={channel:(\\i),name:\\i,muted:(\\i).+?;)/,\n                    replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};`\n                },\n                // Add the hidden eye icon if the channel is hidden\n                {\n                    match: /\\.Children\\.count.+?:null(?<=,channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null`\n                },\n                // Make voice channels also appear as muted if they are muted\n                {\n                    match: /(?<=\\?\\i\\.\\i:\\i\\.\\i,)(.{0,150}?)if\\((\\i)(?:\\)return |\\?)(\\i\\.MUTED)/,\n                    replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:\"\",${otherClasses}if(${isMuted})return \"\"`\n                }\n            ]\n        },\n        {\n            find: \"UNREAD_IMPORTANT:\",\n            replacement: [\n                {\n                    // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden\n                    predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,\n                    match: /(?<=\\.LOCKED;if\\()(?<={channel:(\\i).+?)/,\n                    replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`\n                },\n                {\n                    // Hide unreads\n                    predicate: () => settings.store.hideUnreads === true,\n                    match: /Children\\.count.+?;(?=return\\(0,\\i\\.jsxs?\\)\\(\\i\\.\\i,{focusTarget:)(?<={channel:(\\i),name:\\i,.+?unread:(\\i).+?)/,\n                    replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};`\n                }\n            ]\n        },\n        {\n            // Hide the new version of unreads box for hidden channels\n            find: '\"ChannelListUnreadsStore\"',\n            replacement: {\n                match: /(?<=\\.id\\)\\))(?=&&\\(0,\\i\\.\\i\\)\\((\\i)\\))/,\n                replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`\n            }\n        },\n        {\n            // Make the old version of unreads box not visible for hidden channels\n            find: \"renderBottomUnread(){\",\n            replacement: {\n                match: /(?<=!0\\))(?=&&\\(0,\\i\\.\\i\\)\\((\\i\\.record)\\))/,\n                replace: \"&&!$self.isHiddenChannel($1)\"\n            }\n        },\n        {\n            // Make the state of the old version of unreads box not include hidden channels\n            find: \"ignoreRecents:!0\",\n            replacement: {\n                match: /(?<=\\.id\\)\\))(?=&&\\(0,\\i\\.\\i\\)\\((\\i)\\))/,\n                replace: \"&&!$self.isHiddenChannel($1)\"\n            }\n        },\n        // Only render the channel header and buttons that work when transitioning to a hidden channel\n        {\n            find: \"Missing channel in Channel.renderHeaderToolbar\",\n            replacement: [\n                {\n                    match: /renderHeaderToolbar(?:\",|=)\\(\\)=>{.+?case \\i\\.\\i\\.GUILD_TEXT:(?=.+?(\\i\\.push.{0,50}channel:(\\i)},\"notifications\"\\)\\)))(?<=isLurking:(\\i).+?)/,\n                    replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`\n                },\n                {\n                    match: /renderHeaderToolbar(?:\",|=)\\(\\)=>{.+?case \\i\\.\\i\\.GUILD_MEDIA:(?=.+?(\\i\\.push.{0,40}channel:(\\i)},\"notifications\"\\)\\)))(?<=isLurking:(\\i).+?)/,\n                    replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`\n                },\n                {\n                    match: /renderMobileToolbar(?:\",|=)\\(\\)=>{.+?case \\i\\.\\i\\.GUILD_DIRECTORY:(?<=let{channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;`\n                },\n                {\n                    match: /(?<=renderHeaderBar(?:\",|=)\\(\\)=>{.+?hideSearch:(\\i)\\.isDirectory\\(\\))/,\n                    replace: (_, channel) => `||$self.isHiddenChannel(${channel})`\n                },\n                {\n                    match: /(?<=renderSidebar\\(\\){)/,\n                    replace: \"if($self.isHiddenChannel(this.props.channel))return null;\"\n                },\n                {\n                    match: /(?<=renderChat\\(\\){)/,\n                    replace: \"if($self.isHiddenChannel(this.props.channel))return $self.HiddenChannelLockScreen(this.props.channel);\"\n                }\n            ]\n        },\n        // Avoid trying to fetch messages from hidden channels\n        {\n            find: '\"MessageManager\"',\n            replacement: {\n                match: /forceFetch:\\i,isPreload:.+?}=\\i;(?=.+?getChannel\\((\\i)\\))/,\n                replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;`\n            }\n        },\n        // Patch keybind handlers so you can't accidentally jump to hidden channels\n        {\n            find: '\"alt+shift+down\"',\n            replacement: {\n                match: /(?<=getChannel\\(\\i\\);return null!=(\\i))(?=.{0,200}?>0\\)&&\\(0,\\i\\.\\i\\)\\(\\i\\))/,\n                replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`\n            }\n        },\n        // Patch keybind handlers so you can't accidentally jump to hidden channels\n        {\n            find: \".APPLICATION_STORE&&null!=\",\n            replacement: {\n                match: /getState\\(\\)\\.channelId.+?(?=\\.map\\(\\i=>\\i\\.id)/,\n                replace: \"$&.filter(e=>!$self.isHiddenChannel(e))\"\n            }\n        },\n        {\n            find: \"#{intl::ROLE_REQUIRED_SINGLE_USER_MESSAGE}\",\n            replacement: [\n                {\n                    // Change the role permission check to CONNECT if the channel is locked\n                    match: /(forceRoles:.+?)(\\i\\.\\i\\(\\i\\.\\i\\.ADMINISTRATOR,\\i\\.\\i\\.VIEW_CHANNEL\\))(?<=context:(\\i)}.+?)/,\n                    replace: (_, rest, mergedPermissions, channel) => `${rest}$self.swapViewChannelWithConnectPermission(${mergedPermissions},${channel})`\n                },\n                {\n                    // Change the permissionOverwrite check to CONNECT if the channel is locked\n                    match: /permissionOverwrites\\[.+?\\i=(?<=context:(\\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,\n                    replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`\n                },\n                {\n                    // Include the @everyone role in the allowed roles list for Hidden Channels\n                    match: /getSortedRoles.+?\\.filter\\(\\i=>(?=!)/,\n                    replace: m => `${m}$self.isHiddenChannel(arguments[0]?.channel)?true:`\n                },\n                {\n                    // If the @everyone role has the required permissions, make the array only contain it\n                    match: /forceRoles:.+?.value\\(\\)(?<=channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m}.reduce(...$self.makeAllowedRolesReduce(${channel}.guild_id))`\n                },\n                {\n                    // Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen)\n                    match: /return\\(0,\\i\\.jsxs?\\)\\(\\i\\.\\i,{channelId:(\\i)\\.id(?=.+?(\\(0,\\i\\.jsxs?\\)\\(\"div\",{className:\\i\\.\\i,children:\\[.{0,100}\\i\\.length>0.+?\\]}\\)),)/,\n                    replace: (m, channel, allowedUsersAndRolesComponent) => `if($self.isHiddenChannel(${channel},true)){return${allowedUsersAndRolesComponent};}${m}`\n                },\n                {\n                    // Export the channel for the users allowed component patch\n                    match: /maxUsers:\\d+?,users:\\i(?<=channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m},shcChannel:${channel}`\n                },\n                {\n                    // Always render the component for multiple allowed users\n                    match: /1!==\\i\\.length(?=\\|\\|)/,\n                    replace: \"true\"\n                }\n            ]\n        },\n        {\n            find: '=\"interactive-text-default\",overflowCountClassName:',\n            replacement: [\n                {\n                    // Create a variable for the channel prop\n                    match: /let{users:\\i,maxUsers:\\i,/,\n                    replace: \"let{shcChannel}=arguments[0];$&\"\n                },\n                {\n                    // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen\n                    match: /\\i>0(?=&&!\\i&&!\\i)/,\n                    replace: m => `($self.isHiddenChannel(typeof shcChannel!==\"undefined\"?shcChannel:void 0,true)?true:${m})`\n                },\n                {\n                    // Show only the plus text without overflowed children amount\n                    // if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen\n                    match: /(?<=`\\+\\$\\{)\\i(?=\\})/,\n                    replace: overflowTextAmount => \"\" +\n                        `$self.isHiddenChannel(typeof shcChannel!==\"undefined\"?shcChannel:void 0,true)&&(${overflowTextAmount}-1)<=0?\"\":${overflowTextAmount}`\n                }\n            ]\n        },\n        {\n            find: \"#{intl::CHANNEL_CALL_CURRENT_SPEAKER}\",\n            replacement: [\n                {\n                    // Remove the open chat button for the HiddenChannelLockScreen\n                    match: /(?<=&&)\\i\\.push\\(.{0,120}\"chat-spacer\"/,\n                    replace: \"(arguments[0]?.inCall||!$self.isHiddenChannel(arguments[0]?.channel,true))&&$&\"\n                }\n            ]\n        },\n        {\n            find: \"#{intl::EMBEDDED_ACTIVITIES_DEVELOPER_ACTIVITY_SHELF_FETCH_ERROR}\",\n            replacement: [\n                {\n                    // Render our HiddenChannelLockScreen component instead of the main voice channel component\n                    match: /renderContent\\(\\i\\){.+?this\\.renderVoiceChannelEffects.+?children:/,\n                    replace: \"$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?$self.HiddenChannelLockScreen(this.props.channel):\"\n                },\n                {\n                    // Disable gradients for the HiddenChannelLockScreen of voice channels\n                    match: /renderContent\\(\\i\\){.+?disableGradients:/,\n                    replace: \"$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)||\"\n                },\n                {\n                    // Disable useless components for the HiddenChannelLockScreen of voice channels\n                    match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:/g,\n                    replace: \"$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?()=>null:\"\n                },\n                {\n                    // Disable bad CSS class which mess up hidden voice channels styling\n                    match: /(?=\\i\\|\\|\\i!==\\i\\.\\i\\.FULL_SCREEN.{0,100}?this\\._callContainerRef)/,\n                    replace: '$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?\"\":'\n                }\n            ]\n        },\n        {\n            find: '\"HasBeenInStageChannel\"',\n            replacement: [\n                {\n                    // Render our HiddenChannelLockScreen component instead of the main stage channel component\n                    match: /screenMessage:(\\i)\\?.+?children:(?=!\\1)(?<=let \\i,{channel:(\\i).+?)/,\n                    replace: (m, _isPopoutOpen, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`\n                },\n                {\n                    // Disable useless components for the HiddenChannelLockScreen of stage channels\n                    match: /render(?:BottomLeft|BottomCenter|BottomRight|ChatToasts):\\(\\)=>(?<=let \\i,{channel:(\\i).+?)/g,\n                    replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:`\n                },\n                {\n                    // Disable gradients for the HiddenChannelLockScreen of stage channels\n                    match: /\"124px\".+?disableGradients:(?<=let \\i,{channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||`\n                },\n                {\n                    // Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels\n                    match: /\"124px\".+?style:(?<=let \\i,{channel:(\\i).+?)/,\n                    replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?void 0:`\n                }\n            ]\n        },\n        {\n            find: \"#{intl::STAGE_FULL_MODERATOR_TITLE}\",\n            replacement: [\n                {\n                    // Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen\n                    match: /\\(0,\\i\\.jsx\\)\\(\\i\\.\\i\\.Divider.+?}\\)]}\\)(?=.+?:(\\i)\\.guild_id)/,\n                    replace: (m, channel) => `$self.isHiddenChannel(${channel})?null:(${m})`\n                },\n                {\n                    // Remove the open chat button for the HiddenChannelLockScreen\n                    match: /(?<=numRequestToSpeak:\\i\\}\\)\\}\\):null,!\\i&&)\\(0,\\i\\.jsxs?\\).{0,280}?iconClassName:/,\n                    replace: \"!$self.isHiddenChannel(arguments[0]?.channel,true)&&$&\"\n                }\n            ]\n        },\n        {\n            // Make the chat input bar channel list contain hidden channels\n            find: \",queryStaticRouteChannels(\",\n            replacement: [\n                {\n                    // Make the getChannels call to GuildChannelStore return hidden channels\n                    match: /(?<=queryChannels\\(\\i\\){.+?getChannels\\(\\i)(?=\\))/,\n                    replace: \",true\"\n                },\n                {\n                    // Avoid filtering out hidden channels from the channel list\n                    match: /(?<=queryChannels\\(\\i\\){.+?\\)\\((\\i)\\.type\\))(?=&&!\\i\\.\\i\\.can\\()/,\n                    replace: \"&&!$self.isHiddenChannel($1)\"\n                }\n            ]\n        },\n        {\n            find: \"\\\"^/guild-stages/(\\\\\\\\d+)(?:/)?(\\\\\\\\d+)?\\\"\",\n            replacement: {\n                // Make mentions of hidden channels work\n                match: /\\i\\.\\i\\.can\\(\\i\\.\\i\\.VIEW_CHANNEL,\\i\\)/,\n                replace: \"true\"\n            },\n        },\n        {\n            find: 'getConfig({location:\"channel_mention\"})',\n            replacement: {\n                // Show inside voice channel instead of trying to join them when clicking on a channel mention\n                match: /(?<=getChannel\\(\\i\\);if\\(null!=(\\i)).{0,200}?return void (?=\\i\\.default\\.selectVoiceChannel)/,\n                replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`\n            }\n        },\n        {\n            find: '\"GuildChannelStore\"',\n            replacement: [\n                {\n                    // Make GuildChannelStore contain hidden channels\n                    match: /isChannelGated\\(.+?\\)(?=&&)/,\n                    replace: m => `${m}&&false`\n                },\n                {\n                    // Filter hidden channels from GuildChannelStore.getChannels unless told otherwise\n                    match: /(?<=getChannels\\(\\i)(\\){.*?)return (.+?)}/,\n                    replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??arguments[0]===\"@favorites\");}`\n                },\n            ]\n        },\n        {\n            find: \"GuildTooltip - \",\n            replacement: {\n                // Make GuildChannelStore.getChannels return hidden channels\n                match: /(?<=getChannels\\(\\i)(?=\\))/,\n                replace: \",true\"\n            }\n        },\n        {\n            find: '\"NowPlayingViewStore\"',\n            replacement: {\n                // Make active now voice states on hidden channels\n                match: /(getVoiceStateForUser.{0,150}?)&&\\i\\.\\i\\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\\)(?=\\?)/,\n                replace: \"$1\"\n            }\n        }\n    ],\n\n\n    swapViewChannelWithConnectPermission(mergedPermissions: bigint, channel: Channel) {\n        if (!PermissionStore.can(PermissionsBits.CONNECT, channel)) {\n            mergedPermissions &= ~PermissionsBits.VIEW_CHANNEL;\n            mergedPermissions |= PermissionsBits.CONNECT;\n        }\n\n        return mergedPermissions;\n    },\n\n    isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {\n        try {\n            if (channel == null || Object.hasOwn(channel, \"channelId\") && channel.channelId == null) return false;\n\n            if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);\n            if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;\n            if ([\"browse\", \"customize\", \"guide\"].includes(channel.id)) return false;\n\n            return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);\n        } catch (e) {\n            console.error(\"[ViewHiddenChannels#isHiddenChannel]: \", e);\n            return false;\n        }\n    },\n\n    resolveGuildChannels(channels: Record<string | number, Array<{ channel: Channel; comparator: number; }> | string | number>, shouldIncludeHidden: boolean) {\n        if (shouldIncludeHidden) return channels;\n\n        const res = {};\n        for (const [key, maybeObjChannels] of Object.entries(channels)) {\n            if (!Array.isArray(maybeObjChannels)) {\n                res[key] = maybeObjChannels;\n                continue;\n            }\n\n            res[key] ??= [];\n\n            for (const objChannel of maybeObjChannels) {\n                if (isUncategorized(objChannel) || objChannel.channel.id === null || !this.isHiddenChannel(objChannel.channel)) res[key].push(objChannel);\n            }\n        }\n\n        return res;\n    },\n\n    makeAllowedRolesReduce(guildId: string) {\n        return [\n            (prev: Array<Role>, _: Role, index: number, originalArray: Array<Role>) => {\n                if (index !== 0) return prev;\n\n                const everyoneRole = originalArray.find(role => role.id === guildId);\n\n                if (everyoneRole) return [everyoneRole];\n                return originalArray;\n            },\n            [] as Array<Role>\n        ];\n    },\n\n    HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />,\n\n    LockIcon: ErrorBoundary.wrap(() => (\n        <svg\n            className={ChannelListClasses.icon}\n            height=\"18\"\n            width=\"20\"\n            viewBox=\"0 0 24 24\"\n            aria-hidden={true}\n            role=\"img\"\n        >\n            <path fill=\"currentcolor\" fillRule=\"evenodd\" d=\"M17 11V7C17 4.243 14.756 2 12 2C9.242 2 7 4.243 7 7V11C5.897 11 5 11.896 5 13V20C5 21.103 5.897 22 7 22H17C18.103 22 19 21.103 19 20V13C19 11.896 18.103 11 17 11ZM12 18C11.172 18 10.5 17.328 10.5 16.5C10.5 15.672 11.172 15 12 15C12.828 15 13.5 15.672 13.5 16.5C13.5 17.328 12.828 18 12 18ZM15 11H9V7C9 5.346 10.346 4 12 4C13.654 4 15 5.346 15 7V11Z\" />\n        </svg>\n    ), { noop: true }),\n\n    HiddenChannelIcon: ErrorBoundary.wrap(() => (\n        <Tooltip text=\"Hidden Channel\">\n            {({ onMouseLeave, onMouseEnter }) => (\n                <svg\n                    onMouseLeave={onMouseLeave}\n                    onMouseEnter={onMouseEnter}\n                    className={classes(ChannelListClasses.icon, cl(\"hidden-channel-icon\"))}\n                    width=\"24\"\n                    height=\"24\"\n                    viewBox=\"0 0 24 24\"\n                    aria-hidden={true}\n                    role=\"img\"\n                >\n                    <path fill=\"currentcolor\" fillRule=\"evenodd\" d=\"m19.8 22.6-4.2-4.15q-.875.275-1.762.413Q12.95 19 12 19q-3.775 0-6.725-2.087Q2.325 14.825 1 11.5q.525-1.325 1.325-2.463Q3.125 7.9 4.15 7L1.4 4.2l1.4-1.4 18.4 18.4ZM12 16q.275 0 .512-.025.238-.025.513-.1l-5.4-5.4q-.075.275-.1.513-.025.237-.025.512 0 1.875 1.312 3.188Q10.125 16 12 16Zm7.3.45-3.175-3.15q.175-.425.275-.862.1-.438.1-.938 0-1.875-1.312-3.188Q13.875 7 12 7q-.5 0-.938.1-.437.1-.862.3L7.65 4.85q1.025-.425 2.1-.638Q10.825 4 12 4q3.775 0 6.725 2.087Q21.675 8.175 23 11.5q-.575 1.475-1.512 2.738Q20.55 15.5 19.3 16.45Zm-4.625-4.6-3-3q.7-.125 1.288.112.587.238 1.012.688.425.45.613 1.038.187.587.087 1.162Z\" />\n                </svg>\n            )}\n        </Tooltip>\n    ), { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/showHiddenChannels/style.css",
    "content": ".vc-shc-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n    gap: 0.65em;\n    margin: 0.5em 0;\n    min-height: 100%;\n}\n\n.vc-shc-logo {\n    width: 12em;\n    height: 12em;\n}\n\n.vc-shc-heading-container {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 0.5em;\n}\n\n.vc-shc-heading-nsfw-icon {\n    color: var(--text-default);\n}\n\n.vc-shc-topic-container {\n    color: var(--text-default);\n    background: var(--background-base-lower);\n    border-radius: 5px;\n    padding: 10px;\n    max-width: 70vw;\n}\n\n.vc-shc-default-emoji-container {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    background: var(--background-base-lower);\n    border-radius: 8px;\n    padding: 0.75em;\n    margin-left: 0.75em;\n}\n\n.vc-shc-tags-container {\n    display: flex;\n    flex-direction: column;\n    background: var(--background-base-lower);\n    border-radius: 5px;\n    padding: 0.75em;\n    gap: 0.75em;\n    max-width: 70vw;\n}\n\n.vc-shc-tags {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-wrap: wrap;\n    gap: 0.35em;\n}\n\n.vc-shc-allowed-users-and-roles-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    background: var(--background-base-lower);\n    border-radius: 5px;\n    padding: 0.75em;\n    max-width: 70vw;\n}\n\n.vc-shc-allowed-users-and-roles-container-title {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 0.5em;\n}\n\n.vc-shc-allowed-users-and-roles-container-toggle-btn {\n    all: unset;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    color: var(--text-default);\n}\n\n.vc-shc-allowed-users-and-roles-container-permdetails-btn {\n    all: unset;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    color: var(--text-default);\n}\n\n.vc-shc-allowed-users-and-roles-container > [class*=\"members\"] {\n    margin-left: 12px;\n    flex-wrap: wrap;\n    justify-content: center;\n}\n\n.vc-shc-hidden-channel-icon {\n    cursor: not-allowed;\n    margin-left: 6px;\n    z-index: 0;\n}\n"
  },
  {
    "path": "src/plugins/showHiddenThings/README.md",
    "content": "# ShowHiddenThings\n\nDisplays various hidden & moderator-only things regardless of permissions.\n\n## Features\n\n- Show member timeout icons in chat\n![](https://github.com/Vendicated/Vencord/assets/47677887/75e1f6ba-8921-4188-9c2d-c9c3f9d07101)\n\n- Show the invites paused tooltip in the server list\n![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)\n\n- Show the member mod view context menu item in all servers\n\n![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)\n\n- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria\n\n- Disable filters in Server Discovery search that hide NSFW & disallowed servers\n"
  },
  {
    "path": "src/plugins/showHiddenThings/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType, PluginSettingDef } from \"@utils/types\";\nimport { GuildMember, Role } from \"@vencord/discord-types\";\n\nconst opt = (description: string) => ({\n    type: OptionType.BOOLEAN,\n    description,\n    default: true,\n    restartNeeded: true\n} satisfies PluginSettingDef);\n\nconst settings = definePluginSettings({\n    showTimeouts: opt(\"Show member timeout icons in chat.\"),\n    showInvitesPaused: opt(\"Show the invites paused tooltip in the server list.\"),\n    showModView: opt(\"Show the member mod view context menu item in all servers.\")\n});\n\nexport default definePlugin({\n    name: \"ShowHiddenThings\",\n    tags: [\"ShowTimeouts\", \"ShowInvitesPaused\", \"ShowModView\", \"DisableDiscoveryFilters\"],\n    description: \"Displays various hidden & moderator-only things regardless of permissions.\",\n    authors: [Devs.Dolfies],\n    settings,\n\n    patches: [\n        {\n            find: \"showCommunicationDisabledStyles\",\n            predicate: () => settings.store.showTimeouts,\n            replacement: {\n                match: /&&\\i\\.\\i\\.canManageUser\\(\\i\\.\\i\\.MODERATE_MEMBERS,\\i\\.author,\\i\\)/,\n                replace: \"\",\n            },\n        },\n        {\n            find: \"INVITES_DISABLED)||\",\n            predicate: () => settings.store.showInvitesPaused,\n            replacement: {\n                match: /\\i\\.\\i\\.can\\(\\i\\.\\i.MANAGE_GUILD,\\i\\)/,\n                replace: \"true\",\n            },\n        },\n        {\n            find: /,checkElevated:!1}\\),\\i\\.\\i\\)}(?<=getCurrentUser\\(\\);return.+?)/,\n            predicate: () => settings.store.showModView,\n            replacement: {\n                match: /return \\i\\.\\i\\(\\i\\.\\i\\(\\{user:\\i,context:\\i,checkElevated:!1\\}\\),\\i\\.\\i\\)/,\n                replace: \"return true\",\n            }\n        },\n        // fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?\n        {\n            find: \"#{intl::GUILD_MEMBER_MOD_VIEW_HIGHEST_ROLE}),children:\",\n            predicate: () => settings.store.showModView,\n            replacement: {\n                match: /(#{intl::GUILD_MEMBER_MOD_VIEW_HIGHEST_ROLE}.{0,80})role:\\i(?<=\\[\\i\\.roles,\\i\\.highestRoleId,(\\i)\\].+?)/,\n                replace: (_, rest, roles) => `${rest}role:$self.getHighestRole(arguments[0],${roles})`,\n            }\n        },\n        // allows you to open mod view on yourself\n        {\n            find: 'action:\"PRESS_MOD_VIEW\",icon:',\n            predicate: () => settings.store.showModView,\n            replacement: {\n                match: /\\i(?=\\?null)/,\n                replace: \"false\"\n            }\n        }\n    ],\n\n    getHighestRole({ member }: { member: GuildMember; }, roles: Role[]): Role | undefined {\n        try {\n            return roles.find(role => role.id === member.highestRoleId);\n        } catch (e) {\n            new Logger(\"ShowHiddenThings\").error(\"Failed to find highest role\", e);\n            return undefined;\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/showMeYourName/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 rini\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Channel, Message, User } from \"@vencord/discord-types\";\nimport { RelationshipStore, StreamerModeStore } from \"@webpack/common\";\n\ninterface UsernameProps {\n    author: { nick: string; authorId: string; };\n    channel: Channel;\n    message: Message;\n    withMentionPrefix?: boolean;\n    isRepliedMessage: boolean;\n    userOverride?: User;\n}\n\nconst settings = definePluginSettings({\n    mode: {\n        type: OptionType.SELECT,\n        description: \"How to display usernames and nicks\",\n        options: [\n            { label: \"Username then nickname\", value: \"user-nick\", default: true },\n            { label: \"Nickname then username\", value: \"nick-user\" },\n            { label: \"Username only\", value: \"user\" },\n        ],\n    },\n    friendNicknames: {\n        type: OptionType.SELECT,\n        description: \"How to prioritise friend nicknames over server nicknames\",\n        options: [\n            { label: \"Show friend nicknames only in direct messages\", value: \"dms\", default: true },\n            { label: \"Prefer friend nicknames over server nicknames\", value: \"always\" },\n            { label: \"Prefer server nicknames over friend nicknames\", value: \"fallback\" }\n        ]\n    },\n    displayNames: {\n        type: OptionType.BOOLEAN,\n        description: \"Use display names in place of usernames\",\n        default: false\n    },\n    inReplies: {\n        type: OptionType.BOOLEAN,\n        default: false,\n        description: \"Also apply functionality to reply previews\",\n    },\n});\n\nexport default definePlugin({\n    name: \"ShowMeYourName\",\n    description: \"Display usernames next to nicks, or no nicks at all\",\n    authors: [Devs.Rini, Devs.TheKodeToad, Devs.rae],\n    patches: [\n        {\n            find: '=\"SYSTEM_TAG\"',\n            replacement: {\n                // The field is named \"userName\", but as this is unusual casing, the regex also matches username, in case they change it\n                match: /(?<=onContextMenu:\\i,children:)\\i\\?(?=.{0,100}?user[Nn]ame:)/,\n                replace: \"$self.renderUsername(arguments[0]),_oldChildren:$&\"\n            }\n        },\n    ],\n    settings,\n\n    renderUsername: ErrorBoundary.wrap(({ author, channel, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {\n        try {\n            const { mode, friendNicknames, displayNames, inReplies } = settings.store;\n\n            const user = userOverride ?? message.author;\n            let username = StreamerModeStore.enabled\n                ? user.username[0] + \"…\"\n                : user.username;\n\n            if (displayNames)\n                username = user.globalName || username;\n\n            let { nick } = author;\n\n            const friendNickname = RelationshipStore.getNickname(author.authorId);\n\n            if (friendNickname) {\n                const shouldUseFriendNickname =\n                    friendNicknames === \"always\" ||\n                    (friendNicknames === \"dms\" && channel.isPrivate()) ||\n                    (friendNicknames === \"fallback\" && !nick);\n\n                if (shouldUseFriendNickname)\n                    nick = friendNickname;\n            }\n\n            const prefix = withMentionPrefix ? \"@\" : \"\";\n\n            if (isRepliedMessage && !inReplies || username.toLowerCase() === nick.toLowerCase())\n                return <>{prefix}{nick}</>;\n\n            if (mode === \"user-nick\")\n                return <>{prefix}{username} <span className=\"vc-smyn-suffix\">{nick}</span></>;\n\n            if (mode === \"nick-user\")\n                return <>{prefix}{nick} <span className=\"vc-smyn-suffix\">{username}</span></>;\n\n            return <>{prefix}{username}</>;\n        } catch {\n            return <>{author?.nick}</>;\n        }\n    }, { noop: true }),\n});\n"
  },
  {
    "path": "src/plugins/showMeYourName/styles.css",
    "content": ".vc-smyn-suffix {\n    color: var(--text-muted);\n    -webkit-text-fill-color: initial;\n    isolation: isolate;\n}\n\n.vc-smyn-suffix::before {\n    content: \"(\";\n}\n\n.vc-smyn-suffix::after {\n    content: \")\";\n}\n"
  },
  {
    "path": "src/plugins/showTimeoutDuration/README.md",
    "content": "# ShowTimeoutDuration\n\nDisplays how much longer a user's timeout will last.\nEither in the timeout icon tooltip, or next to it, configurable via settings!\n\n![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)\n\n![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)\n"
  },
  {
    "path": "src/plugins/showTimeoutDuration/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { TooltipContainer } from \"@components/TooltipContainer\";\nimport { Devs } from \"@utils/constants\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { findComponentLazy } from \"@webpack\";\nimport { ChannelStore, GuildMemberStore, Text } from \"@webpack/common\";\nimport { ReactNode } from \"react\";\n\nconst countDownFilter = canonicalizeMatch(/#{intl::MAX_AGE_NEVER}/);\nconst CountDown = findComponentLazy(m => m.prototype?.render && countDownFilter.test(m.prototype.render.toString()));\n\nconst enum DisplayStyle {\n    Tooltip = \"tooltip\",\n    Inline = \"ssalggnikool\"\n}\n\nconst settings = definePluginSettings({\n    displayStyle: {\n        description: \"How to display the timeout duration\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"In the Tooltip\", value: DisplayStyle.Tooltip },\n            { label: \"Next to the timeout icon\", value: DisplayStyle.Inline, default: true },\n        ],\n    }\n});\n\nfunction renderTimeout(message: Message, inline: boolean) {\n    const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;\n    if (!guildId) return null;\n\n    const member = GuildMemberStore.getMember(guildId, message.author.id);\n    if (!member?.communicationDisabledUntil) return null;\n\n    const countdown = () => (\n        <CountDown\n            deadline={new Date(member.communicationDisabledUntil!)}\n            showUnits\n            stopAtOneSec\n        />\n    );\n\n    getIntlMessage(\"GUILD_ENABLE_COMMUNICATION_TIME_REMAINING\", {\n        username: message.author.username,\n        countdown\n    });\n\n    return inline\n        ? countdown()\n        : getIntlMessage(\"GUILD_ENABLE_COMMUNICATION_TIME_REMAINING\", {\n            username: message.author.username,\n            countdown\n        });\n}\n\nexport default definePlugin({\n    name: \"ShowTimeoutDuration\",\n    description: \"Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it\",\n    authors: [Devs.Ven, Devs.Sqaaakoi],\n\n    settings,\n\n    patches: [\n        {\n            find: \"#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\",\n            replacement: [\n                {\n                    match: /\\i\\.\\i,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\\))/,\n                    replace: \"$self.TooltipWrapper,{message:arguments[0].message,$1\"\n                }\n            ]\n        }\n    ],\n\n    TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: ReactNode; text: ReactNode; }) => {\n        if (settings.store.displayStyle === DisplayStyle.Tooltip)\n            return <TooltipContainer text={renderTimeout(message, false)}>{children}</TooltipContainer>;\n\n        return (\n            <div className=\"vc-std-wrapper\">\n                <TooltipContainer text={text}>{children}</TooltipContainer>\n                <Text variant=\"text-md/normal\" color=\"status-danger\">\n                    {renderTimeout(message, true)} timeout remaining\n                </Text>\n            </div>\n        );\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/showTimeoutDuration/styles.css",
    "content": ".vc-std-wrapper {\n    display: flex;\n    align-items: center;\n}\n\n.vc-std-wrapper [class*=\"communicationDisabled\"] {\n    margin-right: 0;\n}\n"
  },
  {
    "path": "src/plugins/silentMessageToggle/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ChatBarButton, ChatBarButtonFactory } from \"@api/ChatButtons\";\nimport { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from \"@api/MessageEvents\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { IconComponent, OptionType } from \"@utils/types\";\nimport { React, useEffect, useState } from \"@webpack/common\";\n\nlet lastState = false;\n\nconst settings = definePluginSettings({\n    persistState: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to persist the state of the silent message toggle when changing channels\",\n        default: false,\n        onChange(newValue: boolean) {\n            if (newValue === false) lastState = false;\n        }\n    },\n    autoDisable: {\n        type: OptionType.BOOLEAN,\n        description: \"Automatically disable the silent message toggle again after sending one\",\n        default: true\n    }\n});\n\nfunction SilentMessageDisabledIcon() {\n    return (\n        <SilentMessageIcon>\n            <mask id=\"vc-silent-msg-mask\">\n                <path fill=\"#fff\" d=\"M0 0h24v24H0Z\" />\n                <path stroke=\"#000\" strokeWidth=\"5.99068\" d=\"M0 24 24 0\" />\n            </mask>\n            <path fill=\"var(--status-danger)\" d=\"m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z\" />\n        </SilentMessageIcon>\n    );\n}\n\nconst SilentMessageIcon: IconComponent = ({ height = 20, width = 20, className, children }) => {\n    return (\n        <svg\n            width={width}\n            height={height}\n            viewBox=\"0 0 24 24\"\n            className={className}\n            style={{ scale: \"1.2\" }}\n        >\n            <path fill=\"currentColor\" mask=\"url(#vc-silent-msg-mask)\" d=\"M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z\" />\n            {children}\n        </svg>\n    );\n};\n\nconst SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {\n    const [enabled, setEnabled] = useState(lastState);\n\n    function setEnabledValue(value: boolean) {\n        if (settings.store.persistState) lastState = value;\n        setEnabled(value);\n    }\n\n    useEffect(() => {\n        const listener: MessageSendListener = (_, message) => {\n            if (enabled) {\n                if (settings.store.autoDisable) setEnabledValue(false);\n                if (!message.content.startsWith(\"@silent \")) message.content = \"@silent \" + message.content;\n            }\n        };\n\n        addMessagePreSendListener(listener);\n        return () => void removeMessagePreSendListener(listener);\n    }, [enabled]);\n\n    if (!isMainChat) return null;\n\n    return (\n        <ChatBarButton\n            tooltip={enabled ? \"Disable Silent Message\" : \"Enable Silent Message\"}\n            onClick={() => setEnabledValue(!enabled)}\n        >\n            {enabled ? <SilentMessageIcon /> : <SilentMessageDisabledIcon />}\n        </ChatBarButton>\n    );\n};\n\nexport default definePlugin({\n    name: \"SilentMessageToggle\",\n    authors: [Devs.Nuckyz, Devs.CatNoir],\n    description: \"Adds a button to the chat bar to toggle sending a silent message.\",\n    settings,\n\n    chatBarButton: {\n        icon: SilentMessageIcon,\n        render: SilentMessageToggle\n    }\n});\n"
  },
  {
    "path": "src/plugins/silentTyping/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ChatBarButton, ChatBarButtonFactory } from \"@api/ChatButtons\";\nimport { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from \"@api/Commands\";\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { IconComponent, OptionType } from \"@utils/types\";\nimport { FluxDispatcher, Menu, React } from \"@webpack/common\";\n\nconst settings = definePluginSettings({\n    showIcon: {\n        type: OptionType.BOOLEAN,\n        default: false,\n        description: \"Show an icon for toggling the plugin\",\n        restartNeeded: true,\n    },\n    contextMenu: {\n        type: OptionType.BOOLEAN,\n        description: \"Add option to toggle the functionality in the chat input context menu\",\n        default: true\n    },\n    isEnabled: {\n        type: OptionType.BOOLEAN,\n        description: \"Toggle functionality\",\n        default: true,\n    }\n});\n\nfunction SilentTypingEnabledIcon() {\n    return (\n        <SilentTypingIcon>\n            <mask id=\"silent-typing-msg-mask\">\n                <path fill=\"#fff\" d=\"M0 0h24v24H0Z\"></path>\n                <path stroke=\"#000\" strokeWidth=\"5.99068\" d=\"M0 24 24 0\" transform=\"translate(-2, -3)\"></path>\n            </mask>\n            <path fill=\"var(--status-danger)\" d=\"m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z\" />\n        </SilentTypingIcon>\n    );\n}\n\nconst SilentTypingIcon: IconComponent = ({ height = 20, width = 20, className, children }) => {\n    return (\n        <svg\n            width={width}\n            height={height}\n            className={className}\n            viewBox=\"0 0 24 24\"\n            style={{ scale: \"1.2\" }}\n        >\n            <path fill=\"currentColor\" mask=\"url(#silent-typing-msg-mask)\" d=\"M18.333 15.556H1.667a1.667 1.667 0 0 1 -1.667 -1.667v-10a1.667 1.667 0 0 1 1.667 -1.667h16.667a1.667 1.667 0 0 1 1.667 1.667v10a1.667 1.667 0 0 1 -1.667 1.667M4.444 6.25V4.861a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333V8.194a0.417 0.417 0 0 0 -0.417 -0.417H4.306a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417H7.639a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m10 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h8.056a0.417 0.417 0 0 0 0.417 -0.417m3.333 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417\" transform=\"translate(2, 3)\" />\n            {children}\n        </svg>\n    );\n};\n\nconst SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => {\n    const { isEnabled, showIcon } = settings.use([\"isEnabled\", \"showIcon\"]);\n    const toggle = () => settings.store.isEnabled = !settings.store.isEnabled;\n\n    if (!isMainChat || !showIcon) return null;\n\n    return (\n        <ChatBarButton\n            tooltip={isEnabled ? \"Disable Silent Typing\" : \"Enable Silent Typing\"}\n            onClick={toggle}\n        >\n            {isEnabled ? <SilentTypingEnabledIcon /> : <SilentTypingIcon />}\n        </ChatBarButton>\n    );\n};\n\n\nconst ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {\n    const { isEnabled, contextMenu } = settings.use([\"isEnabled\", \"contextMenu\"]);\n    if (!contextMenu) return;\n\n    const group = findGroupChildrenByChildId(\"submit-button\", children);\n\n    if (!group) return;\n\n    const idx = group.findIndex(c => c?.props?.id === \"submit-button\");\n\n    group.splice(idx + 1, 0,\n        <Menu.MenuCheckboxItem\n            id=\"vc-silent-typing\"\n            label=\"Enable Silent Typing\"\n            checked={isEnabled}\n            action={() => settings.store.isEnabled = !settings.store.isEnabled}\n        />\n    );\n};\n\n\nexport default definePlugin({\n    name: \"SilentTyping\",\n    authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],\n    description: \"Hide that you are typing\",\n    settings,\n\n    contextMenus: {\n        \"textarea-context\": ChatBarContextCheckbox\n    },\n\n    patches: [\n        {\n            find: '.dispatch({type:\"TYPING_START_LOCAL\"',\n            replacement: {\n                match: /startTyping\\(\\i\\){.+?},stop/,\n                replace: \"startTyping:$self.startTyping,stop\"\n            }\n        },\n    ],\n\n    commands: [{\n        name: \"silenttype\",\n        description: \"Toggle whether you're hiding that you're typing or not.\",\n        inputType: ApplicationCommandInputType.BUILT_IN,\n        options: [\n            {\n                name: \"value\",\n                description: \"whether to hide or not that you're typing (default is toggle)\",\n                required: false,\n                type: ApplicationCommandOptionType.BOOLEAN,\n            },\n        ],\n        execute: async (args, ctx) => {\n            settings.store.isEnabled = !!findOption(args, \"value\", !settings.store.isEnabled);\n            sendBotMessage(ctx.channel.id, {\n                content: settings.store.isEnabled ? \"Silent typing enabled!\" : \"Silent typing disabled!\",\n            });\n        },\n    }],\n\n    async startTyping(channelId: string) {\n        if (settings.store.isEnabled) return;\n        FluxDispatcher.dispatch({ type: \"TYPING_START_LOCAL\", channelId });\n    },\n\n    chatBarButton: {\n        icon: SilentTypingIcon,\n        render: SilentTypingToggle\n    }\n});\n"
  },
  {
    "path": "src/plugins/sortFriendRequests/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { TooltipContainer } from \"@components/TooltipContainer\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { User } from \"@vencord/discord-types\";\nimport { DateUtils, RelationshipStore, Text } from \"@webpack/common\";\nimport { PropsWithChildren } from \"react\";\n\nconst formatter = new Intl.DateTimeFormat(undefined, {\n    month: \"numeric\",\n    day: \"numeric\",\n    year: \"numeric\",\n});\n\nconst cl = classNameFactory(\"vc-sortFriendRequests-\");\n\nfunction getSince(user: User) {\n    return new Date(RelationshipStore.getSince(user.id));\n}\n\nconst settings = definePluginSettings({\n    showDates: {\n        type: OptionType.BOOLEAN,\n        description: \"Show dates on friend requests\",\n        default: false,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"SortFriendRequests\",\n    authors: [Devs.Megu],\n    description: \"Sorts friend requests by date of receipt\",\n    settings,\n\n    patches: [{\n        find: \"getRelationshipCounts(){\",\n        replacement: {\n            match: /\\}\\)\\.sortBy\\((.+?)\\)\\.value\\(\\)/,\n            replace: \"}).sortBy(row => $self.wrapSort(($1), row)).value()\"\n        }\n    }, {\n        find: \"#{intl::FRIEND_REQUEST_CANCEL}\",\n        replacement: {\n            predicate: () => settings.store.showDates,\n            match: /(?<=children:\\[)\\(0,.{0,100}user:\\i,hovered:\\i.+?(?=,\\(0)(?<=user:(\\i).+?)/,\n            replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`\n        }\n    }],\n\n    wrapSort(comparator: Function, row: any) {\n        return row.type === 3 || row.type === 4\n            ? -getSince(row.user)\n            : comparator(row);\n    },\n\n    WrapperDateComponent: ErrorBoundary.wrap(({ user, children }: PropsWithChildren<{ user: User; }>) => {\n        const since = getSince(user);\n\n        return <div className={cl(\"wrapper\")}>\n            {children}\n            {!isNaN(since.getTime()) && (\n                <TooltipContainer text={DateUtils.dateFormat(since, \"LLLL\")} tooltipClassName={cl(\"tooltip\")}>\n                    <Text variant=\"text-xs/normal\" className={cl(\"date\")}>{formatter.format(since)}</Text>\n                </TooltipContainer>\n            )}\n        </div>;\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/sortFriendRequests/styles.css",
    "content": ".vc-sortFriendRequests-wrapper {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    align-items: center;\n    width: 100%;\n    margin-right: 0.5em;\n}\n\n.vc-sortFriendRequests-tooltip {\n    max-width: none;\n    white-space: nowrap;\n}\n\n.vc-sortFriendRequests-date {\n    color: var(--text-muted);\n    font-family: var(--font-code);\n}\n"
  },
  {
    "path": "src/plugins/spotifyControls/PlayerComponent.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./spotifyStyles.css\";\n\nimport { Settings } from \"@api/Settings\";\nimport { Flex } from \"@components/Flex\";\nimport { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from \"@components/Icons\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Span } from \"@components/Span\";\nimport { debounce } from \"@shared/debounce\";\nimport { classNameFactory } from \"@utils/css\";\nimport { copyWithToast, openImageModal } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from \"@webpack/common\";\n\nimport { SeekBar } from \"./SeekBar\";\nimport { SpotifyStore, Track } from \"./SpotifyStore\";\n\nconst cl = classNameFactory(\"vc-spotify-\");\n\nfunction msToHuman(ms: number) {\n    const minutes = ms / 1000 / 60;\n    const m = Math.floor(minutes);\n    const s = Math.floor((minutes - m) * 60);\n    return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n}\n\nfunction Svg(path: string, label: string) {\n    return () => (\n        <svg\n            className={cl(\"button-icon\", label)}\n            height=\"24\"\n            width=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            aria-label={label}\n            focusable={false}\n        >\n            <path d={path} />\n        </svg>\n    );\n}\n\n// KraXen's icons :yesyes:\n// from https://fonts.google.com/icons?icon.style=Rounded&icon.set=Material+Icons\n// older material icon style, but still really good\nconst PlayButton = Svg(\"M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z\", \"play\");\nconst PauseButton = Svg(\"M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z\", \"pause\");\nconst SkipPrev = Svg(\"M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1zm3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z\", \"previous\");\nconst SkipNext = Svg(\"M7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82zM16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1z\", \"next\");\nconst Repeat = Svg(\"M7 7h10v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V5H6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1s1-.45 1-1V7zm10 10H7v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.36V19h11c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1s-1 .45-1 1v3z\", \"repeat\");\nconst Shuffle = Svg(\"M10.59 9.17L6.12 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l4.46 4.46 1.42-1.4zm4.76-4.32l1.19 1.19L4.7 17.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L17.96 7.46l1.19 1.19c.31.31.85.09.85-.36V4.5c0-.28-.22-.5-.5-.5h-3.79c-.45 0-.67.54-.36.85zm-.52 8.56l-1.41 1.41 3.13 3.13-1.2 1.2c-.31.31-.09.85.36.85h3.79c.28 0 .5-.22.5-.5v-3.79c0-.45-.54-.67-.85-.35l-1.19 1.19-3.13-3.14z\", \"shuffle\");\n\nfunction Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {\n    return (\n        <button\n            className={cl(\"button\")}\n            {...props}\n        >\n            {props.children}\n        </button>\n    );\n}\n\nfunction CopyContextMenu({ name, type, path }: { type: string; name: string; path: string; }) {\n    return (\n        <Menu.Menu\n            navId=\"vc-spotify-menu\"\n            onClose={ContextMenuApi.closeContextMenu}\n            aria-label={`Spotify ${type} Menu`}\n        >\n            <Menu.MenuItem\n                id=\"vc-spotify-copy-name\"\n                label={`Copy ${type} Name`}\n                action={() => copyWithToast(name)}\n                icon={CopyIcon}\n            />\n            <Menu.MenuItem\n                id=\"vc-spotify-copy-link\"\n                label={`Copy ${type} Link`}\n                action={() => copyWithToast(\"https://open.spotify.com\" + path)}\n                icon={LinkIcon}\n            />\n            <Menu.MenuItem\n                id=\"vc-spotify-open\"\n                label={`Open ${type} in Spotify`}\n                action={() => SpotifyStore.openExternal(path)}\n                icon={OpenExternalIcon}\n            />\n        </Menu.Menu>\n    );\n}\n\nfunction Controls() {\n    const [isPlaying, shuffle, repeat] = useStateFromStores(\n        [SpotifyStore],\n        () => [SpotifyStore.isPlaying, SpotifyStore.shuffle, SpotifyStore.repeat]\n    );\n\n    const [nextRepeat, repeatClassName] = (() => {\n        switch (repeat) {\n            case \"off\": return [\"context\", \"repeat-off\"] as const;\n            case \"context\": return [\"track\", \"repeat-context\"] as const;\n            case \"track\": return [\"off\", \"repeat-track\"] as const;\n            default: throw new Error(`Invalid repeat state ${repeat}`);\n        }\n    })();\n\n    // the 1 is using position absolute so it does not make the button jump around\n    return (\n        <Flex className={cl(\"button-row\")} gap=\"0\">\n            <Button\n                className={classes(cl(\"button\"), cl(\"shuffle\"), cl(shuffle ? \"shuffle-on\" : \"shuffle-off\"))}\n                onClick={() => SpotifyStore.setShuffle(!shuffle)}\n            >\n                <Shuffle />\n            </Button>\n            <Button onClick={() => {\n                Settings.plugins.SpotifyControls.previousButtonRestartsTrack && SpotifyStore.position > 3000 ? SpotifyStore.seek(0) : SpotifyStore.prev();\n            }}>\n                <SkipPrev />\n            </Button>\n            <Button onClick={() => SpotifyStore.setPlaying(!isPlaying)}>\n                {isPlaying ? <PauseButton /> : <PlayButton />}\n            </Button>\n            <Button onClick={() => SpotifyStore.next()}>\n                <SkipNext />\n            </Button>\n            <Button\n                className={classes(cl(\"button\"), cl(\"repeat\"), cl(repeatClassName))}\n                onClick={() => SpotifyStore.setRepeat(nextRepeat)}\n                style={{ position: \"relative\" }}\n            >\n                {repeat === \"track\" && <span className={cl(\"repeat-1\")}>1</span>}\n                <Repeat />\n            </Button>\n        </Flex>\n    );\n}\n\nconst seek = debounce((v: number) => {\n    SpotifyStore.seek(v);\n});\n\nfunction SpotifySeekBar() {\n    const { duration } = SpotifyStore.track!;\n\n    const [storePosition, isSettingPosition, isPlaying] = useStateFromStores(\n        [SpotifyStore],\n        () => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying]\n    );\n\n    const [position, setPosition] = useState(storePosition);\n\n    useEffect(() => {\n        if (isPlaying && !isSettingPosition) {\n            setPosition(SpotifyStore.position);\n            const interval = setInterval(() => {\n                setPosition(p => p + 1000);\n            }, 1000);\n\n            return () => clearInterval(interval);\n        }\n    }, [storePosition, isSettingPosition, isPlaying]);\n\n    const onChange = (v: number) => {\n        if (isSettingPosition) return;\n        setPosition(v);\n        seek(v);\n    };\n\n    return (\n        <div id={cl(\"progress-bar\")}>\n            <Span\n                size=\"xs\"\n                weight=\"medium\"\n                className={cl(\"progress-time\") + \" \" + cl(\"time-left\")}\n                aria-label=\"Progress\"\n            >\n                {msToHuman(position)}\n            </Span>\n            <SeekBar\n                initialValue={position}\n                minValue={0}\n                maxValue={duration}\n                onValueChange={onChange}\n                asValueChanges={onChange}\n                onValueRender={msToHuman}\n            />\n            <Span\n                size=\"xs\"\n                weight=\"medium\"\n                className={cl(\"progress-time\") + \" \" + cl(\"time-right\")}\n                aria-label=\"Total Duration\"\n            >\n                {msToHuman(duration)}\n            </Span>\n        </div>\n    );\n}\n\n\nfunction AlbumContextMenu({ track }: { track: Track; }) {\n    const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume);\n\n    return (\n        <Menu.Menu\n            navId=\"spotify-album-menu\"\n            onClose={() => FluxDispatcher.dispatch({ type: \"CONTEXT_MENU_CLOSE\" })}\n            aria-label=\"Spotify Album Menu\"\n        >\n            <Menu.MenuItem\n                key=\"open-album\"\n                id=\"open-album\"\n                label=\"Open Album\"\n                action={() => SpotifyStore.openExternal(`/album/${track.album.id}`)}\n                icon={OpenExternalIcon}\n            />\n            <Menu.MenuItem\n                key=\"view-cover\"\n                id=\"view-cover\"\n                label=\"View Album Cover\"\n                // trolley\n                action={() => openImageModal(track.album.image)}\n                icon={ImageIcon}\n            />\n            <Menu.MenuControlItem\n                id=\"spotify-volume\"\n                key=\"spotify-volume\"\n                label=\"Volume\"\n                control={(props, ref) => (\n                    <Menu.MenuSliderControl\n                        {...props}\n                        ref={ref}\n                        value={volume}\n                        minValue={0}\n                        maxValue={100}\n                        onChange={debounce((v: number) => SpotifyStore.setVolume(v))}\n                    />\n                )}\n            />\n        </Menu.Menu>\n    );\n}\n\nfunction makeLinkProps(type: \"Song\" | \"Artist\" | \"Album\", condition: unknown, name: string, path: string) {\n    if (!condition) return {};\n\n    return {\n        role: \"link\",\n        onClick: () => SpotifyStore.openExternal(path),\n        onContextMenu: e =>\n            ContextMenuApi.openContextMenu(e, () => <CopyContextMenu type={type} name={name} path={path} />)\n    } satisfies React.HTMLAttributes<HTMLElement>;\n}\n\nfunction Info({ track }: { track: Track; }) {\n    const img = track?.album?.image;\n\n    const [coverExpanded, setCoverExpanded] = useState(false);\n\n    const i = (\n        <>\n            {img && (\n                <img\n                    id={cl(\"album-image\")}\n                    src={img.url}\n                    alt=\"Album Image\"\n                    onClick={() => setCoverExpanded(!coverExpanded)}\n                    onContextMenu={e => {\n                        ContextMenuApi.openContextMenu(e, () => <AlbumContextMenu track={track} />);\n                    }}\n                />\n            )}\n        </>\n    );\n\n    if (coverExpanded && img)\n        return (\n            <div id={cl(\"album-expanded-wrapper\")}>\n                {i}\n            </div>\n        );\n\n    return (\n        <div id={cl(\"info-wrapper\")}>\n            {i}\n            <div id={cl(\"titles\")}>\n                <Paragraph\n                    weight=\"semibold\"\n                    id={cl(\"song-title\")}\n                    className={cl(\"ellipoverflow\")}\n                    title={track.name}\n                    {...makeLinkProps(\"Song\", track.id, track.name, `/track/${track.id}`)}\n                >\n                    {track.name}\n                </Paragraph>\n                {track.artists.some(a => a.name) && (\n                    <Paragraph className={cl([\"ellipoverflow\", \"secondary-song-info\"])}>\n                        <span className={cl(\"song-info-prefix\")}>by&nbsp;</span>\n                        {track.artists.map((a, i) => (\n                            <React.Fragment key={a.name}>\n                                <span\n                                    className={cl(\"artist\")}\n                                    style={{ fontSize: \"inherit\" }}\n                                    title={a.name}\n                                    {...makeLinkProps(\"Artist\", a.id, a.name, `/artist/${a.id}`)}\n                                >\n                                    {a.name}\n                                </span>\n                                {i !== track.artists.length - 1 && <span className={cl(\"comma\")}>{\", \"}</span>}\n                            </React.Fragment>\n                        ))}\n                    </Paragraph>\n                )}\n                {track.album.name && (\n                    <Paragraph className={cl([\"ellipoverflow\", \"secondary-song-info\"])}>\n                        <span className={cl(\"song-info-prefix\")}>on&nbsp;</span>\n                        <span\n                            id={cl(\"album-title\")}\n                            className={cl(\"album\")}\n                            style={{ fontSize: \"inherit\" }}\n                            title={track.album.name}\n                            {...makeLinkProps(\"Album\", track.album.id, track.album.name, `/album/${track.album.id}`)}\n                        >\n                            {track.album.name}\n                        </span>\n                    </Paragraph>\n                )}\n            </div>\n        </div>\n    );\n}\n\nexport function Player() {\n    const track = useStateFromStores(\n        [SpotifyStore],\n        () => SpotifyStore.track,\n        null,\n        (prev, next) => prev?.id ? (prev.id === next?.id) : prev?.name === next?.name\n    );\n\n    const device = useStateFromStores(\n        [SpotifyStore],\n        () => SpotifyStore.device,\n        null,\n        (prev, next) => prev?.id === next?.id\n    );\n\n    const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying);\n    const [shouldHide, setShouldHide] = useState(false);\n\n    // Hide player after 5 minutes of inactivity\n\n    React.useEffect(() => {\n        setShouldHide(false);\n        if (!isPlaying) {\n            const timeout = setTimeout(() => setShouldHide(true), 1000 * 60 * 5);\n            return () => clearTimeout(timeout);\n        }\n    }, [isPlaying]);\n\n    if (!track || !device?.is_active || shouldHide)\n        return null;\n\n    const exportTrackImageStyle = {\n        \"--vc-spotify-track-image\": `url(${track?.album?.image?.url || \"\"})`,\n    } as React.CSSProperties;\n\n    return (\n        <div id={cl(\"player\")} style={exportTrackImageStyle}>\n            <Info track={track} />\n            <SpotifySeekBar />\n            <Controls />\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/plugins/spotifyControls/SeekBar.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { LazyComponent } from \"@utils/lazyReact\";\nimport { Slider } from \"@webpack/common\";\n\nexport const SeekBar = LazyComponent(() => {\n    const SliderClass = Slider.$$vencordGetWrappedComponent();\n\n    // Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish.\n    // We extend their class and override their `getDerivedStateFromProps` to update the value\n    return class SeekBar extends SliderClass {\n        static getDerivedStateFromProps(props: any, state: any) {\n            const newState = super.getDerivedStateFromProps!(props, state);\n            if (newState) {\n                newState.value = props.initialValue;\n            }\n\n            return newState;\n        }\n    };\n});\n"
  },
  {
    "path": "src/plugins/spotifyControls/SpotifyStore.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport { Settings } from \"@api/Settings\";\nimport OpenInAppPlugin from \"@plugins/openInApp\";\nimport { findByProps, findByPropsLazy, proxyLazyWebpack } from \"@webpack\";\nimport { Flux, FluxDispatcher } from \"@webpack/common\";\n\nexport interface Track {\n    id: string;\n    name: string;\n    duration: number;\n    isLocal: boolean;\n    album: {\n        id: string;\n        name: string;\n        image: {\n            height: number;\n            width: number;\n            url: string;\n        };\n    };\n    artists: {\n        id: string;\n        href: string;\n        name: string;\n        type: string;\n        uri: string;\n    }[];\n}\n\ninterface PlayerState {\n    accountId: string;\n    track: Track | null;\n    volumePercent: number,\n    isPlaying: boolean,\n    repeat: boolean,\n    position: number,\n    context?: any;\n    device?: Device;\n\n    // added by patch\n    actual_repeat: Repeat;\n    shuffle: boolean;\n}\n\ninterface Device {\n    id: string;\n    is_active: boolean;\n}\n\ntype Repeat = \"off\" | \"track\" | \"context\";\n\n// Don't wanna run before Flux and Dispatcher are ready!\nexport const SpotifyStore = proxyLazyWebpack(() => {\n    // For some reason ts hates extends Flux.Store\n    const { Store } = Flux;\n\n    const SpotifySocket = findByProps(\"getActiveSocketAndDevice\");\n    const SpotifyAPI = findByPropsLazy(\"vcSpotifyMarker\");\n\n    const API_BASE = \"https://api.spotify.com/v1/me/player\";\n\n    class SpotifyStore extends Store {\n        public mPosition = 0;\n        public _start = 0;\n\n        public track: Track | null = null;\n        public device: Device | null = null;\n        public isPlaying = false;\n        public repeat: Repeat = \"off\";\n        public shuffle = false;\n        public volume = 0;\n\n        public isSettingPosition = false;\n\n        public openExternal(path: string) {\n            const url = Settings.plugins.SpotifyControls.useSpotifyUris || isPluginEnabled(OpenInAppPlugin.name)\n                ? \"spotify:\" + path.replaceAll(\"/\", (_, idx) => idx === 0 ? \"\" : \":\")\n                : \"https://open.spotify.com\" + path;\n\n            VencordNative.native.openExternal(url);\n        }\n\n        // Need to keep track of this manually\n        public get position(): number {\n            let pos = this.mPosition;\n            if (this.isPlaying) {\n                pos += Date.now() - this._start;\n            }\n            return pos;\n        }\n\n        public set position(p: number) {\n            this.mPosition = p;\n            this._start = Date.now();\n        }\n\n        prev() {\n            this._req(\"post\", \"/previous\");\n        }\n\n        next() {\n            this._req(\"post\", \"/next\");\n        }\n\n        setVolume(percent: number) {\n            this._req(\"put\", \"/volume\", {\n                query: {\n                    volume_percent: Math.round(percent)\n                }\n\n            }).then(() => {\n                this.volume = percent;\n                this.emitChange();\n            });\n        }\n\n        setPlaying(playing: boolean) {\n            this._req(\"put\", playing ? \"/play\" : \"/pause\");\n        }\n\n        setRepeat(state: Repeat) {\n            this._req(\"put\", \"/repeat\", {\n                query: { state }\n            });\n        }\n\n        setShuffle(state: boolean) {\n            this._req(\"put\", \"/shuffle\", {\n                query: { state }\n            }).then(() => {\n                this.shuffle = state;\n                this.emitChange();\n            });\n        }\n\n        seek(ms: number) {\n            if (this.isSettingPosition) return Promise.resolve();\n\n            this.isSettingPosition = true;\n\n            return this._req(\"put\", \"/seek\", {\n                query: {\n                    position_ms: Math.round(ms)\n                }\n            }).catch((e: any) => {\n                console.error(\"[VencordSpotifyControls] Failed to seek\", e);\n                this.isSettingPosition = false;\n            });\n        }\n\n        _req(method: \"post\" | \"get\" | \"put\", route: string, data: any = {}) {\n            if (this.device?.is_active)\n                (data.query ??= {}).device_id = this.device.id;\n\n            const { socket } = SpotifySocket.getActiveSocketAndDevice();\n            return SpotifyAPI[method](socket.accountId, socket.accessToken, {\n                url: API_BASE + route,\n                ...data\n            });\n        }\n    }\n\n    const store = new SpotifyStore(FluxDispatcher, {\n        SPOTIFY_PLAYER_STATE(e: PlayerState) {\n            store.track = e.track;\n            store.device = e.device ?? null;\n            store.isPlaying = e.isPlaying ?? false;\n            store.volume = e.volumePercent ?? 0;\n            store.repeat = e.actual_repeat || \"off\";\n            store.shuffle = e.shuffle ?? false;\n            store.position = e.position ?? 0;\n            store.isSettingPosition = false;\n            store.emitChange();\n        },\n        SPOTIFY_SET_DEVICES({ devices }: { devices: Device[]; }) {\n            store.device = devices.find(d => d.is_active) ?? devices[0] ?? null;\n            store.emitChange();\n        }\n    });\n\n    return store;\n});\n"
  },
  {
    "path": "src/plugins/spotifyControls/hoverOnly.css",
    "content": ".vc-spotify-button-row {\n    height: 0;\n    opacity: 0;\n    pointer-events: none;\n    transition: 0.2s;\n    transition-property: height;\n}\n\n#vc-spotify-player:hover .vc-spotify-button-row {\n    opacity: 1;\n    height: 32px;\n    pointer-events: auto;\n\n    /* only transition opacity on show to prevent clipping */\n    transition-property: height, opacity;\n}\n"
  },
  {
    "path": "src/plugins/spotifyControls/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Settings } from \"@api/Settings\";\nimport { disableStyle, enableStyle } from \"@api/Styles\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nimport hoverOnlyStyle from \"./hoverOnly.css?managed\";\nimport { Player } from \"./PlayerComponent\";\n\nfunction toggleHoverControls(value: boolean) {\n    (value ? enableStyle : disableStyle)(hoverOnlyStyle);\n}\n\nexport default definePlugin({\n    name: \"SpotifyControls\",\n    description: \"Adds a Spotify player above the account panel\",\n    authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev],\n    options: {\n        hoverControls: {\n            description: \"Show controls on hover\",\n            type: OptionType.BOOLEAN,\n            default: false,\n            onChange: v => toggleHoverControls(v)\n        },\n        useSpotifyUris: {\n            type: OptionType.BOOLEAN,\n            description: \"Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms\",\n            default: false\n        },\n        previousButtonRestartsTrack: {\n            type: OptionType.BOOLEAN,\n            description: \"Restart currently playing track when pressing the previous button if playtime is >3s\",\n            default: true\n        }\n    },\n    patches: [\n        {\n            find: \".DISPLAY_NAME_STYLES_COACHMARK)\",\n            replacement: {\n                // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })\n                match: /(?<=\\i\\.jsxs?\\)\\()(\\i),{(?=[^}]*?userTag:\\i,occluded:)/,\n                // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...\n                replace: \"$self.PanelWrapper,{VencordOriginal:$1,\"\n            }\n        },\n        {\n            find: \".PLAYER_DEVICES\",\n            replacement: [{\n                // Adds POST and a Marker to the SpotifyAPI (so we can easily find it)\n                match: /get:(\\i)\\.bind\\(null,(\\i\\.\\i)\\.get\\)/,\n                replace: \"post:$1.bind(null,$2.post),vcSpotifyMarker:1,$&\"\n            },\n            {\n                // Spotify Connect API returns status 202 instead of 204 when skipping tracks.\n                // Discord rejects 202 which causes the request to send twice. This patch prevents this.\n                match: /202===\\i\\.status/,\n                replace: \"false\",\n            }]\n        },\n        {\n            find: 'repeat:\"off\"!==',\n            replacement: [\n                {\n                    // Discord doesn't give you shuffle state and the repeat kind, only a boolean\n                    match: /repeat:\"off\"!==(\\i),/,\n                    replace: \"shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&\"\n                },\n                {\n                    match: /(?<=artists.filter\\(\\i=>).{0,10}\\i\\.id\\)&&/,\n                    replace: \"\"\n                }\n            ]\n        },\n    ],\n\n    start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),\n\n    PanelWrapper({ VencordOriginal, ...props }) {\n        return (\n            <>\n                <ErrorBoundary\n                    fallback={() => (\n                        <div className=\"vc-spotify-fallback\">\n                            <p>Failed to render Spotify Modal :(</p>\n                            <p >Check the console for errors</p>\n                        </div>\n                    )}\n                >\n                    <Player />\n                </ErrorBoundary>\n\n                <VencordOriginal {...props} />\n            </>\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/spotifyControls/spotifyStyles.css",
    "content": "#vc-spotify-player {\n    padding: 12px;\n    background: var(--background-base-low);\n    margin: 0;\n    border-top-left-radius: 10px;\n    border-top-right-radius: 10px;\n    border-bottom: 1px solid var(--border-subtle);\n\n    /* so custom themes can easily change it */\n    --vc-spotify-green: var(--spotify, #1db954);\n    --vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%);\n    --vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%);\n}\n\n.vc-spotify-button {\n    margin: 0 2px;\n    border-radius: var(--radius-sm);\n    background: none;\n    color: var(--interactive-icon-default);\n    padding: 0;\n    width: 32px;\n    height: 32px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n}\n\n.vc-spotify-button:hover {\n    color: var(--interactive-icon-hover);\n    background-color: var(--background-mod-strong);\n}\n\n.vc-spotify-button-icon {\n    height: 24px;\n    width: 24px;\n}\n\n.vc-spotify-shuffle .vc-spotify-button-icon,\n.vc-spotify-repeat .vc-spotify-button-icon {\n    width: 22px;\n    height: 22px;\n}\n\n/* .vc-spotify-button:hover {\n    filter: brightness(1.3);\n} */\n\n.vc-spotify-repeat-context,\n.vc-spotify-repeat-track,\n.vc-spotify-shuffle-on {\n    background-color: var(--vc-spotify-green-90);\n}\n\n.vc-spotify-repeat-context:hover,\n.vc-spotify-repeat-track:hover,\n.vc-spotify-shuffle-on:hover {\n    background-color: var(--vc-spotify-green-80);\n}\n\n.vc-spotify-tooltip-text {\n    overflow: hidden;\n    white-space: nowrap;\n    padding-right: 0.2em;\n    max-width: 100%;\n    margin: unset;\n}\n\n.vc-spotify-repeat-1 {\n    font-size: 70%;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n}\n\n.vc-spotify-button-row {\n    justify-content: center;\n    margin-top: 14px;\n}\n\n.vc-spotify-secondary-song-info {\n    font-size: 12px;\n}\n\n.vc-spotify-song-info-prefix {\n    display: none;\n}\n\n#vc-spotify-info-wrapper {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    height: 3em;\n    gap: 0.5em;\n}\n\n#vc-spotify-album-image {\n    height: 90%;\n    object-fit: contain;\n    border-radius: 3px;\n    transition: filter 0.2s;\n}\n\n#vc-spotify-album-image:hover {\n    filter: brightness(1.2);\n    cursor: pointer;\n}\n\n#vc-spotify-album-expanded-wrapper #vc-spotify-album-image {\n    width: 100%;\n    object-fit: contain;\n}\n\n#vc-spotify-titles {\n    display: flex;\n    flex-direction: column;\n    padding: 0.2rem;\n    align-items: flex-start;\n    place-content: flex-start center;\n    overflow: hidden;\n}\n\n#vc-spotify-song-title {\n    color: var(--text-strong);\n    font-size: 14px;\n    font-weight: 600;\n}\n\n.vc-spotify-ellipoverflow {\n    white-space: nowrap;\n    overflow: hidden;\n    width: 100%;\n    text-overflow: ellipsis;\n}\n\n.vc-spotify-artist,\n.vc-spotify-album {\n    font-size: 12px;\n    text-decoration: none;\n    color: var(--text-strong);\n}\n\n.vc-spotify-comma {\n    color: var(--text-default);\n}\n\n.vc-spotify-artist[role=\"link\"]:hover,\n#vc-spotify-album-title[role=\"link\"]:hover,\n#vc-spotify-song-title[role=\"link\"]:hover {\n    text-decoration: underline;\n    cursor: pointer;\n}\n\n#vc-spotify-progress-bar {\n    position: relative;\n    color: var(--text-default);\n    width: 100%;\n    margin: 0.5em 0;\n    margin-bottom: 5px;\n}\n\n#vc-spotify-progress-bar > [class*=\"slider\"] {\n    flex-grow: 1;\n    width: 100%;\n    padding: 0 !important;\n}\n\n#vc-spotify-progress-bar > [class*=\"slider\"] [class*=\"bar\"] {\n    height: 3px !important;\n    top: calc(12px - 4px / 2 + var(--bar-offset));\n}\n\n#vc-spotify-progress-bar > [class*=\"slider\"] [class*=\"barFill\"] {\n    background-color: var(--interactive-icon-active);\n}\n\n#vc-spotify-progress-bar > [class*=\"slider\"]:hover [class*=\"barFill\"] {\n    background-color: var(--vc-spotify-green);\n}\n\n#vc-spotify-progress-bar > [class*=\"slider\"] [class*=\"grabber\"] {\n    /* these importants are necessary, it applies a width and height through inline styles */\n    height: 16px !important;\n    width: 16px !important;\n    margin-top: calc(17px/-2 + var(--bar-offset)/2);\n    margin-left: -0.5px;\n    background-color: var(--interactive-icon-active);\n    border-color: var(--interactive-icon-default);\n    color: var(--interactive-icon-default);\n    opacity: 0;\n    transition: opacity 0.1s;\n}\n\n#vc-spotify-progress-bar:hover > [class*=\"slider\"] [class*=\"grabber\"] {\n    opacity: 1;\n}\n\n#vc-spotify-progress-text {\n    margin: 0;\n}\n\n.vc-spotify-progress-time {\n    font-size: 12px;\n    top: 10px;\n    position: absolute;\n    margin-top: 8px;\n    font-family: var(--font-code);\n}\n\n.vc-spotify-time-left {\n    left: 0;\n}\n\n.vc-spotify-time-right {\n    right: 0;\n}\n\n.vc-spotify-fallback {\n    padding: 0.5em;\n    color: var(--text-default);\n}"
  },
  {
    "path": "src/plugins/spotifyCrack/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    noSpotifyAutoPause: {\n        description: \"Disable Spotify auto-pause\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    },\n    keepSpotifyActivityOnIdle: {\n        description: \"Keep Spotify activity playing when idling\",\n        type: OptionType.BOOLEAN,\n        default: false,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"SpotifyCrack\",\n    description: \"Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling\",\n    authors: [Devs.Cyn, Devs.Nuckyz],\n    settings,\n\n    patches: [\n        {\n\n            find: 'dispatch({type:\"SPOTIFY_PROFILE_UPDATE\"',\n            replacement: {\n                match: /SPOTIFY_PROFILE_UPDATE.+?isPremium:(?=\"premium\"===(\\i)\\.body\\.product)/,\n                replace: (m, req) => `${m}(${req}.body.product=\"premium\")&&`\n            },\n        },\n        {\n            find: \"}getPlayableComputerDevices(){\",\n            replacement: [\n                {\n                    predicate: () => settings.store.noSpotifyAutoPause,\n                    match: /(?<=function \\i\\(\\){)(?=.{0,200}SPOTIFY_AUTO_PAUSED\\))/,\n                    replace: \"return;\"\n                },\n                {\n                    predicate: () => settings.store.keepSpotifyActivityOnIdle,\n                    match: /(shouldShowActivity\\(\\){.{0,50})&&!\\i\\.\\i\\.isIdle\\(\\)/,\n                    replace: \"$1\"\n                }\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/spotifyShareCommands/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ApplicationCommandInputType, findOption, OptionalMessageOption, sendBotMessage } from \"@api/Commands\";\nimport { Devs } from \"@utils/constants\";\nimport { sendMessage } from \"@utils/discord\";\nimport definePlugin from \"@utils/types\";\nimport { Command } from \"@vencord/discord-types\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { FluxDispatcher, MessageActions, PendingReplyStore } from \"@webpack/common\";\n\ninterface Album {\n    id: string;\n    image: {\n        height: number;\n        width: number;\n        url: string;\n    };\n    name: string;\n}\n\ninterface Artist {\n    external_urls: {\n        spotify: string;\n    };\n    href: string;\n    id: string;\n    name: string;\n    type: \"artist\" | string;\n    uri: string;\n}\n\ninterface Track {\n    id: string | null;\n    album: Album;\n    artists: Artist[];\n    duration: number;\n    isLocal: boolean;\n    name: string;\n}\n\nconst Spotify = findByPropsLazy(\"getPlayerState\");\n\nfunction makeCommand(name: string, formatUrl: (track: Track) => string): Command {\n    return {\n        name,\n        description: `Share your current Spotify ${name} in chat`,\n        inputType: ApplicationCommandInputType.BUILT_IN,\n        options: [OptionalMessageOption],\n        execute(options, { channel }) {\n            const track: Track | null = Spotify.getTrack();\n            if (!track) {\n                return sendBotMessage(channel.id, {\n                    content: \"You're not listening to any music.\"\n                });\n            }\n\n            // local tracks have an id of null\n            if (track.id == null) {\n                return sendBotMessage(channel.id, {\n                    content: \"Failed to find the track on spotify.\"\n                });\n            }\n\n            const data = formatUrl(track);\n            const message = findOption(options, \"message\");\n\n            // Note: Due to how Discord handles commands, we need to manually create and send the message\n\n            sendMessage(\n                channel.id,\n                { content: message ? `${message} ${data}` : data },\n                false,\n                MessageActions.getSendMessageOptionsForReply(PendingReplyStore.getPendingReply(channel.id))\n            ).then(() => {\n                FluxDispatcher.dispatch({ type: \"DELETE_PENDING_REPLY\", channelId: channel.id });\n            });\n\n        }\n    };\n}\n\nexport default definePlugin({\n    name: \"SpotifyShareCommands\",\n    description: \"Share your current Spotify track, album or artist via slash command (/track, /album, /artist)\",\n    authors: [Devs.katlyn],\n    commands: [\n        makeCommand(\"track\", track => `https://open.spotify.com/track/${track.id}`),\n        makeCommand(\"album\", track => `https://open.spotify.com/album/${track.album.id}`),\n        makeCommand(\"artist\", track => track.artists[0].external_urls.spotify)\n    ]\n});\n"
  },
  {
    "path": "src/plugins/startupTimings/StartupTimingPage.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { Forms, React } from \"@webpack/common\";\n\ninterface AppStartPerformance {\n    prefix: string;\n    logs: Log[];\n    logGroups: LogGroup[];\n    endTime_: number;\n    isTracing_: boolean;\n}\n\ninterface LogGroup {\n    index: number;\n    timestamp: number;\n    logs: Log[];\n    nativeLogs: any[];\n    serverTrace: string;\n}\n\ninterface Log {\n    emoji: string;\n    prefix: string;\n    log: string;\n    timestamp?: number;\n    delta?: number;\n}\n\nconst AppStartPerformance = findByPropsLazy(\"markWithDelta\", \"markAndLog\", \"markAt\") as AppStartPerformance;\n\ninterface TimerItemProps extends Log {\n    instance: {\n        sinceStart: number;\n        sinceLast: number;\n    };\n}\n\nfunction TimerItem({ emoji, prefix, log, delta, instance }: TimerItemProps) {\n    return (\n        <React.Fragment>\n            <span>{instance.sinceStart.toFixed(3)}s</span>\n            <span>{instance.sinceLast.toFixed(3)}s</span>\n            <span>{delta?.toFixed(0) ?? \"\"}</span>\n            <span><pre>{emoji} {prefix ?? \" \"}{log}</pre></span>\n        </React.Fragment>\n    );\n}\n\ninterface TimingSectionProps {\n    title: string;\n    logs: Log[];\n    traceEnd?: number;\n}\n\nfunction TimingSection({ title, logs, traceEnd }: TimingSectionProps) {\n    const startTime = logs.find(l => l.timestamp)?.timestamp ?? 0;\n\n    let lastTimestamp = startTime;\n    const timings = logs.map(log => {\n        // Get last log entry with valid timestamp\n        const timestamp = log.timestamp ?? lastTimestamp;\n\n        const sinceStart = (timestamp - startTime) / 1000;\n        const sinceLast = (timestamp - lastTimestamp) / 1000;\n\n        lastTimestamp = timestamp;\n\n        return { sinceStart, sinceLast };\n    });\n\n    return (\n        <section>\n            <Forms.FormTitle tag=\"h2\">{title}</Forms.FormTitle>\n            <code>\n                {traceEnd && (\n                    <div style={{ color: \"var(--text-strong)\", marginBottom: 5, userSelect: \"text\" }}>\n                        Trace ended at: {(new Date(traceEnd)).toTimeString()}\n                    </div>\n                )}\n                <div style={{ color: \"var(--text-strong)\", display: \"grid\", gridTemplateColumns: \"repeat(3, auto) 1fr\", gap: \"2px 10px\", userSelect: \"text\" }}>\n                    <span>Start</span>\n                    <span>Interval</span>\n                    <span>Delta</span>\n                    <span style={{ marginBottom: 5 }}>Event</span>\n                    {AppStartPerformance.logs.map((log, i) => (\n                        <TimerItem key={i} {...log} instance={timings[i]} />\n                    ))}\n                </div>\n            </code>\n        </section>\n    );\n}\n\ninterface ServerTraceProps {\n    trace: string;\n}\n\nfunction ServerTrace({ trace }: ServerTraceProps) {\n    const lines = trace.split(\"\\n\");\n\n    return (\n        <section>\n            <Forms.FormTitle tag=\"h3\">Server Trace</Forms.FormTitle>\n            <code>\n                <Flex flexDirection=\"column\" gap=\"5px\" style={{ color: \"var(--text-strong)\", userSelect: \"text\" }}>\n                    {lines.map((line, idx) => (\n                        <span key={idx}>{line}</span>\n                    ))}\n                </Flex>\n            </code>\n        </section>\n    );\n}\n\nfunction StartupTimingPage() {\n    if (!AppStartPerformance?.logs) return <div>Loading...</div>;\n\n    const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;\n\n    return (\n        <React.Fragment>\n            <TimingSection\n                title=\"Startup Timings\"\n                logs={AppStartPerformance.logs}\n                traceEnd={AppStartPerformance.endTime_}\n            />\n            {/* Lazy Divider */}\n            <div style={{ marginTop: 5 }}>&nbsp;</div>\n            {serverTrace && <ServerTrace trace={serverTrace} />}\n        </React.Fragment>\n    );\n}\n\nexport default ErrorBoundary.wrap(StartupTimingPage);\n"
  },
  {
    "path": "src/plugins/startupTimings/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ClockIcon } from \"@components/Icons\";\nimport SettingsPlugin from \"@plugins/_core/settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nimport StartupTimingPage from \"./StartupTimingPage\";\n\nexport default definePlugin({\n    name: \"StartupTimings\",\n    description: \"Adds Startup Timings to the Settings menu\",\n    authors: [Devs.Megu],\n    start() {\n        SettingsPlugin.customEntries.push({\n            key: \"vencord_startup_timings\",\n            title: \"Startup Timings\",\n            Component: StartupTimingPage,\n            Icon: ClockIcon\n        });\n        SettingsPlugin.settingsSectionMap.push([\"VencordStartupTimings\", \"vencord_startup_timings\"]);\n    },\n    stop() {\n        function removeFromArray<T>(arr: T[], predicate: (e: T) => boolean) {\n            const idx = arr.findIndex(predicate);\n            if (idx !== -1) arr.splice(idx, 1);\n        }\n        removeFromArray(SettingsPlugin.customEntries, e => e.key === \"vencord_startup_timings\");\n        removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === \"vencord_startup_timings\");\n    },\n});\n"
  },
  {
    "path": "src/plugins/stickerPaste/README.md",
    "content": "# StickerPaste\n\nMakes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending.\n"
  },
  {
    "path": "src/plugins/stickerPaste/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"StickerPaste\",\n    description: \"Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending\",\n    authors: [Devs.ImBanana],\n\n    patches: [\n        {\n            find: \".stickers,previewSticker:\",\n            replacement: {\n                match: /if\\(\\i\\.\\i\\.getUploadCount/,\n                replace: \"return true;$&\",\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/streamerModeOnStream/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2024 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { FluxDispatcher, UserStore } from \"@webpack/common\";\n\ninterface StreamEvent {\n    streamKey: string;\n}\n\nfunction toggleStreamerMode({ streamKey }: StreamEvent, value: boolean) {\n    if (!streamKey.endsWith(UserStore.getCurrentUser().id)) return;\n\n    FluxDispatcher.dispatch({\n        type: \"STREAMER_MODE_UPDATE\",\n        key: \"enabled\",\n        value\n    });\n}\n\nexport default definePlugin({\n    name: \"StreamerModeOnStream\",\n    description: \"Automatically enables streamer mode when you start streaming in Discord\",\n    authors: [Devs.IcedMarina],\n    flux: {\n        STREAM_CREATE: d => toggleStreamerMode(d, true),\n        STREAM_DELETE: d => toggleStreamerMode(d, false)\n    }\n});\n"
  },
  {
    "path": "src/plugins/superReactionTweaks/README.md",
    "content": "# Super Reaction Tweaks\n\nThis plugin applies configurable various tweaks to super reactions.\n\n![Screenshot](https://user-images.githubusercontent.com/22851444/281598795-58f07116-9f95-4f64-940b-23a5499f2302.png)\n\n## Features:\n\n**Super React By Default** -  The reaction picker will default to super reactions instead of normal reactions.\n\n**Super Reaction Play Limit** - Allows you to decide how many super reaction animations can play at once, including removing the limit entirely.\n"
  },
  {
    "path": "src/plugins/superReactionTweaks/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated, ant0n, FieryFlames and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { UserStore } from \"@webpack/common\";\n\nexport const settings = definePluginSettings({\n    superReactByDefault: {\n        type: OptionType.BOOLEAN,\n        description: \"Reaction picker will default to Super Reactions\",\n        default: true,\n    },\n    unlimitedSuperReactionPlaying: {\n        type: OptionType.BOOLEAN,\n        description: \"Remove the limit on Super Reactions playing at once\",\n        default: false,\n    },\n\n    superReactionPlayingLimit: {\n        description: \"Max Super Reactions to play at once. 0 to disable playing Super Reactions\",\n        type: OptionType.SLIDER,\n        default: 20,\n        markers: [0, 5, 10, 20, 40, 60, 80, 100],\n        stickToMarkers: true,\n    },\n}, {\n    superReactionPlayingLimit: {\n        disabled() { return this.store.unlimitedSuperReactionPlaying; },\n    }\n});\n\nexport default definePlugin({\n    name: \"SuperReactionTweaks\",\n    description: \"Customize the limit of Super Reactions playing at once, and super react by default\",\n    authors: [Devs.FieryFlames, Devs.ant0n],\n    patches: [\n        {\n            find: \",BURST_REACTION_EFFECT_PLAY\",\n            replacement: [\n                {\n                    // if (inlinedCalculatePlayingCount(a,b) >= limit) return;\n                    match: /(BURST_REACTION_EFFECT_PLAY:\\i=>{.+?if\\()(\\(\\(\\i,\\i\\)=>.+?\\(\\i,\\i\\))>=5+?(?=\\))/,\n                    replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})`\n                },\n                // FIXME(Bundler agressive inline): Remove the non used compability once enough time has passed\n                {\n                    /*\n                     * var limit = 5\n                     * ...\n                     * if (calculatePlayingCount(a,b) >= limit) return;\n                     */\n                    match: /((\\i)=5.+?)if\\((.{0,20}?)>=\\2\\)return;/,\n                    replace: (_, rest, playingCount) => `${rest}if(!$self.shouldPlayBurstReaction(${playingCount}))return;`,\n                    noWarn: true\n                }\n            ]\n        },\n        {\n            find: \".EMOJI_PICKER_CONSTANTS_EMOJI_CONTAINER_PADDING_HORIZONTAL)\",\n            replacement: {\n                match: /(openPopoutType:void 0(?=.+?isBurstReaction:(\\i).+?(\\i===\\i\\.\\i.REACTION)).+?\\[\\2,\\i\\]=\\i\\.useState\\().+?\\)/,\n                replace: (_, rest, _isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})`\n            }\n        }\n    ],\n    settings,\n\n    shouldPlayBurstReaction(playingCount: number) {\n        if (settings.store.unlimitedSuperReactionPlaying) return true;\n        if (settings.store.superReactionPlayingLimit > playingCount) return true;\n        return false;\n    },\n\n    get shouldSuperReactByDefault() {\n        return settings.store.superReactByDefault && UserStore.getCurrentUser().premiumType != null;\n    }\n});\n"
  },
  {
    "path": "src/plugins/textReplace/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Button } from \"@components/Button\";\nimport { ExpandableSection } from \"@components/ExpandableCard\";\nimport { Flex } from \"@components/Flex\";\nimport { HeadingSecondary } from \"@components/Heading\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Span } from \"@components/Span\";\nimport { TooltipContainer } from \"@components/TooltipContainer\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { React, TextInput, useState } from \"@webpack/common\";\n\nconst cl = classNameFactory(\"vc-textReplace-\");\n\ntype Rule = Record<\"find\" | \"replace\" | \"onlyIfIncludes\" | \"id\", string>;\n\ninterface TextReplaceProps {\n    title: string;\n    description: string;\n    rulesArray: Rule[];\n    isRegex?: boolean;\n}\n\nconst makeEmptyRule: () => Rule = () => ({\n    find: \"\",\n    replace: \"\",\n    onlyIfIncludes: \"\",\n    id: crypto.randomUUID()\n});\nconst makeEmptyRuleArray = () => [makeEmptyRule()];\n\nconst settings = definePluginSettings({\n    replace: {\n        type: OptionType.COMPONENT,\n        component: () => {\n            const { stringRules, regexRules } = settings.use([\"stringRules\", \"regexRules\"]);\n\n            return (\n                <>\n                    <TextReplaceTesting />\n                    <TextReplace\n                        title=\"Simple Replacements\"\n                        description=\"Simple find and replace rules. For example, find 'brb' and replace it with 'be right back'\"\n                        rulesArray={stringRules}\n                    />\n                    <TextReplace\n                        title=\"Regex Replacements\"\n                        description=\"More powerful replacements using Regular Expressions. This section is for advanced users. If you don't understand it, just ignore it\"\n                        rulesArray={regexRules}\n                        isRegex\n                    />\n                </>\n            );\n        }\n    },\n    stringRules: {\n        type: OptionType.CUSTOM,\n        default: makeEmptyRuleArray(),\n    },\n    regexRules: {\n        type: OptionType.CUSTOM,\n        default: makeEmptyRuleArray(),\n    }\n});\n\nfunction stringToRegex(str: string) {\n    const match = str.match(/^(\\/)?(.+?)(?:\\/([gimsuyv]*))?$/); // Regex to match regex\n    return match\n        ? new RegExp(\n            match[2], // Pattern\n            match[3]\n                ?.split(\"\") // Remove duplicate flags\n                .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos)\n                .join(\"\")\n            ?? \"g\"\n        )\n        : new RegExp(str); // Not a regex, return string\n}\n\nfunction renderFindError(find: string) {\n    try {\n        stringToRegex(find);\n        return null;\n    } catch (e) {\n        return (\n            <span style={{ color: \"var(--text-feedback-critical)\" }}>\n                {String(e)}\n            </span>\n        );\n    }\n}\n\nfunction Input({ initialValue, onChange, placeholder }: {\n    placeholder: string;\n    initialValue: string;\n    onChange(value: string): void;\n}) {\n    const [value, setValue] = useState(initialValue);\n    return (\n        <TextInput\n            placeholder={placeholder}\n            value={value}\n            onChange={setValue}\n            spellCheck={false}\n            onBlur={() => value !== initialValue && setTimeout(() => onChange(value), 0)}\n        />\n    );\n}\n\nfunction TextRow({ label, description, value, onChange }: { label: string; description: string; value: string; onChange(value: string): void; }) {\n    return (\n        <>\n            <TooltipContainer text={description}>\n                <Span weight=\"medium\" size=\"md\">{label}</Span>\n            </TooltipContainer>\n            <Input\n                placeholder={description}\n                initialValue={value}\n                onChange={onChange}\n            />\n        </>\n    );\n}\n\nconst isEmptyRule = (rule: Rule) => !rule.find;\n\nfunction TextReplace({ title, description, rulesArray, isRegex = false }: TextReplaceProps) {\n    function onClickRemove(index: number) {\n        rulesArray.splice(index, 1);\n    }\n\n    function onChange(e: string, index: number, key: string) {\n        rulesArray[index][key] = e;\n\n        // If a rule is empty after editing and is not the last rule, remove it\n        if (rulesArray[index].find === \"\" && rulesArray[index].replace === \"\" && rulesArray[index].onlyIfIncludes === \"\" && index !== rulesArray.length - 1) {\n            rulesArray.splice(index, 1);\n        }\n    }\n\n    return (\n        <>\n            <div>\n                <HeadingSecondary>{title}</HeadingSecondary>\n                <Paragraph>{description}</Paragraph>\n            </div>\n            <Flex flexDirection=\"column\" style={{ gap: \"0.5em\" }}>\n                {rulesArray.map((rule, index) =>\n                    <ExpandableSection\n                        key={rule.id}\n                        renderContent={() => (\n                            <>\n                                <div className={cl(\"input-grid\")}>\n                                    <TextRow\n                                        label=\"Find\"\n                                        description={isRegex ? \"The regex pattern\" : \"The text to replace\"}\n                                        value={rule.find}\n                                        onChange={e => onChange(e, index, \"find\")}\n                                    />\n                                    <TextRow\n                                        label=\"Replace\"\n                                        description=\"The text to replace the found text with\"\n                                        value={rule.replace}\n                                        onChange={e => onChange(e, index, \"replace\")}\n                                    />\n                                    <TextRow\n                                        label=\"Only if includes\"\n                                        description=\"This rule will only be applied if the message includes this text. This is optional\"\n                                        value={rule.onlyIfIncludes}\n                                        onChange={e => onChange(e, index, \"onlyIfIncludes\")}\n                                    />\n                                </div>\n                                {isRegex && renderFindError(rule.find)}\n                                <Button\n                                    className={cl(\"delete-button\")}\n                                    variant=\"dangerPrimary\"\n                                    onClick={() => onClickRemove(index)}\n                                >\n                                    Delete Rule\n                                </Button>\n                            </>\n                        )}\n                    >\n                        <Paragraph weight=\"medium\" size=\"md\">\n                            {isEmptyRule(rule)\n                                ? `Empty Rule ${index + 1}`\n                                : `Rule ${index + 1} - ${rule.find}`\n                            }\n                        </Paragraph>\n                    </ExpandableSection>\n                )}\n                <Button\n                    onClick={() => rulesArray.push(makeEmptyRule())}\n                    disabled={rulesArray.length > 0 && isEmptyRule(rulesArray[rulesArray.length - 1])}\n                >\n                    Add Rule\n                </Button>\n            </Flex>\n        </>\n    );\n}\n\nfunction TextReplaceTesting() {\n    const [value, setValue] = useState(\"\");\n\n    return (\n        <div>\n            <HeadingSecondary>Rule Tester</HeadingSecondary>\n            <Flex flexDirection=\"column\" gap={6}>\n                <TextInput placeholder=\"Type a message to test rules on\" onChange={setValue} />\n                <TextInput placeholder=\"Message with rules applied\" editable={false} value={applyRules(value)} style={{ opacity: 0.7 }} />\n            </Flex>\n        </div>\n    );\n}\n\nfunction applyRules(content: string): string {\n    if (content.length === 0) {\n        return content;\n    }\n\n    for (const rule of settings.store.stringRules) {\n        if (!rule.find) continue;\n        if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;\n\n        content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll(\"\\\\n\", \"\\n\")).replace(/^\\s|\\s$/g, \"\");\n    }\n\n    for (const rule of settings.store.regexRules) {\n        if (!rule.find) continue;\n        if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;\n\n        try {\n            const regex = stringToRegex(rule.find);\n            content = content.replace(regex, rule.replace.replaceAll(\"\\\\n\", \"\\n\"));\n        } catch (e) {\n            new Logger(\"TextReplace\").error(`Invalid regex: ${rule.find}`);\n        }\n    }\n\n    content = content.trim();\n    return content;\n}\n\nconst TEXT_REPLACE_RULES_CHANNEL_ID = \"1102784112584040479\";\n\nexport default definePlugin({\n    name: \"TextReplace\",\n    description: \"Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server\",\n    authors: [Devs.AutumnVN, Devs.TheKodeToad],\n\n    settings,\n\n    start() {\n        settings.store.regexRules.forEach(rule => rule.id ??= crypto.randomUUID());\n        settings.store.stringRules.forEach(rule => rule.id ??= crypto.randomUUID());\n    },\n\n    onBeforeMessageSend(channelId, msg) {\n        // Channel used for sharing rules, applying rules here would be messy\n        if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;\n        msg.content = applyRules(msg.content);\n    }\n});\n"
  },
  {
    "path": "src/plugins/textReplace/styles.css",
    "content": ".vc-textReplace-input-grid {\n    display: grid;\n    grid-template-columns: max-content 1fr;\n    gap: 0.25em 1em;\n    align-items: center;\n}\n\n.vc-textReplace-delete-button {\n    width: 100%;\n    margin-block: 0.75em 0.5em\n}"
  },
  {
    "path": "src/plugins/themeAttributes/README.md",
    "content": "# ThemeAttributes\n\nThis plugin adds data attributes and CSS variables to various elements inside Discord\n\nThis allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible\n\n## Available Attributes\n\n### All Tab Bars (User Settings, Server Settings, etc)\n\n`data-tab-id` contains the id of that tab\n\n![image](https://github.com/Vendicated/Vencord/assets/45497981/1263b782-f673-4f09-820c-4cc366d062ad)\n\n### Chat Messages\n\n- `data-author-id` contains the id of the author\n- `data-author-username` contains the username of the author\n- `data-is-self` is a boolean indicating whether this is the current user's message\n\n![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)\n\n## CSS Variables\n\n### Avatars\n\n`--avatar-url-<resolution>` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`.\n\n![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723)\n"
  },
  {
    "path": "src/plugins/themeAttributes/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { UserStore } from \"@webpack/common\";\n\n\nexport default definePlugin({\n    name: \"ThemeAttributes\",\n    description: \"Adds data attributes to various elements for theming purposes\",\n    authors: [Devs.Ven, Devs.Board],\n\n    patches: [\n        // Add data-tab-id to all tab bar items\n        // This for examples applies to the User and Server settings sidebars\n        {\n            find: \".tabBarRef\",\n            replacement: {\n                match: /style:this\\.getStyle\\(\\),role:\"tab\"/,\n                replace: \"$&,'data-tab-id':this.props.id\"\n            }\n        },\n\n        // Add data-author-id and data-is-self to all messages\n        {\n            find: \"Message must not be a thread starter message\",\n            replacement: {\n                match: /\"aria-setsize\":-1,(?=.{0,150}?#{intl::MESSAGE_A11Y_ROLE_DESCRIPTION})/,\n                replace: \"...$self.getMessageProps(arguments[0]),$&\"\n            }\n        },\n\n        // add --avatar-url-<resolution> css variable to avatar img elements\n        // popout profiles\n        {\n            find: \"#{intl::LABEL_WITH_ONLINE_STATUS}\",\n            replacement: [\n                {\n                    match: /src:(\\i)\\?\\?void 0.{1,50}\"aria-hidden\":!0/,\n                    replace: \"$&,style:$self.getAvatarStyles($1)\"\n                }\n            ]\n        },\n        // chat avatars\n        {\n            find: \"showCommunicationDisabledStyles\",\n            replacement: {\n                match: /src:(\\i),\"aria-hidden\":!0/,\n                replace: \"$&,style:$self.getAvatarStyles($1)\"\n            }\n        }\n    ],\n\n    getAvatarStyles(src: string | null) {\n        if (!src || src.startsWith(\"data:\")) return {};\n\n        return Object.fromEntries(\n            [128, 256, 512, 1024, 2048, 4096].map(size => [\n                `--avatar-url-${size}`,\n                `url(${src.replace(/\\d+$/, String(size))})`\n            ])\n        );\n    },\n\n    getMessageProps(props: { message: Message; }) {\n        try {\n            const author = props.message?.author;\n            const authorId = author?.id;\n            return {\n                \"data-author-id\": authorId,\n                \"data-author-username\": author?.username,\n                \"data-is-self\": authorId && authorId === UserStore.getCurrentUser()?.id,\n            };\n        } catch (e) {\n            new Logger(\"ThemeAttributes\").error(\"Error in getMessageProps\", e);\n            return {};\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/translate/TranslateIcon.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ChatBarButton, ChatBarButtonFactory } from \"@api/ChatButtons\";\nimport { classes } from \"@utils/misc\";\nimport { openModal } from \"@utils/modal\";\nimport { IconComponent } from \"@utils/types\";\nimport { Alerts, Forms, Tooltip, useEffect, useState } from \"@webpack/common\";\n\nimport { settings } from \"./settings\";\nimport { TranslateModal } from \"./TranslateModal\";\nimport { cl } from \"./utils\";\n\nexport const TranslateIcon: IconComponent = ({ height = 20, width = 20, className }) => {\n    return (\n        <svg\n            viewBox=\"0 96 960 960\"\n            height={height}\n            width={width}\n            className={classes(cl(\"icon\"), className)}\n        >\n            <path fill=\"currentColor\" d=\"m475 976 181-480h82l186 480h-87l-41-126H604l-47 126h-82Zm151-196h142l-70-194h-2l-70 194Zm-466 76-55-55 204-204q-38-44-67.5-88.5T190 416h87q17 33 37.5 62.5T361 539q45-47 75-97.5T487 336H40v-80h280v-80h80v80h280v80H567q-22 69-58.5 135.5T419 598l98 99-30 81-127-122-200 200Z\" />\n        </svg>\n    );\n};\n\nexport let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);\n\nexport const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {\n    const { autoTranslate } = settings.use([\"autoTranslate\"]);\n\n    const [shouldShowTranslateEnabledTooltip, setter] = useState(false);\n    useEffect(() => {\n        setShouldShowTranslateEnabledTooltip = setter;\n        return () => setShouldShowTranslateEnabledTooltip = undefined;\n    }, []);\n\n    if (!isMainChat) return null;\n\n    const toggle = () => {\n        const newState = !autoTranslate;\n        settings.store.autoTranslate = newState;\n        if (newState && settings.store.showAutoTranslateAlert !== false)\n            Alerts.show({\n                title: \"Vencord Auto-Translate Enabled\",\n                body: <>\n                    <Forms.FormText>\n                        You just enabled Auto Translate! Any message <b>will automatically be translated</b> before being sent.\n                    </Forms.FormText>\n                </>,\n                confirmText: \"Disable Auto-Translate\",\n                cancelText: \"Got it\",\n                secondaryConfirmText: \"Don't show again\",\n                onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,\n                onConfirm: () => settings.store.autoTranslate = false,\n                // troll\n                confirmColor: \"vc-notification-log-danger-btn\",\n            });\n    };\n\n    const button = (\n        <ChatBarButton\n            tooltip=\"Open Translate Modal\"\n            onClick={e => {\n                if (e.shiftKey) return toggle();\n\n                openModal(props => (\n                    <TranslateModal rootProps={props} />\n                ));\n            }}\n            onContextMenu={toggle}\n            buttonProps={{\n                \"aria-haspopup\": \"dialog\"\n            }}\n        >\n            <TranslateIcon className={cl({ \"auto-translate\": autoTranslate, \"chat-button\": true })} />\n        </ChatBarButton>\n    );\n\n    if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)\n        return (\n            <Tooltip text=\"Auto Translate Enabled\" forceOpen>\n                {() => button}\n            </Tooltip>\n        );\n\n    return button;\n};\n"
  },
  {
    "path": "src/plugins/translate/TranslateModal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Divider } from \"@components/Divider\";\nimport { FormSwitch } from \"@components/FormSwitch\";\nimport { Margins } from \"@utils/margins\";\nimport { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from \"@utils/modal\";\nimport { Forms, SearchableSelect, useMemo } from \"@webpack/common\";\n\nimport { settings } from \"./settings\";\nimport { cl, getLanguages } from \"./utils\";\n\nconst LanguageSettingKeys = [\"receivedInput\", \"receivedOutput\", \"sentInput\", \"sentOutput\"] as const;\n\nfunction LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof LanguageSettingKeys[number]; includeAuto: boolean; }) {\n    const currentValue = settings.use([settingsKey])[settingsKey];\n\n    const options = useMemo(\n        () => {\n            const options = Object.entries(getLanguages()).map(([value, label]) => ({ value, label }));\n            if (!includeAuto)\n                options.shift();\n\n            return options;\n        }, []\n    );\n\n    return (\n        <section className={Margins.bottom16}>\n            <Forms.FormTitle tag=\"h3\">\n                {settings.def[settingsKey].description}\n            </Forms.FormTitle>\n\n            <SearchableSelect\n                options={options}\n                value={options.find(o => o.value === currentValue)?.value}\n                placeholder=\"Select a language\"\n                maxVisibleItems={5}\n                closeOnSelect={true}\n                onChange={v => settings.store[settingsKey] = v}\n            />\n        </section>\n    );\n}\n\nfunction AutoTranslateToggle() {\n    const value = settings.use([\"autoTranslate\"]).autoTranslate;\n\n    return (\n        <FormSwitch\n            title=\"Auto Translate\"\n            description={settings.def.autoTranslate.description}\n            value={value}\n            onChange={v => settings.store.autoTranslate = v}\n            hideBorder\n        />\n    );\n}\n\n\nexport function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {\n    return (\n        <ModalRoot {...rootProps}>\n            <ModalHeader className={cl(\"modal-header\")}>\n                <Forms.FormTitle tag=\"h2\" className={cl(\"modal-title\")}>\n                    Translate\n                </Forms.FormTitle>\n                <ModalCloseButton onClick={rootProps.onClose} />\n            </ModalHeader>\n\n            <ModalContent className={cl(\"modal-content\")}>\n                {LanguageSettingKeys.map(s => (\n                    <LanguageSelect\n                        key={s}\n                        settingsKey={s}\n                        includeAuto={s.endsWith(\"Input\")}\n                    />\n                ))}\n\n                <Divider className={Margins.bottom16} />\n\n                <AutoTranslateToggle />\n            </ModalContent>\n        </ModalRoot>\n    );\n}\n"
  },
  {
    "path": "src/plugins/translate/TranslationAccessory.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Message } from \"@vencord/discord-types\";\nimport { Parser, useEffect, useState } from \"@webpack/common\";\n\nimport { TranslateIcon } from \"./TranslateIcon\";\nimport { cl, TranslationValue } from \"./utils\";\n\nconst TranslationSetters = new Map<string, (v: TranslationValue) => void>();\n\nexport function handleTranslate(messageId: string, data: TranslationValue) {\n    TranslationSetters.get(messageId)!(data);\n}\n\nfunction Dismiss({ onDismiss }: { onDismiss: () => void; }) {\n    return (\n        <button\n            onClick={onDismiss}\n            className={cl(\"dismiss\")}\n        >\n            Dismiss\n        </button>\n    );\n}\n\nexport function TranslationAccessory({ message }: { message: Message; }) {\n    const [translation, setTranslation] = useState<TranslationValue>();\n\n    useEffect(() => {\n        // Ignore MessageLinkEmbeds messages\n        if ((message as any).vencordEmbeddedBy) return;\n\n        TranslationSetters.set(message.id, setTranslation);\n\n        return () => void TranslationSetters.delete(message.id);\n    }, []);\n\n    if (!translation) return null;\n\n    return (\n        <span className={cl(\"accessory\")}>\n            <TranslateIcon width={16} height={16} className={cl(\"accessory-icon\")} />\n            {Parser.parse(translation.text)}\n            <br />\n            (translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)\n        </span>\n    );\n}\n"
  },
  {
    "path": "src/plugins/translate/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { ChannelStore, Menu } from \"@webpack/common\";\n\nimport { settings } from \"./settings\";\nimport { setShouldShowTranslateEnabledTooltip, TranslateChatBarIcon, TranslateIcon } from \"./TranslateIcon\";\nimport { handleTranslate, TranslationAccessory } from \"./TranslationAccessory\";\nimport { translate } from \"./utils\";\n\nconst messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {\n    const content = getMessageContent(message);\n    if (!content) return;\n\n    const group = findGroupChildrenByChildId(\"copy-text\", children);\n    if (!group) return;\n\n    group.splice(group.findIndex(c => c?.props?.id === \"copy-text\") + 1, 0, (\n        <Menu.MenuItem\n            id=\"vc-trans\"\n            label=\"Translate\"\n            icon={TranslateIcon}\n            action={async () => {\n                const trans = await translate(\"received\", content);\n                handleTranslate(message.id, trans);\n            }}\n        />\n    ));\n};\n\n\nfunction getMessageContent(message: Message) {\n    // Message snapshots is an array, which allows for nested snapshots, which Discord does not do yet.\n    // no point collecting content or rewriting this to render in a certain way that makes sense\n    // for something currently impossible.\n    return message.content\n        || message.messageSnapshots?.[0]?.message.content\n        || message.embeds?.find(embed => embed.type === \"auto_moderation_message\")?.rawDescription || \"\";\n}\n\nlet tooltipTimeout: any;\n\nexport default definePlugin({\n    name: \"Translate\",\n    description: \"Translate messages with Google Translate or DeepL\",\n    authors: [Devs.Ven, Devs.AshtonMemer],\n    settings,\n    contextMenus: {\n        \"message\": messageCtxPatch\n    },\n    // not used, just here in case some other plugin wants it or w/e\n    translate,\n\n    renderMessageAccessory: props => <TranslationAccessory message={props.message} />,\n\n    chatBarButton: {\n        icon: TranslateIcon,\n        render: TranslateChatBarIcon\n    },\n\n    messagePopoverButton: {\n        icon: TranslateIcon,\n        render(message: Message) {\n            const content = getMessageContent(message);\n            if (!content) return null;\n\n            return {\n                label: \"Translate\",\n                icon: TranslateIcon,\n                message,\n                channel: ChannelStore.getChannel(message.channel_id),\n                onClick: async () => {\n                    const trans = await translate(\"received\", content);\n                    handleTranslate(message.id, trans);\n                }\n            };\n        }\n    },\n\n    async onBeforeMessageSend(_, message) {\n        if (!settings.store.autoTranslate) return;\n        if (!message.content) return;\n\n        setShouldShowTranslateEnabledTooltip?.(true);\n        clearTimeout(tooltipTimeout);\n        tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);\n\n        const trans = await translate(\"sent\", message.content);\n        message.content = trans.text;\n    }\n});\n"
  },
  {
    "path": "src/plugins/translate/languages.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\nTo generate:\n- Visit https://translate.google.com/?sl=auto&tl=en&op=translate\n- Open Language dropdown\n- Open Devtools and use the element picker to pick the root of the language picker\n- Right click on the element in devtools and click \"Store as global variable\"\n\ncopy(Object.fromEntries(\n    Array.from(\n        temp1.querySelectorAll(\"[data-language-code]\"),\n        e => [e.dataset.languageCode, e.children[1].textContent]\n    ).sort((a, b) => a[1] === \"Detect language\" ? -1 : b[1] === \"Detect language\" ? 1 : a[1].localeCompare(b[1]))\n))\n*/\n\nexport type GoogleLanguage = keyof typeof GoogleLanguages;\nexport type DeeplLanguage = keyof typeof DeeplLanguages;\n\nexport const GoogleLanguages = {\n    \"auto\": \"Detect language\",\n    \"af\": \"Afrikaans\",\n    \"sq\": \"Albanian\",\n    \"am\": \"Amharic\",\n    \"ar\": \"Arabic\",\n    \"hy\": \"Armenian\",\n    \"as\": \"Assamese\",\n    \"ay\": \"Aymara\",\n    \"az\": \"Azerbaijani\",\n    \"bm\": \"Bambara\",\n    \"eu\": \"Basque\",\n    \"be\": \"Belarusian\",\n    \"bn\": \"Bengali\",\n    \"bho\": \"Bhojpuri\",\n    \"bs\": \"Bosnian\",\n    \"bg\": \"Bulgarian\",\n    \"ca\": \"Catalan\",\n    \"ceb\": \"Cebuano\",\n    \"ny\": \"Chichewa\",\n    \"zh-CN\": \"Chinese (Simplified)\",\n    \"zh-TW\": \"Chinese (Traditional)\",\n    \"co\": \"Corsican\",\n    \"hr\": \"Croatian\",\n    \"cs\": \"Czech\",\n    \"da\": \"Danish\",\n    \"dv\": \"Dhivehi\",\n    \"doi\": \"Dogri\",\n    \"nl\": \"Dutch\",\n    \"en\": \"English\",\n    \"eo\": \"Esperanto\",\n    \"et\": \"Estonian\",\n    \"ee\": \"Ewe\",\n    \"tl\": \"Filipino\",\n    \"fi\": \"Finnish\",\n    \"fr\": \"French\",\n    \"fy\": \"Frisian\",\n    \"gl\": \"Galician\",\n    \"ka\": \"Georgian\",\n    \"de\": \"German\",\n    \"el\": \"Greek\",\n    \"gn\": \"Guarani\",\n    \"gu\": \"Gujarati\",\n    \"ht\": \"Haitian Creole\",\n    \"ha\": \"Hausa\",\n    \"haw\": \"Hawaiian\",\n    \"iw\": \"Hebrew\",\n    \"hi\": \"Hindi\",\n    \"hmn\": \"Hmong\",\n    \"hu\": \"Hungarian\",\n    \"is\": \"Icelandic\",\n    \"ig\": \"Igbo\",\n    \"ilo\": \"Ilocano\",\n    \"id\": \"Indonesian\",\n    \"ga\": \"Irish\",\n    \"it\": \"Italian\",\n    \"ja\": \"Japanese\",\n    \"jw\": \"Javanese\",\n    \"kn\": \"Kannada\",\n    \"kk\": \"Kazakh\",\n    \"km\": \"Khmer\",\n    \"rw\": \"Kinyarwanda\",\n    \"gom\": \"Konkani\",\n    \"ko\": \"Korean\",\n    \"kri\": \"Krio\",\n    \"ku\": \"Kurdish (Kurmanji)\",\n    \"ckb\": \"Kurdish (Sorani)\",\n    \"ky\": \"Kyrgyz\",\n    \"lo\": \"Lao\",\n    \"la\": \"Latin\",\n    \"lv\": \"Latvian\",\n    \"ln\": \"Lingala\",\n    \"lt\": \"Lithuanian\",\n    \"lg\": \"Luganda\",\n    \"lb\": \"Luxembourgish\",\n    \"mk\": \"Macedonian\",\n    \"mai\": \"Maithili\",\n    \"mg\": \"Malagasy\",\n    \"ms\": \"Malay\",\n    \"ml\": \"Malayalam\",\n    \"mt\": \"Maltese\",\n    \"mi\": \"Maori\",\n    \"mr\": \"Marathi\",\n    \"mni-Mtei\": \"Meiteilon (Manipuri)\",\n    \"lus\": \"Mizo\",\n    \"mn\": \"Mongolian\",\n    \"my\": \"Myanmar (Burmese)\",\n    \"ne\": \"Nepali\",\n    \"no\": \"Norwegian\",\n    \"or\": \"Odia (Oriya)\",\n    \"om\": \"Oromo\",\n    \"ps\": \"Pashto\",\n    \"fa\": \"Persian\",\n    \"pl\": \"Polish\",\n    \"pt\": \"Portuguese\",\n    \"pa\": \"Punjabi\",\n    \"qu\": \"Quechua\",\n    \"ro\": \"Romanian\",\n    \"ru\": \"Russian\",\n    \"sm\": \"Samoan\",\n    \"sa\": \"Sanskrit\",\n    \"gd\": \"Scots Gaelic\",\n    \"nso\": \"Sepedi\",\n    \"sr\": \"Serbian\",\n    \"st\": \"Sesotho\",\n    \"sn\": \"Shona\",\n    \"sd\": \"Sindhi\",\n    \"si\": \"Sinhala\",\n    \"sk\": \"Slovak\",\n    \"sl\": \"Slovenian\",\n    \"so\": \"Somali\",\n    \"es\": \"Spanish\",\n    \"su\": \"Sundanese\",\n    \"sw\": \"Swahili\",\n    \"sv\": \"Swedish\",\n    \"tg\": \"Tajik\",\n    \"ta\": \"Tamil\",\n    \"tt\": \"Tatar\",\n    \"te\": \"Telugu\",\n    \"th\": \"Thai\",\n    \"ti\": \"Tigrinya\",\n    \"ts\": \"Tsonga\",\n    \"tr\": \"Turkish\",\n    \"tk\": \"Turkmen\",\n    \"ak\": \"Twi\",\n    \"uk\": \"Ukrainian\",\n    \"ur\": \"Urdu\",\n    \"ug\": \"Uyghur\",\n    \"uz\": \"Uzbek\",\n    \"vi\": \"Vietnamese\",\n    \"cy\": \"Welsh\",\n    \"xh\": \"Xhosa\",\n    \"yi\": \"Yiddish\",\n    \"yo\": \"Yoruba\",\n    \"zu\": \"Zulu\"\n} as const;\n\nexport const DeeplLanguages = {\n    \"\": \"Detect language\",\n    \"ace\": \"Acehnese\",\n    \"af\": \"Afrikaans\",\n    \"sq\": \"Albanian\",\n    \"ar\": \"Arabic\",\n    \"an\": \"Aragonese\",\n    \"hy\": \"Armenian\",\n    \"as\": \"Assamese\",\n    \"ay\": \"Aymara\",\n    \"az\": \"Azerbaijani\",\n    \"ba\": \"Bashkir\",\n    \"eu\": \"Basque\",\n    \"be\": \"Belarusian\",\n    \"bn\": \"Bengali\",\n    \"bho\": \"Bhojpuri\",\n    \"bs\": \"Bosnian\",\n    \"br\": \"Breton\",\n    \"bg\": \"Bulgarian\",\n    \"my\": \"Burmese\",\n    \"yue\": \"Cantonese\",\n    \"ca\": \"Catalan\",\n    \"ceb\": \"Cebuano\",\n    \"zh-hans\": \"Chinese (Simplified)\",\n    \"zh-hant\": \"Chinese (Traditional)\",\n    \"hr\": \"Croatian\",\n    \"cs\": \"Czech\",\n    \"da\": \"Danish\",\n    \"prs\": \"Dari\",\n    \"nl\": \"Dutch\",\n    \"en-us\": \"English (American)\",\n    \"en-gb\": \"English (British)\",\n    \"eo\": \"Esperanto\",\n    \"et\": \"Estonian\",\n    \"fi\": \"Finnish\",\n    \"fr\": \"French\",\n    \"gl\": \"Galician\",\n    \"ka\": \"Georgian\",\n    \"de\": \"German\",\n    \"el\": \"Greek\",\n    \"gn\": \"Guarani\",\n    \"gu\": \"Gujarati\",\n    \"ht\": \"Haitian Creole\",\n    \"ha\": \"Hausa\",\n    \"he\": \"Hebrew\",\n    \"hi\": \"Hindi\",\n    \"hu\": \"Hungarian\",\n    \"is\": \"Icelandic\",\n    \"ig\": \"Igbo\",\n    \"id\": \"Indonesian\",\n    \"ga\": \"Irish\",\n    \"it\": \"Italian\",\n    \"ja\": \"Japanese\",\n    \"jv\": \"Javanese\",\n    \"ko\": \"Korean\",\n    \"pam\": \"Kapampangan\",\n    \"kk\": \"Kazakh\",\n    \"kmr\": \"Kurdish (Kurmanji)\",\n    \"ckb\": \"Kurdish (Sorani)\",\n    \"ky\": \"Kyrgyz\",\n    \"lo\": \"Lao\",\n    \"la\": \"Latin\",\n    \"lv\": \"Latvian\",\n    \"ln\": \"Lingala\",\n    \"lt\": \"Lithuanian\",\n    \"lmo\": \"Lombard\",\n    \"lb\": \"Luxembourgish\",\n    \"mk\": \"Macedonian\",\n    \"mai\": \"Maithili\",\n    \"mg\": \"Malagasy\",\n    \"ms\": \"Malay\",\n    \"ml\": \"Malayalam\",\n    \"mt\": \"Maltese\",\n    \"mi\": \"Maori\",\n    \"mr\": \"Marathi\",\n    \"mn\": \"Mongolian\",\n    \"ne\": \"Nepali\",\n    \"nb\": \"Norwegian (Bokmål)\",\n    \"oc\": \"Occitan\",\n    \"om\": \"Oromo\",\n    \"pag\": \"Pangasinan\",\n    \"ps\": \"Pashto\",\n    \"fa\": \"Persian\",\n    \"pl\": \"Polish\",\n    \"pt-br\": \"Portuguese (Brazilian)\",\n    \"pt-pt\": \"Portuguese (European)\",\n    \"pa\": \"Punjabi\",\n    \"qu\": \"Quechua\",\n    \"ro\": \"Romanian\",\n    \"ru\": \"Russian\",\n    \"sm\": \"Samoan\",\n    \"sa\": \"Sanskrit\",\n    \"gd\": \"Scottish Gaelic\",\n    \"sr\": \"Serbian\",\n    \"st\": \"Sesotho\",\n    \"sn\": \"Shona\",\n    \"scn\": \"Sicilian\",\n    \"sd\": \"Sindhi\",\n    \"si\": \"Sinhala\",\n    \"sk\": \"Slovak\",\n    \"sl\": \"Slovenian\",\n    \"so\": \"Somali\",\n    \"es\": \"Spanish\",\n    \"es-419\": \"Spanish (Latin American)\",\n    \"su\": \"Sundanese\",\n    \"sw\": \"Swahili\",\n    \"sv\": \"Swedish\",\n    \"tl\": \"Tagalog\",\n    \"tg\": \"Tajik\",\n    \"ta\": \"Tamil\",\n    \"tt\": \"Tatar\",\n    \"te\": \"Telugu\",\n    \"th\": \"Thai\",\n    \"ti\": \"Tigrinya\",\n    \"ts\": \"Tsonga\",\n    \"tn\": \"Tswana\",\n    \"tr\": \"Turkish\",\n    \"tk\": \"Turkmen\",\n    \"uk\": \"Ukrainian\",\n    \"ur\": \"Urdu\",\n    \"ug\": \"Uyghur\",\n    \"uz\": \"Uzbek\",\n    \"vi\": \"Vietnamese\",\n    \"cy\": \"Welsh\",\n    \"wo\": \"Wolof\",\n    \"xh\": \"Xhosa\",\n    \"yi\": \"Yiddish\",\n    \"yo\": \"Yoruba\",\n    \"zu\": \"Zulu\"\n} as const;\n\nexport function deeplLanguageToGoogleLanguage(language: string) {\n    switch (language) {\n        case \"\": return \"auto\";\n        case \"nb\": return \"no\";\n        case \"zh-hans\": return \"zh-CN\";\n        case \"zh-hant\": return \"zh-TW\";\n        case \"en-us\":\n        case \"en-gb\":\n            return \"en\";\n        case \"pt-br\":\n        case \"pt-pt\":\n            return \"pt\";\n        default:\n            return language;\n    }\n}\n"
  },
  {
    "path": "src/plugins/translate/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { IpcMainInvokeEvent } from \"electron\";\n\nexport async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: boolean, apiKey: string, payload: string) {\n    const url = pro\n        ? \"https://api.deepl.com/v2/translate\"\n        : \"https://api-free.deepl.com/v2/translate\";\n\n    try {\n        const res = await fetch(url, {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n                \"Authorization\": `DeepL-Auth-Key ${apiKey}`\n            },\n            body: payload\n        });\n\n        const data = await res.text();\n        return { status: res.status, data };\n    } catch (e) {\n        return { status: -1, data: String(e) };\n    }\n}\n"
  },
  {
    "path": "src/plugins/translate/settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { OptionType } from \"@utils/types\";\n\nexport const settings = definePluginSettings({\n    receivedInput: {\n        type: OptionType.STRING,\n        description: \"Language that received messages should be translated from\",\n        default: \"auto\",\n        hidden: true\n    },\n    receivedOutput: {\n        type: OptionType.STRING,\n        description: \"Language that received messages should be translated to\",\n        default: \"en\",\n        hidden: true\n    },\n    sentInput: {\n        type: OptionType.STRING,\n        description: \"Language that your own messages should be translated from\",\n        default: \"auto\",\n        hidden: true\n    },\n    sentOutput: {\n        type: OptionType.STRING,\n        description: \"Language that your own messages should be translated to\",\n        default: \"en\",\n        hidden: true\n    },\n\n    service: {\n        type: OptionType.SELECT,\n        description: IS_WEB ? \"Translation service (Not supported on Web!)\" : \"Translation service\",\n        disabled: () => IS_WEB,\n        options: [\n            { label: \"Google Translate\", value: \"google\", default: true },\n            { label: \"DeepL Free\", value: \"deepl\" },\n            { label: \"DeepL Pro\", value: \"deepl-pro\" }\n        ] as const,\n        onChange: resetLanguageDefaults\n    },\n    deeplApiKey: {\n        type: OptionType.STRING,\n        description: \"DeepL API key\",\n        default: \"\",\n        placeholder: \"Get your API key from https://deepl.com/your-account\",\n        disabled: () => IS_WEB\n    },\n    autoTranslate: {\n        type: OptionType.BOOLEAN,\n        description: \"Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this\",\n        default: false\n    },\n    showAutoTranslateTooltip: {\n        type: OptionType.BOOLEAN,\n        description: \"Show a tooltip on the ChatBar button whenever a message is automatically translated\",\n        default: true\n    },\n}).withPrivateSettings<{\n    showAutoTranslateAlert: boolean;\n}>();\n\nexport function resetLanguageDefaults() {\n    if (IS_WEB || settings.store.service === \"google\") {\n        settings.store.receivedInput = \"auto\";\n        settings.store.receivedOutput = \"en\";\n        settings.store.sentInput = \"auto\";\n        settings.store.sentOutput = \"en\";\n    } else {\n        settings.store.receivedInput = \"\";\n        settings.store.receivedOutput = \"en-us\";\n        settings.store.sentInput = \"\";\n        settings.store.sentOutput = \"en-us\";\n    }\n}\n"
  },
  {
    "path": "src/plugins/translate/styles.css",
    "content": ".vc-trans-modal-content {\n    padding: 1em;\n}\n\n.vc-trans-modal-header {\n    place-content: center space-between;\n}\n\n.vc-trans-modal-title {\n    margin: 0;\n}\n\n.vc-trans-accessory {\n    color: var(--text-muted);\n    margin-top: 0.5em;\n    font-style: italic;\n    font-weight: 400;\n    line-height: 1.2rem;\n    white-space: break-spaces;\n}\n\n.vc-trans-accessory-icon {\n    margin-right: 0.25em;\n}\n\n.vc-trans-dismiss {\n    all: unset;\n    cursor: pointer;\n    color: var(--text-link);\n}\n\n.vc-trans-dismiss:is(:hover, :focus) {\n    text-decoration: underline;\n}\n\n.vc-trans-auto-translate {\n    color: var(--green-360);\n}\n\n.vc-trans-chat-button {\n    scale: 1.085;\n}\n"
  },
  {
    "path": "src/plugins/translate/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { classNameFactory } from \"@utils/css\";\nimport { onlyOnce } from \"@utils/onlyOnce\";\nimport { PluginNative } from \"@utils/types\";\nimport { showToast, Toasts } from \"@webpack/common\";\n\nimport { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from \"./languages\";\nimport { resetLanguageDefaults, settings } from \"./settings\";\n\nexport const cl = classNameFactory(\"vc-trans-\");\n\nconst Native = VencordNative.pluginHelpers.Translate as PluginNative<typeof import(\"./native\")>;\n\ninterface GoogleData {\n    translation: string;\n    sourceLanguage: string;\n}\n\ninterface DeeplData {\n    translations: {\n        detected_source_language: string;\n        text: string;\n    }[];\n}\n\nexport interface TranslationValue {\n    sourceLanguage: string;\n    text: string;\n}\n\nexport const getLanguages = () => IS_WEB || settings.store.service === \"google\"\n    ? GoogleLanguages\n    : DeeplLanguages;\n\nexport async function translate(kind: \"received\" | \"sent\", text: string): Promise<TranslationValue> {\n    const translate = IS_WEB || settings.store.service === \"google\"\n        ? googleTranslate\n        : deeplTranslate;\n\n    try {\n        return await translate(\n            text,\n            settings.store[`${kind}Input`],\n            settings.store[`${kind}Output`]\n        );\n    } catch (e) {\n        const userMessage = typeof e === \"string\"\n            ? e\n            : \"Something went wrong. If this issue persists, please check the console or ask for help in the support server.\";\n\n        showToast(userMessage, Toasts.Type.FAILURE);\n\n        throw e instanceof Error\n            ? e\n            : new Error(userMessage);\n    }\n}\n\nasync function googleTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {\n    const url = \"https://translate-pa.googleapis.com/v1/translate?\" + new URLSearchParams({\n        \"params.client\": \"gtx\",\n        \"dataTypes\": \"TRANSLATION\",\n        \"key\": \"AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA\", // some google API key\n        \"query.sourceLanguage\": sourceLang,\n        \"query.targetLanguage\": targetLang,\n        \"query.text\": text,\n    });\n\n    const res = await fetch(url);\n    if (!res.ok)\n        throw new Error(\n            `Failed to translate \"${text}\" (${sourceLang} -> ${targetLang})`\n            + `\\n${res.status} ${res.statusText}`\n        );\n\n    const { sourceLanguage, translation }: GoogleData = await res.json();\n\n    return {\n        sourceLanguage: GoogleLanguages[sourceLanguage] ?? sourceLanguage,\n        text: translation\n    };\n}\n\nfunction fallbackToGoogle(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {\n    return googleTranslate(\n        text,\n        deeplLanguageToGoogleLanguage(sourceLang),\n        deeplLanguageToGoogleLanguage(targetLang)\n    );\n}\n\nconst showDeeplApiQuotaToast = onlyOnce(\n    () => showToast(\"Deepl API quota exceeded. Falling back to Google Translate\", Toasts.Type.FAILURE)\n);\n\nasync function deeplTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {\n    if (!settings.store.deeplApiKey) {\n        showToast(\"DeepL API key is not set. Resetting to Google\", Toasts.Type.FAILURE);\n\n        settings.store.service = \"google\";\n        resetLanguageDefaults();\n\n        return fallbackToGoogle(text, sourceLang, targetLang);\n    }\n\n    // CORS jumpscare\n    const { status, data } = await Native.makeDeeplTranslateRequest(\n        settings.store.service === \"deepl-pro\",\n        settings.store.deeplApiKey,\n        JSON.stringify({\n            text: [text],\n            target_lang: targetLang,\n            source_lang: sourceLang.split(\"-\")[0]\n        })\n    );\n\n    switch (status) {\n        case 200:\n            break;\n        case -1:\n            throw \"Failed to connect to DeepL API: \" + data;\n        case 403:\n            throw \"Invalid DeepL API key or version\";\n        case 456:\n            showDeeplApiQuotaToast();\n            return fallbackToGoogle(text, sourceLang, targetLang);\n        default:\n            throw new Error(`Failed to translate \"${text}\" (${sourceLang} -> ${targetLang})\\n${status} ${data}`);\n    }\n\n    const { translations }: DeeplData = JSON.parse(data);\n    const src = translations[0].detected_source_language;\n\n    return {\n        sourceLanguage: DeeplLanguages[src] ?? src,\n        text: translations[0].text\n    };\n}\n"
  },
  {
    "path": "src/plugins/typingIndicator/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { definePluginSettings, Settings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { buildSeveralUsers } from \"@plugins/typingTweaks\";\nimport { Devs } from \"@utils/constants\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserGuildSettingsStore, UserStore, UserSummaryItem, useStateFromStores } from \"@webpack/common\";\n\nconst ThreeDots = findComponentByCodeLazy(\"Math.min(1,Math.max(\", \"dotRadius:\");\n\nconst enum IndicatorMode {\n    Dots = 1 << 0,\n    Avatars = 1 << 1\n}\n\nfunction getDisplayName(guildId: string, userId: string) {\n    const user = UserStore.getUser(userId);\n    return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username;\n}\n\nfunction TypingIndicator({ channelId, guildId }: { channelId: string; guildId: string; }) {\n    const typingUsers: Record<string, number> = useStateFromStores(\n        [TypingStore],\n        () => ({ ...TypingStore.getTypingUsers(channelId) }),\n        null,\n        (old, current) => {\n            const oldKeys = Object.keys(old);\n            const currentKeys = Object.keys(current);\n\n            return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);\n        }\n    );\n    const currentChannelId = useStateFromStores([SelectedChannelStore], () => SelectedChannelStore.getChannelId());\n\n    if (!settings.store.includeMutedChannels) {\n        const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);\n        if (isChannelMuted) return null;\n    }\n\n    if (!settings.store.includeCurrentChannel) {\n        if (currentChannelId === channelId) return null;\n    }\n\n    const myId = UserStore.getCurrentUser()?.id;\n\n    const typingUsersArray = Object.keys(typingUsers).filter(id =>\n        id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)\n    );\n    const [a, b, c] = typingUsersArray;\n    let tooltipText: string;\n\n    switch (typingUsersArray.length) {\n        case 0: break;\n        case 1: {\n            tooltipText = getIntlMessage(\"ONE_USER_TYPING\", { a: getDisplayName(guildId, a) });\n            break;\n        }\n        case 2: {\n            tooltipText = getIntlMessage(\"TWO_USERS_TYPING\", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b) });\n            break;\n        }\n        case 3: {\n            tooltipText = getIntlMessage(\"THREE_USERS_TYPING\", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b), c: getDisplayName(guildId, c) });\n            break;\n        }\n        default: {\n            tooltipText = Settings.plugins.TypingTweaks.enabled\n                ? buildSeveralUsers({ users: [a, b].map(UserStore.getUser), count: typingUsersArray.length - 2, guildId })\n                : getIntlMessage(\"SEVERAL_USERS_TYPING\");\n            break;\n        }\n    }\n\n    if (typingUsersArray.length > 0) {\n        return (\n            <Tooltip text={tooltipText!}>\n                {props => (\n                    <div className=\"vc-typing-indicator\" {...props}>\n                        {((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && (\n                            <div\n                                onClick={e => {\n                                    e.stopPropagation();\n                                    e.preventDefault();\n                                }}\n                                onKeyPress={e => e.stopPropagation()}\n                            >\n                                <UserSummaryItem\n                                    users={typingUsersArray.map(id => UserStore.getUser(id))}\n                                    guildId={guildId}\n                                    renderIcon={false}\n                                    max={3}\n                                    showDefaultAvatarsForNullUsers\n                                    showUserPopout\n                                    size={16}\n                                    className=\"vc-typing-indicator-avatars\"\n                                />\n                            </div>\n                        )}\n                        {((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (\n                            <div className=\"vc-typing-indicator-dots\">\n                                <ThreeDots dotRadius={3} themed={true} />\n                            </div>\n                        )}\n                    </div>\n                )}\n            </Tooltip>\n        );\n    }\n\n    return null;\n}\n\nconst settings = definePluginSettings({\n    includeCurrentChannel: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to show the typing indicator for the currently selected channel\",\n        default: true\n    },\n    includeMutedChannels: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to show the typing indicator for muted channels.\",\n        default: false\n    },\n    includeBlockedUsers: {\n        type: OptionType.BOOLEAN,\n        description: \"Whether to show the typing indicator for blocked users.\",\n        default: false\n    },\n    indicatorMode: {\n        type: OptionType.SELECT,\n        description: \"How should the indicator be displayed?\",\n        options: [\n            { label: \"Avatars and animated dots\", value: IndicatorMode.Dots | IndicatorMode.Avatars, default: true },\n            { label: \"Animated dots\", value: IndicatorMode.Dots },\n            { label: \"Avatars\", value: IndicatorMode.Avatars },\n        ],\n    }\n});\n\nexport default definePlugin({\n    name: \"TypingIndicator\",\n    description: \"Adds an indicator if someone is typing on a channel.\",\n    authors: [Devs.Nuckyz, Devs.fawn, Devs.Sqaaakoi],\n    settings,\n\n    patches: [\n        // Normal channel\n        {\n            find: \"UNREAD_IMPORTANT:\",\n            replacement: {\n                match: /\\.Children\\.count.+?:null(?<=,channel:(\\i).+?)/,\n                replace: \"$&,$self.TypingIndicator($1.id,$1.getGuildId())\"\n            }\n        },\n        // Theads\n        {\n            // This is the thread \"spine\" that shows in the left\n            find: \"M0 15H2c0 1.6569\",\n            replacement: {\n                match: /mentionsCount:\\i.+?null(?<=channel:(\\i).+?)/,\n                replace: \"$&,$self.TypingIndicator($1.id,$1.getGuildId())\"\n            }\n        }\n    ],\n\n    TypingIndicator: (channelId: string, guildId: string) => (\n        <ErrorBoundary noop>\n            <TypingIndicator channelId={channelId} guildId={guildId} />\n        </ErrorBoundary>\n    ),\n});\n"
  },
  {
    "path": "src/plugins/typingIndicator/style.css",
    "content": ".vc-typing-indicator {\n    display: flex;\n    align-items: center;\n    height: 20px;\n}\n\n.vc-typing-indicator-avatars {\n    margin-left: 6px;\n}\n\n.vc-typing-indicator-dots {\n    margin-left: 6px;\n    height: 16px;\n    display: flex;\n    align-items: center;\n    z-index: 0;\n    cursor: pointer;\n}\n"
  },
  {
    "path": "src/plugins/typingTweaks/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { openUserProfile } from \"@utils/discord\";\nimport { isNonNullish } from \"@utils/guards\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { Channel, User } from \"@vencord/discord-types\";\nimport { AuthenticationStore, Avatar, GuildMemberStore, React, RelationshipStore, TypingStore, UserStore, useStateFromStores } from \"@webpack/common\";\nimport { PropsWithChildren } from \"react\";\n\nimport managedStyle from \"./style.css?managed\";\n\nconst settings = definePluginSettings({\n    showAvatars: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show avatars in the typing indicator\"\n    },\n    showRoleColors: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show role colors in the typing indicator\"\n    },\n    alternativeFormatting: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show a more useful message when several users are typing\"\n    }\n});\n\nexport const buildSeveralUsers = ErrorBoundary.wrap(function buildSeveralUsers({ users, count, guildId }: { users: User[], count: number; guildId: string; }) {\n    return (\n        <>\n            {users.slice(0, count).map(user => (\n                <React.Fragment key={user.id}>\n                    <TypingUser user={user} guildId={guildId} />\n                    {\", \"}\n                </React.Fragment>\n            ))}\n            and {count} others are typing...\n        </>\n    );\n}, { noop: true });\n\ninterface TypingUserProps {\n    user: User;\n    guildId: string;\n}\n\nconst TypingUser = ErrorBoundary.wrap(function TypingUser({ user, guildId }: TypingUserProps) {\n    return (\n        <strong\n            className=\"vc-typing-user\"\n            role=\"button\"\n            onClick={() => {\n                openUserProfile(user.id);\n            }}\n            style={{\n                color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined,\n            }}\n        >\n            {settings.store.showAvatars && (\n                <Avatar\n                    size=\"SIZE_16\"\n                    src={user.getAvatarURL(guildId, 128)} />\n            )}\n            {GuildMemberStore.getNick(guildId!, user.id)\n                || (!guildId && RelationshipStore.getNickname(user.id))\n                || (user as any).globalName\n                || user.username\n            }\n        </strong>\n    );\n}, { noop: true });\n\nexport default definePlugin({\n    name: \"TypingTweaks\",\n    description: \"Show avatars and role colours in the typing indicator\",\n    authors: [Devs.zt, Devs.sadan],\n    settings,\n\n    managedStyle,\n\n    patches: [\n        {\n            find: \"#{intl::SEVERAL_USERS_TYPING_STRONG}\",\n            group: true,\n            replacement: [\n                {\n                    // Style the indicator and add function call to modify the children before rendering\n                    match: /(?<=\"aria-atomic\":!0,children:)\\i/,\n                    replace: \"$self.renderTypingUsers({ users: arguments[0]?.typingUserObjects, guildId: arguments[0]?.channel?.guild_id, children: $& })\"\n                },\n                {\n                    match: /(?<=function \\i\\(\\i\\)\\{)(?=[^}]+?\\{channel:\\i,isThreadCreation:\\i=!1,\\.\\.\\.\\i\\})/,\n                    replace: \"let typingUserObjects = $self.useTypingUsers(arguments[0]?.channel);\"\n                },\n                {\n                    // Get the typing users as user objects instead of names\n                    match: /typingUsers:(\\i)\\?\\[\\]:\\i,/,\n                    // check by typeof so if the variable is not defined due to other patch failing, it won't throw a ReferenceError\n                    replace: \"$&typingUserObjects: $1 || typeof typingUserObjects === 'undefined' ? [] : typingUserObjects,\"\n                },\n                {\n                    // Adds the alternative formatting for several users typing\n                    // users.length > 3 && (component = intl(key))\n                    match: /(&&\\(\\i=)\\i\\.\\i\\.format\\(\\i\\.\\i#{intl::SEVERAL_USERS_TYPING_STRONG},\\{\\}\\)/,\n                    replace: \"$1$self.buildSeveralUsers({ users: arguments[0]?.typingUserObjects, count: arguments[0]?.typingUserObjects?.length - 2, guildId: arguments[0]?.channel?.guild_id })\",\n                    predicate: () => settings.store.alternativeFormatting\n                }\n            ]\n        }\n    ],\n\n    useTypingUsers(channel: Channel | undefined): User[] {\n        try {\n            if (!channel) {\n                throw new Error(\"No channel\");\n            }\n\n            const typingUsers = useStateFromStores([TypingStore], () => TypingStore.getTypingUsers(channel.id));\n            const myId = useStateFromStores([AuthenticationStore], () => AuthenticationStore.getId());\n\n            return Object.keys(typingUsers)\n                .filter(id => id && id !== myId && !RelationshipStore.isBlockedOrIgnored(id))\n                .map(id => UserStore.getUser(id))\n                .filter(isNonNullish);\n        } catch (e) {\n            new Logger(\"TypingTweaks\").error(\"Failed to get typing users:\", e);\n            return [];\n        }\n    },\n\n\n    buildSeveralUsers,\n\n    renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => {\n        try {\n            if (!Array.isArray(children)) {\n                return children;\n            }\n\n            let element = 0;\n\n            return children.map(c => {\n                if (c.type !== \"strong\" && !(typeof c !== \"string\" && !React.isValidElement(c)))\n                    return c;\n\n                const user = users[element++];\n                return <TypingUser key={user.id} guildId={guildId} user={user} />;\n            });\n        } catch (e) {\n            new Logger(\"TypingTweaks\").error(\"Failed to render typing users:\", e);\n        }\n\n        return children;\n    }, { noop: true })\n});\n"
  },
  {
    "path": "src/plugins/typingTweaks/style.css",
    "content": ".vc-typing-user {\n    cursor: pointer;\n\n    [class*=\"wrapper\"] {\n        display: inline-block;\n        margin-right: 0.25em;\n        vertical-align: -4px;\n    }\n}"
  },
  {
    "path": "src/plugins/unindent/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { MessageObject } from \"@api/MessageEvents\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"Unindent\",\n    description: \"Trims leading indentation from codeblocks\",\n    authors: [Devs.Ven],\n\n    patches: [\n        {\n            find: \"inQuote:\",\n            replacement: {\n                match: /,content:([^,]+),inQuote/,\n                replace: (_, content) => `,content:$self.unindent(${content}),inQuote`\n            }\n        }\n    ],\n\n    unindent(str: string) {\n        // Users cannot send tabs, they get converted to spaces. However, a bot may send tabs, so convert them to 4 spaces first\n        str = str.replace(/\\t/g, \"    \");\n        const minIndent = str.match(/^ *(?=\\S)/gm)\n            ?.reduce((prev, curr) => Math.min(prev, curr.length), Infinity) ?? 0;\n\n        if (!minIndent) return str;\n        return str.replace(new RegExp(`^ {${minIndent}}`, \"gm\"), \"\");\n    },\n\n    unindentMsg(msg: MessageObject) {\n        msg.content = msg.content.replace(/```(.|\\n)*?```/g, m => {\n            const lines = m.split(\"\\n\");\n            if (lines.length < 2) return m; // Do not affect inline codeblocks\n            let suffix = \"\";\n            if (lines[lines.length - 1] === \"```\") suffix = lines.pop()!;\n            return `${lines[0]}\\n${this.unindent(lines.slice(1).join(\"\\n\"))}\\n${suffix}`;\n        });\n    },\n\n    onBeforeMessageSend(_, msg) {\n        return this.unindentMsg(msg);\n    },\n\n    onBeforeMessageEdit(_cid, _mid, msg) {\n        return this.unindentMsg(msg);\n    }\n});\n"
  },
  {
    "path": "src/plugins/unlockedAvatarZoom/README.md",
    "content": "# UnlockedAvatarZoom\n\nAllows you to zoom in further in the image crop tool when changing your avatar\n\n![](https://raw.githubusercontent.com/Vencord/plugin-assets/main/UnlockedAvatarZoom/demo.avif)\n"
  },
  {
    "path": "src/plugins/unlockedAvatarZoom/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { makeRange, OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    zoomMultiplier: {\n        type: OptionType.SLIDER,\n        description: \"Zoom multiplier\",\n        markers: makeRange(2, 16),\n        default: 4,\n    },\n});\n\nexport default definePlugin({\n    name: \"UnlockedAvatarZoom\",\n    description: \"Allows you to zoom in further in the image crop tool when changing your avatar\",\n    authors: [Devs.nakoyasha],\n    settings,\n    patches: [\n        {\n            find: \"#{intl::AVATAR_UPLOAD_EDIT_MEDIA}\",\n            replacement: {\n                match: /maxValue:\\d/,\n                replace: \"maxValue:$self.settings.store.zoomMultiplier\",\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/unsuppressEmbeds/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findGroupChildrenByChildId, NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { ImageInvisible, ImageVisible } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Channel, Message } from \"@vencord/discord-types\";\nimport { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from \"@webpack/common\";\n\n\nconst EMBED_SUPPRESSED = 1 << 2;\n\nconst messageContextMenuPatch: NavContextMenuPatchCallback = (\n    children,\n    { channel, message: { author, messageSnapshots, embeds, flags, id: messageId } }: { channel: Channel; message: Message; }\n) => {\n    const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;\n    const hasEmbedsInSnapshots = messageSnapshots.some(s => s.message.embeds.length);\n\n    if (!isEmbedSuppressed && !embeds.length && !hasEmbedsInSnapshots) return;\n\n    const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS);\n    if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return;\n\n    const menuGroup = findGroupChildrenByChildId(\"delete\", children);\n    const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === \"delete\");\n    if (!deleteIndex || !menuGroup) return;\n\n    menuGroup.splice(deleteIndex - 1, 0, (\n        <Menu.MenuItem\n            id=\"unsuppress-embeds\"\n            key=\"unsuppress-embeds\"\n            label={isEmbedSuppressed ? \"Unsuppress Embeds\" : \"Suppress Embeds\"}\n            color={isEmbedSuppressed ? undefined : \"danger\"}\n            icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}\n            action={() =>\n                RestAPI.patch({\n                    url: Constants.Endpoints.MESSAGE(channel.id, messageId),\n                    body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }\n                })\n            }\n        />\n    ));\n};\n\nexport default definePlugin({\n    name: \"UnsuppressEmbeds\",\n    authors: [Devs.rad, Devs.HypedDomi],\n    description: \"Allows you to unsuppress embeds in messages\",\n    contextMenus: {\n        \"message\": messageContextMenuPatch\n    }\n});\n"
  },
  {
    "path": "src/plugins/userMessagesPronouns/PronounsChatComponent.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { getUserSettingLazy } from \"@api/UserSettings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { getIntlMessage } from \"@utils/discord\";\nimport { classes } from \"@utils/misc\";\nimport { Message } from \"@vencord/discord-types\";\nimport { findCssClassesLazy } from \"@webpack\";\nimport { Tooltip, UserStore } from \"@webpack/common\";\n\nimport { settings } from \"./settings\";\nimport { useFormattedPronouns } from \"./utils\";\n\nconst TimestampClasses = findCssClassesLazy(\"timestampInline\", \"timestamp\");\nconst MessageDisplayCompact = getUserSettingLazy(\"textAndImages\", \"messageDisplayCompact\")!;\n\nconst AUTO_MODERATION_ACTION = 24;\n\nfunction shouldShow(message: Message): boolean {\n    if (message.author.bot || message.author.system || message.type === AUTO_MODERATION_ACTION)\n        return false;\n    if (!settings.store.showSelf && message.author.id === UserStore.getCurrentUser().id)\n        return false;\n\n    return true;\n}\n\nfunction PronounsChatComponent({ message }: { message: Message; }) {\n    const pronouns = useFormattedPronouns(message.author.id);\n\n    return pronouns && (\n        <Tooltip text={getIntlMessage(\"USER_PROFILE_PRONOUNS\")}>\n            {tooltipProps => (\n                <span\n                    {...tooltipProps}\n                    className={classes(TimestampClasses.timestampInline, TimestampClasses.timestamp)}\n                >• {pronouns}</span>\n            )}\n        </Tooltip>\n    );\n}\n\nexport const PronounsChatComponentWrapper = ErrorBoundary.wrap(({ message }: { message: Message; }) => {\n    return shouldShow(message)\n        ? <PronounsChatComponent message={message} />\n        : null;\n}, { noop: true });\n\nexport const CompactPronounsChatComponentWrapper = ErrorBoundary.wrap(({ message }: { message: Message; }) => {\n    const compact = MessageDisplayCompact.useSetting();\n\n    if (!compact || !shouldShow(message)) {\n        return null;\n    }\n\n    return <PronounsChatComponent message={message} />;\n}, { noop: true });\n"
  },
  {
    "path": "src/plugins/userMessagesPronouns/README.md",
    "content": "User Messages Pronouns\n\nAdds pronouns to chat user messages\n\n![](https://github.com/user-attachments/assets/34dc373d-faf4-4420-b49b-08b2647baa3b)\n"
  },
  {
    "path": "src/plugins/userMessagesPronouns/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { migratePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nimport { CompactPronounsChatComponentWrapper, PronounsChatComponentWrapper } from \"./PronounsChatComponent\";\nimport { settings } from \"./settings\";\n\nmigratePluginSettings(\"UserMessagesPronouns\", \"PronounDB\");\nexport default definePlugin({\n    name: \"UserMessagesPronouns\",\n    authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],\n    description: \"Adds pronouns to chat user messages\",\n    settings,\n\n    patches: [\n        {\n            find: \"showCommunicationDisabledStyles\",\n            replacement: {\n                // Add next to timestamp (normal mode)\n                match: /(?<=return\\s*\\(0,\\i\\.jsxs?\\)\\(.+!\\i&&)(\\(0,\\i.jsxs?\\)\\(.+?\\{.+?\\}\\))/,\n                replace: \"[$1, $self.PronounsChatComponentWrapper(arguments[0])]\"\n            }\n        },\n        {\n            find: '=\"SYSTEM_TAG\"',\n            replacement: [\n                {\n                    // Add next to username (compact mode)\n                    match: /className:\\i\\(\\)\\(\\i\\.className(?:,\\i\\.\\i)?,\\i\\)\\}\\)(?:\\))?,(?=\\i)/g,\n                    replace: \"$&$self.CompactPronounsChatComponentWrapper(arguments[0]),\",\n                },\n            ]\n        }\n    ],\n\n    PronounsChatComponentWrapper,\n    CompactPronounsChatComponentWrapper,\n});\n"
  },
  {
    "path": "src/plugins/userMessagesPronouns/settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { OptionType } from \"@utils/types\";\n\nexport const enum PronounsFormat {\n    Lowercase = \"LOWERCASE\",\n    Capitalized = \"CAPITALIZED\"\n}\n\nexport const settings = definePluginSettings({\n    pronounsFormat: {\n        type: OptionType.SELECT,\n        description: \"The format for pronouns to appear in chat\",\n        options: [\n            {\n                label: \"Lowercase\",\n                value: PronounsFormat.Lowercase,\n                default: true\n            },\n            {\n                label: \"Capitalized\",\n                value: PronounsFormat.Capitalized\n            }\n        ]\n    },\n    showSelf: {\n        type: OptionType.BOOLEAN,\n        description: \"Enable or disable showing pronouns for yourself\",\n        default: true\n    }\n});\n"
  },
  {
    "path": "src/plugins/userMessagesPronouns/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { getCurrentChannel } from \"@utils/discord\";\nimport { UserProfileStore, useStateFromStores } from \"@webpack/common\";\n\nimport { PronounsFormat, settings } from \"./settings\";\n\nfunction useDiscordPronouns(id: string, useGlobalProfile: boolean = false): string | undefined {\n    const globalPronouns: string | undefined = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(id)?.pronouns);\n    const guildPronouns: string | undefined = useStateFromStores([UserProfileStore], () => UserProfileStore.getGuildMemberProfile(id, getCurrentChannel()?.getGuildId())?.pronouns);\n\n    if (useGlobalProfile) return globalPronouns;\n    return guildPronouns || globalPronouns;\n}\n\nexport function useFormattedPronouns(id: string, useGlobalProfile: boolean = false) {\n    const pronouns = useDiscordPronouns(id, useGlobalProfile)?.trim().replace(/\\n+/g, \"\");\n    return settings.store.pronounsFormat === PronounsFormat.Lowercase ? pronouns?.toLowerCase() : pronouns;\n}\n"
  },
  {
    "path": "src/plugins/userVoiceShow/README.md",
    "content": "# User Voice Show\n\nShows an indicator when a user is in a Voice Channel\n\n![a preview of the indicator in the user profile](https://github.com/user-attachments/assets/48f825e4-fad5-40d7-bb4f-41d5e595aae0)\n\n![a preview of the indicator in the member list](https://github.com/user-attachments/assets/51be081d-7bbb-45c5-8533-d565228e50c1)\n"
  },
  {
    "path": "src/plugins/userVoiceShow/components.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { isPluginEnabled } from \"@api/PluginManager\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport ShowHiddenChannelsPlugin from \"@plugins/showHiddenChannels\";\nimport { classNameFactory } from \"@utils/css\";\nimport { classes } from \"@utils/misc\";\nimport { Channel } from \"@vencord/discord-types\";\nimport { findByPropsLazy, findCssClassesLazy } from \"@webpack\";\nimport { ChannelRouter, ChannelStore, Parser, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, UserSummaryItem, useStateFromStores, VoiceStateStore } from \"@webpack/common\";\nimport { PropsWithChildren } from \"react\";\n\nconst cl = classNameFactory(\"vc-uvs-\");\n\nconst { selectVoiceChannel } = findByPropsLazy(\"selectVoiceChannel\", \"selectChannel\");\n\nconst ActionButtonClasses = findCssClassesLazy(\"actionButton\", \"highlight\");\n\ntype IconProps = Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> & {\n    size?: number;\n};\n\nfunction Icon(props: PropsWithChildren<IconProps>) {\n    const {\n        size = 16,\n        className,\n        ...restProps\n    } = props;\n\n    return (\n        <div\n            {...restProps}\n            className={classes(cl(\"speaker\"), className)}\n        >\n            <svg\n                width={size}\n                height={size}\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n            >\n                {props.children}\n            </svg>\n        </div>\n    );\n}\n\nfunction SpeakerIcon(props: IconProps) {\n    return (\n        <Icon {...props}>\n            <path d=\"M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1V3ZM15.1 20.75c-.58.14-1.1-.33-1.1-.92v-.03c0-.5.37-.92.85-1.05a7 7 0 0 0 0-13.5A1.11 1.11 0 0 1 14 4.2v-.03c0-.6.52-1.06 1.1-.92a9 9 0 0 1 0 17.5Z\" />\n            <path d=\"M15.16 16.51c-.57.28-1.16-.2-1.16-.83v-.14c0-.43.28-.8.63-1.02a3 3 0 0 0 0-5.04c-.35-.23-.63-.6-.63-1.02v-.14c0-.63.59-1.1 1.16-.83a5 5 0 0 1 0 9.02Z\" />\n        </Icon>\n    );\n}\n\nfunction LockedSpeakerIcon(props: IconProps) {\n    return (\n        <Icon {...props}>\n            <path fillRule=\"evenodd\" clipRule=\"evenodd\" d=\"M16 4h.5v-.5a2.5 2.5 0 0 1 5 0V4h.5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Zm4-.5V4h-2v-.5a1 1 0 1 1 2 0Z\" />\n            <path d=\"M11 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1h-.06a1 1 0 0 1-.74-.32L5.92 17H3a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.92l4.28-4.68a1 1 0 0 1 .74-.32H11ZM20.5 12c-.28 0-.5.22-.52.5a7 7 0 0 1-5.13 6.25c-.48.13-.85.55-.85 1.05v.03c0 .6.52 1.06 1.1.92a9 9 0 0 0 6.89-8.25.48.48 0 0 0-.49-.5h-1ZM16.5 12c-.28 0-.5.23-.54.5a3 3 0 0 1-1.33 2.02c-.35.23-.63.6-.63 1.02v.14c0 .63.59 1.1 1.16.83a5 5 0 0 0 2.82-4.01c.02-.28-.2-.5-.48-.5h-1Z\" />\n        </Icon>\n    );\n}\n\nfunction MutedIcon(props: IconProps) {\n    return (\n        <Icon {...props}>\n            <path d=\"m2.7 22.7 20-20a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4ZM10.8 17.32c-.21.21-.1.58.2.62V20H9a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-2v-2.06A8 8 0 0 0 20 10a1 1 0 0 0-2 0c0 1.45-.52 2.79-1.38 3.83l-.02.02A5.99 5.99 0 0 1 12.32 16a.52.52 0 0 0-.34.15l-1.18 1.18ZM15.36 4.52c.15-.15.19-.38.08-.56A4 4 0 0 0 8 6v4c0 .3.03.58.1.86.07.34.49.43.74.18l6.52-6.52ZM5.06 13.98c.16.28.53.31.75.09l.75-.75c.16-.16.19-.4.08-.61A5.97 5.97 0 0 1 6 10a1 1 0 0 0-2 0c0 1.45.39 2.81 1.06 3.98Z\" />\n        </Icon>\n    );\n}\n\nfunction DeafIcon(props: IconProps) {\n    return (\n        <Icon {...props}>\n            <path d=\"M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM17.06 2.94a.48.48 0 0 0-.11-.77A11 11 0 0 0 2.18 16.94c.14.3.53.35.76.12l3.2-3.2c.25-.25.15-.68-.2-.76a5 5 0 0 0-1.02-.1H3.05a9 9 0 0 1 12.66-9.2c.2.09.44.05.59-.1l.76-.76ZM20.2 8.28a.52.52 0 0 1 .1-.58l.76-.76a.48.48 0 0 1 .77.11 11 11 0 0 1-4.5 14.57c-1.27.71-2.73.23-3.55-.74a3.1 3.1 0 0 1-.17-3.78l1.38-1.97a5 5 0 0 1 4.1-2.13h1.86a9.1 9.1 0 0 0-.75-4.72ZM10.1 17.9c.25-.25.65-.18.74.14a3.1 3.1 0 0 1-.62 2.84 2.85 2.85 0 0 1-3.55.74.16.16 0 0 1-.04-.25l3.48-3.48Z\" />\n        </Icon>\n    );\n}\n\ninterface VoiceChannelTooltipProps {\n    channel: Channel;\n    isLocked: boolean;\n}\n\nfunction VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) {\n    const voiceStates = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStatesForChannel(channel.id));\n\n    const users = useMemo(\n        () => Object.values(voiceStates).map(voiceState => UserStore.getUser(voiceState.userId)).filter(user => user != null),\n        [voiceStates]\n    );\n\n    const Icon = isLocked ? LockedSpeakerIcon : SpeakerIcon;\n    return (\n        <>\n            <Text variant=\"text-sm/bold\">In Voice Chat</Text>\n            <Text variant=\"text-sm/bold\">{Parser.parse(`<#${channel.id}>`)}</Text>\n            <div className={cl(\"vc-members\")}>\n                <Icon size={18} />\n                <UserSummaryItem\n                    users={users}\n                    renderIcon={false}\n                    max={13}\n                    size={18}\n                />\n            </div>\n        </>\n    );\n}\n\nexport interface VoiceChannelIndicatorProps {\n    userId: string;\n    isProfile?: boolean;\n    isActionButton?: boolean;\n    shouldHighlight?: boolean;\n}\n\nconst clickTimers = new Map<string, any>();\n\nexport const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isProfile, isActionButton, shouldHighlight }: VoiceChannelIndicatorProps) => {\n    const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId);\n\n    const { isMuted, isDeaf } = useStateFromStores([VoiceStateStore], () => {\n        const voiceState = VoiceStateStore.getVoiceStateForUser(userId);\n        return {\n            isMuted: voiceState?.mute || voiceState?.selfMute || false,\n            isDeaf: voiceState?.deaf || voiceState?.selfDeaf || false\n        };\n    });\n\n    const channel = channelId == null ? undefined : ChannelStore.getChannel(channelId);\n    if (channel == null) return null;\n\n    const isDM = channel.isDM() || channel.isMultiUserDM();\n    if (!isDM && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) && !isPluginEnabled(ShowHiddenChannelsPlugin.name)) return null;\n\n    const isLocked = !isDM && (!PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || !PermissionStore.can(PermissionsBits.CONNECT, channel));\n\n    function onClick(e: React.MouseEvent) {\n        e.preventDefault();\n        e.stopPropagation();\n\n        if (channel == null || channelId == null) return;\n\n        clearTimeout(clickTimers.get(channelId));\n        clickTimers.delete(channelId);\n\n        if (e.detail > 1) {\n            if (!isDM && !PermissionStore.can(PermissionsBits.CONNECT, channel)) {\n                showToast(\"You cannot join the user's Voice Channel\", Toasts.Type.FAILURE);\n                return;\n            }\n\n            selectVoiceChannel(channelId);\n        } else {\n            const timeoutId = setTimeout(() => {\n                ChannelRouter.transitionToChannel(channelId);\n                clickTimers.delete(channelId);\n            }, 250);\n            clickTimers.set(channelId, timeoutId);\n        }\n    }\n\n    const IconComponent =\n        isLocked\n            ? LockedSpeakerIcon\n            : isDeaf\n                ? DeafIcon\n                : isMuted\n                    ? MutedIcon\n                    : SpeakerIcon;\n\n    return (\n        <Tooltip\n            text={<VoiceChannelTooltip channel={channel} isLocked={isLocked} />}\n            tooltipClassName={cl(\"tooltip-container\")}\n            tooltipContentClassName={cl(\"tooltip-content\")}\n        >\n            {props => (\n                <IconComponent\n                    {...props}\n                    role=\"button\"\n                    onClick={onClick}\n                    className={classes(\n                        cl(\"clickable\"),\n                        isActionButton && ActionButtonClasses.actionButton,\n                        isActionButton && shouldHighlight && ActionButtonClasses.highlight,\n                        cl(isProfile && \"profile-speaker\")\n                    )}\n                    size={isActionButton ? 20 : 16}\n                />\n            )}\n        </Tooltip>\n    );\n}, { noop: true });\n"
  },
  {
    "path": "src/plugins/userVoiceShow/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./style.css\";\n\nimport { addMemberListDecorator, removeMemberListDecorator } from \"@api/MemberListDecorators\";\nimport { addMessageDecoration, removeMessageDecoration } from \"@api/MessageDecorations\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nimport { VoiceChannelIndicator } from \"./components\";\n\nconst settings = definePluginSettings({\n    showInUserProfileModal: {\n        type: OptionType.BOOLEAN,\n        description: \"Show a user's Voice Channel indicator in their profile next to the name\",\n        default: true,\n        restartNeeded: true\n    },\n    showInMemberList: {\n        type: OptionType.BOOLEAN,\n        description: \"Show a user's Voice Channel indicator in the member and DMs list\",\n        default: true,\n        restartNeeded: true\n    },\n    showInMessages: {\n        type: OptionType.BOOLEAN,\n        description: \"Show a user's Voice Channel indicator in messages\",\n        default: true,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"UserVoiceShow\",\n    description: \"Shows an indicator when a user is in a Voice Channel\",\n    authors: [Devs.Nuckyz, Devs.LordElias],\n    dependencies: [\"MemberListDecoratorsAPI\", \"MessageDecorationsAPI\"],\n    settings,\n\n    patches: [\n        // User Popout, User Profile Modal, Direct Messages Side Profile\n        {\n            find: \"#{intl::USER_PROFILE_PRONOUNS}\",\n            replacement: {\n                match: /(?<=children:\\[\\i,\" \",\\i)(?=\\])/,\n                replace: \",$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id,isProfile:true})\"\n            },\n            predicate: () => settings.store.showInUserProfileModal\n        },\n        // To use without the MemberList decorator API\n        /* // Guild Members List\n        {\n            find: \".lostPermission)\",\n            replacement: {\n                match: /\\.lostPermission\\).+?(?=avatar:)/,\n                replace: \"$&children:[$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})],\"\n            },\n            predicate: () => settings.store.showVoiceChannelIndicator\n        },\n        // Direct Messages List\n        {\n            find: \"PrivateChannel.renderAvatar\",\n            replacement: {\n                match: /#{intl::CLOSE_DM}.+?}\\)(?=])/,\n                replace: \"$&,$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})\"\n            },\n            predicate: () => settings.store.showVoiceChannelIndicator\n        }, */\n        // Friends List\n        {\n            find: \"null!=this.peopleListItemRef.current\",\n            replacement: {\n                match: /\\.isProvisional.{0,50}?className:\\i\\.\\i,children:\\[(?<=isFocused:(\\i).+?)/,\n                replace: \"$&$self.VoiceChannelIndicator({userId:this?.props?.user?.id,isActionButton:true,shouldHighlight:$1}),\"\n            },\n            predicate: () => settings.store.showInMemberList\n        }\n    ],\n\n    start() {\n        if (settings.store.showInMemberList) {\n            addMemberListDecorator(\"UserVoiceShow\", ({ user }) => user == null ? null : <VoiceChannelIndicator userId={user.id} />);\n        }\n        if (settings.store.showInMessages) {\n            addMessageDecoration(\"UserVoiceShow\", ({ message }) => message?.author == null ? null : <VoiceChannelIndicator userId={message.author.id} />);\n        }\n    },\n\n    stop() {\n        removeMemberListDecorator(\"UserVoiceShow\");\n        removeMessageDecoration(\"UserVoiceShow\");\n    },\n\n    VoiceChannelIndicator\n});\n"
  },
  {
    "path": "src/plugins/userVoiceShow/style.css",
    "content": ".vc-uvs-speaker {\n    color: var(--interactive-icon-default);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.vc-uvs-clickable {\n    cursor: pointer;\n}\n\n.vc-uvs-clickable:hover {\n    color: var(--interactive-icon-hover);\n}\n\n.vc-uvs-profile-speaker {\n    width: var(--custom-nickname-icon-size);\n    height: var(--custom-nickname-icon-size);\n}\n\n.vc-uvs-tooltip-container {\n    max-width: 50vw;\n}\n\n.vc-uvs-tooltip-content {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n}\n\n.vc-uvs-name {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.vc-uvs-guild-icon {\n    border-radius: 100%;\n    align-self: center;\n}\n\n.vc-uvs-vc-members {\n    display: flex;\n    gap: 6px;\n}"
  },
  {
    "path": "src/plugins/usrbg/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { LinkButton } from \"@components/Button\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\n\nconst API_URL = \"https://usrbg.is-hardly.online/users\";\n\ninterface UsrbgApiReturn {\n    endpoint: string;\n    bucket: string;\n    prefix: string;\n    users: Record<string, string>;\n}\n\nconst settings = definePluginSettings({\n    nitroFirst: {\n        description: \"Banner to use if both Nitro and USRBG banners are present\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Nitro banner\", value: true, default: true },\n            { label: \"USRBG banner\", value: false },\n        ]\n    },\n    voiceBackground: {\n        description: \"Use USRBG banners as voice chat backgrounds\",\n        type: OptionType.BOOLEAN,\n        default: true,\n        restartNeeded: true\n    }\n});\n\nexport default definePlugin({\n    name: \"USRBG\",\n    description: \"Displays user banners from USRBG, allowing anyone to get a banner without Nitro\",\n    authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad],\n    settings,\n    patches: [\n        {\n            find: ':\"SHOULD_LOAD\");',\n            replacement: {\n                match: /\\i(?:\\?)?.getPreviewBanner\\(\\i,\\i,\\i\\)(?=.{0,100}\"COMPLETE\")/,\n                replace: \"$self.patchBannerUrl(arguments[0])||$&\"\n\n            }\n        },\n        {\n            find: \"\\\"data-selenium-video-tile\\\":\",\n            predicate: () => settings.store.voiceBackground,\n            replacement: [\n                {\n                    match: /(?<=function\\((\\i),\\i\\)\\{)(?=let.{20,40},style:)/,\n                    replace: \"$1.style=$self.getVoiceBackgroundStyles($1);\"\n                }\n            ]\n        },\n        {\n            find: '\"VideoBackground-web\"',\n            predicate: () => settings.store.voiceBackground,\n            replacement: {\n                match: /backgroundColor:.{0,25},\\{style:(?=\\i\\?)/,\n                replace: \"$&$self.userHasBackground(arguments[0]?.userId)?null:\",\n            }\n        }\n    ],\n\n    data: null as UsrbgApiReturn | null,\n\n    settingsAboutComponent: () => {\n        return (\n            <LinkButton href=\"https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner\" variant=\"primary\">\n                Get your own USRBG banner\n            </LinkButton>\n        );\n    },\n\n    getVoiceBackgroundStyles({ className, participantUserId }: any) {\n        if (className.includes(\"tile\")) {\n            if (this.userHasBackground(participantUserId)) {\n                return {\n                    backgroundImage: `url(${this.getImageUrl(participantUserId)})`,\n                    backgroundSize: \"cover\",\n                    backgroundPosition: \"center\",\n                    backgroundRepeat: \"no-repeat\"\n                };\n            }\n        }\n    },\n\n    patchBannerUrl({ displayProfile }: any) {\n        if (displayProfile?.banner && settings.store.nitroFirst) return;\n        if (this.userHasBackground(displayProfile?.userId)) return this.getImageUrl(displayProfile?.userId);\n    },\n\n    userHasBackground(userId: string) {\n        return !!this.data?.users[userId];\n    },\n\n    getImageUrl(userId: string): string | null {\n        if (!this.userHasBackground(userId)) return null;\n\n        // We can assert that data exists because userHasBackground returned true\n        const { endpoint, bucket, prefix, users: { [userId]: etag } } = this.data!;\n        return `${endpoint}/${bucket}/${prefix}${userId}?${etag}`;\n    },\n\n    async start() {\n        const res = await fetch(API_URL);\n        if (res.ok) {\n            this.data = await res.json();\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/validReply/README.md",
    "content": "# ValidReply\n\nFixes referenced (replied to) messages showing as \"Message could not be loaded\".\n\nHover the text to load the message!\n\n![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af)\n"
  },
  {
    "path": "src/plugins/validReply/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { Channel, Message, User } from \"@vencord/discord-types\";\nimport { findByCodeLazy } from \"@webpack\";\nimport { FluxDispatcher, RestAPI } from \"@webpack/common\";\n\nconst enum ReferencedMessageState {\n    Loaded,\n    NotLoaded,\n    Deleted\n}\n\ninterface Reply {\n    baseAuthor: User,\n    baseMessage: Message;\n    channel: Channel;\n    referencedMessage: { state: ReferencedMessageState; };\n    compact: boolean;\n    isReplyAuthorBlocked: boolean;\n}\n\nconst fetching = new Map<string, string>();\nlet ReplyStore: any;\n\nconst createMessageRecord = findByCodeLazy(\".createFromServer(\", \".isBlockedForMessage\", \"messageReference:\");\n\nexport default definePlugin({\n    name: \"ValidReply\",\n    description: 'Fixes \"Message could not be loaded\" upon hovering over the reply',\n    authors: [Devs.newwares],\n    patches: [\n        {\n            // Same find as in ReplyTimestamp\n            find: \"#{intl::REPLY_QUOTE_MESSAGE_NOT_LOADED}\",\n            replacement: {\n                match: /#{intl::REPLY_QUOTE_MESSAGE_NOT_LOADED}\\)/,\n                replace: \"$&,onMouseEnter:()=>$self.fetchReply(arguments[0])\"\n            }\n        },\n        {\n            find: \"ReferencedMessageStore\",\n            replacement: [\n                {\n                    match: /constructor\\(\\)\\{\\i\\(this,\"_channelCaches\",new Map\\)/,\n                    replace: \"$&;$self.setReplyStore(this);\",\n                    noWarn: true // TODO: remove legacy compatibility code in the future\n                },\n                {\n                    match: /_channelCaches=new Map;/,\n                    replace: \"$&_=$self.setReplyStore(this);\"\n                }\n            ]\n        }\n    ],\n\n    setReplyStore(store: any) {\n        ReplyStore = store;\n    },\n\n    async fetchReply(reply: Reply) {\n        const { channel_id: channelId, message_id: messageId } = reply.baseMessage.messageReference!;\n\n        if (fetching.has(messageId)) {\n            return;\n        }\n        fetching.set(messageId, channelId);\n\n        RestAPI.get({\n            url: `/channels/${channelId}/messages`,\n            query: {\n                limit: 1,\n                around: messageId\n            },\n            retries: 2\n        })\n            .then(res => {\n                const reply: Message | undefined = res?.body?.[0];\n                if (!reply) return;\n\n                if (reply.id !== messageId) {\n                    ReplyStore.set(channelId, messageId, {\n                        state: ReferencedMessageState.Deleted\n                    });\n\n                    FluxDispatcher.dispatch({\n                        type: \"MESSAGE_DELETE\",\n                        channelId: channelId,\n                        message: messageId\n                    });\n                } else {\n                    ReplyStore.set(reply.channel_id, reply.id, {\n                        state: ReferencedMessageState.Loaded,\n                        message: createMessageRecord(reply)\n                    });\n\n                    FluxDispatcher.dispatch({\n                        type: \"MESSAGE_UPDATE\",\n                        message: reply\n                    });\n                }\n            })\n            .catch(() => { })\n            .finally(() => {\n                fetching.delete(messageId);\n            });\n    }\n});\n"
  },
  {
    "path": "src/plugins/validUser/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { isNonNullish } from \"@utils/guards\";\nimport { sleep } from \"@utils/misc\";\nimport { Queue } from \"@utils/Queue\";\nimport definePlugin from \"@utils/types\";\nimport { ProfileBadge } from \"@vencord/discord-types\";\nimport { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from \"@webpack/common\";\nimport { type ComponentType, type ReactNode } from \"react\";\n\n// LYING to the type checker here\nconst UserFlags = Constants.UserFlags as Record<string, number>;\nconst badges: Record<string, ProfileBadge> = {\n    active_developer: { id: \"active_developer\", description: \"Active Developer\", icon: \"6bdc42827a38498929a4920da12695d9\", link: \"https://support-dev.discord.com/hc/en-us/articles/10113997751447\" },\n    bug_hunter_level_1: { id: \"bug_hunter_level_1\", description: \"Discord Bug Hunter\", icon: \"2717692c7dca7289b35297368a940dd0\", link: \"https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs\" },\n    bug_hunter_level_2: { id: \"bug_hunter_level_2\", description: \"Discord Bug Hunter\", icon: \"848f79194d4be5ff5f81505cbd0ce1e6\", link: \"https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs\" },\n    certified_moderator: { id: \"certified_moderator\", description: \"Moderator Programs Alumni\", icon: \"fee1624003e2fee35cb398e125dc479b\", link: \"https://discord.com/safety\" },\n    discord_employee: { id: \"staff\", description: \"Discord Staff\", icon: \"5e74e9b61934fc1f67c65515d1f7e60d\", link: \"https://discord.com/company\" },\n    get staff() { return this.discord_employee; },\n    hypesquad: { id: \"hypesquad\", description: \"HypeSquad Events\", icon: \"bf01d1073931f921909045f3a39fd264\", link: \"https://discord.com/hypesquad\" },\n    hypesquad_online_house_1: { id: \"hypesquad_house_1\", description: \"HypeSquad Bravery\", icon: \"8a88d63823d8a71cd5e390baa45efa02\", link: \"https://discord.com/settings/hypesquad-online\" },\n    hypesquad_online_house_2: { id: \"hypesquad_house_2\", description: \"HypeSquad Brilliance\", icon: \"011940fd013da3f7fb926e4a1cd2e618\", link: \"https://discord.com/settings/hypesquad-online\" },\n    hypesquad_online_house_3: { id: \"hypesquad_house_3\", description: \"HypeSquad Balance\", icon: \"3aa41de486fa12454c3761e8e223442e\", link: \"https://discord.com/settings/hypesquad-online\" },\n    partner: { id: \"partner\", description: \"Partnered Server Owner\", icon: \"3f9748e53446a137a052f3454e2de41e\", link: \"https://discord.com/partners\" },\n    premium: { id: \"premium\", description: \"Subscriber\", icon: \"2ba85e8026a8614b640c2837bcdfe21b\", link: \"https://discord.com/settings/premium\" },\n    premium_early_supporter: { id: \"early_supporter\", description: \"Early Supporter\", icon: \"7060786766c9c840eb3019e725d2b358\", link: \"https://discord.com/settings/premium\" },\n    verified_developer: { id: \"verified_developer\", description: \"Early Verified Bot Developer\", icon: \"6df5892e0f35b051f8b61eace34f4967\" },\n};\n\nconst fetching = new Set<string>();\nconst queue = new Queue(5);\n\ninterface MentionProps {\n    data: {\n        userId?: string;\n        channelId?: string;\n        content: any;\n    };\n    parse: (content: any, props: MentionProps[\"props\"]) => ReactNode;\n    props: {\n        key: string;\n        formatInline: boolean;\n        noStyleAndInteraction: boolean;\n    };\n    RoleMention: ComponentType<any>;\n    UserMention: ComponentType<any>;\n}\n\nasync function getUser(id: string) {\n    let userObj = UserStore.getUser(id);\n    if (userObj)\n        return userObj;\n\n    const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {\n        FluxDispatcher.dispatch({\n            type: \"USER_UPDATE\",\n            user: response.body,\n        });\n\n        return response.body;\n    });\n\n    // Populate the profile\n    await FluxDispatcher.dispatch(\n        {\n            type: \"USER_PROFILE_FETCH_FAILURE\",\n            userId: id,\n        }\n    );\n\n    userObj = UserStore.getUser(id);\n    const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)\n        .filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))\n        .map(([key]) => badges[key.toLowerCase()])\n        .filter(isNonNullish);\n    if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.(\"a_\")))\n        fakeBadges.push(badges.premium);\n\n    // Fill in what we can deduce\n    const profile = UserProfileStore.getUserProfile(id);\n    if (profile) {\n        profile.accentColor = user.accent_color;\n        profile.badges = fakeBadges;\n        profile.banner = user.banner;\n        profile.premiumType = user.premium_type;\n    }\n\n    return userObj;\n}\n\nfunction MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) {\n    const [userId, setUserId] = useState(data.userId);\n\n    // if userId is set it means the user is cached. Uncached users have userId set to undefined\n    if (userId)\n        return (\n            <UserMention\n                className=\"mention\"\n                userId={userId}\n                channelId={data.channelId}\n                inlinePreview={props.noStyleAndInteraction}\n                key={props.key}\n            />\n        );\n\n    // Parses the raw text node array data.content into a ReactNode[]: [\"<@userid>\"]\n    const children = parse(data.content, props);\n\n    return (\n        // Discord is deranged and renders unknown user mentions as role mentions\n        <RoleMention\n            {...data}\n            inlinePreview={props.formatInline}\n        >\n            <span\n                onMouseEnter={() => {\n                    const mention = children?.[0]?.props?.children;\n                    if (typeof mention !== \"string\") return;\n\n                    const id = mention.match(/<@!?(\\d+)>/)?.[1];\n                    if (!id) return;\n\n                    if (fetching.has(id))\n                        return;\n\n                    if (UserStore.getUser(id))\n                        return setUserId(id);\n\n                    const fetch = () => {\n                        fetching.add(id);\n\n                        queue.unshift(() =>\n                            getUser(id)\n                                .then(() => {\n                                    setUserId(id);\n                                    fetching.delete(id);\n                                })\n                                .catch(e => {\n                                    if (e?.status === 429) {\n                                        queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch));\n                                        fetching.delete(id);\n                                    }\n                                })\n                                .finally(() => sleep(300))\n                        );\n                    };\n\n                    fetch();\n                }}\n            >\n                {children}\n            </span>\n        </RoleMention>\n    );\n}\n\nexport default definePlugin({\n    name: \"ValidUser\",\n    description: \"Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)\",\n    authors: [Devs.Ven, Devs.Dolfies],\n    tags: [\"MentionCacheFix\"],\n\n    patches: [\n        {\n            find: 'className:\"mention\"',\n            replacement: {\n                // mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention()\n                match: /react(?=\\(\\i,\\i,\\i\\).{0,100}return null==.{0,70}\\?\\(0,\\i\\.jsx\\)\\((\\i\\.\\i),.+?jsx\\)\\((\\i\\.\\i),\\{className:\"mention\")/,\n                // react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc\n                replace: \"react:(...args)=>$self.renderMention($1,$2,...args),originalReact\"\n            }\n        },\n        {\n            find: \"unknownUserMentionPlaceholder:\",\n            replacement: {\n                match: /unknownUserMentionPlaceholder:/,\n                replace: \"$&false&&\"\n            }\n        }\n    ],\n\n    renderMention(RoleMention, UserMention, data, parse, props) {\n        return (\n            <ErrorBoundary noop>\n                <MentionWrapper\n                    key={\"mention\" + data.userId}\n                    RoleMention={RoleMention}\n                    UserMention={UserMention}\n                    data={data}\n                    parse={parse}\n                    props={props}\n                />\n            </ErrorBoundary>\n        );\n    },\n});\n"
  },
  {
    "path": "src/plugins/vcDoubleClick/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { ChannelStore, SelectedChannelStore } from \"@webpack/common\";\n\nconst timers = {} as Record<string, {\n    timeout?: NodeJS.Timeout;\n    i: number;\n}>;\n\nexport default definePlugin({\n    name: \"VoiceChatDoubleClick\",\n    description: \"Join voice chats via double click instead of single click\",\n    authors: [Devs.Ven, Devs.D3SOX],\n    patches: [\n        ...[\n            \".handleVoiceStatusClick\", // voice channels\n            \".handleClickChat\" // stage channels\n        ].map(find => ({\n            find,\n            // hack: these are not React onClick, it is a custom prop handled by Discord\n            // thus, replacing this with onDoubleClick won't work, and you also cannot check\n            // e.detail since instead of the event they pass the channel.\n            // do this timer workaround instead\n            replacement: [\n                {\n                    match: /onClick:\\(\\)=>\\{this.handleClick\\(\\)/g,\n                    replace: \"onClick:()=>{$self.schedule(()=>{this.handleClick()},this)\",\n                },\n            ]\n        })),\n        {\n            // channel mentions\n            find: 'className:\"channelMention\",children',\n            replacement: {\n                match: /onClick:(\\i)(?=,.{0,30}className:\"channelMention\".+?(\\i)\\.inContent)/,\n                replace: (_, onClick, props) => \"\"\n                    + `onClick:(vcDoubleClickEvt)=>$self.shouldRunOnClick(vcDoubleClickEvt,${props})&&${onClick}()`,\n            }\n        }\n    ],\n\n    shouldRunOnClick(e: MouseEvent, { channelId }) {\n        const channel = ChannelStore.getChannel(channelId);\n        if (!channel || ![2, 13].includes(channel.type)) return true;\n        return e.detail >= 2;\n    },\n\n    schedule(cb: () => void, e: any) {\n        const id = e.props.channel.id as string;\n        if (SelectedChannelStore.getVoiceChannelId() === id) {\n            cb();\n            return;\n        }\n        // use a different counter for each channel\n        const data = (timers[id] ??= { timeout: void 0, i: 0 });\n        // clear any existing timer\n        clearTimeout(data.timeout);\n\n        // if we already have 2 or more clicks, run the callback immediately\n        if (++data.i >= 2) {\n            cb();\n            delete timers[id];\n        } else {\n            // else reset the counter in 500ms\n            data.timeout = setTimeout(() => {\n                delete timers[id];\n            }, 500);\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/vcNarrator/VoiceSetting.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Forms, SearchableSelect, useMemo, useState } from \"@webpack/common\";\n\nimport { getCurrentVoice, settings } from \"./settings\";\n\n// TODO: replace by [Object.groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) once it has more maturity\n\nfunction groupBy<T extends object, K extends PropertyKey>(arr: T[], fn: (obj: T) => K) {\n    return arr.reduce((acc, obj) => {\n        const value = fn(obj);\n        acc[value] ??= [];\n        acc[value].push(obj);\n        return acc;\n    }, {} as Record<K, T[]>);\n}\n\ninterface PickerProps {\n    voice: string | undefined;\n    voices: SpeechSynthesisVoice[];\n}\n\nfunction SimplePicker({ voice, voices }: PickerProps) {\n    const options = voices.map(voice => ({\n        label: voice.name,\n        value: voice.voiceURI,\n        default: voice.default,\n    }));\n\n    return (\n        <SearchableSelect\n            placeholder=\"Select a voice\"\n            maxVisibleItems={5}\n            options={options}\n            value={options.find(o => o.value === voice)?.value}\n            onChange={v => settings.store.voice = v}\n            closeOnSelect\n        />\n    );\n}\n\nconst languageNames = new Intl.DisplayNames([\"en\"], { type: \"language\" });\n\nfunction ComplexPicker({ voice, voices }: PickerProps) {\n    const groupedVoices = useMemo(() => groupBy(voices, voice => voice.lang), [voices]);\n\n    const languageNameMapping = useMemo(() => {\n        const list = [] as Record<\"name\" | \"friendlyName\", string>[];\n\n        for (const name in groupedVoices) {\n            try {\n                const friendlyName = languageNames.of(name);\n                if (friendlyName) {\n                    list.push({ name, friendlyName });\n                }\n            } catch { }\n        }\n\n        return list;\n    }, [groupedVoices]);\n\n    const [selectedLanguage, setSelectedLanguage] = useState(() => getCurrentVoice()?.lang ?? languageNameMapping[0].name);\n\n    if (languageNameMapping.length === 1) {\n        return (\n            <SimplePicker\n                voice={voice}\n                voices={groupedVoices[languageNameMapping[0].name]}\n            />\n        );\n    }\n\n    const voicesForLanguage = groupedVoices[selectedLanguage];\n\n    const languageOptions = languageNameMapping.map(l => ({\n        label: l.friendlyName,\n        value: l.name\n    }));\n\n    return (\n        <>\n            <Forms.FormTitle>Language</Forms.FormTitle>\n            <SearchableSelect\n                placeholder=\"Select a language\"\n                options={languageOptions}\n                value={languageOptions.find(l => l.value === selectedLanguage)?.value}\n                onChange={v => setSelectedLanguage(v)}\n                maxVisibleItems={5}\n                closeOnSelect\n            />\n            <Forms.FormTitle>Voice</Forms.FormTitle>\n            <SimplePicker\n                voice={voice}\n                voices={voicesForLanguage}\n            />\n        </>\n    );\n}\n\n\nfunction VoiceSetting() {\n    const voices = useMemo(() => window.speechSynthesis?.getVoices() ?? [], []);\n    const { voice } = settings.use([\"voice\"]);\n\n    if (!voices.length)\n        return <Forms.FormText>No voices found.</Forms.FormText>;\n\n    // espeak on Linux has a ridiculous amount of voices (26k for me).\n    // If there are more than 20 voices, we split it up into two pickers, one for language, then one with only the voices for that language.\n    // This way, there are around 200-ish options per language\n    const Picker = voices.length > 20 ? ComplexPicker : SimplePicker;\n    return <Picker voice={voice} voices={voices} />;\n}\n\nexport function VoiceSettingSection() {\n    return (\n        <section>\n            <Forms.FormTitle>Voice</Forms.FormTitle>\n            <VoiceSetting />\n        </section>\n    );\n}\n"
  },
  {
    "path": "src/plugins/vcNarrator/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ErrorCard } from \"@components/ErrorCard\";\nimport { Devs, IS_LINUX } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport { Margins } from \"@utils/margins\";\nimport { wordsToTitle } from \"@utils/text\";\nimport definePlugin, { ReporterTestable } from \"@utils/types\";\nimport { AuthenticationStore, Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore, VoiceStateStore } from \"@webpack/common\";\nimport { ReactElement } from \"react\";\n\nimport { getCurrentVoice, settings } from \"./settings\";\n\ninterface VoiceStateChangeEvent {\n    userId: string;\n    channelId?: string;\n    oldChannelId?: string;\n    deaf: boolean;\n    mute: boolean;\n    selfDeaf: boolean;\n    selfMute: boolean;\n    sessionId: string;\n}\n\n// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying\n// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would\n// not say the second mute, which would lead you to believe they're unmuted\n\nfunction speak(text: string) {\n    if (!text) return;\n\n    const { volume, rate } = settings.store;\n\n    const speech = new SpeechSynthesisUtterance(text);\n    const voice = getCurrentVoice();\n    speech.voice = voice!;\n    speech.volume = volume;\n    speech.rate = rate;\n    speechSynthesis.speak(speech);\n}\n\nfunction clean(str: string) {\n    const replacer = settings.store.latinOnly\n        ? /[^\\p{Script=Latin}\\p{Number}\\p{Punctuation}\\s]/gu\n        : /[^\\p{Letter}\\p{Number}\\p{Punctuation}\\s]/gu;\n\n    return str.normalize(\"NFKC\")\n        .replace(replacer, \"\")\n        .replace(/_{2,}/g, \"_\")\n        .trim();\n}\n\nfunction formatText(str: string, user: string, channel: string, displayName: string, nickname: string) {\n    return str\n        .replaceAll(\"{{USER}}\", clean(user) || (user ? \"Someone\" : \"\"))\n        .replaceAll(\"{{CHANNEL}}\", clean(channel) || \"channel\")\n        .replaceAll(\"{{DISPLAY_NAME}}\", clean(displayName) || (displayName ? \"Someone\" : \"\"))\n        .replaceAll(\"{{NICKNAME}}\", clean(nickname) || (nickname ? \"Someone\" : \"\"));\n}\n\n/*\nlet StatusMap = {} as Record<string, {\n    mute: boolean;\n    deaf: boolean;\n}>;\n*/\n\n// For every user, channelId and oldChannelId will differ when moving channel.\n// Only for the local user, channelId and oldChannelId will be the same when moving channel,\n// for some ungodly reason\nlet myLastChannelId: string | undefined;\n\nfunction getTypeAndChannelId({ channelId, oldChannelId }: VoiceStateChangeEvent, isMe: boolean) {\n    if (isMe && channelId !== myLastChannelId) {\n        oldChannelId = myLastChannelId;\n        myLastChannelId = channelId;\n    }\n\n    if (channelId !== oldChannelId) {\n        if (channelId) return [oldChannelId ? \"move\" : \"join\", channelId];\n        if (oldChannelId) return [\"leave\", oldChannelId];\n    }\n    /*\n    if (channelId) {\n        if (deaf || selfDeaf) return [\"deafen\", channelId];\n        if (mute || selfMute) return [\"mute\", channelId];\n        const oldStatus = StatusMap[userId];\n        if (oldStatus.deaf) return [\"undeafen\", channelId];\n        if (oldStatus.mute) return [\"unmute\", channelId];\n    }\n    */\n    return [\"\", \"\"];\n}\n\n/*\nfunction updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId, channelId }: VoiceState, isMe: boolean) {\n    if (isMe && (type === \"join\" || type === \"move\")) {\n        StatusMap = {};\n        const states = VoiceStateStore.getVoiceStatesForChannel(channelId!) as Record<string, VoiceState>;\n        for (const userId in states) {\n            const s = states[userId];\n            StatusMap[userId] = {\n                mute: s.mute || s.selfMute,\n                deaf: s.deaf || s.selfDeaf\n            };\n        }\n        return;\n    }\n\n    if (type === \"leave\" || (type === \"move\" && channelId !== SelectedChannelStore.getVoiceChannelId())) {\n        if (isMe)\n            StatusMap = {};\n        else\n            delete StatusMap[userId];\n\n        return;\n    }\n\n    StatusMap[userId] = {\n        deaf: deaf || selfDeaf,\n        mute: mute || selfMute\n    };\n}\n*/\n\nfunction playSample(type: string) {\n    const currentUser = UserStore.getCurrentUser();\n    const myGuildId = SelectedGuildStore.getGuildId();\n\n    speak(formatText(\n        settings.store[type + \"Message\"],\n        currentUser.username,\n        \"general\",\n        currentUser.globalName ?? currentUser.username,\n        GuildMemberStore.getNick(myGuildId!, currentUser.id) ?? currentUser.username\n    ));\n}\n\nexport default definePlugin({\n    name: \"VcNarrator\",\n    description: \"Announces when users join, leave, or move voice channels via narrator\",\n    authors: [Devs.Ven],\n    reporterTestable: ReporterTestable.None,\n\n    settings,\n\n    flux: {\n        VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceStateChangeEvent[]; }) {\n            const myGuildId = SelectedGuildStore.getGuildId();\n            const myChanId = SelectedChannelStore.getVoiceChannelId();\n            const myId = UserStore.getCurrentUser().id;\n\n            if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return;\n\n            for (const state of voiceStates) {\n                const { userId, channelId, oldChannelId } = state;\n                const isMe = userId === myId;\n                if (isMe && state.sessionId !== AuthenticationStore.getSessionId()) continue;\n                if (!isMe) {\n                    if (!myChanId) continue;\n                    if (channelId !== myChanId && oldChannelId !== myChanId) continue;\n                }\n\n                const [type, id] = getTypeAndChannelId(state, isMe);\n                if (!type) continue;\n\n                const template = settings.store[type + \"Message\"];\n                const user = isMe && !settings.store.sayOwnName ? \"\" : UserStore.getUser(userId).username;\n                const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);\n                const nickname = user && (GuildMemberStore.getNick(myGuildId!, userId) ?? displayName);\n                const channel = ChannelStore.getChannel(id).name;\n\n                speak(formatText(template, user, channel, displayName, nickname));\n\n                // updateStatuses(type, state, isMe);\n            }\n        },\n\n        AUDIO_TOGGLE_SELF_MUTE() {\n            const chanId = SelectedChannelStore.getVoiceChannelId()!;\n            const s = VoiceStateStore.getVoiceStateForChannel(chanId);\n            if (!s) return;\n\n            const event = s.mute || s.selfMute ? \"unmute\" : \"mute\";\n            speak(formatText(settings.store[event + \"Message\"], \"\", ChannelStore.getChannel(chanId).name, \"\", \"\"));\n        },\n\n        AUDIO_TOGGLE_SELF_DEAF() {\n            const chanId = SelectedChannelStore.getVoiceChannelId()!;\n            const s = VoiceStateStore.getVoiceStateForChannel(chanId);\n            if (!s) return;\n\n            const event = s.deaf || s.selfDeaf ? \"undeafen\" : \"deafen\";\n            speak(formatText(settings.store[event + \"Message\"], \"\", ChannelStore.getChannel(chanId).name, \"\", \"\"));\n        }\n    },\n\n    start() {\n        if (typeof speechSynthesis === \"undefined\" || speechSynthesis.getVoices().length === 0) {\n            new Logger(\"VcNarrator\").warn(\n                \"SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info\"\n            );\n            return;\n        }\n\n    },\n\n    settingsAboutComponent() {\n        const [hasVoices, hasEnglishVoices] = useMemo(() => {\n            const voices = speechSynthesis.getVoices();\n            return [voices.length !== 0, voices.some(v => v.lang.startsWith(\"en\"))];\n        }, []);\n\n        const types = useMemo(\n            () => Object.keys(settings.def).filter(k => k.endsWith(\"Message\")).map(k => k.slice(0, -7)),\n            [],\n        );\n\n        let errorComponent: ReactElement<any> | null = null;\n        if (!hasVoices) {\n            let error = \"No narrator voices found. \";\n            error += IS_LINUX\n                ? \"Install speech-dispatcher or espeak and run Discord with the --enable-speech-dispatcher flag\"\n                : \"Try installing some in the Narrator settings of your Operating System\";\n            errorComponent = <ErrorCard>{error}</ErrorCard>;\n        } else if (!hasEnglishVoices) {\n            errorComponent = <ErrorCard>You don't have any English voices installed, so the narrator might sound weird</ErrorCard>;\n        }\n\n        return (\n            <section>\n                <Forms.FormText>\n                    You can customise the spoken messages below. You can disable specific messages by setting them to nothing\n                </Forms.FormText>\n                <Forms.FormText>\n                    The special placeholders <code>{\"{{USER}}\"}</code>, <code>{\"{{DISPLAY_NAME}}\"}</code>, <code>{\"{{NICKNAME}}\"}</code> and <code>{\"{{CHANNEL}}\"}</code>{\" \"}\n                    will be replaced with the user's name (nothing if it's yourself), the user's display name, the user's nickname on current server and the channel's name respectively\n                </Forms.FormText>\n                {hasEnglishVoices && (\n                    <>\n                        <Forms.FormTitle className={Margins.top20} tag=\"h3\">Play Example Sounds</Forms.FormTitle>\n                        <div\n                            style={{\n                                display: \"grid\",\n                                gridTemplateColumns: \"repeat(4, 1fr)\",\n                                gap: \"1rem\",\n                            }}\n                            className={\"vc-narrator-buttons\"}\n                        >\n                            {types.map(t => (\n                                <Button key={t} onClick={() => playSample(t)}>\n                                    {wordsToTitle([t])}\n                                </Button>\n                            ))}\n                        </div>\n                    </>\n                )}\n                {errorComponent}\n            </section>\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/vcNarrator/settings.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Logger } from \"@utils/Logger\";\nimport { OptionType } from \"@utils/types\";\n\nimport { VoiceSettingSection } from \"./VoiceSetting\";\n\nexport const getDefaultVoice = () => window.speechSynthesis?.getVoices().find(v => v.default);\n\nexport function getCurrentVoice(voices = window.speechSynthesis?.getVoices()) {\n    if (!voices) return undefined;\n\n    if (settings.store.voice) {\n        const voice = voices.find(v => v.voiceURI === settings.store.voice);\n        if (voice) return voice;\n\n        new Logger(\"VcNarrator\").error(`Voice \"${settings.store.voice}\" not found. Resetting to default.`);\n    }\n\n    const voice = voices.find(v => v.default);\n    settings.store.voice = voice?.voiceURI;\n    return voice;\n}\n\nexport const settings = definePluginSettings({\n    voice: {\n        type: OptionType.COMPONENT,\n        component: VoiceSettingSection,\n        get default() {\n            return getDefaultVoice()?.voiceURI;\n        }\n    },\n    volume: {\n        type: OptionType.SLIDER,\n        description: \"Narrator Volume\",\n        default: 1,\n        markers: [0, 0.25, 0.5, 0.75, 1],\n        stickToMarkers: false\n    },\n    rate: {\n        type: OptionType.SLIDER,\n        description: \"Narrator Speed\",\n        default: 1,\n        markers: [0.1, 0.5, 1, 2, 5, 10],\n        stickToMarkers: false\n    },\n    sayOwnName: {\n        description: \"Say own name\",\n        type: OptionType.BOOLEAN,\n        default: false\n    },\n    latinOnly: {\n        description: \"Strip non latin characters from names before saying them\",\n        type: OptionType.BOOLEAN,\n        default: false\n    },\n    joinMessage: {\n        type: OptionType.STRING,\n        description: \"Join Message\",\n        default: \"{{USER}} joined\"\n    },\n    leaveMessage: {\n        type: OptionType.STRING,\n        description: \"Leave Message\",\n        default: \"{{USER}} left\"\n    },\n    moveMessage: {\n        type: OptionType.STRING,\n        description: \"Move Message\",\n        default: \"{{USER}} moved to {{CHANNEL}}\"\n    },\n    muteMessage: {\n        type: OptionType.STRING,\n        description: \"Mute Message (only self for now)\",\n        default: \"{{USER}} muted\"\n    },\n    unmuteMessage: {\n        type: OptionType.STRING,\n        description: \"Unmute Message (only self for now)\",\n        default: \"{{USER}} unmuted\"\n    },\n    deafenMessage: {\n        type: OptionType.STRING,\n        description: \"Deafen Message (only self for now)\",\n        default: \"{{USER}} deafened\"\n    },\n    undeafenMessage: {\n        type: OptionType.STRING,\n        description: \"Undeafen Message (only self for now)\",\n        default: \"{{USER}} undeafened\"\n    }\n});\n"
  },
  {
    "path": "src/plugins/vencordToolbox/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { findComponentByCodeLazy } from \"@webpack\";\nimport { Popout, useRef, useState } from \"@webpack/common\";\nimport type { PropsWithChildren } from \"react\";\n\nimport { renderPopout } from \"./menu\";\n\nconst HeaderBarIcon = findComponentByCodeLazy(\".HEADER_BAR_BADGE_BOTTOM,\", 'position:\"bottom\"');\n\nexport const settings = definePluginSettings({\n    showPluginMenu: {\n        type: OptionType.BOOLEAN,\n        default: true,\n        description: \"Show the plugins menu in the toolbox\",\n    }\n});\n\nfunction Icon({ isShown }: { isShown: boolean; }) {\n    return (\n        <svg viewBox=\"0 0 27 27\" width={18} height={18} className=\"vc-toolbox-icon\">\n            {isShown\n                ? <path fill=\"currentColor\" d=\"M9 0h1v1h1v2h1v2h3V3h1V1h1V0h1v2h1v2h1v7h-1v-1h-3V9h1V6h-1v4h-3v1h1v-1h2v1h3v1h-1v1h-3v2h1v1h1v1h1v3h-1v4h-2v-1h-1v-4h-1v4h-1v1h-2v-4H9v-3h1v-1h1v-1h1v-2H9v-1H8v-1h3V6h-1v3h1v1H8v1H7V4h1V2h1M5 19h2v1h1v1h1v3H4v-1h2v-1H4v-2h1m15-1h2v1h1v2h-2v1h2v1h-5v-3h1v-1h1m4 3h4v1h-4\" />\n                : <path fill=\"currentColor\" d=\"M0 0h7v1H6v1H5v1H4v1H3v1H2v1h5v1H0V6h1V5h1V4h1V3h1V2h1V1H0m13 2h5v1h-1v1h-1v1h-1v1h3v1h-5V7h1V6h1V5h1V4h-3m8 5h1v5h1v-1h1v1h-1v1h1v-1h1v1h-1v3h-1v1h-2v1h-1v1h1v-1h2v-1h1v2h-1v1h-2v1h-1v-1h-1v1h-6v-1h-1v-1h-1v-2h1v1h2v1h3v1h1v-1h-1v-1h-3v-1h-4v-4h1v-2h1v-1h1v-1h1v2h1v1h1v-1h1v1h-1v1h2v-2h1v-2h1v-1h1M8 14h2v1H9v4h1v2h1v1h1v1h1v1h4v1h-6v-1H5v-1H4v-5h1v-1h1v-2h2m17 3h1v3h-1v1h-1v1h-1v2h-2v-2h2v-1h1v-1h1m1 0h1v3h-1v1h-2v-1h1v-1h1\" />\n            }\n        </svg>\n    );\n}\n\nfunction VencordPopoutButton() {\n    const buttonRef = useRef(null);\n    const [show, setShow] = useState(false);\n\n    return (\n        <Popout\n            position=\"bottom\"\n            align=\"right\"\n            animation={Popout.Animation.NONE}\n            shouldShow={show}\n            onRequestClose={() => setShow(false)}\n            targetElementRef={buttonRef}\n            renderPopout={() => renderPopout(() => setShow(false))}\n        >\n            {(_, { isShown }) => (\n                <HeaderBarIcon\n                    ref={buttonRef}\n                    className=\"vc-toolbox-btn\"\n                    onClick={() => setShow(v => !v)}\n                    tooltip={isShown ? null : \"Vencord Toolbox\"}\n                    icon={() => <Icon isShown={isShown} />}\n                    selected={isShown}\n                />\n            )}\n        </Popout>\n    );\n}\n\nexport default definePlugin({\n    name: \"VencordToolbox\",\n    description: \"Adds a button to the titlebar that houses Vencord quick actions\",\n    authors: [Devs.Ven, Devs.AutumnVN],\n\n    settings,\n\n    patches: [\n        {\n            find: '?\"BACK_FORWARD_NAVIGATION\":',\n            replacement: {\n                match: /(?<=trailing:.{0,50})\\i\\.Fragment,(?=\\{children:\\[)/,\n                replace: \"$self.TrailingWrapper,\"\n            }\n        }\n    ],\n\n    TrailingWrapper({ children }: PropsWithChildren) {\n        return (\n            <>\n                {children}\n                <ErrorBoundary key=\"vc-toolbox\" noop>\n                    <VencordPopoutButton />\n                </ErrorBoundary>\n            </>\n        );\n    },\n});\n"
  },
  {
    "path": "src/plugins/vencordToolbox/menu.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { openNotificationLogModal } from \"@api/Notifications/notificationLog\";\nimport { isPluginEnabled, plugins } from \"@api/PluginManager\";\nimport { Settings, useSettings } from \"@api/Settings\";\nimport { openPluginModal, openSettingsTabModal, PluginsTab, ThemesTab } from \"@components/settings\";\nimport { useAwaiter } from \"@utils/react\";\nimport { wordsFromCamel, wordsToTitle } from \"@utils/text\";\nimport { OptionType, Plugin } from \"@utils/types\";\nimport { Menu, showToast, useMemo, useState } from \"@webpack/common\";\nimport type { ReactNode } from \"react\";\n\nimport { settings } from \".\";\n\nfunction buildPluginMenu() {\n    const { showPluginMenu } = settings.use([\"showPluginMenu\"]);\n\n    // has to be here due to hooks\n    const pluginEntries = buildPluginMenuEntries();\n\n    if (!showPluginMenu) return null;\n\n    return (\n        <Menu.MenuItem\n            id=\"plugins\"\n            label=\"Plugins\"\n            action={() => openSettingsTabModal(PluginsTab)}\n        >\n            {pluginEntries}\n        </Menu.MenuItem>\n    );\n}\n\nexport function buildPluginMenuEntries(includeEmpty = false) {\n    const pluginSettings = useSettings().plugins;\n\n    const [search, setSearch] = useState(\"\");\n\n    const lowerSearch = search.toLowerCase();\n\n    const sortedPlugins = useMemo(() =>\n        Object.values(plugins).sort((a, b) => a.name.localeCompare(b.name)),\n        []\n    );\n\n    const candidates = useMemo(() =>\n        sortedPlugins\n            .filter(p => {\n                if (!isPluginEnabled(p.name)) return false;\n                if (p.name.endsWith(\"API\")) return false;\n\n                const name = p.name.toLowerCase();\n                return name.includes(lowerSearch);\n            }),\n        [lowerSearch]\n    );\n\n    return (\n        <>\n            <Menu.MenuControlItem\n                id=\"plugins-search\"\n                control={(props, ref) => (\n                    <Menu.MenuSearchControl\n                        {...props}\n                        query={search}\n                        onChange={setSearch}\n                        ref={ref}\n                    />\n                )}\n            />\n\n            <Menu.MenuSeparator />\n\n            {candidates\n                .map(p => {\n                    const options = [] as ReactNode[];\n\n                    let hasAnyOption = false;\n\n                    if (p.options) for (const [key, option] of Object.entries(p.options)) {\n                        if (\"hidden\" in option && option.hidden) continue;\n\n                        hasAnyOption = true;\n\n                        const s = pluginSettings[p.name];\n\n                        const baseProps = {\n                            id: `${p.name}-${key}`,\n                            key: key,\n                            label: wordsToTitle(wordsFromCamel(key)),\n                            disabled: \"disabled\" in option ? option.disabled?.call(p.settings) : false,\n                        };\n\n                        switch (option.type) {\n                            case OptionType.BOOLEAN:\n                                options.push(\n                                    <Menu.MenuCheckboxItem\n                                        {...baseProps}\n                                        checked={s[key]}\n                                        action={() => {\n                                            s[key] = !s[key];\n                                            if (option.restartNeeded) showToast(\"Restart to apply the change\");\n                                        }}\n                                    />\n                                );\n                                break;\n                            case OptionType.SELECT:\n                                options.push(\n                                    <Menu.MenuItem {...baseProps}>\n                                        {option.options.map(opt => (\n                                            <Menu.MenuRadioItem\n                                                group={`${p.name}-${key}`}\n                                                id={`${p.name}-${key}-${opt.value}`}\n                                                key={opt.label}\n                                                label={opt.label}\n                                                checked={s[key] === opt.value}\n                                                action={() => {\n                                                    s[key] = opt.value;\n                                                    if (option.restartNeeded) showToast(\"Restart to apply the change\");\n                                                }}\n                                            />\n                                        ))}\n                                    </Menu.MenuItem>\n                                );\n                                break;\n                            case OptionType.SLIDER:\n                                // The menu slider doesn't support these options. Skip to avoid confusion\n                                if (option.stickToMarkers || option.componentProps) continue;\n\n                                options.push(\n                                    <Menu.MenuControlItem\n                                        {...baseProps}\n                                        control={(props, ref) => (\n                                            <Menu.MenuSliderControl\n                                                ref={ref}\n                                                {...props}\n                                                minValue={option.markers[0]}\n                                                maxValue={option.markers.at(-1)!}\n                                                value={s[key]}\n                                                onChange={v => s[key] = v}\n                                            />\n                                        )}\n                                    />\n                                );\n                                break;\n                        }\n                    }\n\n                    const hasVisibleOptions = options.length > 0;\n                    const shouldSkip = !hasVisibleOptions && !(includeEmpty && hasAnyOption);\n                    if (shouldSkip) return null;\n\n                    return (\n                        <Menu.MenuItem\n                            id={`${p.name}-menu`}\n                            key={p.name}\n                            label={p.name}\n                            action={() => openPluginModal(p)}\n                        >\n                            {hasVisibleOptions && (\n                                <>\n                                    <Menu.MenuGroup label={p.name}>\n                                        {options}\n                                    </Menu.MenuGroup>\n\n                                    <Menu.MenuSeparator />\n\n                                    <Menu.MenuItem\n                                        id={`${p.name}-open`}\n                                        label={\"Open Settings\"}\n                                        action={() => openPluginModal(p)}\n                                    />\n                                </>\n                            )}\n                        </Menu.MenuItem>\n                    );\n                })\n            }\n        </>\n    );\n}\n\nexport function buildThemeMenu() {\n    return (\n        <Menu.MenuItem\n            id=\"themes\"\n            label=\"Themes\"\n            action={() => openSettingsTabModal(ThemesTab)}\n        >\n            {buildThemeMenuEntries()}\n        </Menu.MenuItem>\n    );\n}\n\nexport function buildThemeMenuEntries() {\n    const { useQuickCss, enabledThemes } = useSettings([\"useQuickCss\", \"enabledThemes\"]);\n    const [themes] = useAwaiter(VencordNative.themes.getThemesList);\n\n    return (\n        <>\n            <Menu.MenuCheckboxItem\n                id=\"toggle-quickcss\"\n                checked={useQuickCss}\n                label={\"Enable QuickCSS\"}\n                action={() => {\n                    Settings.useQuickCss = !useQuickCss;\n                }}\n            />\n            <Menu.MenuItem\n                id=\"edit-quickcss\"\n                label=\"Edit QuickCSS\"\n                action={() => VencordNative.quickCss.openEditor()}\n            />\n            <Menu.MenuItem\n                id=\"manage-themes\"\n                label=\"Manage Themes\"\n                action={() => openSettingsTabModal(ThemesTab)}\n            />\n            {!!themes?.length && (\n                <Menu.MenuGroup>\n                    {themes.map(theme => (\n                        <Menu.MenuCheckboxItem\n                            id={`theme-${theme.fileName}`}\n                            key={theme.fileName}\n                            label={theme.name}\n                            checked={enabledThemes.includes(theme.fileName)}\n                            action={() => {\n                                if (enabledThemes.includes(theme.fileName)) {\n                                    Settings.enabledThemes = enabledThemes.filter(t => t !== theme.fileName);\n                                } else {\n                                    Settings.enabledThemes = [...enabledThemes, theme.fileName];\n                                }\n                            }}\n                        />\n                    ))}\n                </Menu.MenuGroup>\n            )}\n        </>\n    );\n}\n\nfunction buildCustomPluginEntries() {\n    const pluginEntries = [] as { plugin: Plugin, node: ReactNode; }[];\n\n    for (const plugin of Object.values(plugins)) {\n        if (plugin.toolboxActions && isPluginEnabled(plugin.name)) {\n            const entries = typeof plugin.toolboxActions === \"function\"\n                ? plugin.toolboxActions()\n                : Object.entries(plugin.toolboxActions).map(([text, action]) => {\n                    const key = `${plugin.name}-${text}`;\n\n                    return (\n                        <Menu.MenuItem\n                            id={key}\n                            key={key}\n                            label={text}\n                            action={action}\n                        />\n                    );\n                });\n\n            if (!entries || Array.isArray(entries) && entries.length === 0) continue;\n\n            pluginEntries.push({\n                plugin,\n                node:\n                    <Menu.MenuGroup label={plugin.name} key={`${plugin.name}-group`}>\n                        {entries}\n                    </Menu.MenuGroup>\n            });\n        }\n    }\n\n    // If there aren't too many entries, just put them all in the main menu.\n    // Otherwise, add submenus for each plugin\n    // FIXME: the Slider component has broken styles that overlap with higher context menus\n    // https://discord.com/channels/1015060230222131221/1015063227299811479/1440489344631705693\n    if (pluginEntries.length <= 5)\n        return pluginEntries.map(e => e.node);\n\n    const submenuEntries = pluginEntries.map(({ node, plugin }) => (\n        <Menu.MenuItem\n            id={`${plugin.name}-menu`}\n            key={`${plugin.name}-menu`}\n            label={plugin.name}\n            action={() => openPluginModal(plugin)}\n        >\n            {node}\n        </Menu.MenuItem>\n    ));\n\n    return <Menu.MenuGroup>{submenuEntries}</Menu.MenuGroup>;\n}\n\nexport function renderPopout(onClose: () => void) {\n    return (\n        <Menu.Menu\n            navId=\"vc-toolbox\"\n            onClose={onClose}\n        >\n            <Menu.MenuItem\n                id=\"notifications\"\n                label=\"Open Notification Log\"\n                action={openNotificationLogModal}\n            />\n\n            {buildThemeMenu()}\n            {buildPluginMenu()}\n\n            {buildCustomPluginEntries()}\n        </Menu.Menu >\n    );\n}\n"
  },
  {
    "path": "src/plugins/vencordToolbox/styles.css",
    "content": ".vc-toolbox-btn,\n.vc-toolbox-icon {\n    -webkit-app-region: no-drag;\n}\n\n.vc-toolbox-icon {\n    color: var(--interactive-icon-default);\n}\n\n.vc-toolbox-btn[class*=\"selected\"] .vc-toolbox-icon {\n    color: var(--interactive-icon-active);\n}\n\n.vc-toolbox-btn:hover .vc-toolbox-icon {\n    color: var(--interactive-icon-hover);\n}"
  },
  {
    "path": "src/plugins/viewIcons/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { ImageIcon } from \"@components/Icons\";\nimport { Devs } from \"@utils/constants\";\nimport { openImageModal } from \"@utils/discord\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport type { Channel, Guild, User } from \"@vencord/discord-types\";\nimport { GuildMemberStore, IconUtils, Menu } from \"@webpack/common\";\n\n\ninterface UserContextProps {\n    channel: Channel;\n    guildId?: string;\n    user: User;\n}\n\ninterface GuildContextProps {\n    guild?: Guild;\n}\n\ninterface GroupDMContextProps {\n    channel: Channel;\n}\n\nconst settings = definePluginSettings({\n    format: {\n        type: OptionType.SELECT,\n        description: \"Choose the image format to use for non animated images. Animated images will always use .gif\",\n        options: [\n            {\n                label: \"webp\",\n                value: \"webp\",\n                default: true\n            },\n            {\n                label: \"png\",\n                value: \"png\",\n            },\n            {\n                label: \"jpg\",\n                value: \"jpg\",\n            }\n        ]\n    },\n    imgSize: {\n        type: OptionType.SELECT,\n        description: \"The image size to use\",\n        options: [\"128\", \"256\", \"512\", \"1024\", \"2048\", \"4096\"].map(n => ({ label: n, value: n, default: n === \"1024\" }))\n    }\n});\n\nconst openAvatar = (url: string) => openImage(url, 512, 512);\nconst openBanner = (url: string) => openImage(url, 1024);\n\nfunction openImage(url: string, width: number, height?: number) {\n    const u = new URL(url, window.location.href);\n\n    const format = url.startsWith(\"/\")\n        ? \"png\"\n        : u.searchParams.get(\"animated\") === \"true\"\n            ? \"gif\"\n            : settings.store.format;\n\n    u.searchParams.set(\"size\", settings.store.imgSize);\n    u.pathname = u.pathname.replace(/\\.(png|jpe?g|webp)$/, `.${format}`);\n    url = u.toString();\n\n    u.searchParams.set(\"size\", \"4096\");\n    const original = u.toString();\n\n    openImageModal({\n        url,\n        original,\n        width,\n        height\n    });\n}\n\nconst UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => {\n    if (!user) return;\n    const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;\n\n    children.splice(-1, 0, (\n        <Menu.MenuGroup>\n            <Menu.MenuItem\n                id=\"view-avatar\"\n                label=\"View Avatar\"\n                action={() => openAvatar(IconUtils.getUserAvatarURL(user, true))}\n                icon={ImageIcon}\n            />\n            {memberAvatar && (\n                <Menu.MenuItem\n                    id=\"view-server-avatar\"\n                    label=\"View Server Avatar\"\n                    action={() => openAvatar(IconUtils.getGuildMemberAvatarURLSimple({\n                        userId: user.id,\n                        avatar: memberAvatar,\n                        guildId: guildId!,\n                        canAnimate: true\n                    }))}\n                    icon={ImageIcon}\n                />\n            )}\n        </Menu.MenuGroup>\n    ));\n};\n\nconst GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => {\n    if (!guild) return;\n\n    const { id, icon, banner } = guild;\n    if (!banner && !icon) return;\n\n    children.splice(-1, 0, (\n        <Menu.MenuGroup>\n            {icon ? (\n                <Menu.MenuItem\n                    id=\"view-icon\"\n                    label=\"View Icon\"\n                    action={() =>\n                        openAvatar(IconUtils.getGuildIconURL({\n                            id,\n                            icon,\n                            canAnimate: true\n                        })!)\n                    }\n                    icon={ImageIcon}\n                />\n            ) : null}\n            {banner ? (\n                <Menu.MenuItem\n                    id=\"view-banner\"\n                    label=\"View Banner\"\n                    action={() =>\n                        openBanner(IconUtils.getGuildBannerURL(guild, true)!)\n                    }\n                    icon={ImageIcon}\n                />\n            ) : null}\n        </Menu.MenuGroup>\n    ));\n};\n\nconst GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {\n    if (!channel) return;\n\n    children.splice(-1, 0, (\n        <Menu.MenuGroup>\n            <Menu.MenuItem\n                id=\"view-group-channel-icon\"\n                label=\"View Icon\"\n                action={() =>\n                    openAvatar(IconUtils.getChannelIconURL(channel)!)\n                }\n                icon={ImageIcon}\n            />\n        </Menu.MenuGroup>\n    ));\n};\n\nexport default definePlugin({\n    name: \"ViewIcons\",\n    authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx],\n    description: \"Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.\",\n    tags: [\"ImageUtilities\"],\n    dependencies: [\"DynamicImageModalAPI\"],\n\n    settings,\n\n    openAvatar,\n    openBanner,\n\n    contextMenus: {\n        \"user-context\": UserContext,\n        \"guild-context\": GuildContext,\n        \"gdm-context\": GroupDMContext\n    },\n\n    patches: [\n        // Avatar component used in User DMs \"User Profile\" popup in the right and User Profile Modal pfp\n        {\n            find: \"imageClassName:null!=\",\n            replacement: {\n                match: /avatarSrc:(\\i),eventHandlers:(\\i).+?\"div\",.{0,100}className:\\i,/,\n                replace: \"$&style:{cursor:\\\"pointer\\\"},onClick:()=>{$self.openAvatar($1)},\",\n            }\n        },\n        // Banners\n        {\n            find: 'backgroundColor:\"COMPLETE\"',\n            replacement: {\n                match: /(overflow:\"visible\",.{0,125}?!1\\),)style:{(?=.+?backgroundImage:null!=(\\i)\\?`url\\(\\$\\{\\2\\}\\))/,\n                replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?\"pointer\":void 0,`\n            }\n        },\n        // Group DMs top small & large icon\n        {\n            find: '[\"aria-hidden\"],\"aria-label\":',\n            replacement: {\n                match: /null==\\i\\.icon\\?.+?src:(\\(0,\\i\\.\\i\\).+?\\))(?=[,}])/,\n                // We have to check that icon is not an unread GDM in the server bar\n                replace: (m, iconUrl) => `${m},onClick:()=>arguments[0]?.size!==\"SIZE_48\"&&$self.openAvatar(${iconUrl})`\n            }\n        },\n        // User DMs top small icon\n        {\n            find: \".channel.getRecipientId(),\",\n            replacement: {\n                match: /(?=,src:(\\i.getAvatarURL\\(.+?[)]))/,\n                replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`\n            }\n        },\n        // User Dms top large icon\n        {\n            find: \".EMPTY_GROUP_DM)\",\n            replacement: {\n                match: /(?<=SIZE_80,)(?=src:(.+?\\))[,}])/,\n                replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`\n            }\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/viewRaw/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { definePluginSettings } from \"@api/Settings\";\nimport { CodeBlock } from \"@components/CodeBlock\";\nimport { Divider } from \"@components/Divider\";\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Flex } from \"@components/Flex\";\nimport { Devs } from \"@utils/constants\";\nimport { copyWithToast, getCurrentGuild, getIntlMessage } from \"@utils/discord\";\nimport { Margins } from \"@utils/margins\";\nimport { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from \"@utils/modal\";\nimport definePlugin, { IconComponent, OptionType } from \"@utils/types\";\nimport { Message } from \"@vencord/discord-types\";\nimport { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from \"@webpack/common\";\n\n\nconst CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => {\n    return (\n        <svg\n            viewBox=\"0 0 20 20\"\n            fill=\"currentColor\"\n            aria-hidden=\"true\"\n            width={width}\n            height={height}\n            className={className}\n        >\n            <path d=\"M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z\" />\n            <path d=\"M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z\" />\n        </svg>\n    );\n};\n\nfunction sortObject<T extends object>(obj: T): T {\n    return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;\n}\n\nfunction cleanMessage(msg: Message) {\n    const clone = sortObject(JSON.parse(JSON.stringify(msg)));\n    for (const key of [\n        \"email\",\n        \"phone\",\n        \"mfaEnabled\",\n        \"personalConnectionId\"\n    ]) delete clone.author[key];\n\n    // message logger added properties\n    const cloneAny = clone as any;\n    delete cloneAny.editHistory;\n    delete cloneAny.deleted;\n    delete cloneAny.firstEditTimestamp;\n    cloneAny.attachments?.forEach(a => delete a.deleted);\n\n    return clone;\n}\n\nfunction openViewRawModal(json: string, type: string, msgContent?: string) {\n    const key = openModal(props => (\n        <ErrorBoundary>\n            <ModalRoot {...props} size={ModalSize.LARGE}>\n                <ModalHeader>\n                    <Text variant=\"heading-lg/semibold\" style={{ flexGrow: 1 }}>View Raw</Text>\n                    <ModalCloseButton onClick={() => closeModal(key)} />\n                </ModalHeader>\n                <ModalContent>\n                    <div style={{ padding: \"16px 0\" }}>\n                        {!!msgContent && (\n                            <>\n                                <Forms.FormTitle tag=\"h5\">Content</Forms.FormTitle>\n                                <CodeBlock content={msgContent} lang=\"\" />\n                                <Divider className={Margins.bottom20} />\n                            </>\n                        )}\n\n                        <Forms.FormTitle tag=\"h5\">{type} Data</Forms.FormTitle>\n                        <CodeBlock content={json} lang=\"json\" />\n                    </div>\n                </ModalContent >\n                <ModalFooter>\n                    <Flex>\n                        <Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>\n                            Copy {type} JSON\n                        </Button>\n                        {!!msgContent && (\n                            <Button onClick={() => copyWithToast(msgContent, \"Content copied to clipboard!\")}>\n                                Copy Raw Content\n                            </Button>\n                        )}\n                    </Flex>\n                </ModalFooter>\n            </ModalRoot >\n        </ErrorBoundary >\n    ));\n}\n\nfunction openViewRawModalMessage(msg: Message) {\n    msg = cleanMessage(msg);\n    const msgJson = JSON.stringify(msg, null, 4);\n\n    return openViewRawModal(msgJson, \"Message\", msg.content);\n}\n\nconst settings = definePluginSettings({\n    clickMethod: {\n        description: \"Change the button to view the raw content/data of any message.\",\n        type: OptionType.SELECT,\n        options: [\n            { label: \"Left Click to view the raw content.\", value: \"Left\", default: true },\n            { label: \"Right click to view the raw content.\", value: \"Right\" }\n        ]\n    }\n});\n\nfunction MakeContextCallback(name: \"Guild\" | \"Role\" | \"User\" | \"Channel\"): NavContextMenuPatchCallback {\n    return (children, props) => {\n        const value = props[name.toLowerCase()];\n        if (!value) return;\n        if (props.label === getIntlMessage(\"CHANNEL_ACTIONS_MENU_LABEL\")) return; // random shit like notification settings\n\n        const lastChild = children.at(-1);\n        if (lastChild?.key === \"developer-actions\") {\n            const p = lastChild.props;\n            if (!Array.isArray(p.children))\n                p.children = [p.children];\n\n            children = p.children;\n        }\n\n        // typescript parser goes crazy if this is inline\n        const id = `vc-view-${name.toLowerCase()}-raw`;\n        children.splice(-1, 0,\n            <Menu.MenuItem\n                id={id}\n                label=\"View Raw\"\n                action={() => openViewRawModal(JSON.stringify(value, null, 4), name)}\n                icon={CopyIcon}\n            />\n        );\n    };\n}\n\nconst devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {\n    const guild = getCurrentGuild();\n    if (!guild) return;\n\n    const role = GuildRoleStore.getRole(guild.id, id);\n    if (!role) return;\n\n    children.push(\n        <Menu.MenuItem\n            id={\"vc-view-role-raw\"}\n            label=\"View Raw\"\n            action={() => openViewRawModal(JSON.stringify(role, null, 4), \"Role\")}\n            icon={CopyIcon}\n        />\n    );\n};\n\nexport default definePlugin({\n    name: \"ViewRaw\",\n    description: \"Copy and view the raw content/data of any message, channel or guild\",\n    authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],\n    settings,\n\n    contextMenus: {\n        \"guild-context\": MakeContextCallback(\"Guild\"),\n        \"guild-settings-role-context\": MakeContextCallback(\"Role\"),\n        \"channel-context\": MakeContextCallback(\"Channel\"),\n        \"thread-context\": MakeContextCallback(\"Channel\"),\n        \"gdm-context\": MakeContextCallback(\"Channel\"),\n        \"user-context\": MakeContextCallback(\"User\"),\n        \"dev-context\": devContextCallback\n    },\n\n    messagePopoverButton: {\n        icon: CopyIcon,\n        render(msg) {\n            const handleClick = () => {\n                if (settings.store.clickMethod === \"Right\") {\n                    copyWithToast(msg.content);\n                } else {\n                    openViewRawModalMessage(msg);\n                }\n            };\n\n            const handleContextMenu = e => {\n                if (settings.store.clickMethod === \"Left\") {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    copyWithToast(msg.content);\n                } else {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    openViewRawModalMessage(msg);\n                }\n            };\n\n            const label = settings.store.clickMethod === \"Right\"\n                ? \"Copy Raw (Left Click) / View Raw (Right Click)\"\n                : \"View Raw (Left Click) / Copy Raw (Right Click)\";\n\n            return {\n                label,\n                icon: CopyIcon,\n                message: msg,\n                channel: ChannelStore.getChannel(msg.channel_id),\n                onClick: handleClick,\n                onContextMenu: handleContextMenu\n            };\n        }\n    }\n});\n"
  },
  {
    "path": "src/plugins/voiceDownload/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport \"./style.css\";\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"VoiceDownload\",\n    description: \"Adds a download to voice messages. (Opens a new browser tab)\",\n    authors: [Devs.puv],\n    patches: [\n        {\n            find: \"#{intl::VOICE_MESSAGES_PLAYBACK_RATE_LABEL}\",\n            replacement: {\n                match: /(?<=onVolumeHide:\\i\\}\\))/,\n                replace: \",$self.renderDownload(arguments[0].src)\"\n            }\n        }\n    ],\n\n    renderDownload(src: string) {\n        return (\n            <a\n                className=\"vc-voice-download\"\n                href={src}\n                onClick={e => e.stopPropagation()}\n                aria-label=\"Download voice message\"\n                {...IS_DISCORD_DESKTOP\n                    ? { target: \"_blank\" } // open externally\n                    : { download: \"voice-message.ogg\" } // download directly (not supported on discord desktop)\n                }\n            >\n                <this.Icon />\n            </a>\n        );\n    },\n\n    Icon: () => (\n        <svg\n            height=\"24\"\n            width=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n        >\n            <path\n                d=\"M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z\"\n            />\n        </svg>\n    ),\n});\n"
  },
  {
    "path": "src/plugins/voiceDownload/style.css",
    "content": ".vc-voice-download {\n    width: 24px;\n    height: 24px;\n    color: var(--interactive-icon-default);\n    cursor: pointer;\n    position: relative;\n}\n\n.vc-voice-download:hover {\n    color: var(--interactive-icon-active);\n}\n"
  },
  {
    "path": "src/plugins/voiceMessages/DesktopRecorder.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { PluginNative } from \"@utils/types\";\nimport { Button, MediaEngineStore, showToast, Toasts, useState } from \"@webpack/common\";\n\nimport type { VoiceRecorder } from \".\";\nimport { settings } from \"./settings\";\n\nconst Native = VencordNative.pluginHelpers.VoiceMessages as PluginNative<typeof import(\"./native\")>;\n\nexport const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => {\n    const [recording, setRecording] = useState(false);\n\n    const changeRecording = (recording: boolean) => {\n        setRecording(recording);\n        onRecordingChange?.(recording);\n    };\n\n    function toggleRecording() {\n        const discordVoice = DiscordNative.nativeModules.requireModule(\"discord_voice\");\n        const nowRecording = !recording;\n\n        if (nowRecording) {\n            discordVoice.startLocalAudioRecording(\n                {\n                    echoCancellation: settings.store.echoCancellation,\n                    noiseCancellation: settings.store.noiseSuppression,\n                    deviceId: MediaEngineStore.getInputDeviceId(),\n                },\n                (success: boolean) => {\n                    if (success)\n                        changeRecording(true);\n                    else\n                        showToast(\"Failed to start recording\", Toasts.Type.FAILURE);\n                }\n            );\n        } else {\n            discordVoice.stopLocalAudioRecording(async (filePath: string) => {\n                if (filePath) {\n                    const buf = await Native.readRecording(filePath);\n                    if (buf)\n                        setAudioBlob(new Blob([buf], { type: \"audio/ogg; codecs=opus\" }));\n                    else\n                        showToast(\"Failed to finish recording\", Toasts.Type.FAILURE);\n                }\n                changeRecording(false);\n            });\n        }\n    }\n\n    return (\n        <Button onClick={toggleRecording}>\n            {recording ? \"Stop\" : \"Start\"} recording\n        </Button>\n    );\n};\n"
  },
  {
    "path": "src/plugins/voiceMessages/VoicePreview.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { useTimer } from \"@utils/react\";\nimport { findComponentByCodeLazy } from \"@webpack\";\n\nimport { cl } from \".\";\n\ninterface VoiceMessageProps {\n    src: string;\n    waveform: string;\n}\nconst VoiceMessage = findComponentByCodeLazy<VoiceMessageProps>(\"waveform:\", \"onVolumeChange\");\n\nexport type VoicePreviewOptions = {\n    src?: string;\n    waveform: string;\n    recording?: boolean;\n};\nexport const VoicePreview = ({\n    src,\n    waveform,\n    recording,\n}: VoicePreviewOptions) => {\n    const durationMs = useTimer({\n        deps: [recording]\n    });\n\n    const durationSeconds = recording ? Math.floor(durationMs / 1000) : 0;\n    const durationDisplay = Math.floor(durationSeconds / 60) + \":\" + (durationSeconds % 60).toString().padStart(2, \"0\");\n\n    if (src && !recording)\n        return <VoiceMessage key={src} src={src} waveform={waveform} />;\n\n    return (\n        <div className={cl(\"preview\", recording ? \"preview-recording\" : [])}>\n            <div className={cl(\"preview-indicator\")} />\n            <div className={cl(\"preview-time\")}>{durationDisplay}</div>\n            <div className={cl(\"preview-label\")}>{recording ? \"RECORDING\" : \"----\"}</div>\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/plugins/voiceMessages/WebRecorder.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Button, MediaEngineStore, useState } from \"@webpack/common\";\n\nimport type { VoiceRecorder } from \".\";\nimport { settings } from \"./settings\";\n\nexport const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => {\n    const [recording, setRecording] = useState(false);\n    const [paused, setPaused] = useState(false);\n    const [recorder, setRecorder] = useState<MediaRecorder>();\n    const [chunks, setChunks] = useState<Blob[]>([]);\n\n    const changeRecording = (recording: boolean) => {\n        setRecording(recording);\n        onRecordingChange?.(recording);\n    };\n\n    function toggleRecording() {\n        const nowRecording = !recording;\n\n        if (nowRecording) {\n            navigator.mediaDevices.getUserMedia({\n                audio: {\n                    echoCancellation: settings.store.echoCancellation,\n                    noiseSuppression: settings.store.noiseSuppression,\n                    deviceId: MediaEngineStore.getInputDeviceId()\n                }\n            }).then(stream => {\n                const chunks = [] as Blob[];\n                setChunks(chunks);\n\n                const recorder = new MediaRecorder(stream);\n                setRecorder(recorder);\n                recorder.addEventListener(\"dataavailable\", e => {\n                    chunks.push(e.data);\n                });\n                recorder.start();\n\n                changeRecording(true);\n            });\n        } else {\n            if (recorder) {\n                recorder.addEventListener(\"stop\", () => {\n                    setAudioBlob(new Blob(chunks, { type: \"audio/ogg; codecs=opus\" }));\n\n                    changeRecording(false);\n                });\n                recorder.stop();\n            }\n        }\n    }\n\n    return (\n        <>\n            <Button onClick={toggleRecording}>\n                {recording ? \"Stop\" : \"Start\"} recording\n            </Button>\n\n            <Button\n                disabled={!recording}\n                onClick={() => {\n                    setPaused(!paused);\n                    if (paused) recorder?.resume();\n                    else recorder?.pause();\n                }}\n            >\n                {paused ? \"Resume\" : \"Pause\"} recording\n            </Button>\n        </>\n    );\n};\n"
  },
  {
    "path": "src/plugins/voiceMessages/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport \"./styles.css\";\n\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { Card } from \"@components/Card\";\nimport { Microphone } from \"@components/Icons\";\nimport { Link } from \"@components/Link\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { Devs } from \"@utils/constants\";\nimport { classNameFactory } from \"@utils/css\";\nimport { Margins } from \"@utils/margins\";\nimport { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from \"@utils/modal\";\nimport { useAwaiter } from \"@utils/react\";\nimport definePlugin from \"@utils/types\";\nimport { chooseFile } from \"@utils/web\";\nimport { CloudUpload as TCloudUpload } from \"@vencord/discord-types\";\nimport { CloudUploadPlatform } from \"@vencord/discord-types/enums\";\nimport { findLazy } from \"@webpack\";\nimport { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from \"@webpack/common\";\nimport { ComponentType } from \"react\";\n\nimport { VoiceRecorderDesktop } from \"./DesktopRecorder\";\nimport { settings } from \"./settings\";\nimport { VoicePreview } from \"./VoicePreview\";\nimport { VoiceRecorderWeb } from \"./WebRecorder\";\n\nconst CloudUpload: typeof TCloudUpload = findLazy(m => m.prototype?.trackUploadFinished);\n\nexport const cl = classNameFactory(\"vc-vmsg-\");\nexport type VoiceRecorder = ComponentType<{\n    setAudioBlob(blob: Blob): void;\n    onRecordingChange?(recording: boolean): void;\n}>;\n\nconst VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb;\n\nconst ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {\n    if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;\n\n    children.push(\n        <Menu.MenuItem\n            id=\"vc-send-vmsg\"\n            iconLeft={Microphone}\n            leadingAccessory={{\n                type: \"icon\",\n                icon: Microphone\n            }}\n            label=\"Send Voice Message\"\n            action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}\n        />\n    );\n};\n\nexport default definePlugin({\n    name: \"VoiceMessages\",\n    description: \"Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message\",\n    authors: [Devs.Ven, Devs.Vap, Devs.Nickyux],\n    settings,\n    contextMenus: {\n        \"channel-attach\": ctxMenuPatch\n    }\n});\n\ntype AudioMetadata = {\n    waveform: string,\n    duration: number,\n};\nconst EMPTY_META: AudioMetadata = {\n    waveform: \"AAAAAAAAAAAA\",\n    duration: 1,\n};\n\nfunction sendAudio(blob: Blob, meta: AudioMetadata) {\n    const channelId = SelectedChannelStore.getChannelId();\n    const reply = PendingReplyStore.getPendingReply(channelId);\n    if (reply) FluxDispatcher.dispatch({ type: \"DELETE_PENDING_REPLY\", channelId });\n\n    const upload = new CloudUpload({\n        file: new File([blob], \"voice-message.ogg\", { type: \"audio/ogg; codecs=opus\" }),\n        isThumbnail: false,\n        platform: CloudUploadPlatform.WEB,\n    }, channelId);\n\n    upload.on(\"complete\", () => {\n        RestAPI.post({\n            url: Constants.Endpoints.MESSAGES(channelId),\n            body: {\n                flags: 1 << 13,\n                channel_id: channelId,\n                content: \"\",\n                nonce: SnowflakeUtils.fromTimestamp(Date.now()),\n                sticker_ids: [],\n                type: 0,\n                attachments: [{\n                    id: \"0\",\n                    filename: upload.filename,\n                    uploaded_filename: upload.uploadedFilename,\n                    waveform: meta.waveform,\n                    duration_secs: meta.duration,\n                }],\n                message_reference: reply ? MessageActions.getSendMessageOptionsForReply(reply)?.messageReference : null,\n            }\n        });\n    });\n    upload.on(\"error\", () => showToast(\"Failed to upload voice message\", Toasts.Type.FAILURE));\n\n    upload.upload();\n}\n\nfunction useObjectUrl() {\n    const [url, setUrl] = useState<string>();\n    const setWithFree = (blob: Blob) => {\n        if (url)\n            URL.revokeObjectURL(url);\n        setUrl(URL.createObjectURL(blob));\n    };\n\n    return [url, setWithFree] as const;\n}\n\nfunction Modal({ modalProps }: { modalProps: ModalProps; }) {\n    const [isRecording, setRecording] = useState(false);\n    const [blob, setBlob] = useState<Blob>();\n    const [blobUrl, setBlobUrl] = useObjectUrl();\n\n    useEffect(() => () => {\n        if (blobUrl)\n            URL.revokeObjectURL(blobUrl);\n    }, [blobUrl]);\n\n    const [meta, metaError] = useAwaiter(async () => {\n        if (!blob) return EMPTY_META;\n\n        const audioContext = new AudioContext();\n        const audioBuffer = await audioContext.decodeAudioData(await blob.arrayBuffer());\n        const channelData = audioBuffer.getChannelData(0);\n\n        // average the samples into much lower resolution bins, maximum of 256 total bins\n        const bins = new Uint8Array(lodash.clamp(Math.floor(audioBuffer.duration * 10), Math.min(32, channelData.length), 256));\n        const samplesPerBin = Math.floor(channelData.length / bins.length);\n\n        // Get root mean square of each bin\n        for (let binIdx = 0; binIdx < bins.length; binIdx++) {\n            let squares = 0;\n            for (let sampleOffset = 0; sampleOffset < samplesPerBin; sampleOffset++) {\n                const sampleIdx = binIdx * samplesPerBin + sampleOffset;\n                squares += channelData[sampleIdx] ** 2;\n            }\n            bins[binIdx] = ~~(Math.sqrt(squares / samplesPerBin) * 0xFF);\n        }\n\n        // Normalize bins with easing\n        const maxBin = Math.max(...bins);\n        const ratio = 1 + (0xFF / maxBin - 1) * Math.min(1, 100 * (maxBin / 0xFF) ** 3);\n        for (let i = 0; i < bins.length; i++) bins[i] = Math.min(0xFF, ~~(bins[i] * ratio));\n\n        return {\n            waveform: window.btoa(String.fromCharCode(...bins)),\n            duration: audioBuffer.duration,\n        };\n    }, {\n        deps: [blob],\n        fallbackValue: EMPTY_META,\n    });\n\n    const isUnsupportedFormat = blob && (\n        !blob.type.startsWith(\"audio/ogg\")\n        || blob.type.includes(\"codecs\") && !blob.type.includes(\"opus\")\n    );\n\n    return (\n        <ModalRoot {...modalProps}>\n            <ModalHeader>\n                <Forms.FormTitle>Record Voice Message</Forms.FormTitle>\n            </ModalHeader>\n\n            <ModalContent className={cl(\"modal\")}>\n                <div className={cl(\"buttons\")}>\n                    <VoiceRecorder\n                        setAudioBlob={blob => {\n                            setBlob(blob);\n                            setBlobUrl(blob);\n                        }}\n                        onRecordingChange={setRecording}\n                    />\n\n                    <Button\n                        onClick={async () => {\n                            const file = await chooseFile(\"audio/*\");\n                            if (file) {\n                                setBlob(file);\n                                setBlobUrl(file);\n                            }\n                        }}\n                    >\n                        Upload File\n                    </Button>\n                </div>\n\n                <Forms.FormTitle>Preview</Forms.FormTitle>\n                {metaError\n                    ? <Paragraph className={cl(\"error\")}>Failed to parse selected audio file: {metaError.message}</Paragraph>\n                    : (\n                        <VoicePreview\n                            src={blobUrl}\n                            waveform={meta.waveform}\n                            recording={isRecording}\n                        />\n                    )}\n\n                {isUnsupportedFormat && (\n                    <Card variant=\"warning\" className={Margins.top16} defaultPadding>\n                        <Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>\n\n                        <Forms.FormText className={Margins.top8}>\n                            To fix it, first convert it to OggOpus, for example using the <Link href=\"https://convertio.co/mp3-opus/\">convertio web converter</Link>\n                        </Forms.FormText>\n                    </Card>\n                )}\n\n            </ModalContent>\n\n            <ModalFooter>\n                <Button\n                    disabled={!blob}\n                    onClick={() => {\n                        sendAudio(blob!, meta ?? EMPTY_META);\n                        modalProps.onClose();\n                        showToast(\"Now sending voice message... Please be patient\", Toasts.Type.MESSAGE);\n                    }}\n                >\n                    Send\n                </Button>\n            </ModalFooter>\n        </ModalRoot>\n    );\n}\n"
  },
  {
    "path": "src/plugins/voiceMessages/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { app } from \"electron\";\nimport { readFile, rm } from \"fs/promises\";\nimport { basename, normalize } from \"path\";\n\nexport async function readRecording(_, filePath: string) {\n    filePath = normalize(filePath);\n    const filename = basename(filePath);\n    const discordBaseDirWithTrailingSlash = normalize(app.getPath(\"userData\") + \"/\");\n    if (!/^\\d*recording\\.ogg$/.test(filename) || !filePath.startsWith(discordBaseDirWithTrailingSlash)) return null;\n\n    try {\n        const buf = await readFile(filePath);\n        rm(filePath).catch(() => { });\n        return new Uint8Array(buf.buffer);\n    } catch {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/plugins/voiceMessages/settings.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { OptionType } from \"@utils/types\";\n\nexport const settings = definePluginSettings({\n    noiseSuppression: {\n        type: OptionType.BOOLEAN,\n        description: \"Noise Suppression\",\n        default: true,\n    },\n    echoCancellation: {\n        type: OptionType.BOOLEAN,\n        description: \"Echo Cancellation\",\n        default: true,\n    },\n});\n"
  },
  {
    "path": "src/plugins/voiceMessages/styles.css",
    "content": ".vc-vmsg-modal {\n    padding: 1em;\n}\n\n.vc-vmsg-buttons {\n    display: grid;\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    gap: 0.5em;\n    margin-bottom: 1em;\n}\n\n.vc-vmsg-preview {\n    color: var(--text-default);\n    border-radius: 24px;\n    background-color: var(--background-base-lower);\n    position: relative;\n    display: flex;\n    align-items: center;\n    padding: 0 16px;\n    height: 48px;\n}\n\n.vc-vmsg-preview-indicator {\n    background: var(--control-secondary-background-default);\n    width: 16px;\n    height: 16px;\n    border-radius: 50%;\n    transition: background 0.2s ease-in-out;\n}\n\n.vc-vmsg-preview-recording .vc-vmsg-preview-indicator {\n    background: var(--status-danger);\n}\n\n.vc-vmsg-preview-time {\n    opacity: 0.8;\n    margin: 0 0.5em;\n    font-size: 80%;\n\n    /* monospace so different digits have same size */\n    font-family: var(--font-code);\n}\n\n.vc-vmsg-preview-label {\n    opacity: 0.5;\n    letter-spacing: 0.125em;\n    font-weight: 600;\n    flex: 1;\n    text-align: center;\n}\n\n.vc-vmsg-error {\n    color: var(--text-feedback-critical, #FF5C5C);\n}"
  },
  {
    "path": "src/plugins/volumeBooster/README.md",
    "content": "# Volume Booster\n\nAllows you to boost the volume over 200% on desktop and over 100% on other clients.\n\nWorks on users, bots, and streams!\n\n![the volume being moved up to 270% on vesktop](https://github.com/user-attachments/assets/793e012e-c069-4fa4-a3d5-61c2f55edd3e)\n\n![the volume being moved up to 297% on a stream](https://github.com/user-attachments/assets/77463eb9-2537-4821-a3ab-82f60633ccbc)\n"
  },
  {
    "path": "src/plugins/volumeBooster/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { makeRange, OptionType } from \"@utils/types\";\n\nconst settings = definePluginSettings({\n    multiplier: {\n        description: \"Volume Multiplier\",\n        type: OptionType.SLIDER,\n        markers: makeRange(1, 5, 0.5),\n        default: 2,\n        stickToMarkers: true,\n    }\n});\n\ninterface StreamData {\n    audioContext: AudioContext,\n    audioElement: HTMLAudioElement,\n    emitter: any,\n    // added by this plugin\n    gainNode?: GainNode,\n    id: string,\n    levelNode: AudioWorkletNode,\n    sinkId: string | \"default\",\n    stream: MediaStream,\n    streamSourceNode?: MediaStreamAudioSourceNode,\n    videoStreamId: string,\n    _mute: boolean,\n    _speakingFlags: number,\n    _volume: number;\n}\n\nexport default definePlugin({\n    name: \"VolumeBooster\",\n    authors: [Devs.Nuckyz, Devs.sadan],\n    description: \"Allows you to set the user and stream volume above the default maximum\",\n    settings,\n\n    patches: [\n        // Change the max volume for sliders to allow for values above 200\n        {\n            find: \"#{intl::USER_VOLUME}\",\n            replacement: {\n                match: /(?<=maxValue:)\\i\\.isPlatformEmbedded\\?(\\i\\.\\i):\\i\\.\\i(?=,)/,\n                replace: (_, higherMaxVolume) => `${higherMaxVolume}*$self.settings.store.multiplier`\n            }\n        },\n        // Change the max volume for sliders to allow for values above 200\n        {\n            find: \"currentVolume:\",\n            replacement: {\n                match: /(?<=maxValue:)\\i\\.\\i\\?(\\d+?):\\d+?(?=,)/,\n                replace: (_, higherMaxVolume) => `${higherMaxVolume}*$self.settings.store.multiplier`\n            }\n        },\n        // Patches needed for web/vesktop\n        {\n            find: \"streamSourceNode\",\n            predicate: () => !IS_DISCORD_DESKTOP,\n            group: true,\n            replacement: [\n                // Remove rounding algorithm\n                {\n                    match: /Math\\.max.{0,30}\\)\\)/,\n                    replace: \"arguments[0]\"\n                },\n                // Fix streams not playing audio until you update them\n                {\n                    match: /\\}return\"video\"/,\n                    replace: \"this.updateAudioElement();$&\"\n                },\n                // Patch the volume\n                {\n                    match: /\\.volume=this\\._volume\\/100;/,\n                    replace: \".volume=0.00;$self.patchVolume(this);\"\n                }\n            ]\n        },\n        // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord\n        {\n            find: \"AudioContextSettingsMigrated\",\n            replacement: [\n                {\n                    match: /(?<=isLocalMute\\(\\i,\\i\\),volume:(\\i).+?\\i\\(\\i,\\i,)\\1(?=\\))/,\n                    replace: \"$&>200?200:$&\"\n                },\n                {\n                    match: /(?<=Object\\.entries\\(\\i\\.localMutes\\).+?volume:).+?(?=,)/,\n                    replace: \"$&>200?200:$&\"\n                },\n                {\n                    match: /(?<=Object\\.entries\\(\\i\\.localVolumes\\).+?volume:).+?(?=})/,\n                    replace: \"$&>200?200:$&\"\n                }\n            ]\n        },\n        // Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends\n        {\n            find: '=\"MediaEngineStore\",',\n            replacement: [\n                {\n                    match: /(\\.settings\\.audioContextSettings.+?)(\\i\\[\\i\\])=(\\i\\.volume)(.+?setLocalVolume\\(\\i,).+?\\)/,\n                    replace: (_, rest1, localVolume, syncVolume, rest2) => rest1\n                        + `(${localVolume}>200?void 0:${localVolume}=${syncVolume})`\n                        + rest2\n                        + `${localVolume}??${syncVolume})`\n                }\n            ]\n        }\n    ],\n\n    patchVolume(data: StreamData) {\n        if (data.stream.getAudioTracks().length === 0) return;\n\n        data.streamSourceNode ??= data.audioContext.createMediaStreamSource(data.stream);\n\n        if (!data.gainNode) {\n            const gain = data.gainNode = data.audioContext.createGain();\n            data.streamSourceNode.connect(gain);\n            gain.connect(data.audioContext.destination);\n        }\n\n        // @ts-expect-error\n        if (data.sinkId != null && data.sinkId !== data.audioContext.sinkId && \"setSinkId\" in AudioContext.prototype) {\n            // @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId\n            data.audioContext.setSinkId(data.sinkId === \"default\" ? \"\" : data.sinkId);\n        }\n\n        data.gainNode.gain.value = data._mute\n            ? 0\n            : data._volume / 100;\n    }\n});\n"
  },
  {
    "path": "src/plugins/webContextMenus.web/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { copyToClipboard } from \"@utils/clipboard\";\nimport { Devs } from \"@utils/constants\";\nimport definePlugin, { OptionType } from \"@utils/types\";\nimport { saveFile } from \"@utils/web\";\nimport { filters, mapMangledModuleLazy } from \"@webpack\";\nimport { ComponentDispatch } from \"@webpack/common\";\n\nconst ctxMenuCallbacks = mapMangledModuleLazy('closest(\"[contenteditable=true]\")', {\n    contextMenuCallbackWeb: filters.byCode('\"[contenteditable=true]\"'),\n    contextMenuCallbackNative: filters.byCode('.getPropertyValue(\"-webkit-user-select\")')\n});\n\nasync function fetchImage(url: string) {\n    const res = await fetch(url);\n    if (res.status !== 200) return;\n\n    return await res.blob();\n}\n\n\nconst settings = definePluginSettings({\n    // This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context\n    // menu handler instead of the web one, which breaks the other menus that aren't enabled\n    addBack: {\n        type: OptionType.BOOLEAN,\n        description: \"Add back the Discord context menus for images, links and the chat input bar\",\n        default: false,\n        restartNeeded: true,\n        // Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,\n        // so disable this by default. Vesktop just doesn't, so we force enable it there\n        hidden: IS_VESKTOP,\n    }\n});\n\nconst shouldAddBackMenus = () => IS_VESKTOP || settings.store.addBack;\n\nconst MEDIA_PROXY_URL = \"https://media.discordapp.net\";\nconst CDN_URL = \"cdn.discordapp.com\";\n\nfunction fixImageUrl(urlString: string) {\n    const url = new URL(urlString);\n    if (url.host === CDN_URL) return urlString;\n\n    url.searchParams.delete(\"width\");\n    url.searchParams.delete(\"height\");\n\n    if (url.origin === MEDIA_PROXY_URL) {\n        url.host = CDN_URL;\n        url.searchParams.delete(\"size\");\n        url.searchParams.delete(\"quality\");\n        url.searchParams.delete(\"format\");\n    } else {\n        url.searchParams.set(\"quality\", \"lossless\");\n    }\n\n    return url.toString();\n}\n\nexport default definePlugin({\n    name: \"WebContextMenus\",\n    description: \"Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)\",\n    authors: [Devs.Ven],\n    enabledByDefault: true,\n    required: IS_VESKTOP,\n\n    settings,\n\n    start() {\n        if (shouldAddBackMenus()) {\n            window.removeEventListener(\"contextmenu\", ctxMenuCallbacks.contextMenuCallbackWeb);\n            window.addEventListener(\"contextmenu\", ctxMenuCallbacks.contextMenuCallbackNative);\n            this.changedListeners = true;\n        }\n    },\n\n    stop() {\n        if (this.changedListeners) {\n            window.removeEventListener(\"contextmenu\", ctxMenuCallbacks.contextMenuCallbackNative);\n            window.addEventListener(\"contextmenu\", ctxMenuCallbacks.contextMenuCallbackWeb);\n        }\n    },\n\n    patches: [\n        // Add back Copy & Open Link\n        {\n            // There is literally no reason for Discord to make this Desktop only.\n            // The only thing broken is copy, but they already have a different copy function\n            // with web support????\n            find: \"open-native-link\",\n            replacement: [\n                {\n                    // if (IS_DESKTOP || null == ...)\n                    match: /if\\(!\\i\\.\\i\\|\\|null==/,\n                    replace: \"if(null==\"\n                },\n                // Fix silly Discord calling the non web support copy\n                {\n                    match: /\\i\\.\\i\\.copy/,\n                    replace: \"Vencord.Util.copyToClipboard\"\n                }\n            ]\n        },\n\n        {\n            find: \"Copy image not supported\",\n            replacement: [\n                {\n                    match: /(?<=(?:canSaveImage|canCopyImage)\\(.{0,120}?)!\\i\\.isPlatformEmbedded/g,\n                    replace: \"false\"\n                },\n                {\n                    match: /(?<=canCopyImage\\(.+?)typeof \\i\\.clipboard\\.copyImage/,\n                    replace: '\"function\"'\n                }\n            ]\n        },\n        // Add back Copy & Save Image\n        {\n            find: 'id:\"copy-image\"',\n            replacement: [\n                {\n                    // if (!IS_WEB || null ==\n                    match: /!\\i\\.isPlatformEmbedded/,\n                    replace: \"false\"\n                },\n                {\n                    match: /(#{intl::COPY_IMAGE_MENU_ITEM}\\),.{0,75}?)action:/,\n                    replace: \"$1action:()=>$self.copyImage(arguments[0]),oldAction:\"\n                },\n                {\n                    match: /(#{intl::SAVE_IMAGE_MENU_ITEM}\\),.{0,75}?)action:/,\n                    replace: \"$1action:()=>$self.saveImage(arguments[0]),oldAction:\"\n                },\n            ]\n        },\n\n        // Add back image context menu\n        {\n            find: 'navId:\"image-context\"',\n            all: true,\n            predicate: shouldAddBackMenus,\n            replacement: {\n                // return IS_DESKTOP ? React.createElement(Menu, ...)\n                match: /return \\i\\.\\i(?=\\?|&&)/,\n                replace: \"return true\"\n            }\n        },\n\n        // Add back link context menu\n        {\n            find: '\"interactionUsernameProfile\"',\n            predicate: shouldAddBackMenus,\n            replacement: {\n                match: /if\\((?=\"A\"===\\i\\.tagName&&\"\"!==\\i\\.textContent)/,\n                replace: \"if(false&&\"\n            }\n        },\n\n        // Add back slate / text input context menu\n        {\n            find: 'getElementById(\"slate-toolbar\"',\n            predicate: shouldAddBackMenus,\n            replacement: {\n                match: /(?<=handleContextMenu\\(\\i\\)\\{.{0,200}isPlatformEmbedded)\\)/,\n                replace: \"||true)\"\n            }\n        },\n        {\n            find: \".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{\",\n            predicate: shouldAddBackMenus,\n            replacement: [\n                {\n                    // if (!IS_DESKTOP) return null;\n                    match: /if\\(!\\i\\.\\i\\)return null;/,\n                    replace: \"\"\n                },\n                {\n                    // Change calls to DiscordNative.clipboard to us instead\n                    match: /\\b\\i\\.\\i\\.(copy|cut|paste)/g,\n                    replace: \"$self.$1\"\n                }\n            ]\n        },\n        {\n            find: '\"add-to-dictionary\"',\n            predicate: shouldAddBackMenus,\n            replacement: {\n                match: /let\\{text:\\i=\"\"/,\n                replace: \"return [null,null];$&\"\n            }\n        },\n\n        // Add back \"Show My Camera\" context menu\n        {\n            find: '\"MediaEngineWebRTC\");',\n            replacement: {\n                match: /supports\\(\\i\\)\\{switch\\(\\i\\)\\{(case (\\i).\\i)/,\n                replace: \"$&.DISABLE_VIDEO:return true;$1\"\n            }\n        },\n        {\n            find: \"#{intl::SEARCH_WITH_GOOGLE}\",\n            replacement: {\n                match: /\\i\\.isPlatformEmbedded/,\n                replace: \"true\"\n            }\n        },\n        {\n            find: \"#{intl::COPY}),hint:\",\n            replacement: [\n                {\n                    match: /\\i\\.isPlatformEmbedded/,\n                    replace: \"true\"\n                },\n                {\n                    match: /\\i\\.\\i\\.copy(?=\\(\\i)/,\n                    replace: \"Vencord.Util.copyToClipboard\"\n                }\n            ],\n            all: true,\n            noWarn: true\n        },\n        // Automod add filter words\n        {\n            find: '(\"interactionUsernameProfile',\n            replacement:\n            {\n                match: /\\i\\.isPlatformEmbedded(?=.{0,50}\\.tagName)/,\n                replace: \"true\"\n            },\n        }\n    ],\n\n    async copyImage(url: string) {\n        url = fixImageUrl(url);\n\n        let imageData = await fetch(url).then(r => r.blob());\n        if (imageData.type !== \"image/png\") {\n            const bitmap = await createImageBitmap(imageData);\n\n            const canvas = document.createElement(\"canvas\");\n            canvas.width = bitmap.width;\n            canvas.height = bitmap.height;\n            canvas.getContext(\"2d\")!.drawImage(bitmap, 0, 0);\n\n            await new Promise<void>(done => {\n                canvas.toBlob(data => {\n                    imageData = data!;\n                    done();\n                }, \"image/png\");\n            });\n        }\n\n        if (IS_VESKTOP && VesktopNative.clipboard) {\n            VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url);\n            return;\n        } else {\n            navigator.clipboard.write([\n                new ClipboardItem({\n                    \"image/png\": imageData\n                })\n            ]);\n        }\n    },\n\n    async saveImage(url: string) {\n        url = fixImageUrl(url);\n\n        const data = await fetchImage(url);\n        if (!data) return;\n\n        const name = new URL(url).pathname.split(\"/\").pop()!;\n        const file = new File([data], name, { type: data.type });\n\n        saveFile(file);\n    },\n\n    copy() {\n        const selection = document.getSelection();\n        if (!selection) return;\n\n        copyToClipboard(selection.toString());\n    },\n\n    cut() {\n        this.copy();\n        ComponentDispatch.dispatch(\"INSERT_TEXT\", { rawText: \"\" });\n    },\n\n    async paste() {\n        const clip = (await navigator.clipboard.read())[0];\n        if (!clip) return;\n\n        const data = new DataTransfer();\n        for (const type of clip.types) {\n            if (type === \"image/png\") {\n                const file = new File([await clip.getType(type)], \"unknown.png\", { type });\n                data.items.add(file);\n            } else if (type === \"text/plain\") {\n                const blob = await clip.getType(type);\n                data.setData(type, await blob.text());\n            }\n        }\n\n        document.dispatchEvent(\n            new ClipboardEvent(\"paste\", {\n                clipboardData: data\n            })\n        );\n    }\n});\n"
  },
  {
    "path": "src/plugins/webKeybinds.web/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Devs, IS_MAC } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\nimport { findByPropsLazy } from \"@webpack\";\nimport { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from \"@webpack/common\";\n\nconst KeyBinds = findByPropsLazy(\"JUMP_TO_GUILD\", \"SERVER_NEXT\");\n\nexport default definePlugin({\n    name: \"WebKeybinds\",\n    description: \"Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,. Only works fully on Vesktop/Legcord, not inside your browser\",\n    authors: [Devs.Ven],\n    enabledByDefault: true,\n\n    onKey(e: KeyboardEvent) {\n        const hasCtrl = e.ctrlKey || (e.metaKey && IS_MAC);\n\n        if (hasCtrl) switch (e.key) {\n            case \"t\":\n            case \"T\":\n                if (!IS_VESKTOP) return;\n                e.preventDefault();\n                if (e.shiftKey) {\n                    if (SelectedGuildStore.getGuildId()) NavigationRouter.transitionToGuild(\"@me\");\n                    ComponentDispatch.safeDispatch(\"TOGGLE_DM_CREATE\");\n                } else {\n                    FluxDispatcher.dispatch({\n                        type: \"QUICKSWITCHER_SHOW\",\n                        query: \"\",\n                        queryMode: null\n                    });\n                }\n                break;\n            case \"Tab\":\n                if (!IS_VESKTOP) return;\n                const handler = e.shiftKey ? KeyBinds.SERVER_PREV : KeyBinds.SERVER_NEXT;\n                handler.action(e);\n                break;\n            case \",\":\n                e.preventDefault();\n                SettingsRouter.openUserSettings(\"my_account_panel\");\n                break;\n            default:\n                if (e.key >= \"1\" && e.key <= \"9\") {\n                    e.preventDefault();\n                    KeyBinds.JUMP_TO_GUILD.action(e, `mod+${e.key}`);\n                }\n                break;\n        }\n    },\n\n    start() {\n        document.addEventListener(\"keydown\", this.onKey);\n    },\n\n    stop() {\n        document.removeEventListener(\"keydown\", this.onKey);\n    }\n});\n"
  },
  {
    "path": "src/plugins/webScreenShareFixes.web/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\nexport default definePlugin({\n    name: \"WebScreenShareFixes\",\n    authors: [Devs.Kaitlyn],\n    description: \"Removes 2500kbps bitrate cap on chromium and vesktop clients.\",\n    enabledByDefault: true,\n\n    patches: [\n        {\n            find: \"x-google-max-bitrate\",\n            replacement: [\n                {\n                    match: /`x-google-max-bitrate=\\$\\{\\i\\}`/,\n                    replace: '\"x-google-max-bitrate=80_000\"'\n                },\n                {\n                    match: \";level-asymmetry-allowed=1\",\n                    replace: \";b=AS:800000;level-asymmetry-allowed=1\"\n                },\n                {\n                    match: /;usedtx=\\$\\{(\\i)\\?\"0\":\"1\"\\}/,\n                    replace: '$&${$1?\";stereo=1;sprop-stereo=1\":\"\"}'\n                },\n            ]\n        }\n    ]\n});\n"
  },
  {
    "path": "src/plugins/whoReacted/README.md",
    "content": "# WhoReacted\n\nNext to each reaction, display each user's avatar. Each avatar can be clicked and will open the profile.\n\n![](https://github.com/Vendicated/Vencord/assets/57493648/97fec9e8-396f-4f5e-916e-1ec21445113d)\n"
  },
  {
    "path": "src/plugins/whoReacted/index.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport ErrorBoundary from \"@components/ErrorBoundary\";\nimport { Devs } from \"@utils/constants\";\nimport { sleep } from \"@utils/misc\";\nimport { Queue } from \"@utils/Queue\";\nimport { useForceUpdater } from \"@utils/react\";\nimport definePlugin from \"@utils/types\";\nimport { CustomEmoji, Message, ReactionEmoji, User } from \"@vencord/discord-types\";\nimport { ChannelStore, Constants, FluxDispatcher, React, RestAPI, useEffect, useLayoutEffect, UserStore, UserSummaryItem } from \"@webpack/common\";\n\nlet Scroll: any = null;\nconst queue = new Queue();\nlet reactions: Record<string, ReactionCacheEntry>;\n\nfunction fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {\n    const key = emoji.name + (emoji.id ? `:${emoji.id}` : \"\");\n    return RestAPI.get({\n        url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),\n        query: {\n            limit: 100,\n            type\n        },\n        oldFormErrors: true\n    })\n        .then(res => {\n            for (const user of res.body) {\n                FluxDispatcher.dispatch({\n                    type: \"USER_UPDATE\",\n                    user\n                });\n            }\n\n            FluxDispatcher.dispatch({\n                type: \"MESSAGE_REACTION_ADD_USERS\",\n                channelId: msg.channel_id,\n                messageId: msg.id,\n                users: res.body,\n                emoji,\n                reactionType: type\n            });\n        })\n        .catch(console.error)\n        .finally(() => sleep(250));\n}\n\nfunction getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {\n    const key = `${msg.id}:${e.name}:${e.id ?? \"\"}:${type}`;\n    const cache = reactions[key] ??= { fetched: false, users: new Map() };\n    if (!cache.fetched) {\n        queue.unshift(() => fetchReactions(msg, e, type));\n        cache.fetched = true;\n    }\n\n    return cache.users;\n}\n\nfunction handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {\n    event.stopPropagation();\n}\n\nexport default definePlugin({\n    name: \"WhoReacted\",\n    description: \"Renders the avatars of users who reacted to a message\",\n    authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],\n\n    patches: [\n        {\n            find: \",reactionRef:\",\n            replacement: {\n                match: /(\\i)\\?null:\\(0,\\i\\.jsx\\)\\(\\i\\.\\i,{className:\\i\\.reactionCount,.*?}\\),/,\n                replace: \"$&$1?null:$self.renderUsers(this.props),\"\n            }\n        },\n        {\n            find: '\"MessageReactionsStore\"',\n            replacement: {\n                match: /function (\\i)\\(\\){(\\i)={}(?=.*CONNECTION_OPEN:\\1)/,\n                replace: \"$&;$self.reactions=$2;\"\n            }\n        },\n        {\n\n            find: \"cleanAutomaticAnchor(){\",\n            replacement: {\n                match: /constructor\\(\\i\\)\\{(?=.{0,100}(?:automaticAnchor|\\.messages\\.loadingMore))/,\n                replace: \"$&$self.setScrollObj(this);\"\n            }\n        }\n    ],\n\n    setScrollObj(scroll: any) {\n        Scroll = scroll;\n    },\n\n    renderUsers(props: RootObject) {\n        return props.message.reactions.length > 10 ? null : (\n            <ErrorBoundary noop>\n                <this.UsersComponent {...props} />\n            </ErrorBoundary>\n        );\n    },\n\n    UsersComponent({ message, emoji, type }: RootObject) {\n        const forceUpdate = useForceUpdater();\n\n        useLayoutEffect(() => { // bc need to prevent autoscrolling\n            if (Scroll?.scrollCounter > 0) {\n                Scroll.setAutomaticAnchor(null);\n            }\n        });\n\n        useEffect(() => {\n            const cb = (e: any) => {\n                if (e?.messageId === message.id)\n                    forceUpdate();\n            };\n            FluxDispatcher.subscribe(\"MESSAGE_REACTION_ADD_USERS\", cb);\n\n            return () => FluxDispatcher.unsubscribe(\"MESSAGE_REACTION_ADD_USERS\", cb);\n        }, [message.id, forceUpdate]);\n\n        const reactions = getReactionsWithQueue(message, emoji, type);\n        const users = Array.from(reactions, ([id]) => UserStore.getUser(id)).filter(Boolean);\n\n        return (\n            <div\n                style={{ marginLeft: \"0.5em\", transform: \"scale(0.9)\" }}\n            >\n                <div onClick={handleClickAvatar} onKeyDown={handleClickAvatar}>\n                    <UserSummaryItem\n                        users={users}\n                        guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}\n                        renderIcon={false}\n                        max={5}\n                        showDefaultAvatarsForNullUsers\n                        showUserPopout\n                    />\n                </div>\n            </div>\n        );\n    },\n\n    set reactions(value: any) {\n        reactions = value;\n    }\n});\n\ninterface ReactionCacheEntry {\n    fetched: boolean;\n    users: Map<string, User>;\n}\n\ninterface RootObject {\n    message: Message;\n    readOnly: boolean;\n    isLurking: boolean;\n    isPendingMember: boolean;\n    useChatFontScaling: boolean;\n    emoji: CustomEmoji;\n    count: number;\n    burst_user_ids: any[];\n    burst_count: number;\n    burst_colors: any[];\n    burst_me: boolean;\n    me: boolean;\n    type: number;\n    hideEmoji: boolean;\n    remainingBurstCurrency: number;\n}\n"
  },
  {
    "path": "src/plugins/xsOverlay/README.md",
    "content": "# XSOverlay Notifier\n\nSends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.\n\n## Preview\n\n![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)\n\n![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4)\n\n## Usage\n- Enable this plugin\n- Set port and plugin settings as desired (defaults should work fine)\n- Open SteamVR and XSOverlay\n"
  },
  {
    "path": "src/plugins/xsOverlay/index.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { definePluginSettings } from \"@api/Settings\";\nimport { Devs } from \"@utils/constants\";\nimport { Logger } from \"@utils/Logger\";\nimport definePlugin, { makeRange, OptionType, PluginNative, ReporterTestable } from \"@utils/types\";\nimport type { Channel, Embed, GuildMember, MessageAttachment, User } from \"@vencord/discord-types\";\nimport { findByCodeLazy, findLazy } from \"@webpack\";\nimport { Button, ChannelStore, GuildRoleStore, GuildStore, UserStore } from \"@webpack/common\";\n\nconst ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);\n\ninterface Message {\n    guild_id: string,\n    attachments: MessageAttachment[],\n    author: User,\n    channel_id: string,\n    components: any[],\n    content: string,\n    edited_timestamp: string,\n    embeds: Embed[],\n    sticker_items?: Sticker[],\n    flags: number,\n    id: string,\n    member: GuildMember,\n    mention_everyone: boolean,\n    mention_roles: string[],\n    mentions: Mention[],\n    nonce: string,\n    pinned: false,\n    referenced_message: any,\n    timestamp: string,\n    tts: boolean,\n    type: number;\n}\n\ninterface Mention {\n    avatar: string,\n    avatar_decoration_data: any,\n    discriminator: string,\n    global_name: string,\n    id: string,\n    public_flags: number,\n    username: string;\n}\n\ninterface Sticker {\n    t: \"Sticker\";\n    description: string;\n    format_type: number;\n    guild_id: string;\n    id: string;\n    name: string;\n    tags: string;\n    type: number;\n}\n\ninterface Call {\n    channel_id: string,\n    guild_id: string,\n    message_id: string,\n    region: string,\n    ringing: string[];\n}\n\ninterface ApiObject {\n    sender: string,\n    target: string,\n    command: string,\n    jsonData: string,\n    rawData: string | null,\n}\n\ninterface NotificationObject {\n    type: number;\n    timeout: number;\n    height: number;\n    opacity: number;\n    volume: number;\n    audioPath: string;\n    title: string;\n    content: string;\n    useBase64Icon: boolean;\n    icon: string;\n    sourceApp: string;\n}\n\nconst notificationsShouldNotify = findByCodeLazy(\".SUPPRESS_NOTIFICATIONS))return!1\");\nconst logger = new Logger(\"XSOverlay\");\n\nconst settings = definePluginSettings({\n    webSocketPort: {\n        type: OptionType.NUMBER,\n        description: \"Websocket port\",\n        default: 42070,\n        async onChange() {\n            await start();\n        }\n    },\n    preferUDP: {\n        type: OptionType.BOOLEAN,\n        description: \"Enable if you use an older build of XSOverlay unable to connect through websockets. This setting is ignored on web.\",\n        default: false,\n        disabled: () => IS_WEB\n    },\n    botNotifications: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow bot notifications\",\n        default: false\n    },\n    serverNotifications: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow server notifications\",\n        default: true\n    },\n    dmNotifications: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow Direct Message notifications\",\n        default: true\n    },\n    groupDmNotifications: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow Group DM notifications\",\n        default: true\n    },\n    callNotifications: {\n        type: OptionType.BOOLEAN,\n        description: \"Allow call notifications\",\n        default: true\n    },\n    pingColor: {\n        type: OptionType.STRING,\n        description: \"User mention color\",\n        default: \"#7289da\"\n    },\n    channelPingColor: {\n        type: OptionType.STRING,\n        description: \"Channel mention color\",\n        default: \"#8a2be2\"\n    },\n    soundPath: {\n        type: OptionType.STRING,\n        description: \"Notification sound (default/warning/error)\",\n        default: \"default\"\n    },\n    timeout: {\n        type: OptionType.NUMBER,\n        description: \"Notification duration (secs)\",\n        default: 3,\n    },\n    lengthBasedTimeout: {\n        type: OptionType.BOOLEAN,\n        description: \"Extend duration with message length\",\n        default: true\n    },\n    opacity: {\n        type: OptionType.SLIDER,\n        description: \"Notif opacity\",\n        default: 1,\n        markers: makeRange(0, 1, 0.1)\n    },\n    volume: {\n        type: OptionType.SLIDER,\n        description: \"Volume\",\n        default: 0.2,\n        markers: makeRange(0, 1, 0.1)\n    },\n});\n\nlet socket: WebSocket;\n\nasync function start() {\n    if (socket) socket.close();\n    socket = new WebSocket(`ws://127.0.0.1:${settings.store.webSocketPort ?? 42070}/?client=Vencord`);\n    return new Promise((resolve, reject) => {\n        socket.onopen = resolve;\n        socket.onerror = reject;\n        setTimeout(reject, 3000);\n    });\n}\n\nconst Native = VencordNative.pluginHelpers.XSOverlay as PluginNative<typeof import(\"./native\")>;\n\nexport default definePlugin({\n    name: \"XSOverlay\",\n    description: \"Forwards discord notifications to XSOverlay, for easy viewing in VR\",\n    authors: [Devs.Nyako],\n    tags: [\"vr\", \"notify\"],\n    reporterTestable: ReporterTestable.None,\n    settings,\n\n    flux: {\n        CALL_UPDATE({ call }: { call: Call; }) {\n            if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {\n                const channel = ChannelStore.getChannel(call.channel_id);\n                sendOtherNotif(\"Incoming call\", `${channel.name} is calling you...`);\n            }\n        },\n        MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) {\n            if (optimistic) return;\n            const channel = ChannelStore.getChannel(message.channel_id);\n            if (!shouldNotify(message, message.channel_id)) return;\n\n            const pingColor = settings.store.pingColor.replaceAll(\"#\", \"\").trim();\n            const channelPingColor = settings.store.channelPingColor.replaceAll(\"#\", \"\").trim();\n            let finalMsg = message.content;\n            let titleString = \"\";\n\n            if (channel.guild_id) {\n                const guild = GuildStore.getGuild(channel.guild_id);\n                titleString = `${message.author.username} (${guild.name}, #${channel.name})`;\n            }\n\n\n            switch (channel.type) {\n                case ChannelTypes.DM:\n                    titleString = message.author.username.trim();\n                    break;\n                case ChannelTypes.GROUP_DM:\n                    const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(\", \");\n                    titleString = `${message.author.username} (${channelName})`;\n                    break;\n            }\n\n            if (message.referenced_message) {\n                titleString += \" (reply)\";\n            }\n\n            if (message.embeds.length > 0) {\n                finalMsg += \" [embed] \";\n                if (message.content === \"\") {\n                    finalMsg = \"sent message embed(s)\";\n                }\n            }\n\n            if (message.sticker_items) {\n                finalMsg += \" [sticker] \";\n                if (message.content === \"\") {\n                    finalMsg = \"sent a sticker\";\n                }\n            }\n\n            const images = message.attachments.filter(e =>\n                typeof e?.content_type === \"string\"\n                && e?.content_type.startsWith(\"image\")\n            );\n\n\n            images.forEach(img => {\n                finalMsg += ` [image: ${img.filename}] `;\n            });\n\n            message.attachments.filter(a => a && !a.content_type?.startsWith(\"image\")).forEach(a => {\n                finalMsg += ` [attachment: ${a.filename}] `;\n            });\n\n            // make mentions readable\n            if (message.mentions.length > 0) {\n                finalMsg = finalMsg.replace(/<@!?(\\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || \"unknown-user\"}</color></b>`);\n            }\n\n            // color role mentions (unity styling btw lol)\n            if (message.mention_roles.length > 0) {\n                for (const roleId of message.mention_roles) {\n                    const role = GuildRoleStore.getRole(channel.guild_id, roleId);\n                    if (!role) continue;\n                    const roleColor = role.colorString ?? `#${pingColor}`;\n                    finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);\n                }\n            }\n\n            // make emotes and channel mentions readable\n            const emoteMatches = finalMsg.match(new RegExp(\"(<a?:\\\\w+:\\\\d+>)\", \"g\"));\n            const channelMatches = finalMsg.match(new RegExp(\"<(#\\\\d+)>\", \"g\"));\n\n            if (emoteMatches) {\n                for (const eMatch of emoteMatches) {\n                    finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, \"g\"), `:${eMatch.split(\":\")[1]}:`);\n                }\n            }\n\n            // color channel mentions\n            if (channelMatches) {\n                for (const cMatch of channelMatches) {\n                    let channelId = cMatch.split(\"<#\")[1];\n                    channelId = channelId.substring(0, channelId.length - 1);\n                    finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, \"g\"), `<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId).name}</color></b>`);\n                }\n            }\n\n            if (shouldIgnoreForChannelType(channel)) return;\n            sendMsgNotif(titleString, finalMsg, message);\n        }\n    },\n\n    start,\n\n    stop() {\n        socket.close();\n    },\n\n    settingsAboutComponent: () => (\n        <>\n            <Button onClick={() => sendOtherNotif(\"This is a test notification! explode\", \"Hello from Vendor!\")}>\n                Send test notification\n            </Button>\n        </>\n    )\n});\n\nfunction shouldIgnoreForChannelType(channel: Channel) {\n    if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;\n    if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;\n    else return !settings.store.serverNotifications;\n}\n\nfunction sendMsgNotif(titleString: string, content: string, message: Message) {\n    fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`)\n        .then(response => response.blob())\n        .then(blob => new Promise<string>(resolve => {\n            const r = new FileReader();\n            r.onload = () => resolve((r.result as string).split(\",\")[1]);\n            r.readAsDataURL(blob);\n        })).then(result => {\n            const msgData: NotificationObject = {\n                type: 1,\n                timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,\n                height: calculateHeight(content),\n                opacity: settings.store.opacity,\n                volume: settings.store.volume,\n                audioPath: settings.store.soundPath,\n                title: titleString,\n                content: content,\n                useBase64Icon: true,\n                icon: result,\n                sourceApp: \"Vencord\"\n            };\n\n            sendToOverlay(msgData);\n        });\n}\n\nfunction sendOtherNotif(content: string, titleString: string) {\n    const msgData: NotificationObject = {\n        type: 1,\n        timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,\n        height: calculateHeight(content),\n        opacity: settings.store.opacity,\n        volume: settings.store.volume,\n        audioPath: settings.store.soundPath,\n        title: titleString,\n        content: content,\n        useBase64Icon: false,\n        icon: \"default\",\n        sourceApp: \"Vencord\"\n    };\n    sendToOverlay(msgData);\n}\n\nasync function sendToOverlay(notif: NotificationObject) {\n    if (!IS_WEB && settings.store.preferUDP) {\n        Native.sendToOverlay(notif);\n        return;\n    }\n    const apiObject: ApiObject = {\n        sender: \"Vencord\",\n        target: \"xsoverlay\",\n        command: \"SendNotification\",\n        jsonData: JSON.stringify(notif),\n        rawData: null\n    };\n    if (socket.readyState !== socket.OPEN) await start();\n    socket.send(JSON.stringify(apiObject));\n}\n\nfunction shouldNotify(message: Message, channel: string) {\n    const currentUser = UserStore.getCurrentUser();\n    if (message.author.id === currentUser.id) return false;\n    if (message.author.bot && !settings.store.botNotifications) return false;\n    return notificationsShouldNotify(message, channel);\n}\n\nfunction calculateHeight(content: string) {\n    if (content.length <= 100) return 100;\n    if (content.length <= 200) return 150;\n    if (content.length <= 300) return 200;\n    return 250;\n}\n\nfunction calculateTimeout(content: string) {\n    if (content.length <= 100) return 3;\n    if (content.length <= 200) return 4;\n    if (content.length <= 300) return 5;\n    return 6;\n}\n"
  },
  {
    "path": "src/plugins/xsOverlay/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { createSocket, Socket } from \"dgram\";\n\nlet xsoSocket: Socket;\n\nexport function sendToOverlay(_, data: any) {\n    data.messageType = data.type;\n    const json = JSON.stringify(data);\n    xsoSocket ??= createSocket(\"udp4\");\n    xsoSocket.send(json, 42069, \"127.0.0.1\");\n}\n"
  },
  {
    "path": "src/plugins/youtubeAdblock.desktop/README.md",
    "content": "# WatchTogetherAdblock\n\nBlock ads in YouTube embeds and the WatchTogether activity via AdGuard\n\nNote that this only works for yourself, other users in the activity will still see ads.\n\nPowered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut)\n"
  },
  {
    "path": "src/plugins/youtubeAdblock.desktop/adguard.js",
    "content": "/* eslint-disable */\n\n/**\n * This file is part of AdGuard's Block YouTube Ads (https://github.com/AdguardTeam/BlockYouTubeAdsShortcut).\n *\n * Copyright (C) AdGuard Team\n *\n * AdGuard's Block YouTube Ads is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * AdGuard's Block YouTube Ads is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with AdGuard's Block YouTube Ads.  If not, see <http://www.gnu.org/licenses/>.\n */\n\nconst hiddenCSS = [\n    \"#__ffYoutube1\",\n    \"#__ffYoutube2\",\n    \"#__ffYoutube3\",\n    \"#__ffYoutube4\",\n    \"#feed-pyv-container\",\n    \"#feedmodule-PRO\",\n    \"#homepage-chrome-side-promo\",\n    \"#merch-shelf\",\n    \"#offer-module\",\n    '#pla-shelf > ytd-pla-shelf-renderer[class=\"style-scope ytd-watch\"]',\n    \"#pla-shelf\",\n    \"#premium-yva\",\n    \"#promo-info\",\n    \"#promo-list\",\n    \"#promotion-shelf\",\n    \"#related > ytd-watch-next-secondary-results-renderer > #items > ytd-compact-promoted-video-renderer.ytd-watch-next-secondary-results-renderer\",\n    \"#search-pva\",\n    \"#shelf-pyv-container\",\n    \"#video-masthead\",\n    \"#watch-branded-actions\",\n    \"#watch-buy-urls\",\n    \"#watch-channel-brand-div\",\n    \"#watch7-branded-banner\",\n    \"#YtKevlarVisibilityIdentifier\",\n    \"#YtSparklesVisibilityIdentifier\",\n    \".carousel-offer-url-container\",\n    \".companion-ad-container\",\n    \".GoogleActiveViewElement\",\n    '.list-view[style=\"margin: 7px 0pt;\"]',\n    \".promoted-sparkles-text-search-root-container\",\n    \".promoted-videos\",\n    \".searchView.list-view\",\n    \".sparkles-light-cta\",\n    \".watch-extra-info-column\",\n    \".watch-extra-info-right\",\n    \".ytd-carousel-ad-renderer\",\n    \".ytd-compact-promoted-video-renderer\",\n    \".ytd-companion-slot-renderer\",\n    \".ytd-merch-shelf-renderer\",\n    \".ytd-player-legacy-desktop-watch-ads-renderer\",\n    \".ytd-promoted-sparkles-text-search-renderer\",\n    \".ytd-promoted-video-renderer\",\n    \".ytd-search-pyv-renderer\",\n    \".ytd-video-masthead-ad-v3-renderer\",\n    \".ytp-ad-action-interstitial-background-container\",\n    \".ytp-ad-action-interstitial-slot\",\n    \".ytp-ad-image-overlay\",\n    \".ytp-ad-overlay-container\",\n    \".ytp-ad-progress\",\n    \".ytp-ad-progress-list\",\n    '[class*=\"ytd-display-ad-\"]',\n    '[layout*=\"display-ad-\"]',\n    'a[href^=\"http://www.youtube.com/cthru?\"]',\n    'a[href^=\"https://www.youtube.com/cthru?\"]',\n    \"ytd-action-companion-ad-renderer\",\n    \"ytd-banner-promo-renderer\",\n    \"ytd-compact-promoted-video-renderer\",\n    \"ytd-companion-slot-renderer\",\n    \"ytd-display-ad-renderer\",\n    \"ytd-promoted-sparkles-text-search-renderer\",\n    \"ytd-promoted-sparkles-web-renderer\",\n    \"ytd-search-pyv-renderer\",\n    \"ytd-single-option-survey-renderer\",\n    \"ytd-video-masthead-ad-advertiser-info-renderer\",\n    \"ytd-video-masthead-ad-v3-renderer\",\n    \"YTM-PROMOTED-VIDEO-RENDERER\",\n];\n/**\n* Adds CSS to the page\n*/\nconst hideElements = () => {\n    const selectors = hiddenCSS;\n    if (!selectors) {\n        return;\n    }\n    const rule = selectors.join(\", \") + \" { display: none!important; }\";\n    const style = document.createElement(\"style\");\n    style.textContent = rule;\n    document.head.appendChild(style);\n};\n/**\n* Calls the \"callback\" function on every DOM change, but not for the tracked events\n* @param {Function} callback callback function\n*/\nconst observeDomChanges = callback => {\n    const domMutationObserver = new MutationObserver(mutations => {\n        callback(mutations);\n    });\n    domMutationObserver.observe(document.documentElement, {\n        childList: true,\n        subtree: true,\n    });\n};\n/**\n* This function is supposed to be called on every DOM change\n*/\nconst hideDynamicAds = () => {\n    const elements = document.querySelectorAll(\"#contents > ytd-rich-item-renderer ytd-display-ad-renderer\");\n    if (elements.length === 0) {\n        return;\n    }\n    elements.forEach(el => {\n        if (el.parentNode && el.parentNode.parentNode) {\n            const parent = el.parentNode.parentNode;\n            if (parent.localName === \"ytd-rich-item-renderer\") {\n                parent.style.display = \"none\";\n            }\n        }\n    });\n};\n/**\n* This function checks if the video ads are currently running\n* and auto-clicks the skip button.\n*/\nconst autoSkipAds = () => {\n    // If there's a video that plays the ad at this moment, scroll this ad\n    if (document.querySelector(\".ad-showing\")) {\n        const video = document.querySelector(\"video\");\n        if (video && video.duration) {\n            video.currentTime = video.duration;\n            // Skip button should appear after that,\n            // now simply click it automatically\n            setTimeout(() => {\n                const skipBtn = document.querySelector(\"button.ytp-ad-skip-button\");\n                if (skipBtn) {\n                    skipBtn.click();\n                }\n            }, 100);\n        }\n    }\n};\n/**\n* This function overrides a property on the specified object.\n*\n* @param {object} obj object to look for properties in\n* @param {string} propertyName property to override\n* @param {*} overrideValue value to set\n*/\nconst overrideObject = (obj, propertyName, overrideValue) => {\n    if (!obj) {\n        return false;\n    }\n    let overriden = false;\n    for (const key in obj) {\n        if (obj.hasOwnProperty(key) && key === propertyName) {\n            obj[key] = overrideValue;\n            overriden = true;\n        } else if (obj.hasOwnProperty(key) && typeof obj[key] === \"object\") {\n            if (overrideObject(obj[key], propertyName, overrideValue)) {\n                overriden = true;\n            }\n        }\n    }\n    return overriden;\n};\n/**\n* Overrides JSON.parse and Response.json functions.\n* Examines these functions arguments, looks for properties with the specified name there\n* and if it exists, changes it's value to what was specified.\n*\n* @param {string} propertyName name of the property\n* @param {*} overrideValue new value for the property\n*/\nconst jsonOverride = (propertyName, overrideValue) => {\n    const nativeJSONParse = JSON.parse;\n    JSON.parse = (...args) => {\n        const obj = nativeJSONParse.apply(this, args);\n        // Override it's props and return back to the caller\n        overrideObject(obj, propertyName, overrideValue);\n        return obj;\n    };\n    // Override Response.prototype.json\n    Response.prototype.json = new Proxy(Response.prototype.json, {\n        async apply(...args) {\n            // Call the target function, get the original Promise\n            const result = await Reflect.apply(...args);\n            // Create a new one and override the JSON inside\n            overrideObject(result, propertyName, overrideValue);\n            return result;\n        },\n    });\n};\n// Removes ads metadata from YouTube XHR requests\njsonOverride(\"adPlacements\", []);\njsonOverride(\"playerAds\", []);\n// Applies CSS that hides YouTube ad elements\nhideElements();\n// Some changes should be re-evaluated on every page change\nhideDynamicAds();\nautoSkipAds();\nobserveDomChanges(() => {\n    hideDynamicAds();\n    autoSkipAds();\n});\n"
  },
  {
    "path": "src/plugins/youtubeAdblock.desktop/index.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Devs } from \"@utils/constants\";\nimport definePlugin from \"@utils/types\";\n\n// The entire code of this plugin can be found in native.ts\nexport default definePlugin({\n    name: \"YoutubeAdblock\",\n    description: \"Block ads in YouTube embeds and the WatchTogether activity via AdGuard\",\n    authors: [Devs.ImLvna, Devs.Ven],\n});\n"
  },
  {
    "path": "src/plugins/youtubeAdblock.desktop/native.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { RendererSettings } from \"@main/settings\";\nimport { app } from \"electron\";\nimport adguard from \"file://adguard.js?minify\";\n\napp.on(\"browser-window-created\", (_, win) => {\n    win.webContents.on(\"frame-created\", (_, { frame }) => {\n        frame?.once(\"dom-ready\", () => {\n            if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;\n\n            if (frame.url.includes(\"youtube.com/embed/\")) {\n                frame.executeJavaScript(adguard);\n            } else if (frame.parent?.url.includes(\"youtube.com/embed/\")) {\n                frame.parent.executeJavaScript(adguard);\n            }\n        });\n    });\n});\n"
  },
  {
    "path": "src/preload.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { debounce } from \"@shared/debounce\";\nimport { IpcEvents } from \"@shared/IpcEvents\";\nimport { contextBridge, webFrame } from \"electron/renderer\";\n\nimport VencordNative, { invoke, sendSync } from \"./VencordNative\";\n\ncontextBridge.exposeInMainWorld(\"VencordNative\", VencordNative);\n\n// Discord\nif (location.protocol !== \"data:\") {\n    invoke(IpcEvents.INIT_FILE_WATCHERS);\n\n    if (IS_DISCORD_DESKTOP) {\n        webFrame.executeJavaScript(sendSync<string>(IpcEvents.PRELOAD_GET_RENDERER_JS));\n        // Not supported in sandboxed preload scripts but Discord doesn't support it either so who cares\n        require(process.env.DISCORD_PRELOAD!);\n    }\n} // Monaco popout\nelse {\n    contextBridge.exposeInMainWorld(\"setCss\", debounce(VencordNative.quickCss.set));\n    contextBridge.exposeInMainWorld(\"getCurrentCss\", VencordNative.quickCss.get);\n    contextBridge.exposeInMainWorld(\"getTheme\", VencordNative.quickCss.getEditorTheme);\n}\n"
  },
  {
    "path": "src/shared/IpcEvents.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const enum IpcEvents {\n    INIT_FILE_WATCHERS = \"VencordInitFileWatchers\",\n\n    OPEN_QUICKCSS = \"VencordOpenQuickCss\",\n    GET_QUICK_CSS = \"VencordGetQuickCss\",\n    SET_QUICK_CSS = \"VencordSetQuickCss\",\n    QUICK_CSS_UPDATE = \"VencordQuickCssUpdate\",\n\n    GET_SETTINGS = \"VencordGetSettings\",\n    SET_SETTINGS = \"VencordSetSettings\",\n\n    GET_THEMES_LIST = \"VencordGetThemesList\",\n    GET_THEME_DATA = \"VencordGetThemeData\",\n    GET_THEME_SYSTEM_VALUES = \"VencordGetThemeSystemValues\",\n    THEME_UPDATE = \"VencordThemeUpdate\",\n\n    OPEN_EXTERNAL = \"VencordOpenExternal\",\n    OPEN_THEMES_FOLDER = \"VencordOpenThemesFolder\",\n    OPEN_SETTINGS_FOLDER = \"VencordOpenSettingsFolder\",\n\n    GET_UPDATES = \"VencordGetUpdates\",\n    GET_REPO = \"VencordGetRepo\",\n    UPDATE = \"VencordUpdate\",\n    BUILD = \"VencordBuild\",\n\n    OPEN_MONACO_EDITOR = \"VencordOpenMonacoEditor\",\n    GET_MONACO_THEME = \"VencordGetMonacoTheme\",\n\n    GET_PLUGIN_IPC_METHOD_MAP = \"VencordGetPluginIpcMethodMap\",\n\n    CSP_IS_DOMAIN_ALLOWED = \"VencordCspIsDomainAllowed\",\n    CSP_REMOVE_OVERRIDE = \"VencordCspRemoveOverride\",\n    CSP_REQUEST_ADD_OVERRIDE = \"VencordCspRequestAddOverride\",\n\n    GET_RENDERER_CSS = \"VencordGetRendererCss\",\n    RENDERER_CSS_UPDATE = \"VencordRendererCssUpdate\",\n    PRELOAD_GET_RENDERER_JS = \"VencordPreloadGetRendererJs\",\n}\n"
  },
  {
    "path": "src/shared/SettingsStore.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { LiteralUnion } from \"type-fest\";\n\nexport const SYM_IS_PROXY = Symbol(\"SettingsStore.isProxy\");\nexport const SYM_GET_RAW_TARGET = Symbol(\"SettingsStore.getRawTarget\");\n\n// Resolves a possibly nested prop in the form of \"some.nested.prop\" to type of T.some.nested.prop\ntype ResolvePropDeep<T, P> =\n    P extends `${infer Pre}.*` ?\n    Pre extends keyof T\n    ? T[Pre][keyof T[Pre]]\n    : any\n    : P extends `${infer Pre}.${infer Suf}`\n    ? Pre extends keyof T\n    ? ResolvePropDeep<T[Pre], Suf>\n    : any\n    : P extends keyof T\n    ? T[P]\n    : any;\n\ninterface SettingsStoreOptions {\n    readOnly?: boolean;\n    getDefaultValue?: (data: {\n        target: any;\n        key: string;\n        root: any;\n        path: string;\n    }) => any;\n}\n\n// merges the SettingsStoreOptions type into the class\nexport interface SettingsStore<T extends object> extends SettingsStoreOptions { }\n\ninterface ProxyContext<T extends object = any> {\n    root: T;\n    path: string;\n}\n\n/**\n * The SettingsStore allows you to easily create a mutable store that\n * has support for global and path-based change listeners.\n */\nexport class SettingsStore<T extends object> {\n    private pathListeners = new Map<string, Set<(newData: any) => void>>();\n    private prefixListeners = new Map<string, Set<(newData: any, path: string) => void>>();\n    private globalListeners = new Set<(newData: T, path: string) => void>();\n    private readonly proxyContexts = new WeakMap<any, ProxyContext<T>>();\n\n    private readonly proxyHandler: ProxyHandler<any> = (() => {\n        const self = this;\n\n        return {\n            get(target, key: any, receiver) {\n                if (key === SYM_IS_PROXY) {\n                    return true;\n                }\n\n                if (key === SYM_GET_RAW_TARGET) {\n                    return target;\n                }\n\n                let v = Reflect.get(target, key, receiver);\n\n                const proxyContext = self.proxyContexts.get(target);\n                if (proxyContext == null) {\n                    return v;\n                }\n\n                const { root, path } = proxyContext;\n\n                if (!(key in target) && self.getDefaultValue != null) {\n                    v = self.getDefaultValue({\n                        target,\n                        key,\n                        root,\n                        path\n                    });\n                }\n\n                if (typeof v === \"object\" && v !== null && !v[SYM_IS_PROXY]) {\n                    const getPath = `${path}${path && \".\"}${key}`;\n                    return self.makeProxy(v, root, getPath);\n                }\n\n                return v;\n            },\n            set(target, key: string, value) {\n                if (value?.[SYM_IS_PROXY]) {\n                    value = value[SYM_GET_RAW_TARGET];\n                }\n\n                if (target[key] === value) {\n                    return true;\n                }\n\n                if (!Reflect.set(target, key, value)) {\n                    return false;\n                }\n\n                const proxyContext = self.proxyContexts.get(target);\n                if (proxyContext == null) {\n                    return true;\n                }\n\n                const { root, path } = proxyContext;\n\n                const setPath = `${path}${path && \".\"}${key}`;\n                self.notifyListeners(setPath, value, root);\n\n                return true;\n            },\n            deleteProperty(target, key: string) {\n                if (!Reflect.deleteProperty(target, key)) {\n                    return false;\n                }\n\n                const proxyContext = self.proxyContexts.get(target);\n                if (proxyContext == null) {\n                    return true;\n                }\n\n                const { root, path } = proxyContext;\n\n                const deletePath = `${path}${path && \".\"}${key}`;\n                self.notifyListeners(deletePath, undefined, root);\n\n                return true;\n            }\n        };\n    })();\n\n    /**\n     * The store object. Making changes to this object will trigger the applicable change listeners\n     */\n    public declare store: T;\n    /**\n     * The plain data. Changes to this object will not trigger any change listeners\n     */\n    public declare plain: T;\n\n    public constructor(plain: T, options: SettingsStoreOptions = {}) {\n        this.plain = plain;\n        this.store = this.makeProxy(plain);\n        Object.assign(this, options);\n    }\n\n    private makeProxy(object: any, root: T = object, path = \"\") {\n        this.proxyContexts.set(object, {\n            root,\n            path\n        });\n\n        return new Proxy(object, this.proxyHandler);\n    }\n\n    private notifyPrefixListeners(pathString: string, pathElements: string[], value: any) {\n        for (let i = 1; i <= pathElements.length; i++) {\n            const prefix = pathElements.slice(0, i).join(\".\");\n            this.prefixListeners.get(prefix)?.forEach(cb => cb(value, pathString));\n        }\n    }\n\n    private notifyListeners(pathStr: string, value: any, root: T) {\n        const paths = pathStr.split(\".\");\n\n        // Because we support any type of settings with OptionType.CUSTOM, and those objects get proxied recursively,\n        // the path ends up including all the nested paths (plugins.pluginName.settingName.example.one).\n        // So, we need to extract the top-level setting path (plugins.pluginName.settingName),\n        // to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use([\"settingName\"]),\n        // with the new value\n        if (paths.length > 3 && paths[0] === \"plugins\") {\n            const settingPath = paths.slice(0, 3);\n            const settingPathStr = settingPath.join(\".\");\n            const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);\n\n            this.globalListeners.forEach(cb => cb(root, settingPathStr));\n            this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));\n        } else {\n            this.globalListeners.forEach(cb => cb(root, pathStr));\n        }\n\n        this.pathListeners.get(pathStr)?.forEach(cb => cb(value));\n        this.notifyPrefixListeners(pathStr, paths, value);\n    }\n\n    /**\n     * Set the data of the store.\n     * This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables)\n     *\n     * Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data\n     * @param value New data\n     * @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc\n     */\n    public setData(value: T, pathToNotify?: string) {\n        if (this.readOnly) throw new Error(\"SettingsStore is read-only\");\n\n        this.plain = value;\n        this.store = this.makeProxy(value);\n\n        if (pathToNotify) {\n            let v = value;\n\n            const path = pathToNotify.split(\".\");\n            for (const p of path) {\n                if (!v) {\n                    console.warn(\n                        `Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update`\n                    );\n                    return;\n                }\n                v = v[p];\n            }\n\n            this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));\n            this.notifyPrefixListeners(pathToNotify, path, v);\n        }\n\n        this.markAsChanged();\n    }\n\n    /**\n     * Add a global change listener, that will fire whenever any setting is changed\n     *\n     * @param data The new data. This is either the new value set on the path, or the new root object if it was changed\n     * @param path The path of the setting that was changed. Empty string if the root object was changed\n     */\n    public addGlobalChangeListener(cb: (data: any, path: string) => void) {\n        this.globalListeners.add(cb);\n    }\n\n    /**\n     * Add a scoped change listener that will fire whenever a setting matching the specified path is changed.\n     *\n     * For example if path is `\"foo.bar\"`, the listener will fire on\n     * ```js\n     * Setting.store.foo.bar = \"hi\"\n     * ```\n     * but not on\n     * ```js\n     * Setting.store.foo.baz = \"hi\"\n     * ```\n     */\n    public addChangeListener<P extends LiteralUnion<keyof T, string>>(\n        path: P,\n        cb: (data: ResolvePropDeep<T, P>) => void\n    ) {\n        const listeners = this.pathListeners.get(path as string) ?? new Set();\n        listeners.add(cb);\n        this.pathListeners.set(path as string, listeners);\n    }\n\n    /**\n     * Add a prefix change listener that will fire whenever a setting matching the specified prefix is changed.\n     * For example if prefix is `\"foo\"`, the listener will fire on\n     * ```js\n     * Setting.store.foo.bar = \"hi\"\n     * Setting.store.foo.baz = \"hi\"\n     * ```\n     */\n    public addPrefixChangeListener<P extends string>(prefix: P, cb: (data: ResolvePropDeep<T, P>, path: string) => void) {\n        const listeners = this.prefixListeners.get(prefix) ?? new Set();\n        listeners.add(cb);\n        this.prefixListeners.set(prefix, listeners);\n    }\n\n    /**\n     * Remove a global listener\n     * @see {@link addGlobalChangeListener}\n     */\n    public removeGlobalChangeListener(cb: (data: any, path: string) => void) {\n        this.globalListeners.delete(cb);\n    }\n\n    /**\n     * Remove a scoped listener\n     * @see {@link addChangeListener}\n     */\n    public removeChangeListener(path: LiteralUnion<keyof T, string>, cb: (data: any) => void) {\n        const listeners = this.pathListeners.get(path as string);\n        if (!listeners) return;\n\n        listeners.delete(cb);\n        if (!listeners.size) this.pathListeners.delete(path as string);\n    }\n\n    /**\n     * Remove a prefix listener\n     * @see {@link addPrefixChangeListener}\n     */\n    public removePrefixChangeListener(prefix: string, cb: (data: any, path: string) => void) {\n        const listeners = this.prefixListeners.get(prefix);\n        if (!listeners) return;\n\n        listeners.delete(cb);\n        if (!listeners.size) this.prefixListeners.delete(prefix);\n    }\n\n    /**\n     * Call all global change listeners\n     */\n    public markAsChanged() {\n        this.globalListeners.forEach(cb => cb(this.plain, \"\"));\n    }\n}\n"
  },
  {
    "path": "src/shared/debounce.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/**\n * Returns a new function that will call the wrapped function\n * after the specified delay. If the function is called again\n * within the delay, the timer will be reset.\n * @param func The function to wrap\n * @param delay The delay in milliseconds\n */\nexport function debounce<T extends Function>(func: T, delay = 300): T {\n    let timeout: NodeJS.Timeout;\n    return function (...args: any[]) {\n        clearTimeout(timeout);\n        timeout = setTimeout(() => { func(...args); }, delay);\n    } as any;\n}\n"
  },
  {
    "path": "src/shared/onceDefined.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type { LiteralUnion } from \"type-fest\";\n\n/**\n * Wait for a property to be defined on the target, then call the callback with\n * the value\n * @param target Object\n * @param property Property to be defined\n * @param callback Callback\n *\n * @example onceDefined(window, \"webpackChunkdiscord_app\", wpInstance => wpInstance.push(...));\n */\nexport function onceDefined<T extends object, P extends LiteralUnion<keyof T, PropertyKey>>(\n    target: T, property: P, callback: (v: P extends keyof T ? T[P] : any) => void\n): void {\n    const propertyAsAny = property as any;\n\n    if (property in target)\n        return void callback(target[propertyAsAny]);\n\n    Object.defineProperty(target, property, {\n        set(v) {\n            delete target[propertyAsAny];\n            target[propertyAsAny] = v;\n            callback(v);\n        },\n        configurable: true,\n        enumerable: false\n    });\n}\n"
  },
  {
    "path": "src/shared/vencordUserAgent.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport gitHash from \"~git-hash\";\nimport gitRemote from \"~git-remote\";\n\nexport { gitHash, gitRemote };\n\nexport const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : \"\"}`;\n"
  },
  {
    "path": "src/utils/ChangeList.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport class ChangeList<T>{\n    private set = new Set<T>();\n\n    public get changeCount() {\n        return this.set.size;\n    }\n\n    public get hasChanges() {\n        return this.changeCount > 0;\n    }\n\n    public handleChange(item: T) {\n        if (!this.set.delete(item))\n            this.set.add(item);\n    }\n\n    public add(item: T) {\n        return this.set.add(item);\n    }\n\n    public remove(item: T) {\n        return this.set.delete(item);\n    }\n\n    public getChanges() {\n        return this.set.values();\n    }\n\n    public map<R>(mapper: (v: T, idx: number, arr: T[]) => R): R[] {\n        return [...this.getChanges()].map(mapper);\n    }\n}\n"
  },
  {
    "path": "src/utils/Logger.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport class Logger {\n    /**\n     * Returns the console format args for a title with the specified background colour and black text\n     * @param color Background colour\n     * @param title Text\n     * @returns Array. Destructure this into {@link Logger}.errorCustomFmt or console.log\n     *\n     * @example logger.errorCustomFmt(...Logger.makeTitleElements(\"white\", \"Hello\"), \"World\");\n     */\n    static makeTitle(color: string, title: string): [string, ...string[]] {\n        return [\"%c %c %s \", \"\", `background: ${color}; color: black; font-weight: bold; border-radius: 5px;`, title];\n    }\n\n    constructor(public name: string, public color: string = \"white\") { }\n\n    private _log(level: \"log\" | \"error\" | \"warn\" | \"info\" | \"debug\", levelColor: string, args: any[], customFmt = \"\") {\n        if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {\n            console[level](\"[Vencord]\", this.name + \":\", ...args);\n            return;\n        }\n\n        console[level](\n            `%c Vencord %c %c ${this.name} ${customFmt}`,\n            `background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`,\n            \"\",\n            `background: ${this.color}; color: black; font-weight: bold; border-radius: 5px;`\n            , ...args\n        );\n    }\n\n    public log(...args: any[]) {\n        this._log(\"log\", \"#a6d189\", args);\n    }\n\n    public info(...args: any[]) {\n        this._log(\"info\", \"#a6d189\", args);\n    }\n\n    public error(...args: any[]) {\n        this._log(\"error\", \"#e78284\", args);\n    }\n\n    public errorCustomFmt(fmt: string, ...args: any[]) {\n        this._log(\"error\", \"#e78284\", args, fmt);\n    }\n\n    public warn(...args: any[]) {\n        this._log(\"warn\", \"#e5c890\", args);\n    }\n\n    public debug(...args: any[]) {\n        this._log(\"debug\", \"#eebebe\", args);\n    }\n}\n"
  },
  {
    "path": "src/utils/Queue.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Promisable } from \"type-fest\";\n\n/**\n * A queue that can be used to run tasks consecutively.\n * Highly recommended for things like fetching data from Discord\n */\nexport class Queue {\n    /**\n     * @param maxSize The maximum amount of functions that can be queued at once.\n     *                If the queue is full, the oldest function will be removed.\n     */\n    constructor(public readonly maxSize = Infinity) { }\n\n    private queue = [] as Array<() => Promisable<unknown>>;\n\n    private promise?: Promise<any>;\n\n    private next() {\n        const func = this.queue.shift();\n        if (func)\n            this.promise = Promise.resolve()\n                .then(func)\n                .finally(() => this.next());\n        else\n            this.promise = undefined;\n    }\n\n    private run() {\n        if (!this.promise)\n            this.next();\n    }\n\n    /**\n     * Append a task at the end of the queue. This task will be executed after all other tasks\n     * If the queue exceeds the specified maxSize, the first task in queue will be removed.\n     * @param func Task\n     */\n    push<T>(func: () => Promisable<T>) {\n        if (this.size >= this.maxSize)\n            this.queue.shift();\n\n        this.queue.push(func);\n        this.run();\n    }\n\n    /**\n     * Prepend a task at the beginning of the queue. This task will be executed next\n     * If the queue exceeds the specified maxSize, the last task in queue will be removed.\n     * @param func Task\n     */\n    unshift<T>(func: () => Promisable<T>) {\n        if (this.size >= this.maxSize)\n            this.queue.pop();\n\n        this.queue.unshift(func);\n        this.run();\n    }\n\n    /**\n     * The amount of tasks in the queue\n     */\n    get size() {\n        return this.queue.length;\n    }\n}\n"
  },
  {
    "path": "src/utils/apng.ts",
    "content": "/* eslint-disable */\n\n/**\n * apng-canvas v2.1.2 - heavily modified and converted to Typescript\n *\n * @copyright 2011-2019 David Mzareulyan\n * @link https://github.com/davidmz/apng-canvas\n * @license MIT\n */\n\n// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk\nexport const enum ApngDisposeOp {\n    /**\n     * no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.\n     */\n    NONE,\n    /**\n     * the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.\n     */\n    BACKGROUND,\n    /**\n     * the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.\n     */\n    PREVIOUS\n}\n\n// TODO: Might need to somehow implement this\nexport const enum ApngBlendOp {\n    SOURCE,\n    OVER\n}\n\ninterface Frame {\n    width: number;\n    height: number;\n    left: number;\n    top: number;\n    delay: number;\n    disposeOp: ApngDisposeOp;\n    blendOp: ApngBlendOp;\n    dataParts?: U8Arr[];\n    img: HTMLImageElement;\n}\n\nclass Animation {\n    width = 0;\n    height = 0;\n    numPlays = 0;\n    playTime = 0;\n    frames: Frame[] = [];\n}\n\n// Typescript has become more strict, requiring you to explicitly specify the ArrayBuffer generic\ntype U8Arr = Uint8Array<ArrayBuffer>;\n\nconst table = new Uint32Array(256);\n\nfor (let i = 0; i < 256; i++) {\n    let c = i;\n    for (let k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;\n    table[i] = c;\n}\n\nfunction crc32(bytes: U8Arr, start: number = 0, length?: number): number {\n    length = length ?? (bytes.length - start);\n    let crc = -1;\n    for (let i = start, l = start + length; i < l; i++) {\n        crc = (crc >>> 8) ^ table[(crc ^ bytes[i]) & 0xFF];\n    }\n    return crc ^ (-1);\n}\n\n\nconst PNG_SIGNATURE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n\nexport function parseAPNG(buffer: ArrayBuffer): Promise<Animation> {\n    const bytes = new Uint8Array(buffer);\n    return new Promise(function (resolve, reject) {\n\n        for (let i = 0; i < PNG_SIGNATURE_BYTES.length; i++) {\n            if (PNG_SIGNATURE_BYTES[i] != bytes[i]) {\n                reject(\"Not a PNG file (invalid file signature)\");\n                return;\n            }\n        }\n\n        // fast animation test\n        let isAnimated = false;\n        parseChunks(bytes, function (type) {\n            if (type == \"acTL\") {\n                isAnimated = true;\n                return false;\n            }\n            return true;\n        });\n        if (!isAnimated) {\n            reject(\"Not an animated PNG\");\n            return;\n        }\n\n        const preDataParts: U8Arr[] = [];\n        const postDataParts: U8Arr[] = [];\n        let headerDataBytes: U8Arr | null = null;\n        let frame: Frame | null = null;\n        const anim = new Animation();\n\n        parseChunks(bytes, function (type, bytes, off, length) {\n            switch (type) {\n                case \"IHDR\":\n                    headerDataBytes = bytes.subarray(off + 8, off + 8 + length);\n                    anim.width = readDWord(bytes, off + 8);\n                    anim.height = readDWord(bytes, off + 12);\n                    break;\n                case \"acTL\":\n                    anim.numPlays = readDWord(bytes, off + 8 + 4);\n                    break;\n                case \"fcTL\":\n                    if (frame) anim.frames.push(frame);\n                    frame = {} as Frame;\n                    frame.width = readDWord(bytes, off + 8 + 4);\n                    frame.height = readDWord(bytes, off + 8 + 8);\n                    frame.left = readDWord(bytes, off + 8 + 12);\n                    frame.top = readDWord(bytes, off + 8 + 16);\n                    let delayN = readWord(bytes, off + 8 + 20);\n                    let delayD = readWord(bytes, off + 8 + 22);\n                    if (delayD == 0) delayD = 100;\n                    frame.delay = 1000 * delayN / delayD;\n                    // see http://mxr.mozilla.org/mozilla/source/gfx/src/shared/gfxImageFrame.cpp#343\n                    if (frame.delay <= 10) frame.delay = 100;\n                    anim.playTime += frame.delay;\n                    frame.disposeOp = readByte(bytes, off + 8 + 24);\n                    frame.blendOp = readByte(bytes, off + 8 + 25);\n                    frame.dataParts = [];\n                    break;\n                case \"fdAT\":\n                    if (frame) frame.dataParts!.push(bytes.subarray(off + 8 + 4, off + 8 + length));\n                    break;\n                case \"IDAT\":\n                    if (frame) frame.dataParts!.push(bytes.subarray(off + 8, off + 8 + length));\n                    break;\n                case \"IEND\":\n                    postDataParts.push(subBuffer(bytes, off, 12 + length));\n                    break;\n                default:\n                    preDataParts.push(subBuffer(bytes, off, 12 + length));\n            }\n        });\n\n        if (frame) anim.frames.push(frame);\n\n        if (anim.frames.length == 0) {\n            reject(\"Not an animated PNG\");\n            return;\n        }\n\n        // creating images\n        let createdImages = 0;\n        const preBlob = new Blob(preDataParts);\n        const postBlob = new Blob(postDataParts);\n        for (let f = 0; f < anim.frames.length; f++) {\n            frame = anim.frames[f];\n\n            const bb: (U8Arr | Blob)[] = [];\n            bb.push(PNG_SIGNATURE_BYTES);\n            headerDataBytes!.set(makeDWordArray(frame.width), 0);\n            headerDataBytes!.set(makeDWordArray(frame.height), 4);\n            bb.push(makeChunkBytes(\"IHDR\", headerDataBytes!));\n            bb.push(preBlob);\n            for (let j = 0; j < frame.dataParts!.length; j++) {\n                bb.push(makeChunkBytes(\"IDAT\", frame.dataParts![j]));\n            }\n            bb.push(postBlob);\n            const url = URL.createObjectURL(new Blob(bb, { \"type\": \"image/png\" }));\n            delete frame.dataParts;\n\n            /**\n             * Using \"createElement\" instead of \"new Image\" because of bug in Chrome 27\n             * https://code.google.com/p/chromium/issues/detail?id=238071\n             * http://stackoverflow.com/questions/16377375/using-canvas-drawimage-in-chrome-extension-content-script/16378270\n             */\n            const img = frame.img = new Image();\n            img.onload = function () {\n                URL.revokeObjectURL(img.src);\n                createdImages++;\n                if (createdImages == anim.frames.length) {\n                    resolve(anim);\n                }\n            };\n            img.onerror = function () {\n                reject(\"Image creation error\");\n            };\n            img.src = url;\n        }\n    });\n}\n\nfunction parseChunks(bytes: U8Arr, callback: (type: string, bytes: U8Arr, off: number, length: number) => boolean | void): void {\n    let off = 8;\n    let res: boolean | void;\n    let type: string;\n\n    do {\n        const length = readDWord(bytes, off);\n        type = readString(bytes, off + 4, 4);\n        res = callback(type, bytes, off, length);\n        off += 12 + length;\n    } while (res !== false && type != \"IEND\" && off < bytes.length);\n}\n\nfunction readDWord(bytes: U8Arr, off: number): number {\n    let x = 0;\n    // Force the most-significant byte to unsigned.\n    x += ((bytes[0 + off] << 24) >>> 0);\n    for (let i = 1; i < 4; i++) x += ((bytes[i + off] << ((3 - i) * 8)));\n    return x;\n}\n\nfunction readWord(bytes: U8Arr, off: number): number {\n    let x = 0;\n    for (let i = 0; i < 2; i++) x += (bytes[i + off] << ((1 - i) * 8));\n    return x;\n}\n\nfunction readByte(bytes: U8Arr, off: number): number {\n    return bytes[off];\n}\n\nfunction subBuffer(bytes: U8Arr, start: number, length: number): U8Arr {\n    const a = new Uint8Array(length);\n    a.set(bytes.subarray(start, start + length));\n    return a;\n}\n\nfunction readString(bytes: U8Arr, off: number, length: number): string {\n    const chars = Array.prototype.slice.call(bytes.subarray(off, off + length));\n    return String.fromCharCode.apply(String, chars);\n}\n\nfunction makeDWordArray(x: number): number[] {\n    return [(x >>> 24) & 0xff, (x >>> 16) & 0xff, (x >>> 8) & 0xff, x & 0xff];\n}\n\nfunction makeStringArray(x: string): number[] {\n    const res: number[] = [];\n    for (let i = 0; i < x.length; i++) res.push(x.charCodeAt(i));\n    return res;\n}\n\nfunction makeChunkBytes(type: string, dataBytes: U8Arr): U8Arr {\n    const crcLen = type.length + dataBytes.length;\n    const bytes = new Uint8Array(new ArrayBuffer(crcLen + 8));\n    bytes.set(makeDWordArray(dataBytes.length), 0);\n    bytes.set(makeStringArray(type), 4);\n    bytes.set(dataBytes, 8);\n    const crc = crc32(bytes, 4, crcLen);\n    bytes.set(makeDWordArray(crc), crcLen + 4);\n    return bytes;\n};\n"
  },
  {
    "path": "src/utils/clipboard.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport function copyToClipboard(text: string): Promise<void> {\n    return IS_DISCORD_DESKTOP ? DiscordNative.clipboard.copy(text) : navigator.clipboard.writeText(text);\n}\n"
  },
  {
    "path": "src/utils/constants.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const REACT_GLOBAL = \"Vencord.Webpack.Common.React\";\nexport const VENBOT_USER_ID = \"1017176847865352332\";\nexport const VENCORD_GUILD_ID = \"1015060230222131221\";\nexport const DONOR_ROLE_ID = \"1042507929485586532\";\nexport const CONTRIB_ROLE_ID = \"1026534353167208489\";\nexport const REGULAR_ROLE_ID = \"1026504932959977532\";\nexport const SUPPORT_CHANNEL_ID = \"1026515880080842772\";\nexport const SUPPORT_CATEGORY_ID = \"1108135649699180705\";\nexport const KNOWN_ISSUES_CHANNEL_ID = \"1222936386626129920\";\n\nconst platform = navigator.platform.toLowerCase();\nexport const IS_WINDOWS = platform.startsWith(\"win\");\nexport const IS_MAC = platform.startsWith(\"mac\");\nexport const IS_LINUX = platform.startsWith(\"linux\");\n// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop\n// \"In summary, we recommend looking for the string Mobi anywhere in the User Agent to detect a mobile device.\"\nexport const IS_MOBILE = navigator.userAgent.includes(\"Mobi\");\n\nexport interface Dev {\n    name: string;\n    id: bigint;\n    badge?: boolean;\n}\n\n/**\n * If you made a plugin or substantial contribution, add yourself here.\n * This object is used for the plugin author list, as well as to add a contributor badge to your profile.\n * If you wish to stay fully anonymous, feel free to set ID to 0n.\n * If you are fine with attribution but don't want the badge, add badge: false\n */\nexport const Devs = /* #__PURE__*/ Object.freeze({\n    Ven: {\n        name: \"V\",\n        id: 343383572805058560n\n    },\n    Apexo: {\n        name: \"Apexo\",\n        id: 228548952687902720n\n    },\n    Arjix: {\n        name: \"ArjixWasTaken\",\n        id: 674710789138939916n,\n        badge: false\n    },\n    Cyn: {\n        name: \"Cynosphere\",\n        id: 150745989836308480n\n    },\n    Trwy: {\n        name: \"trey\",\n        id: 354427199023218689n\n    },\n    Megu: {\n        name: \"Megumin\",\n        id: 545581357812678656n\n    },\n    botato: {\n        name: \"botato\",\n        id: 440990343899643943n\n    },\n    fawn: {\n        name: \"fawn\",\n        id: 336678828233588736n,\n    },\n    rushii: {\n        name: \"rushii\",\n        id: 295190422244950017n\n    },\n    Glitch: {\n        name: \"Glitchy\",\n        id: 269567451199569920n\n    },\n    Samu: {\n        name: \"Samu\",\n        id: 702973430449832038n,\n    },\n    Nyako: {\n        name: \"nyako\",\n        id: 118437263754395652n\n    },\n    MaiKokain: {\n        name: \"Mai\",\n        id: 722647978577363026n\n    },\n    amy: {\n        name: \"Amy\",\n        id: 603229858612510720n\n    },\n    katlyn: {\n        name: \"katlyn\",\n        id: 250322741406859265n\n    },\n    nea: {\n        name: \"nea\",\n        id: 310702108997320705n,\n    },\n    Nuckyz: {\n        name: \"Nuckyz\",\n        id: 235834946571337729n\n    },\n    D3SOX: {\n        name: \"D3SOX\",\n        id: 201052085641281538n\n    },\n    Nickyux: {\n        name: \"Nickyux\",\n        id: 427146305651998721n\n    },\n    mantikafasi: {\n        name: \"mantikafasi\",\n        id: 287555395151593473n\n    },\n    Xinto: {\n        name: \"Xinto\",\n        id: 423915768191647755n\n    },\n    JacobTm: {\n        name: \"Jacob.Tm\",\n        id: 302872992097107991n\n    },\n    DustyAngel47: {\n        name: \"DustyAngel47\",\n        id: 714583473804935238n\n    },\n    BanTheNons: {\n        name: \"BanTheNons\",\n        id: 460478012794863637n\n    },\n    BigDuck: {\n        name: \"BigDuck\",\n        id: 1024588272623681609n\n    },\n    AverageReactEnjoyer: {\n        name: \"Average React Enjoyer\",\n        id: 1004904120056029256n\n    },\n    adryd: {\n        name: \"adryd\",\n        id: 0n\n    },\n    Tyman: {\n        name: \"Tyman\",\n        id: 487443883127472129n\n    },\n    afn: {\n        name: \"afn\",\n        id: 420043923822608384n\n    },\n    KraXen72: {\n        name: \"KraXen72\",\n        id: 379304073515499530n\n    },\n    kemo: {\n        name: \"kemo\",\n        id: 715746190813298788n\n    },\n    dzshn: {\n        name: \"dzshn\",\n        id: 310449948011528192n\n    },\n    Ducko: {\n        name: \"Ducko\",\n        id: 506482395269169153n\n    },\n    jewdev: {\n        name: \"jewdev\",\n        id: 222369866529636353n\n    },\n    Luna: {\n        name: \"Luny\",\n        id: 821472922140803112n\n    },\n    Vap: {\n        name: \"Vap0r1ze\",\n        id: 454072114492866560n\n    },\n    KingFish: {\n        name: \"King Fish\",\n        id: 499400512559382538n\n    },\n    Commandtechno: {\n        name: \"Commandtechno\",\n        id: 296776625432035328n,\n    },\n    TheSun: {\n        name: \"sunnie\",\n        id: 406028027768733696n\n    },\n    rae: {\n        name: \"rae\",\n        id: 1398136199503282277n\n    },\n    pointy: {\n        name: \"pointy\",\n        id: 99914384989519872n\n    },\n    SammCheese: {\n        name: \"Samm-Cheese\",\n        id: 372148345894076416n\n    },\n    zt: {\n        name: \"zt\",\n        id: 289556910426816513n\n    },\n    captain: {\n        name: \"Captain\",\n        id: 347366054806159360n\n    },\n    nick: {\n        name: \"nick\",\n        id: 347884694408265729n,\n        badge: false\n    },\n    whqwert: {\n        name: \"whqwert\",\n        id: 586239091520176128n\n    },\n    lewisakura: {\n        name: \"lewisakura\",\n        id: 96269247411400704n\n    },\n    RuiNtD: {\n        name: \"RuiNtD\",\n        id: 157917665162297344n\n    },\n    hunt: {\n        name: \"hunt-g\",\n        id: 222800179697287168n\n    },\n    cloudburst: {\n        name: \"cloudburst\",\n        id: 892128204150685769n\n    },\n    Aria: {\n        name: \"Syncxv\",\n        id: 549244932213309442n,\n    },\n    TheKodeToad: {\n        name: \"TheKodeToad\",\n        id: 706152404072267788n\n    },\n    LordElias: {\n        name: \"LordElias\",\n        id: 319460781567639554n\n    },\n    juby: {\n        name: \"Juby210\",\n        id: 324622488644616195n\n    },\n    Alyxia: {\n        name: \"Alyxia Sother\",\n        id: 952185386350829688n\n    },\n    Remty: {\n        name: \"Remty\",\n        id: 335055032204656642n\n    },\n    skyevg: {\n        name: \"skyevg\",\n        id: 1090310844283363348n\n    },\n    Dziurwa: {\n        name: \"Dziurwa\",\n        id: 1001086404203389018n\n    },\n    arHSM: {\n        name: \"arHSM\",\n        id: 841509053422632990n\n    },\n    AutumnVN: {\n        name: \"AutumnVN\",\n        id: 393694671383166998n\n    },\n    pylix: {\n        name: \"pylix\",\n        id: 492949202121261067n\n    },\n    Tyler: {\n        name: \"\\\\\\\\GGTyler\\\\\\\\\",\n        id: 143117463788191746n\n    },\n    RyanCaoDev: {\n        name: \"RyanCaoDev\",\n        id: 952235800110694471n,\n    },\n    FieryFlames: {\n        name: \"Fiery\",\n        id: 890228870559698955n\n    },\n    KannaDev: {\n        name: \"Kanna\",\n        id: 317728561106518019n\n    },\n    carince: {\n        name: \"carince\",\n        id: 818323528755314698n\n    },\n    PandaNinjas: {\n        name: \"PandaNinjas\",\n        id: 455128749071925248n\n    },\n    CatNoir: {\n        name: \"CatNoir\",\n        id: 260371016348336128n\n    },\n    outfoxxed: {\n        name: \"outfoxxed\",\n        id: 837425748435796060n\n    },\n    UwUDev: {\n        name: \"UwU\",\n        id: 691413039156690994n,\n    },\n    amia: {\n        name: \"amia\",\n        id: 142007603549962240n\n    },\n    phil: {\n        name: \"phil\",\n        id: 305288513941667851n\n    },\n    ImLvna: {\n        name: \"lillith <3\",\n        id: 799319081723232267n\n    },\n    rad: {\n        name: \"rad\",\n        id: 610945092504780823n\n    },\n    AndrewDLO: {\n        name: \"Andrew-DLO\",\n        id: 434135504792059917n\n    },\n    HypedDomi: {\n        name: \"HypedDomi\",\n        id: 354191516979429376n\n    },\n    Rini: {\n        name: \"Rini\",\n        id: 1079479184478441643n\n    },\n    castdrian: {\n        name: \"castdrian\",\n        id: 224617799434108928n\n    },\n    Arrow: {\n        name: \"arrow\",\n        id: 958158495302176778n\n    },\n    bb010g: {\n        name: \"bb010g\",\n        id: 72791153467990016n,\n    },\n    Dolfies: {\n        name: \"Dolfies\",\n        id: 852892297661906993n,\n    },\n    RuukuLada: {\n        name: \"RuukuLada\",\n        id: 119705748346241027n,\n    },\n    blahajZip: {\n        name: \"blahaj.zip\",\n        id: 683954422241427471n,\n    },\n    archeruwu: {\n        name: \"archer_uwu\",\n        id: 160068695383736320n\n    },\n    ProffDea: {\n        name: \"ProffDea\",\n        id: 609329952180928513n\n    },\n    UlyssesZhan: {\n        name: \"UlyssesZhan\",\n        id: 586808226058862623n\n    },\n    ant0n: {\n        name: \"ant0n\",\n        id: 145224646868860928n\n    },\n    Board: {\n        name: \"BoardTM\",\n        id: 285475344817848320n,\n    },\n    philipbry: {\n        name: \"philipbry\",\n        id: 554994003318276106n\n    },\n    Korbo: {\n        name: \"Korbo\",\n        id: 455856406420258827n\n    },\n    maisymoe: {\n        name: \"maisy\",\n        id: 257109471589957632n,\n    },\n    Lexi: {\n        name: \"Lexi\",\n        id: 506101469787717658n\n    },\n    Mopi: {\n        name: \"Mopi\",\n        id: 1022189106614243350n\n    },\n    Grzesiek11: {\n        name: \"Grzesiek11\",\n        id: 368475654662127616n,\n    },\n    Samwich: {\n        name: \"Samwich\",\n        id: 976176454511509554n,\n    },\n    coolelectronics: {\n        name: \"coolelectronics\",\n        id: 696392247205298207n,\n    },\n    Av32000: {\n        name: \"Av32000\",\n        id: 593436735380127770n,\n    },\n    Noxillio: {\n        name: \"Noxillio\",\n        id: 138616536502894592n,\n    },\n    Kyuuhachi: {\n        name: \"Kyuuhachi\",\n        id: 236588665420251137n,\n    },\n    nin0dev: {\n        name: \"nin0dev\",\n        id: 1395533040914141235n\n    },\n    Elvyra: {\n        name: \"Elvyra\",\n        id: 708275751816003615n,\n    },\n    HappyEnderman: {\n        name: \"Happy enderman\",\n        id: 1083437693347827764n\n    },\n    Vishnya: {\n        name: \"Vishnya\",\n        id: 282541644484575233n\n    },\n    Inbestigator: {\n        name: \"Inbestigator\",\n        id: 761777382041714690n\n    },\n    newwares: {\n        name: \"newwares\",\n        id: 421405303951851520n\n    },\n    JohnyTheCarrot: {\n        name: \"JohnyTheCarrot\",\n        id: 132819036282159104n\n    },\n    puv: {\n        name: \"puv\",\n        id: 469441552251355137n\n    },\n    IcedMarina: {\n        name: \"icedmarina\",\n        id: 594406131670188042n\n    },\n    nakoyasha: {\n        name: \"nakoyasha\",\n        id: 222069018507345921n\n    },\n    Sqaaakoi: {\n        name: \"Sqaaakoi\",\n        id: 259558259491340288n\n    },\n    iamme: {\n        name: \"i am me\",\n        id: 984392761929256980n\n    },\n    Byeoon: {\n        name: \"byeoon\",\n        id: 1167275288036655133n\n    },\n    Kaitlyn: {\n        name: \"kaitlyn\",\n        id: 306158896630988801n\n    },\n    PolisanTheEasyNick: {\n        name: \"Oleh Polisan\",\n        id: 242305263313485825n\n    },\n    HAHALOSAH: {\n        name: \"HAHALOSAH\",\n        id: 903418691268513883n\n    },\n    GabiRP: {\n        name: \"GabiRP\",\n        id: 507955112027750401n\n    },\n    ImBanana: {\n        name: \"Im_Banana\",\n        id: 635250116688871425n\n    },\n    xocherry: {\n        name: \"xocherry\",\n        id: 221288171013406720n\n    },\n    ScattrdBlade: {\n        name: \"ScattrdBlade\",\n        id: 678007540608532491n\n    },\n    goodbee: {\n        name: \"goodbee\",\n        id: 658968552606400512n\n    },\n    Moxxie: {\n        name: \"Moxxie\",\n        id: 712653921692155965n,\n    },\n    Ethan: {\n        name: \"Ethan\",\n        id: 721717126523781240n,\n    },\n    nyx: {\n        name: \"verticalsync.\",\n        id: 1207087393929171095n\n    },\n    nekohaxx: {\n        name: \"nekohaxx\",\n        id: 1176270221628153886n\n    },\n    Antti: {\n        name: \"Antti\",\n        id: 312974985876471810n\n    },\n    Joona: {\n        name: \"Joona\",\n        id: 297410829589020673n\n    },\n    sadan: {\n        name: \"sadan\",\n        id: 521819891141967883n,\n    },\n    Kylie: {\n        name: \"Cookie\",\n        id: 721853658941227088n\n    },\n    AshtonMemer: {\n        name: \"AshtonMemer\",\n        id: 373657230530052099n\n    },\n    surgedevs: {\n        name: \"Chloe\",\n        id: 1084592643784331324n\n    },\n    Lumap: {\n        name: \"Lumap\",\n        id: 585278686291427338n,\n    },\n    Obsidian: {\n        name: \"Obsidian\",\n        id: 683171006717755446n,\n    },\n    SerStars: {\n        name: \"SerStars\",\n        id: 861631850681729045n,\n    },\n    niko: {\n        name: \"niko\",\n        id: 341377368075796483n,\n    },\n    relitrix: {\n        name: \"Relitrix\",\n        id: 423165393901715456n,\n    },\n    RamziAH: {\n        name: \"RamziAH\",\n        id: 1279957227612147747n,\n    },\n    SomeAspy: {\n        name: \"SomeAspy\",\n        id: 516750892372852754n,\n    },\n    jamesbt365: {\n        name: \"jamesbt365\",\n        id: 158567567487795200n,\n    },\n    samsam: {\n        name: \"samsam\",\n        id: 400482410279469056n,\n    },\n    Cootshk: {\n        name: \"Cootshk\",\n        id: 921605971577548820n\n    },\n    thororen: {\n        name: \"thororen\",\n        id: 848339671629299742n\n    },\n    alfred: {\n        name: \"alfred\",\n        id: 1038466644353232967n\n    },\n    vv: {\n        name: \"VV\",\n        id: 254866377087778816n\n    },\n    u32: {\n        name: \"u32\",\n        id: 1063237286818488351n,\n    },\n    prism: {\n        name: \"prism\",\n        id: 390884143749136386n,\n    },\n} satisfies Record<string, Dev>);\n\n// iife so #__PURE__ works correctly\nexport const DevsById = /* #__PURE__*/ (() =>\n    Object.freeze(Object.fromEntries(\n        Object.entries(Devs)\n            .filter(d => d[1].id !== 0n)\n            .map(([_, v]) => [v.id, v] as const)\n    ))\n)() as Record<string, Dev>;\n"
  },
  {
    "path": "src/utils/cspViolations.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { useLayoutEffect } from \"@webpack/common\";\n\nimport { useForceUpdater } from \"./react\";\n\nconst cssRelevantDirectives = [\"style-src\", \"style-src-elem\", \"img-src\", \"font-src\"] as const;\n\nexport const CspBlockedUrls = new Set<string>();\nconst CspErrorListeners = new Set<() => void>();\n\ndocument.addEventListener(\"securitypolicyviolation\", ({ effectiveDirective, blockedURI }) => {\n    if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return;\n\n    CspBlockedUrls.add(blockedURI);\n\n    CspErrorListeners.forEach(listener => listener());\n});\n\nexport function useCspErrors() {\n    const forceUpdate = useForceUpdater();\n\n    useLayoutEffect(() => {\n        CspErrorListeners.add(forceUpdate);\n\n        return () => void CspErrorListeners.delete(forceUpdate);\n    }, [forceUpdate]);\n\n    return [...CspBlockedUrls] as const;\n}\n"
  },
  {
    "path": "src/utils/css.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport function createAndAppendStyle(id: string, target: HTMLElement) {\n    const style = document.createElement(\"style\");\n    style.id = id;\n    target.append(style);\n    return style;\n}\n\nexport const classNameToSelector = (name: string, prefix = \"\") => name.split(\" \").map(n => `.${prefix}${n}`).join(\"\");\n\nexport type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | \"\";\n\n/**\n * @param prefix The prefix to add to each class, defaults to `\"\"`\n * @returns A classname generator function\n * @example\n * const cl = classNameFactory(\"plugin-\");\n *\n * cl(\"base\", [\"item\", \"editable\"], { selected: null, disabled: true })\n * // => \"plugin-base plugin-item plugin-editable plugin-disabled\"\n */\nexport const classNameFactory = (prefix: string = \"\") => (...args: ClassNameFactoryArg[]) => {\n    const classNames = new Set<string>();\n    for (const arg of args) {\n        if (arg && typeof arg === \"string\") classNames.add(arg);\n        else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));\n        else if (arg && typeof arg === \"object\") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));\n    }\n    return Array.from(classNames, name => prefix + name).join(\" \");\n};\n"
  },
  {
    "path": "src/utils/dependencies.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// The below code is only used on the Desktop (electron) build of Vencord.\n// Browser (extension) builds do not contain these remote imports.\n\nexport const shikiWorkerSrc = `https://cdn.jsdelivr.net/npm/@vap/shiki-worker@0.0.8/dist/${IS_DEV ? \"index.js\" : \"index.min.js\"}`;\nexport const shikiOnigasmSrc = \"https://cdn.jsdelivr.net/npm/@vap/shiki@0.10.3/dist/onig.wasm\";\n"
  },
  {
    "path": "src/utils/discord.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type { MessageObject } from \"@api/MessageEvents\";\nimport type { Channel, CloudUpload, Guild, GuildFeatures, Message, User } from \"@vencord/discord-types\";\nimport { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, InviteActions, MessageActions, RestAPI, SelectedChannelStore, SelectedGuildStore, Toasts, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from \"@webpack/common\";\nimport { Except } from \"type-fest\";\n\nimport { copyToClipboard } from \"./clipboard\";\nimport { runtimeHashMessageKey } from \"./intlHash\";\nimport { Logger } from \"./Logger\";\nimport { MediaModalItem, MediaModalProps, openMediaModal } from \"./modal\";\n\nconst IntlManagerLogger = new Logger(\"IntlManager\");\n\n/**\n * Get an internationalized message from a non hashed key\n * @param key The plain message key\n * @param values The values to interpolate, if it's a rich message\n */\nexport function getIntlMessage(key: string, values?: Record<PropertyKey, any>): any {\n    return getIntlMessageFromHash(runtimeHashMessageKey(key), values, key);\n}\n\n/**\n * Get an internationalized message from a hashed key\n * @param hashedKey The hashed message key\n * @param values The values to interpolate, if it's a rich message\n */\nexport function getIntlMessageFromHash(hashedKey: string, values?: Record<PropertyKey, any>, originalKey?: string): any {\n    try {\n        return values == null ? i18n.intl.string(i18n.t[hashedKey]) : i18n.intl.format(i18n.t[hashedKey], values);\n    } catch (e) {\n        IntlManagerLogger.error(`Failed to get intl message for key: ${originalKey ?? hashedKey}`, e);\n        return originalKey ?? \"\";\n    }\n}\n\n/**\n * Open the invite modal\n * @param code The invite code\n * @returns Whether the invite was accepted\n */\nexport async function openInviteModal(code: string) {\n    const { invite } = await InviteActions.resolveInvite(code, \"Desktop Modal\");\n    if (!invite) throw new Error(\"Invalid invite: \" + code);\n\n    FluxDispatcher.dispatch({\n        type: \"INVITE_MODAL_OPEN\",\n        invite,\n        code,\n        context: \"APP\"\n    });\n\n    return new Promise<boolean>(r => {\n        let onClose: () => void, onAccept: () => void;\n        let inviteAccepted = false;\n\n        FluxDispatcher.subscribe(\"INVITE_ACCEPT\", onAccept = () => {\n            inviteAccepted = true;\n        });\n\n        FluxDispatcher.subscribe(\"INVITE_MODAL_CLOSE\", onClose = () => {\n            FluxDispatcher.unsubscribe(\"INVITE_MODAL_CLOSE\", onClose);\n            FluxDispatcher.unsubscribe(\"INVITE_ACCEPT\", onAccept);\n            r(inviteAccepted);\n        });\n    });\n}\n\nexport function getCurrentChannel(): Channel | undefined {\n    return ChannelStore.getChannel(SelectedChannelStore.getChannelId());\n}\n\nexport function getCurrentGuild(): Guild | undefined {\n    return GuildStore.getGuild(getCurrentChannel()?.guild_id!);\n}\n\nexport function openPrivateChannel(userId: string) {\n    ChannelActionCreators.openPrivateChannel(userId);\n}\n\nexport const enum Theme {\n    Dark = 1,\n    Light = 2\n}\n\nexport function getTheme(): Theme {\n    try {\n        return UserSettingsActionCreators.PreloadedUserSettingsActionCreators.getCurrentValue()?.appearance?.theme;\n    } catch {\n        return Theme.Dark;\n    }\n}\n\nexport function insertTextIntoChatInputBox(text: string) {\n    ComponentDispatch.dispatchToLastSubscribed(\"INSERT_TEXT\", {\n        rawText: text,\n        plainText: text\n    });\n}\n\nexport async function copyWithToast(text: string, toastMessage = \"Copied to clipboard!\") {\n    await copyToClipboard(text);\n    Toasts.show({\n        message: toastMessage,\n        id: Toasts.genId(),\n        type: Toasts.Type.SUCCESS\n    });\n}\n\ninterface MessageOptions {\n    messageReference: Message[\"messageReference\"];\n    allowedMentions: {\n        parse: string[];\n        replied_user: boolean;\n    };\n    stickerIds: string[];\n    attachmentsToUpload: CloudUpload[];\n    poll: {\n        allow_multiselect: boolean;\n        answers: Array<{\n            poll_media: {\n                text: string;\n                attachment_ids?: unknown;\n                emoji?: { name: string; id?: string; };\n            };\n        }>;\n        duration: number;\n        layout_type: number;\n        question: { text: string; };\n    };\n}\n\nexport function sendMessage(\n    channelId: string,\n    data: Partial<MessageObject>,\n    waitForChannelReady = true,\n    options: Partial<MessageOptions> = {}\n) {\n    const messageData = {\n        content: \"\",\n        invalidEmojis: [],\n        tts: false,\n        validNonShortcutEmojis: [],\n        ...data\n    };\n\n    return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, options);\n}\n\n/**\n * You must specify either height or width in the item\n */\nexport function openImageModal(item: Except<MediaModalItem, \"type\">, mediaModalProps?: Omit<MediaModalProps, \"items\">) {\n    return openMediaModal({\n        items: [{\n            type: \"IMAGE\",\n            original: item.original ?? item.url,\n            ...item,\n        }],\n        ...mediaModalProps\n    });\n}\n\nexport async function openUserProfile(id: string) {\n    const user = await UserUtils.getUser(id);\n    if (!user) throw new Error(\"No such user: \" + id);\n\n    const guildId = SelectedGuildStore.getGuildId();\n    UserProfileActions.openUserProfileModal({\n        userId: id,\n        guildId,\n        channelId: SelectedChannelStore.getChannelId(),\n        analyticsLocation: {\n            page: guildId ? \"Guild Channel\" : \"DM Channel\",\n            section: \"Profile Popout\"\n        }\n    });\n}\n\ninterface FetchUserProfileOptions {\n    friend_token?: string;\n    connections_role_id?: string;\n    guild_id?: string;\n    with_mutual_guilds?: boolean;\n    with_mutual_friends_count?: boolean;\n}\n\n/**\n * Fetch a user's profile\n */\nexport async function fetchUserProfile(id: string, options?: FetchUserProfileOptions) {\n    const cached = UserProfileStore.getUserProfile(id);\n    if (cached) return cached;\n\n    FluxDispatcher.dispatch({ type: \"USER_PROFILE_FETCH_START\", userId: id });\n\n    const { body } = await RestAPI.get({\n        url: Constants.Endpoints.USER_PROFILE(id),\n        query: {\n            with_mutual_guilds: false,\n            with_mutual_friends_count: false,\n            ...options\n        },\n        oldFormErrors: true,\n    });\n\n    FluxDispatcher.dispatch({ type: \"USER_UPDATE\", user: body.user });\n    await FluxDispatcher.dispatch({ type: \"USER_PROFILE_FETCH_SUCCESS\", userProfile: body });\n    if (options?.guild_id && body.guild_member)\n        FluxDispatcher.dispatch({ type: \"GUILD_MEMBER_PROFILE_UPDATE\", guildId: options.guild_id, guildMember: body.guild_member });\n\n    return UserProfileStore.getUserProfile(id);\n}\n\n/**\n * Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise\n */\nexport function getUniqueUsername(user: User) {\n    return user.discriminator === \"0\" ? user.username : user.tag;\n}\n\n// Discord has a similar function in their code\nexport function getGuildAcronym(guild: Guild): string {\n    return guild.name\n        .replaceAll(\"'s \", \" \")\n        .replace(/\\w+/g, m => m[0])\n        .replace(/\\s/g, \"\");\n}\n\nexport function hasGuildFeature(guild: Guild, feature: GuildFeatures): boolean {\n    return guild.features?.has(feature) ?? false;\n}\n"
  },
  {
    "path": "src/utils/guards.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function isTruthy<T>(item: T): item is Exclude<T, 0 | \"\" | false | null | undefined> {\n    return Boolean(item);\n}\n\nexport function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> {\n    return item != null;\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "/*!\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport * from \"../shared/debounce\";\nexport * from \"../shared/onceDefined\";\nexport * from \"./ChangeList\";\nexport * from \"./clipboard\";\nexport * from \"./constants\";\nexport * from \"./cspViolations\";\nexport * from \"./css\";\nexport * from \"./discord\";\nexport * from \"./guards\";\nexport * from \"./intlHash\";\nexport * from \"./lazy\";\nexport * from \"./lazyReact\";\nexport * from \"./localStorage\";\nexport * from \"./Logger\";\nexport * from \"./margins\";\nexport * from \"./mergeDefaults\";\nexport * from \"./misc\";\nexport * from \"./modal\";\nexport * from \"./onlyOnce\";\nexport * from \"./patches\";\nexport * from \"./Queue\";\nexport * from \"./react\";\nexport * from \"./text\";\n"
  },
  {
    "path": "src/utils/intlHash.ts",
    "content": "/* eslint-disable simple-header/header */\n\n/**\n * discord-intl\n *\n * @copyright 2024 Discord, Inc.\n * @link https://github.com/discord/discord-intl\n * @license MIT\n */\n\nimport { hash as h64 } from \"@intrnl/xxhash64\";\n\nconst BASE64_TABLE = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".split(\"\");\nconst IS_BIG_ENDIAN = (() => {\n    const array = new Uint8Array(4);\n    const view = new Uint32Array(array.buffer);\n    return !((view[0] = 1) & array[0]);\n})();\n\nfunction numberToBytes(number: number | bigint) {\n    number = BigInt(number);\n    const array: number[] = [];\n    const byteCount = Math.ceil(Math.floor(Math.log2(Number(number)) + 1) / 8);\n    for (let i = 0; i < byteCount; i++) {\n        array.unshift(Number((number >> BigInt(8 * i)) & BigInt(255)));\n    }\n\n    const bytes = new Uint8Array(array);\n    // The native `hashToMessageKey` always works in Big/Network Endian bytes, so this array\n    // needs to be converted to the same endianness to get the same base64 result.\n    return IS_BIG_ENDIAN ? bytes : bytes.reverse();\n}\n\n/**\n * Returns a consistent, short hash of the given key by first processing it through a hash digest,\n * then encoding the first few bytes to base64.\n *\n * This function is specifically written to mirror the native backend hashing function used by\n * `@discord/intl-loader-core`, to be able to hash names at runtime.\n */\nexport function runtimeHashMessageKey(key: string): string {\n    const hash = h64(key, 0);\n    const bytes = numberToBytes(hash);\n    return [\n        BASE64_TABLE[bytes[0] >> 2],\n        BASE64_TABLE[((bytes[0] & 0x03) << 4) | (bytes[1] >> 4)],\n        BASE64_TABLE[((bytes[1] & 0x0f) << 2) | (bytes[2] >> 6)],\n        BASE64_TABLE[bytes[2] & 0x3f],\n        BASE64_TABLE[bytes[3] >> 2],\n        BASE64_TABLE[((bytes[3] & 0x03) << 4) | (bytes[4] >> 4)],\n    ].join(\"\");\n}\n"
  },
  {
    "path": "src/utils/lazy.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function makeLazy<T>(factory: () => T, attempts = 5): () => T {\n    let tries = 0;\n    let cache: T;\n    return () => {\n        if (cache === undefined && attempts > tries++) {\n            cache = factory();\n            if (cache === undefined && attempts === tries)\n                console.error(\"Lazy factory failed:\", factory);\n        }\n        return cache;\n    };\n}\n\n// Proxies demand that these properties be unmodified, so proxyLazy\n// will always return the function default for them.\nconst unconfigurable = [\"arguments\", \"caller\", \"prototype\"];\n\nconst handler: ProxyHandler<any> = {};\n\nexport const SYM_LAZY_GET = Symbol.for(\"vencord.lazy.get\");\nexport const SYM_LAZY_CACHED = Symbol.for(\"vencord.lazy.cached\");\n\nfor (const method of [\n    \"apply\",\n    \"construct\",\n    \"defineProperty\",\n    \"deleteProperty\",\n    \"getOwnPropertyDescriptor\",\n    \"getPrototypeOf\",\n    \"has\",\n    \"isExtensible\",\n    \"ownKeys\",\n    \"preventExtensions\",\n    \"set\",\n    \"setPrototypeOf\"\n]) {\n    handler[method] =\n        (target: any, ...args: any[]) => Reflect[method](target[SYM_LAZY_GET](), ...args);\n}\n\nhandler.ownKeys = target => {\n    const v = target[SYM_LAZY_GET]();\n    const keys = Reflect.ownKeys(v);\n    for (const key of unconfigurable) {\n        if (!keys.includes(key)) keys.push(key);\n    }\n    return keys;\n};\n\nhandler.getOwnPropertyDescriptor = (target, p) => {\n    if (typeof p === \"string\" && unconfigurable.includes(p))\n        return Reflect.getOwnPropertyDescriptor(target, p);\n\n    const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p);\n\n    if (descriptor) Object.defineProperty(target, p, descriptor);\n    return descriptor;\n};\n\n/**\n * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy.\n * On first property access, the lazy is evaluated\n * @param factory lazy factory\n * @param attempts how many times to try to evaluate the lazy before giving up\n * @returns Proxy\n *\n * Note that the example below exists already as an api, see {@link findByPropsLazy}\n * @example const mod = proxyLazy(() => findByProps(\"blah\")); console.log(mod.blah);\n */\nexport function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T {\n    let isSameTick = true;\n    if (!isChild)\n        setTimeout(() => isSameTick = false, 0);\n\n    let tries = 0;\n    const proxyDummy = Object.assign(function () { }, {\n        [SYM_LAZY_CACHED]: void 0 as T | undefined,\n        [SYM_LAZY_GET]() {\n            if (!proxyDummy[SYM_LAZY_CACHED] && attempts > tries++) {\n                proxyDummy[SYM_LAZY_CACHED] = factory();\n                if (!proxyDummy[SYM_LAZY_CACHED] && attempts === tries)\n                    console.error(\"Lazy factory failed:\", factory);\n            }\n            return proxyDummy[SYM_LAZY_CACHED];\n        }\n    });\n\n    return new Proxy(proxyDummy, {\n        ...handler,\n        get(target, p, receiver) {\n            if (p === SYM_LAZY_CACHED || p === SYM_LAZY_GET)\n                return Reflect.get(target, p, receiver);\n\n            // if we're still in the same tick, it means the lazy was immediately used.\n            // thus, we lazy proxy the get access to make things like destructuring work as expected\n            // meow here will also be a lazy\n            // `const { meow } = findByPropsLazy(\"meow\");`\n            if (!isChild && isSameTick)\n                return proxyLazy(\n                    () => Reflect.get(target[SYM_LAZY_GET](), p, receiver),\n                    attempts,\n                    true\n                );\n            const lazyTarget = target[SYM_LAZY_GET]();\n            if (typeof lazyTarget === \"object\" || typeof lazyTarget === \"function\") {\n                return Reflect.get(lazyTarget, p, receiver);\n            }\n            throw new Error(\"proxyLazy called on a primitive value\");\n        }\n    }) as any;\n}\n"
  },
  {
    "path": "src/utils/lazyReact.tsx",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport type { ComponentType } from \"react\";\n\nimport { makeLazy } from \"./lazy\";\n\nconst NoopComponent = () => null;\n\nexport type LazyComponentWrapper<ComponentType> = ComponentType & { $$vencordGetWrappedComponent(): ComponentType; };\n\n/**\n * A lazy component. The factory method is called on first render.\n * @param factory Function returning a Component\n * @param attempts How many times to try to get the component before giving up\n * @returns Result of factory function\n */\nexport function LazyComponent<T extends object = any>(factory: () => ComponentType<T>, attempts = 5): LazyComponentWrapper<ComponentType<T>> {\n    const get = makeLazy(factory, attempts);\n    const LazyComponent = (props: T) => {\n        const Component = get() ?? NoopComponent;\n        return <Component {...props} />;\n    };\n\n    LazyComponent.$$vencordGetWrappedComponent = get;\n\n    return LazyComponent;\n}\n"
  },
  {
    "path": "src/utils/localStorage.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport const { localStorage } = window;\n"
  },
  {
    "path": "src/utils/margins.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// TODO: Migrate all usages from utils to components\nexport { Margins } from \"@components/margins\";\n"
  },
  {
    "path": "src/utils/mergeDefaults.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\n/**\n * Recursively merges defaults into an object and returns the same object\n * @param obj Object\n * @param defaults Defaults\n * @returns obj\n */\nexport function mergeDefaults<T>(obj: T, defaults: T): T {\n    for (const key in defaults) {\n        const v = defaults[key];\n        if (typeof v === \"object\" && !Array.isArray(v)) {\n            obj[key] ??= {} as any;\n            mergeDefaults(obj[key], v);\n        } else {\n            obj[key] ??= v;\n        }\n    }\n    return obj;\n}\n"
  },
  {
    "path": "src/utils/misc.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { DevsById } from \"./constants\";\n\n/**\n * Calls .join(\" \") on the arguments\n * classes(\"one\", \"two\") => \"one two\"\n */\nexport function classes(...classes: Array<string | null | undefined | false>) {\n    return classes.filter(Boolean).join(\" \");\n}\n\n/**\n * Returns a promise that resolves after the specified amount of time\n */\nexport function sleep(ms: number): Promise<void> {\n    return new Promise(r => setTimeout(r, ms));\n}\n\n/**\n * Check if obj is a true object: of type \"object\" and not null or array\n */\nexport function isObject(obj: unknown): obj is object {\n    return typeof obj === \"object\" && obj !== null && !Array.isArray(obj);\n}\n\n/**\n * Check if an object is empty or in other words has no own properties\n */\nexport function isObjectEmpty(obj: object) {\n    for (const k in obj)\n        if (Object.hasOwn(obj, k)) return false;\n\n    return true;\n}\n\n/**\n * Returns null if value is not a URL, otherwise return URL object.\n * Avoids having to wrap url checks in a try/catch\n */\nexport function parseUrl(urlString: string): URL | null {\n    try {\n        return new URL(urlString);\n    } catch {\n        return null;\n    }\n}\n\n/**\n * Checks whether an element is on screen\n */\nexport const checkIntersecting = (el: Element) => {\n    const elementBox = el.getBoundingClientRect();\n    const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);\n    return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0);\n};\n\nexport function identity<T>(value: T): T {\n    return value;\n}\n\nexport const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);\nexport const shouldShowContributorBadge = (id: string) => isPluginDev(id) && DevsById[id].badge !== false;\n\nexport function pluralise(amount: number, singular: string, plural = singular + \"s\") {\n    return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;\n}\n\nexport function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {\n    if (args.some(arg => arg == null)) return \"\";\n    return String.raw({ raw: strings }, ...args);\n}\n\nexport function tryOrElse<T>(func: () => T, fallback: T): T {\n    try {\n        const res = func();\n        return res instanceof Promise\n            ? res.catch(() => fallback) as T\n            : res;\n    } catch {\n        return fallback;\n    }\n}\n"
  },
  {
    "path": "src/utils/modal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { filters, findByCodeLazy, mapMangledModuleLazy } from \"@webpack\";\nimport type { ComponentType, PropsWithChildren, ReactNode, Ref } from \"react\";\n\nimport { LazyComponent } from \"./react\";\n\nexport const enum ModalSize {\n    SMALL = \"small\",\n    MEDIUM = \"medium\",\n    LARGE = \"large\",\n    DYNAMIC = \"dynamic\",\n}\n\nconst enum ModalTransitionState {\n    ENTERING,\n    ENTERED,\n    EXITING,\n    EXITED,\n    HIDDEN,\n}\n\nexport interface ModalProps {\n    transitionState: ModalTransitionState;\n    onClose(): void;\n}\n\nexport interface ModalOptions {\n    modalKey?: string;\n    onCloseRequest?: (() => void);\n    onCloseCallback?: (() => void);\n}\n\ntype RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>;\n\ninterface Modals {\n    ModalRoot: ComponentType<PropsWithChildren<{\n        transitionState: ModalTransitionState;\n        size?: ModalSize;\n        role?: \"alertdialog\" | \"dialog\";\n        className?: string;\n        fullscreenOnMobile?: boolean;\n        \"aria-label\"?: string;\n        \"aria-labelledby\"?: string;\n        onAnimationEnd?(): string;\n    }>>;\n    ModalHeader: ComponentType<PropsWithChildren<{\n        /** Flex.Justify.START */\n        justify?: string;\n        /** Flex.Direction.HORIZONTAL */\n        direction?: string;\n        /** Flex.Align.CENTER */\n        align?: string;\n        /** Flex.Wrap.NO_WRAP */\n        wrap?: string;\n        separator?: boolean;\n\n        className?: string;\n    }>>;\n    /** This also accepts Scroller props but good luck with that */\n    ModalContent: ComponentType<PropsWithChildren<{\n        className?: string;\n        scrollerRef?: Ref<HTMLElement>;\n        [prop: string]: any;\n    }>>;\n    ModalFooter: ComponentType<PropsWithChildren<{\n        /** Flex.Justify.START */\n        justify?: string;\n        /** Flex.Direction.HORIZONTAL_REVERSE */\n        direction?: string;\n        /** Flex.Align.STRETCH */\n        align?: string;\n        /** Flex.Wrap.NO_WRAP */\n        wrap?: string;\n        separator?: boolean;\n\n        className?: string;\n    }>>;\n    ModalCloseButton: ComponentType<{\n        focusProps?: any;\n        onClick(): void;\n        withCircleBackground?: boolean;\n        hideOnFullscreen?: boolean;\n        className?: string;\n    }>;\n}\n\n// TODO: move to new modal api\nexport const Modals: Modals = mapMangledModuleLazy(\".MODAL_ROOT_LEGACY,\", {\n    ModalRoot: filters.componentByCode('.MODAL,\"aria-labelledby\":'),\n    ModalHeader: filters.componentByCode(\",id:\"),\n    ModalContent: filters.componentByCode(\"scrollbarType:\"),\n    ModalFooter: filters.componentByCode(\".HORIZONTAL_REVERSE,\"),\n    ModalCloseButton: filters.componentByCode(\".withCircleBackground\")\n});\n\nexport const ModalRoot = LazyComponent(() => Modals.ModalRoot);\nexport const ModalHeader = LazyComponent(() => Modals.ModalHeader);\nexport const ModalContent = LazyComponent(() => Modals.ModalContent);\nexport const ModalFooter = LazyComponent(() => Modals.ModalFooter);\nexport const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);\n\nexport type MediaModalItem = {\n    url: string;\n    type: \"IMAGE\" | \"VIDEO\";\n    original?: string;\n    alt?: string;\n    width?: number;\n    height?: number;\n    animated?: boolean;\n    maxWidth?: number;\n    maxHeight?: number;\n} & Record<PropertyKey, any>;\n\nexport type MediaModalProps = {\n    location?: string;\n    contextKey?: string;\n    onCloseCallback?: () => void;\n    className?: string;\n    items: MediaModalItem[];\n    startingIndex?: number;\n    onIndexChange?: (...args: any[]) => void;\n    fit?: string;\n    shouldRedactExplicitContent?: boolean;\n    shouldHideMediaOptions?: boolean;\n};\n\n// Modal key: \"Media Viewer Modal\"\nexport const openMediaModal: (props: MediaModalProps) => void = findByCodeLazy(\"hasMediaOptions\", \"shouldHideMediaOptions\");\n\ninterface ModalAPI {\n    /**\n     * Wait for the render promise to resolve, then open a modal with it.\n     * This is equivalent to render().then(openModal)\n     * You should use the Modal components exported by this file\n     */\n    openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;\n    /**\n     * Open a Modal with the given render function.\n     * You should use the Modal components exported by this file\n     */\n    openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string;\n    /**\n     * Close a modal by its key\n     */\n    closeModal: (modalKey: string, contextKey?: string) => void;\n    /**\n     * Close all open modals\n     */\n    closeAllModals: () => void;\n}\n\nexport const ModalAPI: ModalAPI = mapMangledModuleLazy(\".modalKey?\", {\n    openModalLazy: filters.byCode(\".modalKey?\"),\n    openModal: filters.byCode(\",instant:\"),\n    closeModal: filters.byCode(\".onCloseCallback()\"),\n    closeAllModals: filters.byCode(\".getState();for\")\n});\n\nexport const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;\n"
  },
  {
    "path": "src/utils/native.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function relaunch() {\n    if (IS_DISCORD_DESKTOP)\n        window.DiscordNative.app.relaunch();\n    else if (IS_VESKTOP)\n        window.VesktopNative.app.relaunch();\n    else\n        location.reload();\n}\n"
  },
  {
    "path": "src/utils/onlyOnce.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport function onlyOnce<F extends Function>(f: F): F {\n    let called = false;\n    let result: any;\n    return function onlyOnceWrapper(this: unknown) {\n        if (called) return result;\n\n        called = true;\n\n        return (result = f.apply(this, arguments));\n    } as unknown as F;\n}\n"
  },
  {
    "path": "src/utils/patches.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { runtimeHashMessageKey } from \"./intlHash\";\nimport { Patch, PatchReplacement, ReplaceFn } from \"./types\";\n\nexport function canonicalizeMatch<T extends RegExp | string>(match: T): T {\n    let partialCanon = typeof match === \"string\" ? match : match.source;\n    partialCanon = partialCanon.replaceAll(/#{intl::([\\w$+/]*)(?:::(\\w+))?}/g, (_, key, modifier) => {\n        const hashed = modifier === \"raw\" ? key : runtimeHashMessageKey(key);\n\n        const isString = typeof match === \"string\";\n        const hasSpecialChars = !Number.isNaN(Number(hashed[0])) || hashed.includes(\"+\") || hashed.includes(\"/\");\n\n        if (hasSpecialChars) {\n            return isString\n                ? `[\"${hashed}\"]`\n                : String.raw`(?:\\[\"${hashed}\"\\])`.replaceAll(\"+\", \"\\\\+\");\n        }\n\n        return isString ? `.${hashed}` : String.raw`(?:\\.${hashed})`;\n    });\n\n    if (typeof match === \"string\") {\n        return partialCanon as T;\n    }\n\n    const canonSource = partialCanon.replaceAll(/(\\\\*)\\\\i/g, (match, leadingEscapes) =>\n        leadingEscapes.length % 2 === 0\n            ? `${leadingEscapes}${String.raw`(?:[A-Za-z_$][\\w$]*)`}`\n            : match.slice(1)\n    );\n    const canonRegex = new RegExp(canonSource, match.flags);\n    canonRegex.toString = match.toString.bind(match);\n\n    return canonRegex as T;\n}\n\nexport function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginPath: string): T {\n    if (typeof replace !== \"function\")\n        return replace.replaceAll(\"$self\", pluginPath) as T;\n\n    return ((...args) => replace(...args).replaceAll(\"$self\", pluginPath)) as T;\n}\n\nexport function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {\n    if (descriptor.get) {\n        const original = descriptor.get;\n        descriptor.get = function () {\n            return canonicalize(original.call(this));\n        };\n    } else if (descriptor.value) {\n        descriptor.value = canonicalize(descriptor.value);\n    }\n    return descriptor;\n}\n\nexport function canonicalizeReplacement(replacement: Pick<PatchReplacement, \"match\" | \"replace\">, pluginPath: string) {\n    const descriptors = Object.getOwnPropertyDescriptors(replacement);\n    descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);\n    descriptors.replace = canonicalizeDescriptor(\n        descriptors.replace,\n        replace => canonicalizeReplace(replace, pluginPath),\n    );\n    Object.defineProperties(replacement, descriptors);\n}\n\nexport function canonicalizeFind(patch: Patch) {\n    const descriptors = Object.getOwnPropertyDescriptors(patch);\n    descriptors.find = canonicalizeDescriptor(descriptors.find, canonicalizeMatch);\n    Object.defineProperties(patch, descriptors);\n}\n"
  },
  {
    "path": "src/utils/react.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { React, useEffect, useMemo, useReducer, useState } from \"@webpack/common\";\nimport type { ActionDispatch, ReactNode } from \"react\";\n\nimport { checkIntersecting } from \"./misc\";\n\nexport * from \"./lazyReact\";\n\nexport const NoopComponent = () => null;\n\n/**\n * Check if a React node is a primitive (string, number, bigint, boolean, undefined)\n */\nexport function isPrimitiveReactNode(node: ReactNode): boolean {\n    const t = typeof node;\n    return t === \"string\" || t === \"number\" || t === \"bigint\" || t === \"boolean\" || t === \"undefined\";\n}\n\n/**\n * Check if an element is on screen\n * @param intersectOnly If `true`, will only update the state when the element comes into view\n * @returns [refCallback, isIntersecting]\n */\nexport const useIntersection = (intersectOnly = false): [\n    refCallback: React.RefCallback<Element>,\n    isIntersecting: boolean,\n] => {\n    const observerRef = React.useRef<IntersectionObserver | null>(null);\n    const [isIntersecting, setIntersecting] = useState(false);\n\n    const refCallback = (element: Element | null) => {\n        observerRef.current?.disconnect();\n        observerRef.current = null;\n\n        if (!element) return;\n\n        if (checkIntersecting(element)) {\n            setIntersecting(true);\n            if (intersectOnly) return;\n        }\n\n        observerRef.current = new IntersectionObserver(entries => {\n            for (const entry of entries) {\n                if (entry.target !== element) continue;\n                if (entry.isIntersecting && intersectOnly) {\n                    setIntersecting(true);\n                    observerRef.current?.disconnect();\n                    observerRef.current = null;\n                } else {\n                    setIntersecting(entry.isIntersecting);\n                }\n            }\n        });\n        observerRef.current.observe(element);\n    };\n\n    return [refCallback, isIntersecting];\n};\n\ntype AwaiterRes<T> = [T, any, boolean];\ninterface AwaiterOpts<T> {\n    fallbackValue: T;\n    deps?: unknown[];\n    onError?(e: any): void;\n    onSuccess?(value: T): void;\n}\n/**\n * Await a promise\n * @param factory Factory\n * @param fallbackValue The fallback value that will be used until the promise resolved\n * @returns [value, error, isPending]\n */\nexport function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>;\nexport function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>;\nexport function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> {\n    const opts: Required<AwaiterOpts<T | null>> = Object.assign({\n        fallbackValue: null,\n        deps: [],\n        onError: null,\n    }, providedOpts);\n    const [state, setState] = useState({\n        value: opts.fallbackValue,\n        error: null,\n        pending: true\n    });\n\n    useEffect(() => {\n        let isAlive = true;\n        if (!state.pending) setState({ ...state, pending: true });\n\n        factory()\n            .then(value => {\n                if (!isAlive) return;\n                setState({ value, error: null, pending: false });\n                opts.onSuccess?.(value);\n            })\n            .catch(error => {\n                if (!isAlive) return;\n                setState({ value: null, error, pending: false });\n                opts.onError?.(error);\n            });\n\n        return () => void (isAlive = false);\n    }, opts.deps);\n\n    return [state.value, state.error, state.pending];\n}\n\n/**\n * Returns a function that can be used to force rerender react components\n */\nexport function useForceUpdater(): ActionDispatch<[]>;\nexport function useForceUpdater(withDep: true): [any, ActionDispatch<[]>];\nexport function useForceUpdater(withDep?: true) {\n    const r = useReducer(x => x + 1, 0);\n    return withDep ? r : r[1];\n}\n\ninterface TimerOpts {\n    interval?: number;\n    deps?: unknown[];\n}\n\nexport function useTimer({ interval = 1000, deps = [] }: TimerOpts) {\n    const [time, setTime] = useState(0);\n    const start = useMemo(() => Date.now(), deps);\n\n    useEffect(() => {\n        const intervalId = setInterval(() => setTime(Date.now() - start), interval);\n\n        return () => {\n            setTime(0);\n            clearInterval(intervalId);\n        };\n    }, deps);\n\n    return time;\n}\n\nexport function useCleanupEffect(\n    effect: () => void,\n    deps?: React.DependencyList\n): void {\n    useEffect(() => effect, deps);\n}\n"
  },
  {
    "path": "src/utils/text.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Utils for readable text transformations eg: `toTitle(fromKebab())`\n\n// Case style to words\nexport const wordsFromCamel = (text: string) =>\n    text.split(/(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])/).map(w => /^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase());\nexport const wordsFromSnake = (text: string) => text.toLowerCase().split(\"_\");\nexport const wordsFromKebab = (text: string) => text.toLowerCase().split(\"-\");\nexport const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());\nexport const wordsFromTitle = (text: string) => text.toLowerCase().split(\" \");\n\n// Words to case style\nexport const wordsToCamel = (words: string[]) =>\n    words.map((w, i) => (i ? w[0].toUpperCase() + w.slice(1) : w)).join(\"\");\nexport const wordsToSnake = (words: string[]) => words.join(\"_\").toUpperCase();\nexport const wordsToKebab = (words: string[]) => words.join(\"-\").toLowerCase();\nexport const wordsToPascal = (words: string[]) =>\n    words.map(w => w[0].toUpperCase() + w.slice(1)).join(\"\");\nexport const wordsToTitle = (words: string[]) =>\n    words.map(w => w[0].toUpperCase() + w.slice(1)).join(\" \");\n\nconst units = [\"years\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", \"seconds\"] as const;\ntype Units = typeof units[number];\n\nfunction getUnitStr(unit: Units, isOne: boolean, short: boolean) {\n    if (short === false) return isOne ? unit.slice(0, -1) : unit;\n\n    return unit[0];\n}\n\n/**\n * Forms time into a human readable string link \"1 day, 2 hours, 3 minutes and 4 seconds\"\n * @param time The time on the specified unit\n * @param unit The unit the time is on\n * @param short Whether to use short units like \"d\" instead of \"days\"\n */\nexport function formatDuration(time: number, unit: Units, short: boolean = false) {\n    const { moment } = require(\"@webpack/common\") as typeof import(\"@webpack/common\");\n    const dur = moment.duration(time, unit);\n\n    let unitsAmounts = units.map(unit => ({ amount: dur[unit](), unit }));\n\n    let amountsToBeRemoved = 0;\n\n    outer:\n    for (let i = 0; i < unitsAmounts.length; i++) {\n        if (unitsAmounts[i].amount === 0 || !(i + 1 < unitsAmounts.length)) continue;\n        for (let v = i + 1; v < unitsAmounts.length; v++) {\n            if (unitsAmounts[v].amount !== 0) continue outer;\n        }\n\n        amountsToBeRemoved = unitsAmounts.length - (i + 1);\n    }\n    unitsAmounts = amountsToBeRemoved === 0 ? unitsAmounts : unitsAmounts.slice(0, -amountsToBeRemoved);\n\n    const daysAmountIndex = unitsAmounts.findIndex(({ unit }) => unit === \"days\");\n    if (daysAmountIndex !== -1) {\n        const daysAmount = unitsAmounts[daysAmountIndex];\n\n        const daysMod = daysAmount.amount % 7;\n        if (daysMod === 0) unitsAmounts.splice(daysAmountIndex, 1);\n        else daysAmount.amount = daysMod;\n    }\n\n    let res: string = \"\";\n    while (unitsAmounts.length) {\n        const { amount, unit } = unitsAmounts.shift()!;\n\n        if (res.length) res += unitsAmounts.length ? \", \" : \" and \";\n\n        if (amount > 0 || res.length) {\n            res += `${amount} ${getUnitStr(unit, amount === 1, short)}`;\n        }\n    }\n\n    return res.length ? res : `0 ${getUnitStr(unit, false, short)}`;\n}\n\n/**\n * Join an array of strings in a human readable way (1, 2 and 3)\n * @param elements Elements\n */\nexport function humanFriendlyJoin(elements: string[]): string;\n/**\n * Join an array of strings in a human readable way (1, 2 and 3)\n * @param elements Elements\n * @param mapper Function that converts elements to a string\n */\nexport function humanFriendlyJoin<T>(elements: T[], mapper: (e: T) => string): string;\nexport function humanFriendlyJoin(elements: any[], mapper: (e: any) => string = s => s): string {\n    const { length } = elements;\n    if (length === 0)\n        return \"\";\n    if (length === 1)\n        return mapper(elements[0]);\n\n    let s = \"\";\n\n    for (let i = 0; i < length; i++) {\n        s += mapper(elements[i]);\n        if (length - i > 2)\n            s += \", \";\n        else if (length - i > 1)\n            s += \" and \";\n    }\n\n    return s;\n}\n\n/**\n * Wrap the text in ``` with an optional language\n */\nexport function makeCodeblock(text: string, language?: string) {\n    const chars = \"```\";\n    return `${chars}${language || \"\"}\\n${text.replaceAll(\"```\", \"\\\\`\\\\`\\\\`\")}\\n${chars}`;\n}\n\nexport function stripIndent(strings: TemplateStringsArray, ...values: any[]) {\n    const string = String.raw({ raw: strings }, ...values);\n\n    const match = string.match(/^[ \\t]*(?=\\S)/gm);\n    if (!match) return string.trim();\n\n    const minIndent = match.reduce((r, a) => Math.min(r, a.length), Infinity);\n    return string.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, \"gm\"), \"\").trim();\n}\n\nexport const ZWSP = \"\\u200b\";\nexport function toInlineCode(s: string) {\n    return \"``\" + ZWSP + s.replaceAll(\"`\", ZWSP + \"`\" + ZWSP) + ZWSP + \"``\";\n}\n\n// @ts-expect-error Missing RegExp.escape\nexport const escapeRegExp: (s: string) => string = RegExp.escape ?? function (s: string) {\n    return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n};\n"
  },
  {
    "path": "src/utils/types.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { ProfileBadge } from \"@api/Badges\";\nimport { ChatBarButtonData } from \"@api/ChatButtons\";\nimport { NavContextMenuPatchCallback } from \"@api/ContextMenu\";\nimport { MemberListDecoratorFactory } from \"@api/MemberListDecorators\";\nimport { MessageAccessoryFactory } from \"@api/MessageAccessories\";\nimport { MessageDecorationFactory } from \"@api/MessageDecorations\";\nimport { MessageClickListener, MessageEditListener, MessageSendListener } from \"@api/MessageEvents\";\nimport { MessagePopoverButtonData } from \"@api/MessagePopover\";\nimport { Command, FluxEvents } from \"@vencord/discord-types\";\nimport { ReactNode } from \"react\";\nimport { LiteralUnion } from \"type-fest\";\n\n// exists to export default definePlugin({...})\nexport default function definePlugin<P extends PluginDef>(p: P & Record<PropertyKey, any>) {\n    return p as typeof p & Plugin;\n}\n\nexport function makeRange(start: number, end: number, step = 1) {\n    const ranges: number[] = [];\n    for (let value = start; value <= end; value += step) {\n        ranges.push(Math.round(value * 100) / 100);\n    }\n    return ranges;\n}\n\nexport type ReplaceFn = (match: string, ...groups: string[]) => string;\n\nexport interface PatchReplacement {\n    /** The match for the patch replacement. If you use a string it will be implicitly converted to a RegExp */\n    match: string | RegExp;\n    /** The replacement string or function which returns the string for the patch replacement */\n    replace: string | ReplaceFn;\n    /** Do not warn if this replacement did no changes */\n    noWarn?: boolean;\n    /**\n     * A function which returns whether this patch replacement should be applied.\n     * This is ran before patches are registered, so if this returns false, the patch will never be registered.\n     */\n    predicate?(): boolean;\n    /** The minimum build number for this patch to be applied */\n    fromBuild?: number;\n    /** The maximum build number for this patch to be applied */\n    toBuild?: number;\n}\n\nexport interface Patch {\n    plugin: string;\n    /** A string or RegExp which is only include/matched in the module code you wish to patch. Prefer only using a RegExp if a simple string test is not enough */\n    find: string | RegExp;\n    /** The replacement(s) for the module being patched */\n    replacement: PatchReplacement | PatchReplacement[];\n    /** Whether this patch should apply to multiple modules */\n    all?: boolean;\n    /** Do not warn if this patch did no changes */\n    noWarn?: boolean;\n    /** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */\n    group?: boolean;\n    /**\n     * A function which returns whether this patch replacement should be applied.\n     * This is ran before patches are registered, so if this returns false, the patch will never be registered.\n     */\n    predicate?(): boolean;\n    /** The minimum build number for this patch to be applied */\n    fromBuild?: number;\n    /** The maximum build number for this patch to be applied */\n    toBuild?: number;\n}\n\nexport interface PluginAuthor {\n    name: string;\n    id: BigInt;\n}\n\nexport interface Plugin extends PluginDef {\n    patches?: Patch[];\n    started: boolean;\n    isDependency?: boolean;\n}\n\nexport type IconComponent = (props: IconProps & Record<string, any>) => ReactNode;\nexport type IconProps = { height?: number | string; width?: number | string; className?: string; };\nexport interface PluginDef {\n    name: string;\n    description: string;\n    authors: PluginAuthor[];\n    start?(): void;\n    stop?(): void;\n    patches?: Omit<Patch, \"plugin\">[];\n    /**\n     * List of commands that your plugin wants to register\n     */\n    commands?: Command[];\n    /**\n     * A list of other plugins that your plugin depends on.\n     * These will automatically be enabled and loaded before your plugin\n     * Generally these will be API plugins\n     */\n    dependencies?: string[],\n    /**\n     * Whether this plugin is required and forcefully enabled\n     */\n    required?: boolean;\n    /**\n     * Whether this plugin should be hidden from the user\n     */\n    hidden?: boolean;\n    /**\n     * Whether this plugin should be enabled by default, but can be disabled\n     */\n    enabledByDefault?: boolean;\n    /**\n     * Whether enabling or disabling this plugin requires a restart. Defaults to true if the plugin has patches.\n     */\n    requiresRestart?: boolean;\n    /**\n     * When to call the start() method\n     * @default StartAt.WebpackReady\n     */\n    startAt?: StartAt,\n    /**\n     * Which parts of the plugin can be tested by the reporter. Defaults to all parts\n     */\n    reporterTestable?: number;\n    /**\n     * Optionally provide settings that the user can configure in the Plugins tab of settings.\n     * @deprecated Use `settings` instead\n     */\n    // TODO: Remove when everything is migrated to `settings`\n    options?: Record<string, PluginOptionsItem>;\n    /**\n     * Optionally provide settings that the user can configure in the Plugins tab of settings.\n     */\n    settings?: DefinedSettings;\n    /**\n     * Allows you to specify a custom Component that will be rendered in your\n     * plugin's settings page\n     */\n    settingsAboutComponent?: React.ComponentType<{}>;\n    /**\n     * Allows you to subscribe to Flux events\n     */\n    flux?: Partial<{\n        [E in LiteralUnion<FluxEvents, string>]: (event: any) => void | Promise<void>;\n    }>;\n    /**\n     * Allows you to manipulate context menus\n     */\n    contextMenus?: Record<string, NavContextMenuPatchCallback>;\n    /**\n     * Allows you to add custom actions to the Vencord Toolbox.\n     *\n     * Can either be an object mapping labels to action functions or a Function returning Menu components.\n     * Please note that you can only use Menu components.\n     *\n     * @example\n     * toolboxActions: {\n     *   \"Click Me\": () => alert(\"Hi\")\n     * }\n     */\n    toolboxActions?: Record<string, () => void> | (() => ReactNode);\n\n    tags?: string[];\n\n    /**\n     * Managed style to automatically enable and disable when the plugin is enabled or disabled\n     */\n    managedStyle?: string;\n\n    userProfileBadge?: ProfileBadge;\n\n    messagePopoverButton?: MessagePopoverButtonData;\n    chatBarButton?: ChatBarButtonData;\n\n    onMessageClick?: MessageClickListener;\n    onBeforeMessageSend?: MessageSendListener;\n    onBeforeMessageEdit?: MessageEditListener;\n\n    renderMessageAccessory?: MessageAccessoryFactory;\n    renderMessageDecoration?: MessageDecorationFactory;\n\n    renderMemberListDecorator?: MemberListDecoratorFactory;\n\n    // TODO: Remove eventually\n    /**\n     * @deprecated Use {@link chatBarButton} instead\n     */\n    renderChatBarButton?: never;\n    /**\n     * @deprecated Use {@link messagePopoverButton} instead\n     */\n    renderMessagePopoverButton?: never;\n}\n\nexport const enum StartAt {\n    /** Right away, as soon as Vencord initialised */\n    Init = \"Init\",\n    /** On the DOMContentLoaded event, so once the document is ready */\n    DOMContentLoaded = \"DOMContentLoaded\",\n    /** Once Discord's core webpack modules have finished loading, so as soon as things like react and flux are available */\n    WebpackReady = \"WebpackReady\"\n}\n\nexport const enum ReporterTestable {\n    None = 1 << 1,\n    Start = 1 << 2,\n    Patches = 1 << 3,\n    FluxEvents = 1 << 4\n}\n\nexport function defineDefault<T = any>(value: T) {\n    return value;\n}\n\nexport const enum OptionType {\n    STRING,\n    NUMBER,\n    BIGINT,\n    BOOLEAN,\n    SELECT,\n    SLIDER,\n    COMPONENT,\n    CUSTOM\n}\n\nexport type SettingsDefinition = Record<string, PluginSettingDef>;\nexport type SettingsChecks<D extends SettingsDefinition> = {\n    [K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled<DefinedSettings<D>> :\n    (IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);\n};\n\nexport type PluginSettingDef =\n    (PluginSettingCustomDef & Pick<PluginSettingCommon, \"onChange\">) |\n    (PluginSettingComponentDef & Omit<PluginSettingCommon, \"description\" | \"placeholder\">) | ((\n        | PluginSettingStringDef\n        | PluginSettingNumberDef\n        | PluginSettingBooleanDef\n        | PluginSettingSelectDef\n        | PluginSettingSliderDef\n        | PluginSettingBigIntDef\n    ) & PluginSettingCommon);\n\nexport interface PluginSettingCommon {\n    description: string;\n    placeholder?: string;\n    onChange?(newValue: any): void;\n    /**\n     * Whether changing this setting requires a restart\n     */\n    restartNeeded?: boolean;\n    componentProps?: Record<string, any>;\n    /**\n     * Hide this setting from the settings UI\n     */\n    hidden?: boolean;\n    /**\n     * Set this if the setting only works on Browser or Desktop, not both\n     */\n    target?: \"WEB\" | \"DESKTOP\" | \"BOTH\";\n}\n\ninterface IsDisabled<D = unknown> {\n    /**\n     * Checks if this setting should be disabled\n     */\n    disabled?(this: D): boolean;\n}\n\ninterface IsValid<T, D = unknown> {\n    /**\n     * Prevents the user from saving settings if this is false or a string\n     */\n    isValid?(this: D, value: T): boolean | string;\n}\n\nexport interface PluginSettingStringDef {\n    type: OptionType.STRING;\n    default?: string;\n    /** Whether to use a multiline text area */\n    multiline?: boolean;\n}\nexport interface PluginSettingNumberDef {\n    type: OptionType.NUMBER;\n    default?: number;\n}\nexport interface PluginSettingBigIntDef {\n    type: OptionType.BIGINT;\n    default?: BigInt;\n}\nexport interface PluginSettingBooleanDef {\n    type: OptionType.BOOLEAN;\n    default?: boolean;\n}\n\nexport interface PluginSettingSelectDef {\n    type: OptionType.SELECT;\n    options: readonly PluginSettingSelectOption[];\n}\n\nexport interface PluginSettingSelectOption {\n    label: string;\n    value: string | number | boolean;\n    default?: boolean;\n}\n\nexport interface PluginSettingCustomDef {\n    type: OptionType.CUSTOM;\n    default?: any;\n}\n\nexport interface PluginSettingSliderDef {\n    type: OptionType.SLIDER;\n    /**\n     * All the possible values in the slider. Needs at least two values.\n     */\n    markers: number[];\n    /**\n     * Default value to use\n     */\n    default: number;\n    /**\n     * If false, allow users to select values in-between your markers.\n     */\n    stickToMarkers?: boolean;\n}\n\nexport interface IPluginOptionComponentProps {\n    /**\n     * Run this when the value changes.\n     *\n     * NOTE: The user will still need to click save to apply these changes.\n     */\n    setValue(newValue: any): void;\n    /**\n     * The options object\n     */\n    option: PluginSettingComponentDef;\n}\n\nexport interface PluginSettingComponentDef {\n    type: OptionType.COMPONENT;\n    component: (props: IPluginOptionComponentProps) => ReactNode | Promise<ReactNode>;\n    default?: any;\n}\n\n/** Maps a `PluginSettingDef` to its value type */\ntype PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStringDef ? string :\n    O extends PluginSettingNumberDef ? number :\n    O extends PluginSettingBigIntDef ? BigInt :\n    O extends PluginSettingBooleanDef ? boolean :\n    O extends PluginSettingSelectDef ? O[\"options\"][number][\"value\"] :\n    O extends PluginSettingSliderDef ? number :\n    O extends PluginSettingComponentDef ? O extends { default: infer Default; } ? Default : any :\n    O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any :\n    never;\n\ntype PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (\n    O[\"options\"] extends { default?: boolean; }[] ? O[\"options\"][number][\"value\"] : undefined\n) : O extends { default: infer T; } ? T : undefined;\n\ntype SettingsStore<D extends SettingsDefinition> = {\n    [K in keyof D]: PluginSettingType<D[K]> | PluginSettingDefaultType<D[K]>;\n};\n\n/** An instance of defined plugin settings */\nexport interface DefinedSettings<\n    Def extends SettingsDefinition = SettingsDefinition,\n    Checks extends SettingsChecks<Def> = {},\n    PrivateSettings extends object = {}\n> {\n    /** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */\n    store: SettingsStore<Def> & PrivateSettings;\n    /** Shorthand for `Vencord.PlainSettings.plugins.PluginName`, but with typings */\n    plain: SettingsStore<Def> & PrivateSettings;\n    /**\n     * React hook for getting the settings for this plugin\n     * @param filter optional filter to avoid rerenders for irrelevent settings\n     */\n    use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;\n    /** Definitions of each setting */\n    def: Def;\n    /** Setting methods with return values that could rely on other settings */\n    checks: Checks;\n    /**\n     * Name of the plugin these settings belong to,\n     * will be an empty string until plugin is initialized\n     */\n    pluginName: string;\n\n    withPrivateSettings<T extends object>(): DefinedSettings<Def, Checks, T>;\n}\n\nexport type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>;\n\nexport type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };\n\n/* -------------------------------------------- */\n/*             Legacy Options Types             */\n/* -------------------------------------------- */\n\nexport type PluginOptionBase = PluginSettingCommon & IsDisabled;\nexport type PluginOptionsItem =\n    | PluginOptionString\n    | PluginOptionNumber\n    | PluginOptionBoolean\n    | PluginOptionSelect\n    | PluginOptionSlider\n    | PluginOptionComponent\n    | PluginOptionCustom;\nexport type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;\nexport type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;\nexport type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;\nexport type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;\nexport type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;\nexport type PluginOptionComponent = PluginSettingComponentDef & Omit<PluginSettingCommon, \"description\" | \"placeholder\">;\nexport type PluginOptionCustom = PluginSettingCustomDef & Pick<PluginSettingCommon, \"onChange\">;\n\nexport type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {\n    [key in keyof PluginExports]:\n    PluginExports[key] extends (event: Electron.IpcMainInvokeEvent, ...args: infer Args) => infer Return\n    ? (...args: Args) => Return extends Promise<any> ? Return : Promise<Return>\n    : never;\n};\n\nexport type AllOrNothing<T> = T | { [K in keyof T]?: never; };\n"
  },
  {
    "path": "src/utils/updater.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport gitHash from \"~git-hash\";\n\nimport { Logger } from \"./Logger\";\nimport { relaunch } from \"./native\";\nimport { IpcRes } from \"./types\";\n\nexport const UpdateLogger = /* #__PURE__*/ new Logger(\"Updater\", \"white\");\nexport let isOutdated = false;\nexport let isNewer = false;\nexport let updateError: any;\nexport let changes: Record<\"hash\" | \"author\" | \"message\", string>[];\n\nasync function Unwrap<T>(p: Promise<IpcRes<T>>) {\n    const res = await p;\n\n    if (res.ok) return res.value;\n\n    updateError = res.error;\n    throw res.error;\n}\n\nexport async function checkForUpdates() {\n    changes = await Unwrap(VencordNative.updater.getUpdates());\n\n    // we only want to check this for the git updater, not the http updater\n    if (!IS_STANDALONE) {\n        if (changes.some(c => c.hash === gitHash)) {\n            isNewer = true;\n            return (isOutdated = false);\n        }\n    }\n\n    return (isOutdated = changes.length > 0);\n}\n\nexport async function update() {\n    if (!isOutdated) return true;\n\n    const res = await Unwrap(VencordNative.updater.update());\n\n    if (res) {\n        isOutdated = false;\n        if (!await Unwrap(VencordNative.updater.rebuild()))\n            throw new Error(\"The Build failed. Please try manually building the new update\");\n    }\n\n    return res;\n}\n\nexport const getRepo = () => Unwrap(VencordNative.updater.getRepo());\n\nexport async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {\n    if (IS_WEB || IS_UPDATER_DISABLED) return;\n    if (checkForDev && IS_DEV) return;\n\n    try {\n        const isOutdated = await checkForUpdates();\n        if (isOutdated) {\n            const wantsUpdate = confirm(confirmMessage);\n            if (wantsUpdate && isNewer) return alert(\"Your local copy has more recent commits. Please stash or reset them.\");\n            if (wantsUpdate) {\n                await update();\n                relaunch();\n            }\n        }\n    } catch (err) {\n        UpdateLogger.error(err);\n        alert(\"That also failed :( Try updating or re-installing with the installer!\");\n    }\n}\n"
  },
  {
    "path": "src/utils/web-metadata.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nexport let EXTENSION_VERSION: string;\nexport let EXTENSION_BASE_URL: string;\nexport let RENDERER_CSS_URL: string;\n\nlet resolveMetaReady: Function;\nexport const metaReady = new Promise<void>(res => resolveMetaReady = res);\n\nif (IS_EXTENSION) {\n    const listener = (e: MessageEvent) => {\n        if (e.data?.type === \"vencord:meta\") {\n            ({ EXTENSION_BASE_URL, EXTENSION_VERSION, RENDERER_CSS_URL } = e.data.meta);\n            window.removeEventListener(\"message\", listener);\n            resolveMetaReady();\n        }\n    };\n\n    window.addEventListener(\"message\", listener);\n}\n"
  },
  {
    "path": "src/utils/web.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/**\n * Prompts the user to save a file to their system\n * @param file The file to save\n */\nexport function saveFile(file: File) {\n    const a = document.createElement(\"a\");\n    a.href = URL.createObjectURL(file);\n    a.download = file.name;\n\n    document.body.appendChild(a);\n    a.click();\n    setImmediate(() => {\n        URL.revokeObjectURL(a.href);\n        document.body.removeChild(a);\n    });\n}\n\n/**\n * Prompts the user to choose a file from their system\n * @param mimeTypes A comma separated list of mime types to accept, see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers\n * @returns A promise that resolves to the chosen file or null if the user cancels\n */\nexport function chooseFile(mimeTypes: string) {\n    return new Promise<File | null>(resolve => {\n        const input = document.createElement(\"input\");\n        input.type = \"file\";\n        input.style.display = \"none\";\n        input.accept = mimeTypes;\n        input.onchange = async () => {\n            resolve(input.files?.[0] ?? null);\n        };\n\n        document.body.appendChild(input);\n        input.click();\n        setImmediate(() => document.body.removeChild(input));\n    });\n}\n\nexport function getStylusWebStoreUrl() {\n    const isChromium = (navigator as any).userAgentData?.brands?.some(b => b.brand === \"Chromium\");\n\n    return isChromium\n        ? \"https://chromewebstore.google.com/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne\"\n        : \"https://addons.mozilla.org/firefox/addon/styl-us/\";\n}\n"
  },
  {
    "path": "src/webpack/common/components.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { TextCompat } from \"@components/BaseText\";\nimport { ButtonCompat } from \"@components/Button\";\nimport { Divider } from \"@components/Divider\";\nimport { FormSwitchCompat } from \"@components/FormSwitch\";\nimport { Heading } from \"@components/Heading\";\nimport { Paragraph } from \"@components/Paragraph\";\nimport { TooltipContainer as TooltipContainerComponent } from \"@components/TooltipContainer\";\nimport { TooltipFallback } from \"@components/TooltipFallback\";\nimport { LazyComponent } from \"@utils/lazyReact\";\nimport * as t from \"@vencord/discord-types\";\nimport { filters, find, findCssClassesLazy, mapMangledCssClasses, mapMangledModuleLazy, proxyLazyWebpack, waitFor } from \"@webpack\";\n\nimport { waitForComponent } from \"./internal\";\n\nexport const Forms = {\n    // TODO: Stop using this and use Heading/Paragraph directly\n    /** @deprecated use Heading from Vencord */\n    FormTitle: Heading,\n    /** @deprecated use Paragraph from Vencord */\n    FormText: Paragraph,\n    /** @deprecated don't use this */\n    FormSection: \"section\" as never, // Backwards compat since Vesktop uses this\n    /** @deprecated use `@components/Divider` */\n    FormDivider: Divider as never, // Backwards compat since Vesktop uses this\n};\n\n// TODO: Stop using this and use Paragraph/Span directly\n/** @deprecated use Paragraph, Span, or BaseText from Vencord */\nexport const Text = TextCompat;\n/** @deprecated use Button from Vencord */\nexport const Button = ButtonCompat;\n/** @deprecated Use FormSwitch from Vencord */\nexport const Switch = FormSwitchCompat as never;\n\nexport const Checkbox = waitForComponent<t.Checkbox>(\"Checkbox\", filters.componentByCode('\"data-toggleable-component\":\"checkbox'));\n\nexport const Tooltip = waitForComponent<t.Tooltip>(\"Tooltip\", m => m.prototype?.shouldShowTooltip && m.prototype.render, TooltipFallback);\n/** @deprecated import from @vencord/components */\nexport const TooltipContainer = TooltipContainerComponent as never;\n\nexport const TextInput = waitForComponent<t.TextInput>(\"TextInput\", filters.componentByCode(\"#{intl::MAXIMUM_LENGTH_ERROR}\", '\"input\"'));\nexport const TextArea = waitForComponent<t.TextArea>(\"TextArea\", filters.componentByCode(\"this.getPaddingRight()},id:\"));\nexport const Select = waitForComponent<t.Select>(\"Select\", filters.componentByCode('\"Select\"'));\nexport const SearchableSelect = waitForComponent<t.SearchableSelect>(\"SearchableSelect\", filters.componentByCode('\"SearchableSelect\"'));\nexport const Slider = waitForComponent<t.Slider>(\"Slider\", filters.componentByCode(\"markDash\", \"this.renderMark(\"));\nexport const Popout = waitForComponent<t.Popout>(\"Popout\", filters.componentByCode(\"ref:this.ref,\", \"renderPopout:this.renderPopout,\"));\nexport const Dialog = waitForComponent<t.Dialog>(\"Dialog\", filters.componentByCode('role:\"dialog\",tabIndex:-1'));\nexport const TabBar = waitForComponent(\"TabBar\", filters.componentByCode(\"ref:this.tabBarRef,className:\"));\nexport const Paginator = waitForComponent<t.Paginator>(\"Paginator\", filters.componentByCode('rel:\"prev\",children:'));\n// TODO: remake this component\nexport const Clickable = waitForComponent<t.Clickable>(\"Clickable\", filters.componentByCode(\"this.context?this.renderNonInteractive():\"));\nexport const Avatar = waitForComponent<t.Avatar>(\"Avatar\", filters.componentByCode(\".size-1.375*\"));\n\nexport const ColorPicker = waitForComponent<t.ColorPicker>(\"ColorPicker\", filters.componentByCode(\"#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}\", \"showEyeDropper\"));\n\nexport const UserSummaryItem = waitForComponent(\"UserSummaryItem\", filters.componentByCode(\"defaultRenderUser\", \"showDefaultAvatarsForNullUsers\"));\n\nexport let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin;\nexport let createListScroller: (scrollBarClassName: string, fadeClassName: string, someOtherClassIdkMan: string, resizeObserverClass: typeof ResizeObserver) => t.ListScrollerThin;\n\nconst listScrollerClassnames = [\"thin\", \"auto\", \"fade\"] as const;\nexport const scrollerClasses = findCssClassesLazy(\"thin\", \"auto\", \"fade\", \"customTheme\", \"none\");\n\nconst isListScroller = filters.byClassNames(...listScrollerClassnames);\nconst isNotNormalScroller = filters.byClassNames(\"customTheme\");\nexport const listScrollerClasses = proxyLazyWebpack(() => {\n    const mod = find(m => isListScroller(m) && !isNotNormalScroller(m), { topLevelOnly: true });\n    if (!mod) return {} as Record<typeof listScrollerClassnames[number], string>;\n\n    return mapMangledCssClasses(mod, listScrollerClassnames);\n});\n\nwaitFor(filters.byCode('=\"ltr\",orientation:', \"customTheme:\", \"forwardRef\"), m => createScroller = m);\nwaitFor(filters.byCode(\"getScrollerNode:\", \"resizeObserver:\", \"sectionHeight:\"), m => createListScroller = m);\n\nexport const ScrollerNone = LazyComponent(() => createScroller(scrollerClasses.none, scrollerClasses.fade, scrollerClasses.customTheme));\nexport const ScrollerThin = LazyComponent(() => createScroller(scrollerClasses.thin, scrollerClasses.fade, scrollerClasses.customTheme));\nexport const ScrollerAuto = LazyComponent(() => createScroller(scrollerClasses.auto, scrollerClasses.fade, scrollerClasses.customTheme));\n\nexport const ListScrollerThin = LazyComponent(() => createListScroller(listScrollerClasses.thin, listScrollerClasses.fade, \"\", ResizeObserver));\nexport const ListScrollerAuto = LazyComponent(() => createListScroller(listScrollerClasses.auto, listScrollerClasses.fade, \"\", ResizeObserver));\n\nexport const FocusLock = waitForComponent<t.FocusLock>(\"FocusLock\", filters.componentByCode(\".containerRef,{keyboardModeEnabled:\"));\n\nexport let useToken: t.useToken;\nwaitFor(m => {\n    if (typeof m !== \"function\") {\n        return false;\n    }\n\n    const str = String(m);\n    return str.includes(\".resolve({theme:\") && str.includes('\"refresh-fast-follow-avatars\"') && !str.includes(\"useMemo\");\n}, m => useToken = m);\n\nexport const MaskedLink = waitForComponent<t.MaskedLink>(\"MaskedLink\", filters.componentByCode(\"MASKED_LINK)\"));\nexport const Timestamp = waitForComponent<t.Timestamp>(\"Timestamp\", filters.componentByCode(\"#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}\"));\nexport const OAuth2AuthorizeModal = waitForComponent(\"OAuth2AuthorizeModal\", filters.componentByCode(\"hasContentBackground\", \"nextStep\", \"onClose?.()\"));\n\nexport const Animations = mapMangledModuleLazy(\".assign({colorNames:\", {\n    Transition: filters.componentByCode('[\"items\",\"children\"]', \",null,\"),\n    animated: filters.byProps(\"div\", \"text\")\n});\n"
  },
  {
    "path": "src/webpack/common/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport * from \"./components\";\nexport * from \"./menu\";\nexport * from \"./react\";\nexport * from \"./stores\";\nexport * from \"./userSettings\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "src/webpack/common/internal.tsx",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { Logger } from \"@utils/Logger\";\nimport { LazyComponent, LazyComponentWrapper } from \"@utils/react\";\nimport { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from \"@webpack\";\nimport { ComponentType } from \"react\";\n\nconst logger = new Logger(\"Webpack\");\n\nexport function waitForComponent<T extends ComponentType<any> = ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[], fallbackValue: ComponentType<any> | null = null) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"waitForComponent\", Array.isArray(filter) ? filter : [filter]]);\n\n    let myValue: T | null = null;\n\n    const lazyComponent = LazyComponent(() => {\n        if (myValue) return myValue;\n\n        const error = new Error(`Vencord could not find the ${name} Component`);\n        logger.error(error);\n\n        if (IS_DEV) throw error;\n\n        return fallbackValue!;\n    }) as LazyComponentWrapper<T>;\n\n    waitFor(filter, (v: any) => {\n        myValue = v;\n        Object.assign(lazyComponent, v);\n    }, { isIndirect: true });\n\n    return lazyComponent;\n}\n\nexport function waitForStore(name: string, cb: (v: any) => void) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"waitForStore\", [name]]);\n\n    waitFor(filters.byStoreName(name), cb, { isIndirect: true });\n}\n"
  },
  {
    "path": "src/webpack/common/menu.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type * as t from \"@vencord/discord-types\";\nimport { filters, mapMangledModuleLazy, waitFor, wreq } from \"@webpack\";\n\nexport const Menu = {} as t.Menu;\n\n// Relies on .name properties added by the MenuItemDemanglerAPI\nwaitFor(m => m.name === \"MenuCheckboxItem\", (_, id) => {\n    // We have to do this manual require by ID because m in this case is the MenuCheckBoxItem instead of the entire module\n    const exports = wreq(id);\n\n    for (const exportKey in exports) {\n        // Some exports might have not been initialized yet due to circular imports, so try catch it.\n        try {\n            var exportValue = exports[exportKey];\n        } catch {\n            continue;\n        }\n\n        if (typeof exportValue === \"function\" && exportValue.name.startsWith(\"Menu\")) {\n            Menu[exportValue.name] = exportValue;\n        }\n    }\n});\n\nwaitFor(filters.componentByCode('path:[\"empty\"]'), m => Menu.Menu = m);\nwaitFor(filters.componentByCode(\"SLIDER)\", \"handleSize:16\"), m => Menu.MenuSliderControl = m);\nwaitFor(filters.componentByCode(\".SEARCH)\", \".focus()\", \"query:\"), m => Menu.MenuSearchControl = m);\n\nexport const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:\"CONTEXT_MENU_OPEN', {\n    closeContextMenu: filters.byCode(\"CONTEXT_MENU_CLOSE\"),\n    openContextMenu: filters.byCode(\"renderLazy:\"),\n    openContextMenuLazy: e => typeof e === \"function\" && e.toString().length < 100\n});\n"
  },
  {
    "path": "src/webpack/common/react.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { findByCodeLazy, findByPropsLazy, waitFor } from \"@webpack\";\n\nexport let React: typeof import(\"react\");\nexport let useState: typeof React.useState;\nexport let useEffect: typeof React.useEffect;\nexport let useLayoutEffect: typeof React.useLayoutEffect;\nexport let useMemo: typeof React.useMemo;\nexport let useRef: typeof React.useRef;\nexport let useReducer: typeof React.useReducer;\nexport let useCallback: typeof React.useCallback;\n\nexport const ReactDOM: typeof import(\"react-dom\") = findByPropsLazy(\"createPortal\");\n// 299 is an error code used in createRoot and createPortal\nexport const createRoot: typeof import(\"react-dom/client\").createRoot = findByCodeLazy(\"(299));\", \".onRecoverableError\");\n\nwaitFor(\"useState\", m => {\n    React = m;\n    ({ useEffect, useState, useLayoutEffect, useMemo, useRef, useReducer, useCallback } = React);\n});\n"
  },
  {
    "path": "src/webpack/common/stores.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport * as t from \"@vencord/discord-types\";\nimport { findByCodeLazy, findByPropsLazy } from \"@webpack\";\n\nimport { waitForStore } from \"./internal\";\n\nexport const Flux: t.Flux = findByPropsLazy(\"connectStores\");\n\nexport type GenericStore = t.FluxStore & Record<string, any>;\n\nexport const DraftType = findByPropsLazy(\"ChannelMessage\", \"SlashCommand\");\n\nexport let MessageStore: Omit<t.MessageStore, \"getMessages\"> & GenericStore & {\n    getMessages(chanId: string): any;\n};\n\nexport let PermissionStore: t.PermissionStore;\nexport let GuildChannelStore: t.GuildChannelStore;\nexport let ReadStateStore: t.ReadStateStore;\nexport let PresenceStore: t.PresenceStore;\nexport let AccessibilityStore: t.AccessibilityStore;\nexport let PendingReplyStore: t.PendingReplyStore;\n\nexport let GuildStore: t.GuildStore;\nexport let GuildRoleStore: t.GuildRoleStore;\nexport let GuildScheduledEventStore: t.GuildScheduledEventStore;\nexport let GuildMemberCountStore: t.GuildMemberCountStore;\nexport let GuildMemberStore: t.GuildMemberStore;\nexport let UserStore: t.UserStore;\nexport let AuthenticationStore: t.AuthenticationStore;\nexport let ApplicationStore: t.ApplicationStore;\nexport let UserProfileStore: t.UserProfileStore;\nexport let SelectedChannelStore: t.SelectedChannelStore;\nexport let SelectedGuildStore: t.SelectedGuildStore;\nexport let ChannelStore: t.ChannelStore;\nexport let TypingStore: t.TypingStore;\nexport let RelationshipStore: t.RelationshipStore;\nexport let VoiceStateStore: t.VoiceStateStore;\n\nexport let EmojiStore: t.EmojiStore;\nexport let StickersStore: t.StickersStore;\nexport let ThemeStore: t.ThemeStore;\nexport let WindowStore: t.WindowStore;\nexport let DraftStore: t.DraftStore;\nexport let StreamerModeStore: t.StreamerModeStore;\nexport let SpotifyStore: t.SpotifyStore;\n\nexport let MediaEngineStore: t.MediaEngineStore;\nexport let NotificationSettingsStore: t.NotificationSettingsStore;\nexport let SpellCheckStore: t.SpellCheckStore;\nexport let UploadAttachmentStore: t.UploadAttachmentStore;\nexport let OverridePremiumTypeStore: t.OverridePremiumTypeStore;\nexport let RunningGameStore: t.RunningGameStore;\nexport let ActiveJoinedThreadsStore: t.ActiveJoinedThreadsStore;\nexport let UserGuildSettingsStore: t.UserGuildSettingsStore;\nexport let UserSettingsProtoStore: t.UserSettingsProtoStore;\nexport let CallStore: t.CallStore;\nexport let ChannelRTCStore: t.ChannelRTCStore;\nexport let FriendsStore: t.FriendsStore;\nexport let InstantInviteStore: t.InstantInviteStore;\nexport let InviteStore: t.InviteStore;\nexport let LocaleStore: t.LocaleStore;\nexport let RTCConnectionStore: t.RTCConnectionStore;\nexport let SoundboardStore: t.SoundboardStore;\nexport let PopoutWindowStore: t.PopoutWindowStore;\n\n/**\n * @see jsdoc of {@link t.useStateFromStores}\n */\nexport const useStateFromStores: t.useStateFromStores = findByCodeLazy(\"useStateFromStores\");\n\nwaitForStore(\"AccessibilityStore\", s => AccessibilityStore = s);\nwaitForStore(\"ApplicationStore\", s => ApplicationStore = s);\nwaitForStore(\"AuthenticationStore\", s => AuthenticationStore = s);\nwaitForStore(\"DraftStore\", s => DraftStore = s);\nwaitForStore(\"UserStore\", s => UserStore = s);\nwaitForStore(\"UserProfileStore\", m => UserProfileStore = m);\nwaitForStore(\"ChannelStore\", m => ChannelStore = m);\nwaitForStore(\"SelectedChannelStore\", m => SelectedChannelStore = m);\nwaitForStore(\"SelectedGuildStore\", m => SelectedGuildStore = m);\nwaitForStore(\"GuildStore\", m => GuildStore = m);\nwaitForStore(\"GuildMemberStore\", m => GuildMemberStore = m);\nwaitForStore(\"RelationshipStore\", m => RelationshipStore = m);\nwaitForStore(\"MediaEngineStore\", m => MediaEngineStore = m);\nwaitForStore(\"NotificationSettingsStore\", m => NotificationSettingsStore = m);\nwaitForStore(\"SpellcheckStore\", m => SpellCheckStore = m);\nwaitForStore(\"PermissionStore\", m => PermissionStore = m);\nwaitForStore(\"PresenceStore\", m => PresenceStore = m);\nwaitForStore(\"ReadStateStore\", m => ReadStateStore = m);\nwaitForStore(\"GuildChannelStore\", m => GuildChannelStore = m);\nwaitForStore(\"GuildRoleStore\", m => GuildRoleStore = m);\nwaitForStore(\"GuildScheduledEventStore\", m => GuildScheduledEventStore = m);\nwaitForStore(\"GuildMemberCountStore\", m => GuildMemberCountStore = m);\nwaitForStore(\"MessageStore\", m => MessageStore = m);\nwaitForStore(\"WindowStore\", m => WindowStore = m);\nwaitForStore(\"EmojiStore\", m => EmojiStore = m);\nwaitForStore(\"StickersStore\", m => StickersStore = m);\nwaitForStore(\"TypingStore\", m => TypingStore = m);\nwaitForStore(\"VoiceStateStore\", m => VoiceStateStore = m);\nwaitForStore(\"StreamerModeStore\", m => StreamerModeStore = m);\nwaitForStore(\"SpotifyStore\", m => SpotifyStore = m);\nwaitForStore(\"OverridePremiumTypeStore\", m => OverridePremiumTypeStore = m);\nwaitForStore(\"UploadAttachmentStore\", m => UploadAttachmentStore = m);\nwaitForStore(\"RunningGameStore\", m => RunningGameStore = m);\nwaitForStore(\"ActiveJoinedThreadsStore\", m => ActiveJoinedThreadsStore = m);\nwaitForStore(\"UserGuildSettingsStore\", m => UserGuildSettingsStore = m);\nwaitForStore(\"UserSettingsProtoStore\", m => UserSettingsProtoStore = m);\nwaitForStore(\"CallStore\", m => CallStore = m);\nwaitForStore(\"ChannelRTCStore\", m => ChannelRTCStore = m);\nwaitForStore(\"FriendsStore\", m => FriendsStore = m);\nwaitForStore(\"InstantInviteStore\", m => InstantInviteStore = m);\nwaitForStore(\"InviteStore\", m => InviteStore = m);\nwaitForStore(\"LocaleStore\", m => LocaleStore = m);\nwaitForStore(\"RTCConnectionStore\", m => RTCConnectionStore = m);\nwaitForStore(\"SoundboardStore\", m => SoundboardStore = m);\nwaitForStore(\"PopoutWindowStore\", m => PopoutWindowStore = m);\nwaitForStore(\"PendingReplyStore\", m => PendingReplyStore = m);\nwaitForStore(\"ThemeStore\", m => {\n    ThemeStore = m;\n    // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.\n    // For this reason, use a non import access here.\n    Vencord.Api.Themes.initQuickCssThemeStore(m);\n});\n"
  },
  {
    "path": "src/webpack/common/userSettings.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2023 Vendicated and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { findLazy } from \"@webpack\";\n\nexport const UserSettingsActionCreators = {\n    FrecencyUserSettingsActionCreators: findLazy(m => m.ProtoClass?.typeName?.endsWith(\".FrecencyUserSettings\")),\n    PreloadedUserSettingsActionCreators: findLazy(m => m.ProtoClass?.typeName?.endsWith(\".PreloadedUserSettings\")),\n};\n"
  },
  {
    "path": "src/webpack/common/utils.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2023 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport type * as t from \"@vencord/discord-types\";\nimport { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from \"@webpack\";\nimport type * as TSPattern from \"ts-pattern\";\n\nexport let FluxDispatcher: t.FluxDispatcher;\nwaitFor([\"dispatch\", \"subscribe\"], m => {\n    FluxDispatcher = m;\n    // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.\n    // For this reason, use a non import access here.\n    Vencord.Api.PluginManager.subscribeAllPluginsFluxEvents(m);\n\n    const cb = () => {\n        m.unsubscribe(\"CONNECTION_OPEN\", cb);\n        _resolveReady();\n    };\n    m.subscribe(\"CONNECTION_OPEN\", cb);\n});\n\nexport let ComponentDispatch: any;\nwaitFor([\"dispatchToLastSubscribed\"], m => ComponentDispatch = m);\n\nexport const Constants: t.Constants = mapMangledModuleLazy('ME:\"/users/@me\"', {\n    Endpoints: filters.byProps(\"USER\", \"ME\"),\n    UserFlags: filters.byProps(\"STAFF\", \"SPAMMER\"),\n    FriendsSections: m => m.PENDING === \"PENDING\" && m.ADD_FRIEND\n});\n\nexport const RestAPI: t.RestAPI = findLazy(m => typeof m === \"object\" && m.del && m.put);\nexport const moment: typeof import(\"moment\") = findByPropsLazy(\"parseTwoDigitYear\");\n\nexport const hljs: typeof import(\"highlight.js\").default = findByPropsLazy(\"highlight\", \"registerLanguage\");\n\nexport const { match, P }: { match: typeof TSPattern[\"match\"], P: typeof TSPattern[\"P\"]; } = mapMangledModuleLazy(\"@ts-pattern/matcher\", {\n    match: filters.byCode(\"return new\"),\n    P: filters.byProps(\"when\")\n});\n\nexport const lodash: typeof import(\"lodash\") = findByPropsLazy(\"debounce\", \"cloneDeep\");\n\nexport const i18n = mapMangledModuleLazy(['defaultLocale:\"en-US\"', /initialLocale:\\i/], {\n    t: m => m?.[Symbol.toStringTag] === \"IntlMessagesProxy\",\n    intl: m => m != null && Object.getPrototypeOf(m)?.withFormatters != null\n}, true);\n\nexport let SnowflakeUtils: t.SnowflakeUtils;\nwaitFor([\"fromTimestamp\", \"extractTimestamp\"], m => SnowflakeUtils = m);\n\nexport let Parser: t.Parser;\nwaitFor(\"parseTopic\", m => Parser = m);\nexport let Alerts: t.Alerts;\nwaitFor([\"show\", \"close\"], m => Alerts = m);\n\nconst ToastType = {\n    MESSAGE: \"message\",\n    SUCCESS: \"success\",\n    FAILURE: \"failure\",\n    CUSTOM: \"custom\",\n    CLIP: \"clip\",\n    LINK: \"link\",\n    FORWARD: \"forward\",\n    BOOKMARK: \"bookmark\",\n    CLOCK: \"clock\"\n};\nconst ToastPosition = {\n    TOP: 0,\n    BOTTOM: 1\n};\n\nexport interface ToastData {\n    message: string,\n    id: string,\n    /**\n     * Toasts.Type\n     */\n    type: string,\n    options?: ToastOptions;\n}\n\nexport interface ToastOptions {\n    /**\n     * Toasts.Position\n     */\n    position?: number;\n    component?: React.ReactNode,\n    duration?: number;\n}\n\nexport const Toasts = {\n    Type: ToastType,\n    Position: ToastPosition,\n    // what's less likely than getting 0 from Math.random()? Getting it twice in a row\n    genId: () => (Math.random() || Math.random()).toString(36).slice(2),\n\n    // hack to merge with the following interface, dunno if there's a better way\n    ...{} as {\n        show(data: ToastData): void;\n        pop(): void;\n        create(message: string, type: string, options?: ToastOptions): ToastData;\n    }\n};\n\n// This is the same module but this is easier\nwaitFor(\"showToast\", m => {\n    Toasts.show = m.showToast;\n    Toasts.pop = m.popToast;\n    Toasts.create = m.createToast;\n});\n\n\n/**\n * Show a simple toast. If you need more options, use Toasts.show manually\n */\nexport function showToast(message: string, type = ToastType.MESSAGE, options?: ToastOptions) {\n    Toasts.show(Toasts.create(message, type, options));\n}\n\nexport const UserUtils = {\n    getUser: findByCodeLazy(\".USER(\")\n};\n\nexport const UploadManager = findByPropsLazy(\"clearAll\", \"addFile\");\nexport const UploadHandler = {\n    promptToUpload: findByCodeLazy(\"Unexpected mismatch between files and file metadata\") as (files: File[], channel: t.Channel, draftType: Number) => void\n};\n\nexport const ApplicationAssetUtils = mapMangledModuleLazy(\"getAssetImage: size must === [\", {\n    fetchAssetIds: filters.byCode('.startsWith(\"http:\")', \".dispatch({\"),\n    getAssetFromImageURL: filters.byCode(\"].serialize(\", \":null\"),\n    getAssetImage: filters.byCode(\"getAssetImage: size must === [\"),\n    getAssets: filters.byCode(\".assets\")\n});\n\nexport const NavigationRouter: t.NavigationRouter = mapMangledModuleLazy(\"transitionTo - Transitioning to\", {\n    transitionTo: filters.byCode(\"transitionTo -\"),\n    transitionToGuild: filters.byCode(\"transitionToGuild -\"),\n    back: filters.byCode(\"goBack()\"),\n    forward: filters.byCode(\"goForward()\"),\n});\nexport const ChannelRouter: t.ChannelRouter = mapMangledModuleLazy('\"Thread must have a parent ID.\"', {\n    transitionToChannel: filters.byCode(\".preload\"),\n    transitionToThread: filters.byCode('\"Thread must have a parent ID.\"')\n});\n\nexport let SettingsRouter: any;\nwaitFor([\"openUserSettings\", \"USER_SETTINGS_MODAL_KEY\"], m => SettingsRouter = m);\n\nexport const PermissionsBits: t.PermissionsBits = findLazy(m => typeof m.ADMINISTRATOR === \"bigint\");\n\nexport const { zustandCreate } = mapMangledModuleLazy([\"useSyncExternalStoreWithSelector:\", \"Object.assign\"], {\n    zustandCreate: filters.byCode(/=>(\\i)\\?\\i\\(\\1/)\n});\n\nexport const { zustandPersist } = mapMangledModuleLazy(\".onRehydrateStorage)?\", {\n    zustandPersist: filters.byCode(/(\\(\\i,\\i\\))=>.+?\\i\\1/)\n});\n\nexport const MessageActions = findByPropsLazy(\"editMessage\", \"sendMessage\");\nexport const MessageCache = findByPropsLazy(\"clearCache\", \"_channelMessages\");\nexport const UserProfileActions = findByPropsLazy(\"openUserProfileModal\", \"closeUserProfileModal\");\nexport const InviteActions = findByPropsLazy(\"resolveInvite\");\nexport const ChannelActionCreators = findByPropsLazy(\"openPrivateChannel\");\n\nexport const IconUtils: t.IconUtils = findByPropsLazy(\"getGuildBannerURL\", \"getUserAvatarURL\");\n\nexport const ExpressionPickerStore: t.ExpressionPickerStore = mapMangledModuleLazy(\"expression-picker-last-active-view\", {\n    openExpressionPicker: filters.byCode(/setState\\({activeView:(?:(?!null)\\i),activeViewType:/),\n    closeExpressionPicker: filters.byCode(\"setState({activeView:null\"),\n    toggleMultiExpressionPicker: filters.byCode(\".EMOJI,\"),\n    toggleExpressionPicker: filters.byCode(/getState\\(\\)\\.activeView===\\i\\?\\i\\(\\):\\i\\(/),\n    setExpressionPickerView: filters.byCode(/setState\\({activeView:\\i,lastActiveView:/),\n    setSearchQuery: filters.byCode(\"searchQuery:\"),\n    useExpressionPickerStore: filters.byCode(/\\(\\i,\\i=\\i\\)=>/)\n});\n\nexport const PopoutActions: t.PopoutActions = mapMangledModuleLazy('type:\"POPOUT_WINDOW_OPEN\"', {\n    open: filters.byCode('type:\"POPOUT_WINDOW_OPEN\"'),\n    close: filters.byCode('type:\"POPOUT_WINDOW_CLOSE\"'),\n    setAlwaysOnTop: filters.byCode('type:\"POPOUT_WINDOW_SET_ALWAYS_ON_TOP\"'),\n});\n\nexport const UsernameUtils: t.UsernameUtils = findByPropsLazy(\"useName\", \"getGlobalName\");\nexport const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/=\\i\\.getUserProfile\\(\\i\\),\\i=\\i\\.getGuildMemberProfile\\(/, {\n    getDisplayProfile: filters.byCode(\".getGuildMemberProfile(\"),\n    useDisplayProfile: filters.byCode(/\\[\\i\\.\\i,\\i\\.\\i],\\(\\)=>/)\n});\n\nexport const DateUtils: t.DateUtils = mapMangledModuleLazy(\"millisecondsInUnit:\", {\n    calendarFormat: filters.byCode('<-1?\"sameElse\":'),\n    dateFormat: filters.byCode('<2?\"nextDay\":\"sameElse\";'),\n    isSameDay: filters.byCode(/Math\\.abs\\(\\i-\\i\\)/),\n    diffAsUnits: filters.byCode(\"days:0\", \"millisecondsInUnit\")\n});\n\nexport const MessageTypeSets: t.MessageTypeSets = findByPropsLazy(\"REPLYABLE\", \"FORWARDABLE\");\n"
  },
  {
    "path": "src/webpack/index.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nexport * as Common from \"./common\";\nexport * from \"./types\";\nexport * from \"./webpack\";\n"
  },
  {
    "path": "src/webpack/patchWebpack.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2024 Vendicated, Nuckyz, and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Settings } from \"@api/Settings\";\nimport { makeLazy } from \"@utils/lazy\";\nimport { Logger } from \"@utils/Logger\";\nimport { interpolateIfDefined } from \"@utils/misc\";\nimport { Patch, PatchReplacement } from \"@utils/types\";\nimport { WebpackRequire } from \"@vencord/discord-types/webpack\";\n\nimport { traceFunctionWithResults } from \"../debug/Tracer\";\nimport { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, PatchedModuleFactory } from \"./types\";\nimport { _blacklistBadModules, _initWebpack, factoryListeners, findModuleFactory, moduleListeners, waitForSubscriptions, wreq } from \"./webpack\";\n\nexport const patches = [] as Patch[];\n\nexport const SYM_IS_PROXIED_FACTORY = Symbol(\"WebpackPatcher.isProxiedFactory\");\nexport const SYM_ORIGINAL_FACTORY = Symbol(\"WebpackPatcher.originalFactory\");\nexport const SYM_PATCHED_SOURCE = Symbol(\"WebpackPatcher.patchedSource\");\nexport const SYM_PATCHED_BY = Symbol(\"WebpackPatcher.patchedBy\");\nexport const allWebpackInstances = new Set<AnyWebpackRequire>();\n\nexport const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: PatchReplacement[\"match\"], totalTime: number]>;\n\nexport const getBuildNumber = makeLazy(() => {\n    try {\n        function matchBuildNumber(factoryStr: string) {\n            const buildNumberMatch = factoryStr.match(/\"Trying to open a changelog for an invalid build number (\\d+?)\"\\)/);\n            if (buildNumberMatch == null) {\n                return -1;\n            }\n\n            return Number(buildNumberMatch[1]);\n        }\n\n        const hardcodedFactoryStr = String(wreq.m[446023]);\n        if (hardcodedFactoryStr.includes(\"Trying to open a changelog for an invalid build number\")) {\n            const hardcodedBuildNumber = matchBuildNumber(hardcodedFactoryStr);\n\n            if (hardcodedBuildNumber !== -1) {\n                return hardcodedBuildNumber;\n            }\n        } else if (IS_DEV || IS_REPORTER) {\n            logger.error(\"Hardcoded build number module id is invalid\");\n        }\n\n        const moduleFactory = findModuleFactory(\"Trying to open a changelog for an invalid build number\");\n        return matchBuildNumber(String(moduleFactory));\n    } catch {\n        return -1;\n    }\n});\n\nexport function getFactoryPatchedSource(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {\n    return webpackRequire.m[moduleId]?.[SYM_PATCHED_SOURCE];\n}\n\nexport function getFactoryPatchedBy(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {\n    return webpackRequire.m[moduleId]?.[SYM_PATCHED_BY];\n}\n\nconst logger = new Logger(\"WebpackPatcher\", \"#8caaee\");\n\n/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */\nlet wreqFallbackApplied = false;\n\nconst define: typeof Reflect.defineProperty = (target, p, attributes) => {\n    if (Object.hasOwn(attributes, \"value\")) {\n        attributes.writable = true;\n    }\n\n    return Reflect.defineProperty(target, p, {\n        configurable: true,\n        enumerable: true,\n        ...attributes\n    });\n};\n\n// wreq.m is the Webpack object containing module factories. It is pre-populated with factories, and is also populated via webpackGlobal.push\n// We use this setter to intercept when wreq.m is defined and setup our setters which decide whether we should patch these module factories\n// and the Webpack instance where they are being defined.\n\n// Factories can be patched in two ways. Eagerly or lazily.\n// If we are patching eagerly, pre-populated factories are patched immediately and new factories are patched when set.\n// Else, we only patch them when called.\n\n// Factories are always wrapped in a proxy, which allows us to intercept the call to them, patch if they werent eagerly patched,\n// and call them with our wrapper which notifies our listeners.\n\n// wreq.m is also wrapped in a proxy to intercept when new factories are set, patch them eargely, if enabled, and wrap them in the factory proxy.\n\n// If this is the main Webpack, we also set up the internal references to WebpackRequire.\ndefine(Function.prototype, \"m\", {\n    enumerable: false,\n\n    set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire[\"m\"]) {\n        define(this, \"m\", { value: originalModules });\n\n        // Ensure this is likely one of Discord main Webpack instances.\n        // We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.\n        const { stack } = new Error();\n        if (!stack?.includes(\"http\") || stack.match(/at \\d+? \\(/) || !String(this).includes(\"exports:{}\")) {\n            return;\n        }\n\n        const fileName = stack.match(/\\/assets\\/(.+?\\.js)/)?.[1];\n\n        // Currently, sentry and libdiscore Webpack instances are not meant to be patched.\n        // As an extra measure, take advatange of the fact their files include the names and return early if it's one of them.\n        // Later down we also include other measures to avoid patching them.\n        if ([\"sentry\", \"libdiscore\"].some(name => fileName?.toLowerCase()?.includes(name))) {\n            return;\n        }\n\n        // Define a setter for the bundlePath property of WebpackRequire. Only Webpack instances which include chunk loading functionality,\n        // like the main Discord Webpack, have this property.\n        // So if the setter is called with the Discord bundlePath, this means we should patch this instance and initialize the internal references to WebpackRequire.\n        define(this, \"p\", {\n            enumerable: false,\n\n            set(this: AnyWebpackRequire, bundlePath: NonNullable<AnyWebpackRequire[\"p\"]>) {\n                define(this, \"p\", { value: bundlePath });\n                clearTimeout(bundlePathTimeout);\n\n                // libdiscore init Webpack instance always returns a constant string for the js filename of a chunk.\n                // In that case, avoid patching this instance,\n                // as it runs before the main Webpack instance and will make the WebpackRequire fallback not work properly, or init an wrongful main WebpackRequire.\n                if (bundlePath !== \"/assets/\" || /(?:=>|{return)\"[^\"]/.exec(String(this.u))) {\n                    return;\n                }\n\n                if (wreq == null && this.c != null) {\n                    logger.info(\"Main WebpackInstance found\" + interpolateIfDefined` in ${fileName}` + \", initializing internal references to WebpackRequire\");\n                    _initWebpack(this as WebpackRequire);\n                }\n\n                patchThisInstance();\n            }\n        });\n\n        // In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initing sentry.\n        // This Webpack instance did not include actual chunk loading, and only awaited for them to be loaded, which means it did not include the bundlePath property.\n        // To keep backwards compability, if this is ever the case again, and keep patching this type of instance, we explicity patch instances which include wreq.O and not wreq.p.\n        // Since we cannot check what is the bundlePath of the instance to filter for the Discord bundlePath, we only patch it if wreq.p is not included,\n        // which means the instance relies on another instance which does chunk loading, and that makes it very likely to only target Discord Webpack instances like the old sentry.\n\n        // Instead of patching when wreq.O is defined, wait for when wreq.O.j is defined, since that will be one of the last things to happen,\n        // which can assure wreq.p could have already been defined before.\n        define(this, \"O\", {\n            enumerable: false,\n\n            set(this: AnyWebpackRequire, onChunksLoaded: NonNullable<AnyWebpackRequire[\"O\"]>) {\n                define(this, \"O\", { value: onChunksLoaded });\n                clearTimeout(onChunksLoadedTimeout);\n\n                const wreq = this;\n                define(onChunksLoaded, \"j\", {\n                    enumerable: false,\n\n                    set(this: NonNullable<AnyWebpackRequire[\"O\"]>, j: NonNullable<AnyWebpackRequire[\"O\"]>[\"j\"]) {\n                        define(this, \"j\", { value: j });\n\n                        if (wreq.p == null) {\n                            patchThisInstance();\n                        }\n                    }\n                });\n            }\n        });\n\n        // If neither of these properties setters were triggered, delete them as they are not needed anymore.\n        const bundlePathTimeout = setTimeout(() => Reflect.deleteProperty(this, \"p\"), 0);\n        const onChunksLoadedTimeout = setTimeout(() => Reflect.deleteProperty(this, \"O\"), 0);\n\n        /**\n         * Patch the current Webpack instance assigned to `this` context.\n         * This should only be called if this instance was later found to be one we need to patch.\n         */\n        const patchThisInstance = () => {\n            logger.info(\"Found Webpack module factories\" + interpolateIfDefined` in ${fileName}`);\n            allWebpackInstances.add(this);\n\n            // Proxy (and maybe patch) pre-populated factories\n            for (const moduleId in originalModules) {\n                updateExistingOrProxyFactory(originalModules, moduleId, originalModules[moduleId], originalModules, true);\n            }\n\n            define(originalModules, Symbol.toStringTag, {\n                value: \"ModuleFactories\",\n                enumerable: false\n            });\n\n            const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);\n            /*\n            If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype\n            Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));\n            */\n\n            define(this, \"m\", { value: proxiedModuleFactories });\n\n            // Overwrite Webpack's defineExports function to define the export descriptors configurable.\n            // This is needed so we can later blacklist specific exports from Webpack search by making them non-enumerable\n            this.d = function (exports, definition) {\n                for (const key in definition) {\n                    if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {\n                        Object.defineProperty(exports, key, {\n                            enumerable: true,\n                            configurable: true,\n                            get: definition[key],\n                        });\n                    }\n                }\n            };\n        };\n    }\n});\n\n// The proxy for patching eagerly and/or wrapping factories in their proxy.\nconst moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire[\"m\"]> = {\n    /*\n    If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype\n    and that requires defining additional traps for keeping the object working\n\n    // Proxies on the prototype don't intercept \"get\" when the property is in the object itself. But in case it isn't we need to return undefined,\n    // to avoid Reflect.get having no effect and causing a stack overflow\n    get(target, p, receiver) {\n        return undefined;\n    },\n    // Same thing as get\n    has(target, p) {\n        return false;\n    },\n    */\n\n    set: updateExistingOrProxyFactory\n};\n\n// The proxy for patching lazily and/or running factories with our wrapper.\nconst moduleFactoryHandler: ProxyHandler<MaybePatchedModuleFactory> = {\n    apply(target, thisArg: unknown, argArray: Parameters<AnyModuleFactory>) {\n        // SYM_ORIGINAL_FACTORY means the factory has already been patched\n        if (target[SYM_ORIGINAL_FACTORY] != null) {\n            return runFactoryWithWrap(target as PatchedModuleFactory, thisArg, argArray);\n        }\n\n        // SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id\n        const moduleId: string = target.name;\n\n        const patchedFactory = patchFactory(moduleId, target);\n        return runFactoryWithWrap(patchedFactory, thisArg, argArray);\n    },\n\n    get(target, p, receiver) {\n        if (p === SYM_IS_PROXIED_FACTORY) {\n            return true;\n        }\n\n        const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target;\n\n        // Redirect these properties to the original factory, including making `toString` return the original factory `toString`\n        if (p === \"toString\" || p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY) {\n            const v = Reflect.get(originalFactory, p, originalFactory);\n            return p === \"toString\" ? v.bind(originalFactory) : v;\n        }\n\n        return Reflect.get(target, p, receiver);\n    }\n};\n\nfunction updateExistingOrProxyFactory(moduleFactories: AnyWebpackRequire[\"m\"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) {\n    if (updateExistingFactory(moduleFactories, moduleId, newFactory, receiver, ignoreExistingInTarget)) {\n        return true;\n    }\n\n    notifyFactoryListeners(moduleId, newFactory);\n\n    const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler);\n    return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver);\n}\n\n/**\n * Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory.\n *\n * @param moduleFactories The module factories where this new original factory is being set\n * @param moduleId The id of the module\n * @param newFactory The new original factory\n * @param receiver The receiver of the factory\n * @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactories where it is being set\n * @returns Whether the original factory was updated, or false if it doesn't exist in any of the tracked Webpack instances\n */\nfunction updateExistingFactory(moduleFactories: AnyWebpackRequire[\"m\"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget) {\n    let existingFactory: AnyModuleFactory | undefined;\n    let moduleFactoriesWithFactory: AnyWebpackRequire[\"m\"] | undefined;\n    for (const wreq of allWebpackInstances) {\n        if (ignoreExistingInTarget && wreq.m === moduleFactories) {\n            continue;\n        }\n\n        if (Object.hasOwn(wreq.m, moduleId)) {\n            existingFactory = wreq.m[moduleId];\n            moduleFactoriesWithFactory = wreq.m;\n            break;\n        }\n    }\n\n    if (existingFactory != null) {\n        // If existingFactory exists in any of the Webpack instances we track, it's either wrapped in our proxy, or it has already been required.\n        // In the case it is wrapped in our proxy, and the instance we are setting does not already have it, we need to make sure the instance contains our proxy too.\n        if (moduleFactoriesWithFactory !== moduleFactories && existingFactory[SYM_IS_PROXIED_FACTORY]) {\n            Reflect.set(moduleFactories, moduleId, existingFactory, receiver);\n        }\n        // Else, if it is not wrapped in our proxy, set this new original factory in all the instances\n        else {\n            defineInWebpackInstances(moduleId, newFactory);\n        }\n\n        // Update existingFactory with the new original, if it does have a current original factory\n        if (existingFactory[SYM_ORIGINAL_FACTORY] != null) {\n            existingFactory[SYM_ORIGINAL_FACTORY] = newFactory;\n        }\n\n        // Persist patched source and patched by in the new original factory\n        if (IS_DEV) {\n            newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE];\n            newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY];\n        }\n\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * Define a module factory in all the Webpack instances we track.\n *\n * @param moduleId The id of the module\n * @param factory The factory\n */\nfunction defineInWebpackInstances(moduleId: PropertyKey, factory: AnyModuleFactory) {\n    for (const wreq of allWebpackInstances) {\n        define(wreq.m, moduleId, { value: factory });\n    }\n}\n\n/**\n * Notify all factory listeners.\n *\n * @param moduleId The id of the module\n * @param factory The original factory to notify for\n */\nfunction notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory) {\n    for (const factoryListener of factoryListeners) {\n        try {\n            factoryListener(factory, moduleId);\n        } catch (err) {\n            logger.error(\"Error in Webpack factory listener:\\n\", err, factoryListener);\n        }\n    }\n}\n\n/**\n * Run a (possibly) patched module factory with a wrapper which notifies our listeners.\n *\n * @param patchedFactory The (possibly) patched module factory\n * @param thisArg The `value` of the call to the factory\n * @param argArray The arguments of the call to the factory\n */\nfunction runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters<MaybePatchedModuleFactory>) {\n    const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY];\n\n    if (patchedFactory === originalFactory) {\n        // @ts-expect-error Clear up ORIGINAL_FACTORY if the factory did not have any patch applied\n        delete patchedFactory[SYM_ORIGINAL_FACTORY];\n    }\n\n    let [module, exports, require] = argArray;\n\n    // Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected\n    defineInWebpackInstances(module.id, originalFactory);\n\n    if (wreq == null) {\n        if (!wreqFallbackApplied) {\n            wreqFallbackApplied = true;\n\n            // Make sure the require argument is actually the WebpackRequire function\n            if (typeof require === \"function\" && require.m != null && require.c != null) {\n                const { stack } = new Error();\n                const webpackInstanceFileName = stack?.match(/\\/assets\\/(.+?\\.js)/)?.[1];\n\n                logger.warn(\n                    \"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called wrapped module factory (\" +\n                    `id: ${String(module.id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +\n                    \")\"\n                );\n\n                // Could technically be wrong, but it's better than nothing\n                _initWebpack(require as WebpackRequire);\n            } else if (IS_DEV) {\n                logger.error(\"WebpackRequire was not initialized, running modules without patches instead.\");\n                return originalFactory.apply(thisArg, argArray);\n            }\n        } else if (IS_DEV) {\n            return originalFactory.apply(thisArg, argArray);\n        }\n    }\n\n    let factoryReturn: unknown;\n    try {\n        factoryReturn = patchedFactory.apply(thisArg, argArray);\n    } catch (err) {\n        // Just re-throw Discord errors\n        if (patchedFactory === originalFactory) {\n            throw err;\n        }\n\n        logger.error(\"Error in patched module factory:\\n\", err);\n        return originalFactory.apply(thisArg, argArray);\n    }\n\n    exports = module.exports;\n\n    if (typeof require === \"function\" && require.c) {\n        if (_blacklistBadModules(require.c, exports, module.id)) {\n            return factoryReturn;\n        }\n    }\n\n    if (exports == null) {\n        return factoryReturn;\n    }\n\n    for (const callback of moduleListeners) {\n        try {\n            callback(exports, module.id);\n        } catch (err) {\n            logger.error(\"Error in Webpack module listener:\\n\", err, callback);\n        }\n    }\n\n    for (const [filter, callback] of waitForSubscriptions) {\n        try {\n            if (filter(exports)) {\n                waitForSubscriptions.delete(filter);\n                callback(exports, module.id);\n                continue;\n            }\n        } catch (err) {\n            logger.error(\n                \"Error while filtering or firing callback for Webpack waitFor subscription:\\n\", err,\n                \"\\n\\nModule exports:\", exports,\n                \"\\n\\nFilter:\", filter,\n                \"\\n\\nCallback:\", callback\n            );\n        }\n\n        if (typeof exports !== \"object\") {\n            continue;\n        }\n\n        for (const exportKey in exports) {\n            try {\n                // Some exports might have not been initialized yet due to circular imports, so try catch it.\n                try {\n                    var exportValue = exports[exportKey];\n                } catch {\n                    continue;\n                }\n\n                if (exportValue != null && filter(exportValue)) {\n                    waitForSubscriptions.delete(filter);\n                    callback(exportValue, module.id);\n                    break;\n                }\n            } catch (err) {\n                logger.error(\n                    \"Error while filtering or firing callback for Webpack waitFor subscription:\\n\", err,\n                    \"\\n\\nExport value:\", exports,\n                    \"\\n\\nFilter:\", filter,\n                    \"\\n\\nCallback:\", callback\n                );\n            }\n        }\n    }\n\n    return factoryReturn;\n}\n\n/**\n * Patches a module factory.\n *\n * @param moduleId The id of the module\n * @param originalFactory The original module factory\n * @returns The patched module factory\n */\nfunction patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory): PatchedModuleFactory {\n    const originalFactoryCode = String(originalFactory);\n    const isArrowFunction = originalFactoryCode.startsWith(\"(\");\n\n    // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,\n    let code = \"0,\" + (!isArrowFunction ? \"function\" : \"\") + originalFactoryCode.slice(originalFactoryCode.indexOf(\"(\"));\n    let patchedSource = code;\n    let patchedFactory = originalFactory;\n\n    const patchedBy = new Set<string>();\n\n    for (let i = 0; i < patches.length; i++) {\n        const patch = patches[i];\n\n        const buildNumber = getBuildNumber();\n        const shouldCheckBuildNumber = buildNumber !== -1;\n\n        if (\n            shouldCheckBuildNumber &&\n            (patch.fromBuild != null && buildNumber < patch.fromBuild) ||\n            (patch.toBuild != null && buildNumber > patch.toBuild)\n        ) {\n            patches.splice(i--, 1);\n            continue;\n        }\n\n        const moduleMatches = typeof patch.find === \"string\"\n            ? code.includes(patch.find)\n            : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));\n\n        if (!moduleMatches) {\n            continue;\n        }\n\n        const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {\n            if (typeof match !== \"string\" && match.global) {\n                match.lastIndex = 0;\n            }\n\n            return code.replace(match, replace);\n        });\n\n        const previousCode = code;\n        const previousFactory = originalFactory;\n        let markedAsPatched = false;\n\n        // We change all patch.replacement to array in PluginManager\n        for (const replacement of patch.replacement as PatchReplacement[]) {\n            if (\n                shouldCheckBuildNumber &&\n                (replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||\n                (replacement.toBuild != null && buildNumber > replacement.toBuild)\n            ) {\n                continue;\n            }\n\n            const lastCode = code;\n            const lastFactory = originalFactory;\n\n            try {\n                const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);\n\n                if (IS_REPORTER) {\n                    patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);\n                }\n\n                if (newCode === code) {\n                    if (!(patch.noWarn || replacement.noWarn)) {\n                        logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);\n                        if (IS_DEV) {\n                            logger.debug(\"Function Source:\\n\", code);\n                        }\n                    }\n\n                    if (patch.group) {\n                        logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);\n                        code = previousCode;\n                        patchedFactory = previousFactory;\n\n                        if (markedAsPatched) {\n                            patchedBy.delete(patch.plugin);\n                        }\n\n                        break;\n                    }\n\n                    continue;\n                }\n\n                const pluginsList = [...patchedBy];\n                if (!patchedBy.has(patch.plugin)) {\n                    pluginsList.push(patch.plugin);\n                }\n\n                code = newCode;\n                patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(\", \")}\\n${code}\\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;\n                patchedFactory = (0, eval)(patchedSource);\n\n                if (!patchedBy.has(patch.plugin)) {\n                    patchedBy.add(patch.plugin);\n                    markedAsPatched = true;\n                }\n            } catch (err) {\n                // FIXME: Maybe fix this properly\n                const shouldSuppressError = patch.plugin === \"ContextMenuAPI\" && err instanceof SyntaxError && err.message.includes(\"arguments\");\n                if (!shouldSuppressError) {\n                    logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\\n`, err);\n\n                    if (IS_DEV) {\n                        diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);\n                    }\n                }\n\n                if (markedAsPatched) {\n                    patchedBy.delete(patch.plugin);\n                }\n\n                if (patch.group) {\n                    logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);\n                    code = previousCode;\n                    patchedFactory = previousFactory;\n                    break;\n                }\n\n                code = lastCode;\n                patchedFactory = lastFactory;\n            }\n        }\n\n        if (!patch.all) {\n            patches.splice(i--, 1);\n        }\n    }\n\n    patchedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;\n\n    if (IS_DEV && patchedFactory !== originalFactory) {\n        originalFactory[SYM_PATCHED_SOURCE] = patchedSource;\n        originalFactory[SYM_PATCHED_BY] = patchedBy;\n    }\n\n    return patchedFactory as PatchedModuleFactory;\n}\n\nfunction diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {\n    const changeSize = code.length - lastCode.length;\n\n    // Use 200 surrounding characters of context\n    const start = Math.max(0, match.index! - 200);\n    const end = Math.min(lastCode.length, match.index! + match[0].length + 200);\n    // (changeSize may be negative)\n    const endPatched = end + changeSize;\n\n    const context = lastCode.slice(start, end);\n    const patchedContext = code.slice(start, endPatched);\n\n    // Inline require to avoid including it in !IS_DEV builds\n    const diff = (require(\"diff\") as typeof import(\"diff\")).diffWordsWithSpace(context, patchedContext);\n    let fmt = \"%c %s \";\n    const elements: string[] = [];\n    for (const d of diff) {\n        const color = d.removed\n            ? \"red\"\n            : d.added\n                ? \"lime\"\n                : \"grey\";\n        fmt += \"%c%s\";\n        elements.push(\"color:\" + color, d.value);\n    }\n\n    logger.errorCustomFmt(...Logger.makeTitle(\"white\", \"Before\"), context);\n    logger.errorCustomFmt(...Logger.makeTitle(\"white\", \"After\"), patchedContext);\n    const [titleFmt, ...titleElements] = Logger.makeTitle(\"white\", \"Diff\");\n    logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);\n}\n"
  },
  {
    "path": "src/webpack/types.ts",
    "content": "/*\n * Vencord, a Discord client mod\n * Copyright (c) 2025 Vendicated, Nuckyz and contributors\n * SPDX-License-Identifier: GPL-3.0-or-later\n */\n\nimport { Module, ModuleExports, WebpackRequire } from \"@vencord/discord-types/webpack\";\n\nimport { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from \"./patchWebpack\";\n\nexport type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial<Omit<WebpackRequire, \"m\">> & {\n    /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */\n    m: Record<PropertyKey, AnyModuleFactory>;\n};\n\n/** exports can be anything, however initially it is always an empty object */\nexport type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void) & {\n    [SYM_PATCHED_SOURCE]?: string;\n    [SYM_PATCHED_BY]?: Set<string>;\n};\n\nexport type PatchedModuleFactory = AnyModuleFactory & {\n    [SYM_ORIGINAL_FACTORY]: AnyModuleFactory;\n    [SYM_PATCHED_SOURCE]?: string;\n    [SYM_PATCHED_BY]?: Set<string>;\n};\n\nexport type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory;\n"
  },
  {
    "path": "src/webpack/webpack.ts",
    "content": "/*\n * Vencord, a modification for Discord's desktop app\n * Copyright (c) 2022 Vendicated and contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport { makeLazy, proxyLazy } from \"@utils/lazy\";\nimport { LazyComponent } from \"@utils/lazyReact\";\nimport { Logger } from \"@utils/Logger\";\nimport { canonicalizeMatch } from \"@utils/patches\";\nimport { escapeRegExp } from \"@utils/text\";\nimport type { FluxStore } from \"@vencord/discord-types\";\nimport type { ModuleExports, ModuleFactory, WebpackRequire } from \"@vencord/discord-types/webpack\";\n\nimport { traceFunction } from \"../debug/Tracer\";\nimport type { AnyModuleFactory, AnyWebpackRequire } from \"./types\";\n\nconst logger = new Logger(\"Webpack\");\n\nexport let _resolveReady: () => void;\n/**\n * Fired once a gateway connection to Discord has been established.\n * This indicates that the core webpack modules have been initialised\n */\nexport const onceReady = new Promise<void>(r => _resolveReady = r);\n\nexport let wreq: WebpackRequire;\nexport let cache: WebpackRequire[\"c\"];\n\nexport const fluxStores = new Map<string, FluxStore>();\n\nexport type FilterFn = (mod: any) => boolean;\n\nexport type PropsFilter = Array<string>;\nexport type CodeFilter = Array<string | RegExp>;\nexport type StoreNameFilter = string;\n\nexport const stringMatches = (s: string, filter: CodeFilter) =>\n    filter.every(f =>\n        typeof f === \"string\"\n            ? s.includes(f)\n            : (f.global && (f.lastIndex = 0), f.test(s))\n    );\n\nexport function makeClassNameRegex(className: string) {\n    return new RegExp(`(?:\\\\b|_)${escapeRegExp(className)}(?:\\\\b|_)`);\n}\n\nexport const filters = {\n    byProps: (...props: PropsFilter): FilterFn =>\n        props.length === 1\n            ? m => m[props[0]] !== void 0\n            : m => props.every(p => m[p] !== void 0),\n\n    byCode: (...code: CodeFilter): FilterFn => {\n        const parsedCode = code.map(canonicalizeMatch);\n        const filter = m => {\n            if (typeof m !== \"function\") return false;\n            return stringMatches(Function.prototype.toString.call(m), parsedCode);\n        };\n\n        filter.$$vencordProps = [...code];\n        return filter;\n    },\n    byStoreName: (name: StoreNameFilter): FilterFn => m =>\n        m.constructor?.displayName === name,\n\n    componentByCode: (...code: CodeFilter): FilterFn => {\n        const byCodeFilter = filters.byCode(...code);\n        const filter = (m: any) => {\n            let inner = m;\n\n            while (inner != null) {\n                if (byCodeFilter(inner)) return true;\n                else if (!inner.$$typeof) return false;\n                else if (inner.type) inner = inner.type; // memos\n                else if (inner.render) inner = inner.render; // forwardRefs\n                else return false;\n            }\n\n            return false;\n        };\n\n        filter.$$vencordProps = [...code];\n        return filter;\n    },\n\n    byClassNames: (...classes: string[]): FilterFn => {\n        const regexes = classes.map(makeClassNameRegex);\n\n        return (m: any) => {\n            if (typeof m !== \"object\") return false;\n\n            const values = Object.values(m);\n            return regexes.every(cls => values.some(v => typeof v === \"string\" && cls.test(v)));\n        };\n    }\n};\n\nexport type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;\nexport type FactoryListernFn = (factory: AnyModuleFactory, moduleId: PropertyKey) => void;\n\nexport const waitForSubscriptions = new Map<FilterFn, CallbackFn>();\nexport const moduleListeners = new Set<CallbackFn>();\nexport const factoryListeners = new Set<FactoryListernFn>();\n\nexport function _initWebpack(webpackRequire: WebpackRequire) {\n    wreq = webpackRequire;\n    cache = webpackRequire.c;\n\n    Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, {\n        value: \"ModuleCache\",\n        configurable: true,\n        writable: true,\n        enumerable: false\n    });\n}\n\n// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too\nconst TypedArray = Object.getPrototypeOf(Int8Array);\n\nconst PROXY_CHECK = \"is this a proxy that returns values for any key?\";\nfunction shouldIgnoreValue(value: any) {\n    if (value == null) return true;\n    if (value === window) return true;\n    if (value === document || value === document.documentElement) return true;\n    if (value[Symbol.toStringTag] === \"DOMTokenList\" || value[Symbol.toStringTag] === \"IntlMessagesProxy\") return true;\n    // Discord might export a Proxy that returns non-null values for any property key which would pass all findByProps filters.\n    // One example of this is their i18n Proxy. However, that is already covered by the IntlMessagesProxy check above.\n    // As a fallback if they ever change the name or add a new Proxy, use a unique string to detect such proxies and ignore them\n    if (value[PROXY_CHECK] !== void 0) {\n        // their i18n Proxy \"caches\" by setting each accessed property to the return, so try to delete\n        Reflect.deleteProperty(value, PROXY_CHECK);\n        return true;\n    }\n    if (value instanceof TypedArray) return true;\n\n    return false;\n}\n\nfunction makePropertyNonEnumerable(target: Record<PropertyKey, any>, key: PropertyKey) {\n    const descriptor = Object.getOwnPropertyDescriptor(target, key);\n    if (descriptor == null) return;\n\n    Reflect.defineProperty(target, key, {\n        ...descriptor,\n        enumerable: false\n    });\n}\n\nexport function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire[\"c\"]>, exports: ModuleExports, moduleId: PropertyKey) {\n    try {\n        if (shouldIgnoreValue(exports)) {\n            makePropertyNonEnumerable(requireCache, moduleId);\n            return true;\n        }\n    } catch (err) {\n        logger.error(\n            \"Error while blacklisting module:\\n\", err,\n            \"\\n\\nModule id:\", moduleId,\n            \"\\n\\nModule exports:\", exports,\n        );\n    }\n\n    if (typeof exports !== \"object\") {\n        return false;\n    }\n\n    let hasOnlyBadProperties = true;\n    for (const exportKey in exports) {\n        try {\n            // Some exports might have not been initialized yet due to circular imports, so try catch it.\n            try {\n                var exportValue = exports[exportKey];\n            } catch {\n                continue;\n            }\n\n            if (shouldIgnoreValue(exportValue)) {\n                makePropertyNonEnumerable(exports, exportKey);\n            } else {\n                hasOnlyBadProperties = false;\n            }\n        } catch (err) {\n            logger.error(\n                \"Error while blacklistng module:\\n\", err,\n                \"\\n\\nModule id:\", moduleId,\n                \"\\n\\nExport value:\", exportValue,\n            );\n        }\n    }\n\n    return hasOnlyBadProperties;\n}\n\nlet devToolsOpen = false;\nif (IS_DEV && IS_DISCORD_DESKTOP) {\n    // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed\n    setTimeout(() => {\n        DiscordNative/* just to make sure */?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false);\n    }, 0);\n}\n\nexport function handleModuleNotFound(method: string, ...filter: unknown[]) {\n    const err = new Error(`webpack.${method} found no module`);\n    logger.error(err, \"Filter:\", filter);\n\n    // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n    if (IS_DEV && !devToolsOpen)\n        throw err;\n}\n\n/**\n * Find the first module that matches the filter\n */\nexport const find = traceFunction(\"find\", function find(filter: FilterFn, { isIndirect = false, isWaitFor = false, topLevelOnly = false }: { isIndirect?: boolean; isWaitFor?: boolean; topLevelOnly?: boolean; } = {}) {\n    if (IS_ANTI_CRASH_TEST) return null;\n\n    if (typeof filter !== \"function\")\n        throw new Error(\"Invalid filter. Expected a function got \" + typeof filter);\n\n    for (const key in cache) {\n        const mod = cache[key];\n        if (!mod?.loaded || mod.exports == null) continue;\n\n        if (filter(mod.exports)) {\n            return isWaitFor ? [mod.exports, key] : mod.exports;\n        }\n\n        if (typeof mod.exports !== \"object\" || topLevelOnly) continue;\n\n        for (const nestedMod in mod.exports) {\n            const nested = mod.exports[nestedMod];\n            if (nested && filter(nested)) {\n                return isWaitFor ? [nested, key] : nested;\n            }\n        }\n    }\n\n    if (!isIndirect) {\n        handleModuleNotFound(\"find\", filter);\n    }\n\n    return isWaitFor ? [null, null] : null;\n});\n\nexport function findAll(filter: FilterFn, { topLevelOnly = false }: { topLevelOnly?: boolean; } = {}) {\n    if (typeof filter !== \"function\")\n        throw new Error(\"Invalid filter. Expected a function got \" + typeof filter);\n\n    const ret = [] as any[];\n    for (const key in cache) {\n        const mod = cache[key];\n        if (!mod?.loaded || mod.exports == null) continue;\n\n        if (filter(mod.exports))\n            ret.push(mod.exports);\n\n        if (typeof mod.exports !== \"object\" || topLevelOnly)\n            continue;\n\n        for (const nestedMod in mod.exports) {\n            const nested = mod.exports[nestedMod];\n            if (nested && filter(nested)) ret.push(nested);\n        }\n    }\n\n    return ret;\n}\n\n/**\n * Same as {@link find} but in bulk\n * @param filterFns Array of filters. Please note that this array will be modified in place, so if you still\n *                need it afterwards, pass a copy.\n * @returns Array of results in the same order as the passed filters\n */\nexport const findBulk = traceFunction(\"findBulk\", function findBulk(...filterFns: FilterFn[]) {\n    if (IS_ANTI_CRASH_TEST) return [];\n\n    if (!Array.isArray(filterFns))\n        throw new Error(\"Invalid filters. Expected function[] got \" + typeof filterFns);\n\n    const { length } = filterFns;\n\n    if (length === 0)\n        throw new Error(\"Expected at least two filters.\");\n\n    if (length === 1) {\n        if (IS_DEV) {\n            throw new Error(\"bulk called with only one filter. Use find\");\n        }\n        return find(filterFns[0]);\n    }\n\n    const filters = filterFns as Array<FilterFn | undefined>;\n\n    let found = 0;\n    const results = Array(length);\n\n    outer:\n    for (const key in cache) {\n        const mod = cache[key];\n        if (!mod?.loaded || mod.exports == null) continue;\n\n        for (let j = 0; j < length; j++) {\n            const filter = filters[j];\n            // Already done\n            if (filter === undefined) continue;\n\n            if (filter(mod.exports)) {\n                results[j] = mod.exports;\n                filters[j] = undefined;\n                if (++found === length) break outer;\n                break;\n            }\n\n            if (typeof mod.exports !== \"object\")\n                continue;\n\n            for (const nestedMod in mod.exports) {\n                const nested = mod.exports[nestedMod];\n                if (nested && filter(nested)) {\n                    results[j] = nested;\n                    filters[j] = undefined;\n                    if (++found === length) break outer;\n                    continue outer;\n                }\n            }\n        }\n    }\n\n    if (found !== length) {\n        const err = new Error(`Got ${length} filters, but only found ${found} modules!`);\n        if (IS_DEV) {\n            if (!devToolsOpen)\n                // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n                throw err;\n        } else {\n            logger.warn(err);\n        }\n    }\n\n    return results;\n});\n\n/**\n * Find the id of the first module factory that includes all the given code\n * @returns string or null\n */\nexport const findModuleId = traceFunction(\"findModuleId\", function findModuleId(...code: CodeFilter) {\n    code = code.map(canonicalizeMatch);\n\n    for (const id in wreq.m) {\n        if (stringMatches(wreq.m[id].toString(), code)) return id;\n    }\n\n    const err = new Error(\"Didn't find module with code(s):\\n\" + code.join(\"\\n\"));\n    if (IS_DEV) {\n        if (!devToolsOpen)\n            // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n            throw err;\n    } else {\n        logger.warn(err);\n    }\n\n    return null;\n});\n\n/**\n * Find the first module factory that includes all the given code\n * @returns The module factory or null\n */\nexport function findModuleFactory(...code: CodeFilter) {\n    const id = findModuleId(...code);\n    if (!id) return null;\n\n    return wreq.m[id];\n}\n\nexport const lazyWebpackSearchHistory = [] as Array<[\"find\" | \"findByProps\" | \"findByCode\" | \"findCssClasses\" | \"findStore\" | \"findComponent\" | \"findComponentByCode\" | \"findExportedComponent\" | \"waitFor\" | \"waitForComponent\" | \"waitForStore\" | \"proxyLazyWebpack\" | \"LazyComponentWebpack\" | \"extractAndLoadChunks\" | \"mapMangledModule\", any[]]>;\n\n/**\n * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.\n *\n * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy.\n * On first property access, the lazy is evaluated\n * @param factory lazy factory\n * @param attempts how many times to try to evaluate the lazy before giving up\n * @returns Proxy\n *\n * Note that the example below exists already as an api, see {@link findByPropsLazy}\n * @example const mod = proxyLazy(() => findByProps(\"blah\")); console.log(mod.blah);\n */\nexport function proxyLazyWebpack<T = any>(factory: () => T, attempts?: number) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"proxyLazyWebpack\", [factory]]);\n\n    return proxyLazy<T>(factory, attempts);\n}\n\n/**\n * This is just a wrapper around {@link LazyComponent} to make our reporter test for your webpack finds.\n *\n * A lazy component. The factory method is called on first render.\n * @param factory Function returning a Component\n * @param attempts How many times to try to get the component before giving up\n * @returns Result of factory function\n */\nexport function LazyComponentWebpack<T extends object = any>(factory: () => any, attempts?: number) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"LazyComponentWebpack\", [factory]]);\n\n    return LazyComponent<T>(factory, attempts);\n}\n\n/**\n * Find the first module that matches the filter, lazily\n */\nexport function findLazy(filter: FilterFn) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"find\", [filter]]);\n\n    return proxyLazy(() => find(filter));\n}\n\n/**\n * Find the first module that has the specified properties\n */\nexport function findByProps(...props: PropsFilter) {\n    const res = find(filters.byProps(...props), { isIndirect: true });\n    if (!res)\n        handleModuleNotFound(\"findByProps\", ...props);\n    return res;\n}\n\n/**\n * Find the first module that has the specified properties, lazily\n */\nexport function findByPropsLazy(...props: PropsFilter) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findByProps\", props]);\n\n    return proxyLazy(() => findByProps(...props));\n}\n\n/**\n * Find the first function that includes all the given code\n */\nexport function findByCode(...code: CodeFilter) {\n    const res = find(filters.byCode(...code), { isIndirect: true });\n    if (!res)\n        handleModuleNotFound(\"findByCode\", ...code);\n    return res;\n}\n\n/**\n * Find the first function that includes all the given code, lazily\n */\nexport function findByCodeLazy(...code: CodeFilter) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findByCode\", code]);\n\n    return proxyLazy(() => findByCode(...code));\n}\n\nfunction populateFluxStoreMap() {\n    const { Flux } = require(\"./common\") as typeof import(\"./common\");\n\n    Flux.Store.getAll?.().forEach(store =>\n        fluxStores.set(store.getName(), store)\n    );\n\n    try {\n        const getLibdiscore = findByCode(\"libdiscoreWasm is not initialized\");\n        const libdiscoreExports = getLibdiscore();\n\n        for (const libdiscoreExportName in libdiscoreExports) {\n            if (!libdiscoreExportName.endsWith(\"Store\")) {\n                continue;\n            }\n\n            const storeName = libdiscoreExportName;\n            const store = libdiscoreExports[storeName];\n\n            fluxStores.set(storeName, store);\n        }\n    } catch { }\n}\n\n/**\n * Find a store by its displayName\n */\nexport function findStore(name: StoreNameFilter) {\n    if (!fluxStores.has(name)) {\n        populateFluxStoreMap();\n    }\n\n    if (fluxStores.has(name)) {\n        return fluxStores.get(name);\n    }\n\n    const res = find(filters.byStoreName(name), { isIndirect: true });\n    if (res) {\n        fluxStores.set(name, res);\n        return res;\n    }\n\n    handleModuleNotFound(\"findStore\", name);\n    return null;\n}\n\n/**\n * Find a store by its displayName, lazily\n */\nexport function findStoreLazy(name: StoreNameFilter) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findStore\", [name]]);\n\n    return proxyLazy(() => findStore(name));\n}\n\n/**\n * Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs\n */\nexport function findComponentByCode(...code: CodeFilter) {\n    const res = find(filters.componentByCode(...code), { isIndirect: true });\n    if (!res)\n        handleModuleNotFound(\"findComponentByCode\", ...code);\n    return res;\n}\n\n/**\n * Finds the first component that matches the filter, lazily.\n */\nexport function findComponentLazy<T extends object = any>(filter: FilterFn) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findComponent\", [filter]]);\n\n\n    return LazyComponent<T>(() => {\n        const res = find(filter, { isIndirect: true });\n        if (!res)\n            handleModuleNotFound(\"findComponent\", filter);\n        return res;\n    });\n}\n\n/**\n * Finds the first component that includes all the given code, lazily\n */\nexport function findComponentByCodeLazy<T extends object = any>(...code: CodeFilter) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findComponentByCode\", code]);\n\n    return LazyComponent<T>(() => {\n        const res = find(filters.componentByCode(...code), { isIndirect: true });\n        if (!res)\n            handleModuleNotFound(\"findComponentByCode\", ...code);\n        return res;\n    });\n}\n\n/**\n * Finds the first component that is exported by the first prop name, lazily\n */\nexport function findExportedComponentLazy<T extends object = any>(...props: PropsFilter) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findExportedComponent\", props]);\n\n    return LazyComponent<T>(() => {\n        const res = find(filters.byProps(...props), { isIndirect: true });\n        if (!res)\n            handleModuleNotFound(\"findExportedComponent\", ...props);\n        return res[props[0]];\n    });\n}\n\nexport function mapMangledCssClasses<S extends string>(mappedModule: object, classes: S[] | ReadonlyArray<S>): Record<S, string> {\n    const values = Object.values(mappedModule);\n    const mapped = {} as Record<S, string>;\n\n    for (const cls of classes) {\n        const re = makeClassNameRegex(cls);\n        mapped[cls] = values.find(v => typeof v === \"string\" && re.test(v)) as string;\n\n        if (!mapped[cls]) // this should never happen unless this is used manually with invalid input\n            throw new Error(`mapMangledCssClasses: Invalid input. ${cls} not found in module`);\n    }\n\n    return mapped;\n}\n\nexport function findCssClasses<S extends string>(...classes: S[]): Record<S, string> {\n    const res = find(filters.byClassNames(...classes), { isIndirect: true, topLevelOnly: true });\n\n    if (!res) {\n        handleModuleNotFound(\"findCssClasses\", ...classes);\n\n        if (IS_REPORTER) return null as any;\n        return {} as Record<S, string>;\n    }\n\n    return mapMangledCssClasses(res, classes);\n}\n\nexport function findCssClassesLazy<S extends string>(...classes: S[]) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"findCssClasses\", classes]);\n\n    return proxyLazy(() => findCssClasses(...classes));\n}\n\nfunction getAllPropertyNames(object: Record<PropertyKey, any>, includeNonEnumerable: boolean) {\n    const names = new Set<PropertyKey>();\n\n    const getKeys = includeNonEnumerable ? Object.getOwnPropertyNames : Object.keys;\n    do {\n        getKeys(object).forEach(name => name !== \"__esModule\" && names.add(name));\n        object = Object.getPrototypeOf(object);\n    } while (object != null);\n\n    return names;\n}\n\n/**\n * Finds a mangled module by the provided code \"code\" (must be unique and can be anywhere in the module)\n * then maps it into an easily usable module via the specified mappers.\n *\n * @param code The code to look for\n * @param mappers Mappers to create the non mangled exports\n * @param includeBlacklistedExports Whether to include blacklisted exports in the search.\n *                                  These exports are dangerous. Accessing properties on them may throw errors\n *                                  or always return values (so a byProps filter will always return true)\n * @returns Unmangled exports as specified in mappers\n *\n * @example mapMangledModule(\"headerIdIsManaged:\", {\n *             openModal: filters.byCode(\"headerIdIsManaged:\"),\n *             closeModal: filters.byCode(\"key==\")\n *          })\n */\nexport const mapMangledModule = traceFunction(\"mapMangledModule\", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {\n    const exports = {} as Record<S, any>;\n\n    // whitelist Modal API to be able to test modals\n    if (IS_ANTI_CRASH_TEST && code !== ':\"thin\")' && code !== \".modalKey?\") return exports;\n\n    const id = findModuleId(...Array.isArray(code) ? code : [code]);\n    if (id === null)\n        return exports;\n\n    const mod = wreq(id as any);\n    const keys = getAllPropertyNames(mod, includeBlacklistedExports);\n    outer:\n    for (const key of keys) {\n        const member = mod[key];\n        for (const newName in mappers) {\n            // if the current mapper matches this module\n            if (mappers[newName](member)) {\n                exports[newName] = member;\n                continue outer;\n            }\n        }\n    }\n    return exports;\n});\n\n/**\n * lazy mapMangledModule\n  * @see {@link mapMangledModule}\n */\nexport function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"mapMangledModule\", [code, mappers, includeBlacklistedExports]]);\n\n    return proxyLazy(() => mapMangledModule(code, mappers, includeBlacklistedExports));\n}\n\nexport const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\\.all\\(\\[)?((?:\\i\\.e\\(\"?[^)]+?\"?\\),?)+?)(?:\\]\\))?|Promise\\.resolve\\(\\))\\.then\\(\\i\\.bind\\(\\i,\"?([^)]+?)\"?\\)\\)/;\nexport const ChunkIdsRegex = /\\(\"([^\"]+?)\"\\)/g;\n\n/**\n * Extract and load chunks using their entry point\n * @param code An array of all the code the module factory containing the lazy chunk loading must include\n * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory\n * @returns A promise that resolves with a boolean whether the chunks were loaded\n */\nexport async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {\n    if (IS_ANTI_CRASH_TEST) return false;\n\n    const module = findModuleFactory(...code);\n    if (!module) {\n        const err = new Error(\"extractAndLoadChunks: Couldn't find module factory\");\n        logger.warn(err, \"Code:\", code, \"Matcher:\", matcher);\n\n        // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n        if (IS_DEV && !devToolsOpen)\n            throw err;\n\n        return false;\n    }\n\n    const match = String(module).match(canonicalizeMatch(matcher));\n    if (!match) {\n        const err = new Error(\"extractAndLoadChunks: Couldn't find chunk loading in module factory code\");\n        logger.warn(err, \"Code:\", code, \"Matcher:\", matcher);\n\n        // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n        if (IS_DEV && !devToolsOpen)\n            throw err;\n\n        return false;\n    }\n\n    const [, rawChunkIds, entryPointId] = match;\n\n    if (entryPointId == null) {\n        const err = new Error(\"extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array or the entry point id\");\n        logger.warn(err, \"Code:\", code, \"Matcher:\", matcher);\n\n        // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n        if (IS_DEV && !devToolsOpen)\n            throw err;\n\n        return false;\n    }\n\n    const numEntryPoint = Number(entryPointId);\n    const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : numEntryPoint;\n\n    if (rawChunkIds) {\n        const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map(m => {\n            const numChunkId = Number(m[1]);\n            return Number.isNaN(numChunkId) ? m[1] : numChunkId;\n        });\n\n        await Promise.all(chunkIds.map(id => wreq.e(id)));\n    }\n\n    if (wreq.m[entryPoint] == null) {\n        const err = new Error(\"extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load\");\n        logger.warn(err, \"Code:\", code, \"Matcher:\", matcher);\n\n        // Strict behaviour in DevBuilds to fail early and make sure the issue is found\n        if (IS_DEV && !devToolsOpen)\n            throw err;\n\n        return false;\n    }\n\n    wreq(entryPoint);\n    return true;\n}\n\n/**\n * This is just a wrapper around {@link extractAndLoadChunks} to make our reporter test for your webpack finds.\n *\n * Extract and load chunks using their entry point\n * @param code An array of all the code the module factory containing the lazy chunk loading must include\n * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory\n * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call\n */\nexport function extractAndLoadChunksLazy(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {\n    if (IS_REPORTER) lazyWebpackSearchHistory.push([\"extractAndLoadChunks\", [code, matcher]]);\n\n    return makeLazy(() => extractAndLoadChunks(code, matcher));\n}\n\n/**\n * Wait for a module that matches the provided filter to be registered,\n * then call the callback with the module as the first argument\n */\nexport function waitFor(filter: string | PropsFilter | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {\n    // if react find fails then we are fully cooked\n    if (IS_ANTI_CRASH_TEST && filter !== \"useState\") return;\n\n    if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push([\"waitFor\", Array.isArray(filter) ? filter : [filter]]);\n\n    if (typeof filter === \"string\")\n        filter = filters.byProps(filter);\n    else if (Array.isArray(filter))\n        filter = filters.byProps(...filter);\n    else if (typeof filter !== \"function\")\n        throw new Error(\"filter must be a string, string[] or function, got \" + typeof filter);\n\n    if (cache != null) {\n        const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true });\n        if (existing) return void callback(existing, id);\n    }\n\n    waitForSubscriptions.set(filter, callback);\n}\n\n/**\n * Search modules by keyword. This searches the factory methods,\n * meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc\n * @param code One or more strings or regexes\n * @returns Mapping of found modules\n */\nexport function search(...code: CodeFilter) {\n    code = code.map(canonicalizeMatch);\n\n    const results = {} as Record<number, Function>;\n    const factories = wreq.m;\n\n    for (const id in factories) {\n        const factory = factories[id];\n\n        if (stringMatches(factory.toString(), code))\n            results[id] = factory;\n    }\n\n    return results;\n}\n\n/**\n * Extract a specific module by id into its own Source File. This has no effect on\n * the code, it is only useful to be able to look at a specific module without having\n * to view a massive file. extract then returns the extracted module so you can jump to it.\n * As mentioned above, note that this extracted module is not actually used,\n * so putting breakpoints or similar will have no effect.\n * @param moduleId The id of the module to extract\n */\nexport function extract(moduleId: PropertyKey) {\n    const originalFactory = wreq.m[moduleId];\n    if (!originalFactory) return null;\n\n    const originalFactoryCode = String(originalFactory);\n    const isArrowFunction = originalFactoryCode.startsWith(\"(\");\n\n    const wrappedCode = \"0,\" + (!isArrowFunction ? \"function\" : \"\") + originalFactoryCode.slice(originalFactoryCode.indexOf(\"(\"));\n    const code = `\n// [EXTRACTED] WebpackModule${String(moduleId)}\n// WARNING: This module was extracted to be more easily readable.\n//          This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!!\n\n0,${wrappedCode}\n//# sourceURL=file:///ExtractedWebpackModule${String(moduleId)}\n`;\n\n    return (0, eval)(code) as ModuleFactory;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"esModuleInterop\": true,\n        \"skipLibCheck\": false,\n        \"allowJs\": true,\n        \"lib\": [\n            \"DOM\",\n            \"DOM.Iterable\",\n            \"esnext\",\n            \"esnext.array\",\n            \"esnext.asynciterable\",\n            \"esnext.symbol\"\n        ],\n        \"module\": \"esnext\",\n        \"moduleResolution\": \"bundler\",\n        \"strict\": true,\n        \"noImplicitAny\": false,\n        \"target\": \"ESNEXT\",\n        \"jsx\": \"preserve\",\n\n        \"baseUrl\": \"./src/\",\n        \"paths\": {\n            \"@main/*\": [\"./main/*\"],\n            \"@api/*\": [\"./api/*\"],\n            \"@components/*\": [\"./components/*\"],\n            \"@utils/*\": [\"./utils/*\"],\n            \"@plugins/*\": [\"./plugins/*\"],\n            \"@shared/*\": [\"./shared/*\"],\n            \"@webpack/common\": [\"./webpack/common\"],\n            \"@webpack/common/*\": [\"./webpack/common/*\"],\n            \"@webpack\": [\"./webpack/webpack\"],\n            \"@webpack/patcher\": [\"./webpack/patchWebpack\"],\n            \"@webpack/wreq.d\": [\"./webpack/wreq.d\"],\n        },\n\n        \"plugins\": [\n            // Transform paths in output .d.ts files (Include this line if you output declarations files)\n            {\n                \"transform\": \"typescript-transform-paths\",\n                \"afterDeclarations\": true\n            }\n        ],\n        \"outDir\": \"who-fucking-cares-dude\"\n    },\n    \"include\": [\"src/**/*\", \"browser/**/*\", \"scripts/**/*\", \"eslint.config.mjs\"],\n}\n"
  }
]