Merge branch 'dev' into dev2

This commit is contained in:
thororen1234 2025-05-23 08:48:00 -04:00
commit 10ecc2e251
No known key found for this signature in database
246 changed files with 6010 additions and 3000 deletions

View file

@ -16,7 +16,7 @@ permissions: write-all
jobs:
Build:
name: Build Equicord
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -39,7 +39,7 @@ jobs:
run: pnpm buildStandalone
- name: Generate plugin list
run: pnpm generatePluginJson dist/vencordplugins.json
run: pnpm generatePluginJson dist/plugins.json
- name: Generate Equicord plugin list
run: pnpm generateEquicordPluginJson dist/equicordplugins.json
@ -66,15 +66,6 @@ jobs:
rm release/package.json
rm release/*.map
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Upload Equicord Tagged
if: startsWith(github.ref, 'refs/tags/')
run: |
gh release upload v${{ steps.package-version.outputs.current-version}} --clobber dist/release/*
- name: Upload Equicord Stable
if: ${{ github.ref_name == 'main' }}
run: |

72
.github/workflows/nixosBuild.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: NixOS Build
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * *
env:
FORCE_COLOR: true
GITHUB_TOKEN: ${{ secrets.ETOKEN }}
permissions: write-all
jobs:
Build:
name: Build Equicord
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Build web
run: pnpm buildWebStandalone
- name: Build
run: pnpm buildStandalone
- name: Generate plugin list
run: pnpm generatePluginJson dist/vencordplugins.json
- name: Generate Equicord plugin list
run: pnpm generateEquicordPluginJson dist/equicordplugins.json
- name: Collect files to be released
run: |
cd dist
mkdir release
cp browser/browser.* release
cp Vencord.user.{js,js.LEGAL.txt} release
# copy the plugin data jsons, the extension zips and the desktop/vesktop asars
cp *.{json,zip,asar} release
# legacy un-asared files
cp desktop/* release
for file in equibop/*; do
filename=$(basename "$file")
cp "$file" "release/equibop${filename^}"
done
find release -size 0 -delete
rm release/package.json
rm release/*.map
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: Upload Equicord Stable
run: |
gh release create ${{ steps.date.outputs.date }} --latest=false
gh release upload ${{ steps.date.outputs.date }} --clobber dist/release/*

View file

@ -10,6 +10,10 @@ on:
- stable
- canary
default: both
webhook_url:
type: string
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."
required: false
schedule:
# # Every day at midnight
- cron: 0 0 * * *
@ -17,7 +21,7 @@ on:
jobs:
TestPlugins:
name: Test Patches
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -87,5 +91,5 @@ jobs:
cat "$stable_output_file" "$canary_output_file" >> $GITHUB_STEP_SUMMARY
exit $exit_code
env:
WEBHOOK_URL: ${{ secrets.WEBHOOK }}
WEBHOOK_URL: ${{ inputs.webhook_url || secrets.WEBHOOK_URL }}
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}

View file

@ -1,4 +1,4 @@
# Equicord [<img src="./browser/icon.png" width="225" align="left" alt="Equicord">](https://github.com/Equicord/Equicord)
# [<img src="./browser/icon.png" width="40" align="left" alt="Equicord">](https://github.com/Equicord/Equicord) Equicord
[![Equibop](https://img.shields.io/badge/Equibop-grey?style=flat)](https://github.com/Equicord/Equibop)
[![Tests](https://github.com/Equicord/Equicord/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/Equicord/Equicord/actions/workflows/test.yml)
@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Extra included plugins
<details>
<summary>163 additional plugins</summary>
<summary>175 additional plugins</summary>
### All Platforms
@ -25,20 +25,21 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- BetterActivities by D3SOX, Arjix, AutumnVN
- BetterAudioPlayer by creations
- BetterBanReasons by Inbestigator
- BetterBlockedUsers by TheArmagan & Elvyra
- BetterBlockedUsers by TheArmagan
- BetterInvites by iamme
- BetterPlusReacts by Joona
- BetterQuickReact by Ven & Sqaaakoi
- BlockKeywords by catcraft
- BlockKrisp by D3SOX
- BypassPinPrompt by thororen
- BypassStatus by Inbestigator & thororen
- ChannelBadges by Creations
- ChannelTabs by TheSun, TheKodeToad, keifufu, Nickyux
- CharacterCounter by Creations & Panniku
- CleanChannelName by AutumnVN
- ClientSideBlock by Samwich
- ClipsEnhancements by niko
- CommandPalette by Ethan
- CopyStickerLinks by Byeoon
- CopyUserMention by Cortex & castdrian
- CustomSounds by TheKodeToad & SpikeHD
- CustomTimestamps by Rini & nvhrr
@ -51,17 +52,18 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- DisableAnimations by S€th
- DisableCameras by Joona
- DontFilterMe by Samwich
- EmojiDumper by Cortex, Samwich, Woosh
- Encryptcord by Inbestigator
- EquicordCSS by thororen, Panniku, Dablulite, Coolesding, MiniDiscordThemes, LuckFire, gold_me
- EquicordHelper by thororen & nyx
- Equissant by SomeAspy & thororen
- ExportContacts by dat_insanity
- FakeProfileThemesAndEffects by ryan
- CopyProfileColors by Crxa
- FastDeleteChannels by thororen
- FindReply by newwares
- FixFileExtensions by thororen
- FollowVoiceUser by TheArmagan
- FontLoader by vmohammad
- ForwardAnywhere by thororen
- Freaky by nyx
- FrequentQuickSwitcher by Samwich
- FriendCodes by HypedDomi
@ -75,6 +77,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- Glide by Samwich
- GlobalBadges by HypedDomi & Hosted by Wolfie
- GoogleThat by Samwich
- GuildPickerDumper by Cortex, Samwich, Synth, thororen
- HideChatButtons by iamme
- HideServers by bepvte
- HolyNotes by Wolfie
@ -87,6 +90,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- IgnoreTerms by D3SOX
- ImagePreview by Creations
- ImgToGif by zyqunix
- Ingtoninator by zyqunix
- InRole by nin0dev
- InstantScreenshare by HAHALOSAH & thororen
- IRememberYou by zoodogood
@ -94,6 +98,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- JumpToStart by Samwich
- KeyboardSounds by HypedDomi
- KeywordNotify by camila314 & x3rt
- LastActive by Crxa
- LimitMiddleClickPaste by no dev listed
- LoginWithQR by nexpid
- MediaPlaybackSpeed by D3SOX
@ -105,6 +110,8 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- MessageTranslate by Samwich
- ModalFade by Kyuuhachi
- MoreStickers by Leko & Arjix
- MoreUserTags by Cyn, TheSun, RyanCaoDev, LordElias, AutumnVN, hen
- Morse by zyqunix
- NeverPausePreviews by vappstar
- NewPluginsManager by Sqaaakoi
- NoAppsAllowed by kvba
@ -113,9 +120,9 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- NoMirroredCamera by Nyx
- NoModalAnimation by AutumnVN
- NoNitroUpsell by thororen
- NoOnboarding by omaw & Glitch
- NoRoleHeaders by Samwich
- NotificationTitle by Kyuuhachi
- OnePingPerDM by ProffDea
- PingNotifications by smuki
- PinIcon by iamme
- PlatformSpoofer by Drag
@ -124,7 +131,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- QuestCompleter by Amia
- QuestionMarkReplacement by nyx
- Quoter by Samwich
- RandomVoice by xijexo & omaw
- RandomVoice by xijexo, omaw, thororen
- Remix by MrDiamond
- RemixMe by kvba
- RepeatMessage by Tolgchu
@ -138,7 +145,10 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- SidebarChat by Joona
- Signature by Ven, Rini, ImBanana, KrystalSkull
- Slap by Korbo
- Soggy by sliwka
- SoundBoardLogger by Moxxie, fres, echo, maintained by thororen
- SplitLargeMessages by Reycko
- SpotifyActivityToggle by thororen
- SpotifyLyrics by Joona
- StatsfmPresence by Crxa
- StatusPresets by iamme
@ -150,6 +160,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- ThemeLibrary by Fafa
- Timezones by Aria
- Title by Kyuuhachi
- ToastNotifications by Skully, Ethan, Buzzy
- ToggleVideoBind by mochie
- TosuRPC by AutumnVN
- Translate+ by Prince527 & Ven
@ -166,6 +177,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- ViewRawVariant by Kyuuhachi
- VoiceChatUtilities by D3SOX
- VoiceJoinMessages by Sqaaakoi & maintained by thororen
- WallpaperFree by Joona
- WebpackTarball by Kyuuhachi
- WhitelistedEmojis by Creations
- WhosWatching by fres
@ -184,6 +196,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Discord Desktop Only
- ClipsEnhancements by niko
- MediaDownloader by Colorman
- StatusWhilePlaying by thororen
@ -209,7 +222,6 @@ MacOS
Linux
- [GUI-X11](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-x11)
- [GUI-Wayland](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-wayland)
- [CLI](https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux)
- [AUR](https://aur.archlinux.org/packages?O=0&K=equicord)

View file

@ -17,9 +17,10 @@
*/
function parseHeaders(headers) {
const result = new Headers();
if (!headers)
return {};
const result = {};
return result;
const headersArr = headers.trim().split("\n");
for (var i = 0; i < headersArr.length; i++) {
var row = headersArr[i];
@ -27,13 +28,7 @@ function parseHeaders(headers) {
, key = row.slice(0, index).trim().toLowerCase()
, value = row.slice(index + 1).trim();
if (result[key] === undefined) {
result[key] = value;
} else if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
result.append(key, value);
}
return result;
}

View file

@ -9,6 +9,7 @@
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey

12
misc/install.sh Normal file → Executable file
View file

@ -62,8 +62,16 @@ check_for_updates() {
local_modified=$(stat -c "%y" "$INSTALLER_PATH" | cut -d' ' -f1-2) || error "Failed to get local modified date"
if [ "$local_modified" != "$latest_modified" ]; then
echo -e "${YELLOW}Installer is outdated. Updating...${NC}"
download_installer
echo -e "${YELLOW}Installer is outdated. Do you wish to update? [y/n]${NC}"
read -p "" -n 1 -r retval
# Create a new line before printing our next notice, otherwise it will be printed on the same line
# that the prompt was created on!
echo ""
case "$retval" in
y|Y ) download_installer;;
n|N ) echo -e "${YELLOW}Update cancelled. Running installer...${NC}" && return;;
esac
else
echo -e "${GREEN}Installer is up-to-date.${NC}"
fi

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.11.8",
"version": "1.12.2",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {

View file

@ -115,7 +115,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/main/index.ts")],
outfile: "dist/desktop/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
footer: { js: "//# sourceURL=file:///VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
plugins: [
// @ts-ignore this is never undefined
@ -135,7 +135,7 @@ const buildConfigs = ([
outfile: "dist/desktop/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
footer: { js: "//# sourceURL=file:///VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
@ -153,7 +153,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/preload.ts")],
outfile: "dist/desktop/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
footer: { js: "//# sourceURL=file:///VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
define: {
...defines,
@ -168,7 +168,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/main/index.ts")],
outfile: "dist/equibop/main.js",
footer: { js: "//# sourceURL=VencordMain\n" + sourceMapFooter("main") },
footer: { js: "//# sourceURL=file:///VencordDesktopMain\n" + sourceMapFooter("main") },
sourcemap,
plugins: [
...nodeCommonOpts.plugins,
@ -187,7 +187,7 @@ const buildConfigs = ([
outfile: "dist/equibop/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
footer: { js: "//# sourceURL=file:///VencordDesktopRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
@ -205,7 +205,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/preload.ts")],
outfile: "dist/equibop/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
footer: { js: "//# sourceURL=file:///VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
define: {
...defines,

View file

@ -92,7 +92,7 @@ const buildConfigs = [
{
...commonOptions,
outfile: "dist/browser/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
footer: { js: "//# sourceURL=file:///VencordWeb" }
},
{
...commonOptions,
@ -101,7 +101,7 @@ const buildConfigs = [
...commonOptions.define,
IS_EXTENSION: "true"
},
footer: { js: "//# sourceURL=VencordWeb" }
footer: { js: "//# sourceURL=file:///VencordWeb" }
},
{
...commonOptions,

View file

@ -186,7 +186,7 @@ export const globPlugins = kind => ({
const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
pluginsCode += `[${mod}.name]:${mod},\n`;
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`;
i++;
}
}

View file

@ -261,7 +261,7 @@ page.on("console", async e => {
const [, tag, message, otherMessage] = args as Array<string>;
switch (tag) {
case "WebpackInterceptor:":
case "WebpackPatcher:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/);
const patchSlowMatch = message.match(/Patch by (.+?) (took [\d.]+?ms) \(Module id is (.+?)\): (.+)/);
const match = patchFailMatch ?? patchSlowMatch;
@ -315,11 +315,9 @@ page.on("console", async e => {
report.badWebpackFinds.push(otherMessage);
break;
case "Finished test":
await printReport();
setTimeout(async () => {
await browser.close();
await printReport();
process.exit();
}, 10000);
}
}
}
@ -358,4 +356,4 @@ await page.evaluateOnNewDocument(`
}
`);
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login", { timeout: 120000 });

View file

@ -33,7 +33,7 @@ import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { StartAt } from "@utils/types";
import { get as dsGet } from "./api/DataStore";
import { showNotification } from "./api/Notifications";
import { NotificationData, showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/Settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage";
@ -105,6 +105,46 @@ async function syncSettings() {
}
}
let notifiedForUpdatesThisSession = false;
async function runUpdateCheck() {
const notify = (data: NotificationData) => {
if (notifiedForUpdatesThisSession) return;
notifiedForUpdatesThisSession = true;
setTimeout(() => showNotification({
permanent: true,
noPersist: true,
...data
}), 10_000);
};
try {
const isOutdated = await checkForUpdates();
if (!isOutdated) return;
if (Settings.autoUpdate) {
await update();
if (Settings.autoUpdateNotification) {
notify({
title: "Equicord has been updated!",
body: "Click here to restart",
onClick: relaunch
});
}
return;
}
notify({
title: "A Equicord update is available!",
body: "Click here to view the update",
onClick: openUpdaterModal!
});
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
}
async function init() {
await onceReady;
startAllPlugins(StartAt.WebpackReady);
@ -112,34 +152,8 @@ async function init() {
syncSettings();
if (!IS_WEB && !IS_UPDATER_DISABLED) {
try {
const isOutdated = await checkForUpdates();
if (!isOutdated) return;
if (Settings.autoUpdate) {
await update();
if (Settings.updateRelaunch) return relaunch;
if (Settings.autoUpdateNotification)
setTimeout(() => showNotification({
title: "Equicord has been updated!",
body: "Click here to restart",
permanent: true,
noPersist: true,
onClick: relaunch
}), 10_000);
return;
}
setTimeout(() => showNotification({
title: "A Equicord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick: openUpdaterModal!
}), 10_000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
runUpdateCheck();
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
}
if (IS_DEV) {
@ -149,7 +163,7 @@ async function init() {
"Webpack has finished initialising, but some patches haven't been applied yet.",
"This might be expected since some Modules are lazy loaded, but please verify",
"that all plugins are working as intended.",
"You are seeing this warning because this is a Development build of Vencord.",
"You are seeing this warning because this is a Development build of Equicord.",
"\nThe following patches have not been applied:",
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
);

View file

@ -21,25 +21,14 @@ import { Channel, User } from "discord-types/general/index.js";
import { JSX } from "react";
interface DecoratorProps {
activities: any[];
channel: Channel;
/**
* Only for DM members
*/
channelName?: string;
/**
* Only for server members
*/
currentUser?: User;
guildId?: string;
isMobile: boolean;
isOwner?: boolean;
isTyping: boolean;
selected: boolean;
status: string;
type: "guild" | "dm";
user: User;
[key: string]: any;
/** only present when this is a DM list item */
channel: Channel;
/** only present when this is a guild list item */
isOwner: boolean;
}
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms";
@ -53,18 +42,16 @@ export function removeMemberListDecorator(identifier: string) {
decoratorsFactories.delete(identifier);
}
export function __getDecorators(props: DecoratorProps): JSX.Element {
const isInGuild = !!(props.guildId);
export function __getDecorators(props: DecoratorProps, type: "guild" | "dm"): JSX.Element {
const decorators = Array.from(
decoratorsFactories.entries(),
([key, { render: Decorator, onlyIn }]) => {
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
if ((onlyIn === "guilds" && type !== "guild") || (onlyIn === "dms" && type !== "dm"))
return null;
return (
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
<Decorator {...props} />
<Decorator {...props} type={type} />
</ErrorBoundary>
);
}

40
src/api/NicknameIcons.tsx Normal file
View file

@ -0,0 +1,40 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { ReactNode } from "react";
export interface NicknameIconProps {
userId: string;
}
export type NicknameIconFactory = (props: NicknameIconProps) => ReactNode | Promise<ReactNode>;
export interface NicknameIcon {
priority: number;
factory: NicknameIconFactory;
}
const nicknameIcons = new Map<string, NicknameIcon>();
const logger = new Logger("NicknameIcons");
export function addNicknameIcon(id: string, factory: NicknameIconFactory, priority = 0) {
return nicknameIcons.set(id, {
priority,
factory: ErrorBoundary.wrap(factory, { noop: true, onError: error => logger.error(`Failed to render ${id}`, error) })
});
}
export function removeNicknameIcon(id: string) {
return nicknameIcons.delete(id);
}
export function _renderIcons(props: NicknameIconProps) {
return Array.from(nicknameIcons)
.sort((a, b) => b[1].priority - a[1].priority)
.map(([id, { factory: NicknameIcon }]) => <NicknameIcon key={id} {...props} />);
}

View file

@ -18,7 +18,7 @@
import { Settings } from "@api/Settings";
import { Queue } from "@utils/Queue";
import { ReactDOM } from "@webpack/common";
import { createRoot } from "@webpack/common";
import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
@ -35,7 +35,7 @@ function getRoot() {
const container = document.createElement("div");
container.id = "vc-notification-container";
document.body.append(container);
reactRoot = ReactDOM.createRoot(container);
reactRoot = createRoot(container);
}
return reactRoot;
}

View file

@ -11,10 +11,6 @@
width: 100%;
}
.visual-refresh .vc-notification-root {
background-color: var(--bg-overlay-floating, var(--background-base-low));
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;

View file

@ -39,7 +39,6 @@ export interface Settings {
themeLinks: string[];
frameless: boolean;
transparent: boolean;
updateRelaunch: boolean;
winCtrlQ: boolean;
macosVibrancyStyle:
| "content"
@ -101,7 +100,6 @@ const DefaultSettings: Settings = {
winCtrlQ: false,
macosVibrancyStyle: undefined,
disableMinSize: false,
updateRelaunch: false,
winNativeTitleBar: false,
plugins: {},

View file

@ -27,6 +27,7 @@ import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
import * as $MessageUpdater from "./MessageUpdater";
import * as $NicknameIcons from "./NicknameIcons";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
@ -123,6 +124,11 @@ export const MessageUpdater = $MessageUpdater;
*/
export const UserSettings = $UserSettings;
/**
* An API allowing you to add icons to the nickname, in profiles
*/
export const NicknameIcons = $NicknameIcons;
/**
* Just used to identify if user is on Equicord as Vencord doesnt have this
*/

View file

@ -22,7 +22,26 @@ import { ButtonProps } from "@webpack/types";
import { Heart } from "./Heart";
export default function DonateButton({
export function VCDonateButton({
look = Button.Looks.LINK,
color = Button.Colors.TRANSPARENT,
...props
}: Partial<ButtonProps>) {
return (
<Button
{...props}
look={look}
color={color}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
innerClassName="vc-donate-button"
>
<Heart />
Donate
</Button>
);
}
export function DonateButton({
look = Button.Looks.LINK,
color = Button.Colors.TRANSPARENT,
...props

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent, LazyComponentWrapper } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/react";
import { React } from "@webpack/common";
import type { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
@ -46,7 +46,9 @@ const NO_ERROR = {};
// We might want to import this in a place where React isn't ready yet.
// Thus, wrap in a LazyComponent
const ErrorBoundary = LazyComponent(() => {
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
// This component is used in a lot of files which end up importing other Webpack commons and causing circular imports.
// For this reason, use a non import access here.
return class ErrorBoundary extends Vencord.Webpack.Common.React.PureComponent<React.PropsWithChildren<Props>> {
state = {
error: NO_ERROR as any,
stack: "",
@ -107,9 +109,9 @@ const ErrorBoundary = LazyComponent(() => {
}
};
}) as
React.ComponentType<React.PropsWithChildren<Props>> & {
LazyComponentWrapper<React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
};
}>;
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>

View file

@ -179,20 +179,18 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
}
}
function renderMoreUsers(_label: string, count: number) {
const sliceCount = plugin.authors.length - count;
const sliceStart = plugin.authors.length - sliceCount;
const sliceEnd = sliceStart + plugin.authors.length - count;
function renderMoreUsers(_label: string) {
const remainingAuthors = plugin.authors.slice(6);
return (
<Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(", ")}>
<Tooltip text={remainingAuthors.map(u => u.name).join(", ")}>
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{sliceCount}
+{remainingAuthors.length}
</div>
)}
</Tooltip>
@ -250,7 +248,6 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
count={plugin.authors.length}
guildId={undefined}
renderIcon={false}
max={6}
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}

View file

@ -255,3 +255,21 @@
background-color: var(--status-danger-background) !important;
color: var(--status-danger-text) !important;
}
.visual-refresh .vc-plugins-info-card {
background-color: var(--card-primary-bg) !important;
border: 1px solid var(--border-subtle) !important;
&:hover {
background-color: var(--card-primary-bg) !important;
}
}
.visual-refresh .vc-plugin-stats {
background-color: var(--card-primary-bg) !important;
border: 1px solid var(--border-subtle) !important;
&:hover {
background-color: var(--card-primary-bg) !important;
}
}

View file

@ -18,12 +18,13 @@
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@shared/debounce";
import { copyToClipboard } from "@utils/clipboard";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text";
import { Patch, ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
import { Button, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
@ -378,8 +379,8 @@ function PatchHelper() {
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
<Button onClick={() => copyToClipboard(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => copyToClipboard("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</>
)}
</SettingsTab>

View file

@ -106,8 +106,6 @@ function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const settings = useSettings(["updateRelaunch"]);
const isOutdated = (updates?.length ?? 0) > 0;
return (
@ -119,7 +117,6 @@ function Updatable(props: CommonProps) {
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
if (settings.updateRelaunch) return relaunch();
return await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
@ -191,7 +188,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
const settings = useSettings(["autoUpdate", "updateRelaunch", "autoUpdateNotification"]);
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
@ -217,30 +214,12 @@ function Updater() {
</Switch>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => {
settings.autoUpdateNotification = v;
if (settings.updateRelaunch) {
settings.updateRelaunch = !v;
}
}}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Equicord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</Switch>
<Switch
value={settings.updateRelaunch}
onChange={(v: boolean) => {
settings.updateRelaunch = v;
if (settings.autoUpdateNotification) {
settings.autoUpdateNotification = !v;
}
}}
note="Relaunches the app after updating with no prompt"
disabled={!settings.autoUpdate}
>
Automatically relaunch after updating
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>

View file

@ -9,7 +9,7 @@ import "./VencordTab.css";
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton, { InviteButton } from "@components/DonateButton";
import { DonateButton, InviteButton } from "@components/DonateButton";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent";
@ -82,7 +82,7 @@ function EquicordSettings() {
(!IS_DISCORD_DESKTOP || !isWindows
? {
key: "frameless",
title: "Disable the window frame",
title: "Disable the Window Frame",
note: "Requires a full restart",
warning: { enabled: false },
}
@ -95,7 +95,7 @@ function EquicordSettings() {
}),
!IS_WEB && {
key: "transparent",
title: "Enable window transparency.",
title: "Enable Window Transparency",
note: "You need a theme that supports transparency or this will do nothing. Requires a full restart!",
warning: {
enabled: isWindows,
@ -112,7 +112,7 @@ function EquicordSettings() {
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable minimum window size",
title: "Disable Minimum Window Size",
note: "Requires a full restart",
warning: { enabled: false },
},

View file

@ -48,7 +48,7 @@ async function runReporter() {
for (const patch of patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
new Logger("WebpackPatcher").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
if (IS_COMPANION_TEST)
reporterData.failedPatches.foundNoModule.push(patch);
}
@ -56,7 +56,7 @@ async function runReporter() {
for (const [plugin, moduleId, match, totalTime] of patchTimings) {
if (totalTime > 5) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
new Logger("WebpackPatcher").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
}
}
@ -92,7 +92,7 @@ async function runReporter() {
result = Webpack[method](...args);
}
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
if (result == null || (result.$$vencordGetWrappedComponent != null && result.$$vencordGetWrappedComponent() == null)) throw new Error("Webpack Find Fail");
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {

View file

@ -18,10 +18,26 @@
import "@equicordplugins/_misc/styles.css";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
import { Forms } from "@webpack/common";
import clanBadges from "../_misc/clanBadges.css?managed";
const settings = definePluginSettings({
hideClanBadges: {
type: OptionType.BOOLEAN,
description: "Hide clan badges",
default: false,
onChange: value => {
if (value) enableStyle(clanBadges);
else disableStyle(clanBadges);
}
}
});
export default definePlugin({
name: "EquicordHelper",
description: "Fixes some misc issues with discord",
@ -31,6 +47,7 @@ export default definePlugin({
This Plugin is used for fixing misc issues with discord such as some crashes
</Forms.FormText>
</>,
settings,
required: true,
patches: [
{
@ -45,13 +62,12 @@ export default definePlugin({
replace: "return $1;"
}
]
}
],
start() {
if (settings.store.hideClanBadges) enableStyle(clanBadges);
},
{
find: '"Slate: Unable to find syntax characters"',
replacement: {
match: /((let )(\i)=\i\.indexOf\(\i,(\i)\)),/,
replace: "$1;if ($3 === -1) {return $4;}$2"
stop() {
if (settings.store.hideClanBadges) disableStyle(clanBadges);
}
}
]
});

View file

@ -0,0 +1,3 @@
[class*="chipletContainerInner_"]:has([src *="/clan-badges/"]) {
display: none;
}

View file

@ -0,0 +1,69 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, Menu, MessageActions, NavigationRouter } from "@webpack/common";
interface ChannelSelectEvent {
type: "CHANNEL_SELECT";
channelId: string | null;
guildId: string | null;
}
let lastChannelId = "0";
function autoJump({ guild_id, id: channelId }) {
const guildId = guild_id ?? "@me";
lastChannelId = channelId;
NavigationRouter.transitionTo(`/channels/${guildId}/${channelId}`);
MessageActions.jumpToPresent(channelId, { limit: null });
}
const MenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
children.push(
<Menu.MenuItem
id="auto-jump"
label="Jump to Last Message"
action={() => {
autoJump(channel);
}}
/>
);
};
const settings = definePluginSettings({
autoJumping: {
type: OptionType.BOOLEAN,
description: "Automatically jump to the last message in the channel when switching channels",
default: false
}
});
export default definePlugin({
name: "AutoJump",
description: "Jumps to Last Message in Channel",
authors: [EquicordDevs.omaw],
settings,
contextMenus: {
"channel-context": MenuPatch,
"user-context": MenuPatch,
"thread-context": MenuPatch
},
flux: {
async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {
if (!settings.store.autoJumping || !channelId) return;
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.id === lastChannelId) return;
autoJump({ guild_id: guildId, id: channelId });
}
}
});

View file

@ -58,8 +58,8 @@ export default definePlugin({
replacement: [
{
// We add the banner as a property while we can still access the user id
match: /(?<=nameplate:(\i).*?)verified:(\i).isVerifiedBot.*?name:null.*?(?=avatar:)/,
replace: "$&banner:$self.memberListBannerHook($2, $1),",
match: /user:(\i).{0,150}nameplate:(\i).*?name:null.*?(?=avatar:)/,
replace: "$&banner:$self.memberListBannerHook($1, $2),",
},
{
match: /(?<=\),nameplate:)(\i)/,
@ -112,7 +112,7 @@ export default definePlugin({
}
return (
<img id={`vc-banners-everywhere-${user.id}`} src={url} className="vc-banners-everywhere-memberlist"></img>
<img alt="" id={`vc-banners-everywhere-${user.id}`} src={url} className="vc-banners-everywhere-memberlist"></img>
);
},

View file

@ -35,7 +35,7 @@ export default definePlugin({
patches: [
{
// Patch activity icons
find: '"activity-status-web"',
find: "isBlockedOrIgnored(null",
replacement: {
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
replace: ",$self.patchActivityList($1)]"
@ -44,9 +44,9 @@ export default definePlugin({
},
{
// Show all activities in the user popout/sidebar
find: '"UserProfilePopoutBody"',
find: "hasAvatarForGuild(null",
replacement: {
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.activity\}\)/,
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/,
replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })"
},
predicate: () => settings.store.userPopout

View file

@ -66,7 +66,7 @@ async function addListeners(audioElement: HTMLAudioElement, url: string, parentB
const madeURL = new URL(url);
madeURL.searchParams.set("t", Date.now().toString());
const corsProxyUrl = "https://corsproxy.io?" + encodeURIComponent(madeURL.href);
const corsProxyUrl = "https://corsproxy.io/?url=" + encodeURIComponent(madeURL.href);
const response = await fetch(corsProxyUrl);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);

View file

@ -4,53 +4,55 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { DeleteIcon, PlusIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms, TextInput } from "@webpack/common";
const cl = classNameFactory("vc-bbr-");
function ReasonsComponent() {
const { reasons } = settings.use(["reasons"]);
const { reasons } = settings.store;
return (
<Forms.FormSection title="Reasons">
{reasons.map((reason: string, index: number) => (
{reasons.map((r, i) => (
<div
className="vc-bbr-reason-wrapper"
key={index}
key={i}
className={cl("reason-wrapper")}
>
<TextInput
type="text"
key={index}
value={reason}
value={r}
onChange={v => {
reasons[index] = v;
settings.store.reasons = [...reasons];
reasons[i] = v;
settings.store.reasons = reasons;
}}
placeholder="Reason"
/>
<Button
color={Button.Colors.RED}
className="vc-bbr-remove-button"
className={cl("remove-button")}
color={Button.Colors.TRANSPARENT}
onClick={() => {
reasons.splice(index, 1);
settings.store.reasons = [...reasons];
reasons.splice(i, 1);
settings.store.reasons = reasons;
}}
look={Button.Looks.BLANK}
size={Button.Sizes.MIN}
>
Remove
<DeleteIcon />
</Button>
</div>
))}
<Button
onClick={() => {
settings.store.reasons = [...reasons, ""];
}}
>
Add new
<div className={cl("reason-wrapper")}>
<Button onClick={() => settings.store.reasons.push("")} className={cl("add-button")} size={Button.Sizes.LARGE} color={Button.Colors.TRANSPARENT}>
<PlusIcon /> Add another reason
</Button>
</div>
</Forms.FormSection>
);
}
@ -59,10 +61,10 @@ const settings = definePluginSettings({
reasons: {
description: "Your custom reasons",
type: OptionType.COMPONENT,
default: [""],
default: [] as string[],
component: ReasonsComponent,
},
textInputDefault: {
isTextInputDefault: {
type: OptionType.BOOLEAN,
description: 'Shows a text input instead of a select menu by default. (Equivalent to clicking the "Other" option)'
}
@ -74,9 +76,9 @@ export default definePlugin({
authors: [Devs.Inbestigator],
patches: [
{
find: "#{intl::BAN_MULTIPLE_CONFIRM_TITLE}",
find: "#{intl::BAN_REASON_OPTION_SPAM_ACCOUNT}",
replacement: [{
match: /\[\{name:\i\.\i\.string\(\i\.\i#{intl::BAN_REASON_OPTION_SPAM_ACCOUNT}\).+?\}\]/,
match: /\[(\{((name|value):\i\.\i\.string\(\i\.\i\.\i\),?){2}\},?){3}\]/,
replace: "$self.getReasons()"
},
{
@ -86,17 +88,16 @@ export default definePlugin({
}
],
getReasons() {
const reasons = settings.store.reasons.length
? settings.store.reasons
const storedReasons = settings.store.reasons.filter((r: string) => r.trim());
const reasons: string[] = storedReasons.length
? storedReasons
: [
getIntlMessage("BAN_REASON_OPTION_SPAM_ACCOUNT"),
getIntlMessage("BAN_REASON_OPTION_HACKED_ACCOUNT"),
getIntlMessage("BAN_REASON_OPTION_BREAKING_RULES")
getIntlMessage("BAN_REASON_OPTION_BREAKING_RULES"),
];
return reasons.map(s => ({ name: s, value: s }));
},
getDefaultState() {
return settings.store.textInputDefault ? 1 : 0;
},
getDefaultState: () => settings.store.isTextInputDefault ? 1 : 0,
settings,
});

View file

@ -1,11 +0,0 @@
.vc-bbr-reason-wrapper {
display: grid;
padding: 0;
padding-bottom: 0.5rem;
gap: 0.5rem;
grid-template-columns: 6fr 1fr;
}
.vc-bbr-remove-button {
height: 100%;
}

View file

@ -0,0 +1,31 @@
.vc-bbr-reason-wrapper {
display: grid;
padding-bottom: 12px;
gap: 4px 12px;
align-items: center;
grid-template-columns: 1fr 24px;
}
.vc-bbr-remove-button {
color: var(--text-muted);
}
.vc-bbr-remove-button:hover {
color: var(--button-danger-background-hover);
}
.vc-bbr-add-button {
justify-content: start !important;
border: 0;
padding: 2px 12px;
color: var(--text-muted) !important;
font-size: 16px;
}
.vc-bbr-add-button div {
display: flex;
justify-content: start;
align-items: center;
gap: 12px;
margin: 0 !important;
}

View file

@ -4,58 +4,23 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
import { openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, FluxDispatcher, React, RelationshipStore, Text, TextInput, UserStore } from "@webpack/common";
import { ButtonProps } from "@webpack/types";
import { User } from "discord-types/general";
import "./styles.css";
import { EquicordDevs } from "@utils/constants";
import { getIntlMessage, openUserProfile } from "@utils/discord";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, React, RelationshipStore, TextInput, UserStore } from "@webpack/common";
const ChannelActions = findByPropsLazy("openPrivateChannel");
let lastSearch = "";
let updateFunc = (v: any) => { };
const ChannelActions = findByPropsLazy("openPrivateChannel");
const ButtonComponent = findComponentByCodeLazy('submittingStartedLabel","submittingFinishedLabel"]);');
const ConfirmationModal = findByCodeLazy('"ConfirmModal")', "useLayoutEffect");
const settings = definePluginSettings({
addDmsButton: {
default: true,
type: OptionType.BOOLEAN,
description: "Adds a 'View DMs' button to the users in the blocked/ignored list.",
},
hideBlockedWarning: {
default: false,
type: OptionType.BOOLEAN,
description: "Skip the warning about blocked/ignored users when opening any profile anywhere on discord outside of the blocklist.",
restartNeeded: true,
},
showUnblockConfirmation: {
default: true,
type: OptionType.BOOLEAN,
description: "Show a warning before unblocking a user from the blocklist.",
},
showUnblockConfirmationEverywhere: {
default: false,
type: OptionType.BOOLEAN,
description: "Show a warning before unblocking a user anywhere on discord.",
restartNeeded: true,
},
unblockButtonDanger: {
default: false,
type: OptionType.BOOLEAN,
description: "Color the unblock button in the blocklist red instead of gray.",
},
});
export default definePlugin({
name: "BetterBlockedUsers",
description: "Allows you to search in blocked users list and makes names clickable in settings.",
authors: [EquicordDevs.TheArmagan, Devs.Elvyra],
settings,
description: "Allows you to search in blocked users list and makes names selectable in settings.",
authors: [EquicordDevs.TheArmagan],
patches: [
{
find: '"],{numberOfBlockedUsers:',
@ -65,12 +30,8 @@ export default definePlugin({
replace: ",$1.listType==='blocked'?$self.renderSearchInput():null"
},
{
match: /(?<=className:\i.userInfo,)(?=children:.{0,20}user:(\i))/,
replace: "style:{cursor:'pointer'},onClick:()=>$self.openUserProfile($1),"
},
{
match: /(?<=children:null!=(\i).globalName\?.+?}\),).*?(\{color:.{0,65}?string\((\i).+?"8wXU9P"]\)})\)/,
replace: "$self.generateButtons({user:$1, originalProps:$2, isBlocked:$3})",
match: /(?<=userId:(\i).*?\}\)\]\}\),)(\(.*?\)\}\))/,
replace: "$self.renderUser($1,$2),",
},
{
match: /(?<=\}=(\i).{0,10}(\i).useState\(.{0,1}\);)/,
@ -81,64 +42,6 @@ export default definePlugin({
replace: "$1(searchResults.length?searchResults:$2)"
},
]
},
{
find: "UserProfileModalHeaderActionButtons",
replacement: [
{
match: /(?<=return \i)\|\|(\i)===.*?.FRIEND/,
replace: (_, type) => `?null:${type} === 1|| ${type} === 2`,
},
{
match: /(?<=\i.bot.{0,50}children:.*?onClose:)(\i)/,
replace: "() => {$1();$self.closeSettingsWindow()}",
}
],
},
{
find: ',["user"])',
replacement: {
match: /(?<=isIgnored:.*?,\[\i,\i]=\i.useState\()\i\|\|\i\|\|\i.*?]\);/,
replace: "false);"
},
},
// If the users wishes to, they can disable the warning in all other places as well.
...[
"UserProfilePanelWrapper: currentUser cannot be undefined",
"UserProfilePopoutWrapper: currentUser cannot be undefined",
].map(x => ({
find: x,
replacement: {
match: /(?<=isIgnored:.*?,\[\i,\i]=\i.useState\()\i\|\|\i\|\|\i\)(?:;\i.useEffect.*?]\))?/,
replace: "false)",
},
predicate: () => settings.store.hideBlockedWarning,
})),
{
find: ".BLOCKED:return",
replacement: {
match: /(?<=\i.BLOCKED:return.{0,65}onClick:)\(\)=>\{(\i.\i.unblockUser\((\i).+?}\))/,
replace: "(event) => {$self.openConfirmationModal(event,()=>{$1}, $2)",
},
predicate: () => settings.store.showUnblockConfirmationEverywhere,
},
{
find: "#{intl::UNBLOCK}),",
replacement: {
match: /(?<=#{intl::UNBLOCK}.+?Click=)\(\)=>(\{.+?(\i.getRecipientId\(\))\)})/,
replace: "event => $self.openConfirmationModal(event, ()=>$1, $2)",
},
predicate: () => settings.store.showUnblockConfirmationEverywhere,
},
{
find: "#{intl::BLOCK}),action",
replacement: {
match: /(?<=id:"block".{0,100}action:\i\?)\(\)=>(\{.{0,25}unblockUser\((\i).{0,60}:void 0\)})/,
replace: "event => {$self.openConfirmationModal(event, ()=>$1,$2)}",
},
predicate: () => settings.store.showUnblockConfirmationEverywhere,
}
],
renderSearchInput() {
@ -161,6 +64,19 @@ export default definePlugin({
}} value={value}
></TextInput>;
},
renderUser(userId: string, rest: any) {
return (
<div style={{ display: "flex", gap: "8px" }}>
<Button color={Button.Colors.PRIMARY} onClick={() => openUserProfile(userId)}>
{getIntlMessage("SHOW_USER_PROFILE")}
</Button>
{rest}
</div>
);
},
getSearchResults() {
return !!lastSearch;
},
setUpdateFunc(e, setResults) {
if (e.listType !== "blocked") return;
updateFunc = setResults;
@ -173,72 +89,5 @@ export default definePlugin({
if (!user) return id === search;
return id === search || user?.username?.toLowerCase()?.includes(search) || user?.globalName?.toLowerCase()?.includes(search);
}) as string[];
},
closeSettingsWindow() {
FluxDispatcher.dispatch({ type: "LAYER_POP" });
},
openUserProfile(user: User) {
openUserProfile(user.id);
},
generateButtons(props: { user: User, originalProps: ButtonProps, isBlocked: boolean; }) {
const { user, originalProps, isBlocked } = props;
if (settings.store.unblockButtonDanger) originalProps.color = Button.Colors.RED;
// TODO add extra unblock confirmation after the click + setting.
if (settings.store.showUnblockConfirmation || settings.store.showUnblockConfirmationEverywhere) {
const originalOnClick = originalProps.onClick!;
originalProps.onClick = e => {
if (!isBlocked) return originalOnClick(e);
this.openConfirmationModal(e as unknown as MouseEvent, () => originalOnClick(e), user, true);
};
}
const unblockButton = <ButtonComponent {...originalProps} />;
if (!settings.store.addDmsButton) return unblockButton;
const dmButton = <ButtonComponent color={Button.Colors.BRAND_NEW} onClick={() => this.openDMChannel(user)}>Show DMs</ButtonComponent>;
return <div style={{ display: "flex", gap: "8px" }} className="vc-bbc-button-container">
{dmButton}
{unblockButton}
</div>;
},
openDMChannel(user: User) {
ChannelActions.openPrivateChannel(user.id);
this.closeSettingsWindow();
return null;
},
openConfirmationModal(event: MouseEvent, callback: () => any, user: User | string, isSettingsOrigin: boolean = false) {
if (event.shiftKey) return callback();
if (typeof user === "string") {
user = UserStore.getUser(user);
}
return openModal(m => <ConfirmationModal
{...m}
className="vc-bbc-confirmation-modal"
header={`Unblock ${user?.username ?? "?"}?`}
cancelText="Cancel"
confirmText="Unblock"
onConfirm={() => {
callback();
}}>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }} className="vc-bbc-confirmation-modal-text">
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<Text variant="text-md/semibold">{`Are you sure you want to unblock ${user?.username ?? "this user"}?`}</Text>
<Text variant="text-md/normal">{`This will allow ${user?.username ?? "them"} to see your profile and message you again.`}</Text>
</div>
<Text variant="text-md/normal">{"You can always block them again later."}</Text>
{isSettingsOrigin ? <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<Text variant="text-sm/medium" style={{ color: "var(--text-muted)" }}>{"If you just want to read the chat logs instead, you can just click on their profile."}</Text>
<Text variant="text-sm/normal" style={{ color: "var(--text-muted)" }}>{"Alternatively, you can enable a button to jump to DMs in the blocklist through the plugin settings."}</Text>
</div> : <Text variant="text-sm/medium" style={{ color: "var(--text-muted)" }}>{"If you just want to read the chat logs, you can do this without unblocking them."}</Text>}
</div>
</ConfirmationModal>);
},
});

View file

@ -0,0 +1,3 @@
[class*="usersList_"] [class*="text_"] {
user-select: text !important;
}

View file

@ -0,0 +1,49 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "BypassPinPrompt",
description: "Bypass the pin prompt when pinning messages",
authors: [EquicordDevs.thororen],
patches: [
{
find: '"Channel Pins"',
replacement: {
match: /(?<=(\i\.\i\.unpinMessage\(\i,\i\.id\)):)\i\.\i\.confirmUnpin\(\i,\i\)/,
replace: "$1"
}
},
{
find: 'source:"message-actions"',
replacement: [
{
match: /(?<=(\i\.\i\.pinMessage\(\i,\i\.id\)):)\i\.\i\.confirmPin\(\i,\i\)/,
replace: "$1"
},
{
match: /(?<=(\i\.\i\.unpinMessage\(\i,\i\.id\)):)\i\.\i\.confirmUnpin\(\i,\i\)/,
replace: "$1"
}
]
},
{
find: 'id:"pin"',
replacement: [
{
match: /(?<=(\i\.\i\.pinMessage\(\i,\i\.id\)):)\i\.\i\.confirmPin\(\i,\i\)/,
replace: "$1"
},
{
match: /(?<=(\i\.\i\.unpinMessage\(\i,\i\.id\)):)\i\.\i\.confirmUnpin\(\i,\i\)/,
replace: "$1"
}
]
},
],
});

View file

@ -20,6 +20,8 @@ const PlusSmallIcon = findComponentByCodeLazy("0v-5h5a1");
const cl = classNameFactory("vc-channeltabs-");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
const [userId, setUserId] = useState("");
const { showBookmarkBar, widerTabsAndBookmarks } = settings.use(["showBookmarkBar", "widerTabsAndBookmarks"]);
@ -66,6 +68,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
className={cl("container")}
ref={ref}
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <BasicContextMenu />)}
style={{ marginTop: isMac ? "28px" : "0" }}
>
<div className={cl("tab-container")}>
{openedTabs.map((tab, i) =>

View file

@ -48,7 +48,7 @@ export default definePlugin({
{
find: ".COLLECTIBLES_SHOP_FULLSCREEN))",
replacement: {
match: /(\?void 0:(\i)\.channelId.{0,300}return)((.{0,15})"div",{.*?\])(\}\)\}\})/,
match: /(\?void 0:(\i)\.channelId.{0,500}return)((.{0,15})"div",{.*?\])(\}\)\}\})/,
replace: "$1$4$self.render,{currentChannel:$2,children:$3})$5"
}
},

View file

@ -152,8 +152,8 @@ export default definePlugin({
{
find: "._areActivitiesExperimentallyHidden=(",
replacement: {
match: /BOOST_GEM_ICON\}\}\)\)\};/,
replace: "$&if($self.shouldHideUser(this.props.user.id, this.props.channel.id)) return null; "
match: /(?<=user:(\i),guildId:\i,channel:(\i).*?)BOOST_GEM_ICON.{0,10}\);/,
replace: "$&if($self.shouldHideUser($1.id, $2.id)) return null; "
}
},
// stop the role header from displaying if all users with that role are hidden (wip sorta)

View file

@ -6,9 +6,10 @@
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { copyToClipboard } from "@utils/clipboard";
import { relaunch, showItemInFolder } from "@utils/native";
import { checkForUpdates, getRepo } from "@utils/updater";
import { Clipboard, GuildStore, NavigationRouter, SettingsRouter, Toasts } from "@webpack/common";
import { GuildStore, NavigationRouter, SettingsRouter, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
@ -89,7 +90,7 @@ export const actions: ButtonAction[] = [
const newUrl = url.replace(/(https?:\/\/)?([a-zA-Z0-9-]+)\.([a-zA-Z0-9-]+)/, "https://$2.$3");
const res = (await fetch(newUrl));
const text = await res.text();
Clipboard.copy(text);
copyToClipboard(text);
Toasts.show({
message: "Copied response to clipboard!",
@ -115,7 +116,7 @@ export const actions: ButtonAction[] = [
{
id: "copyGitInfo", label: "Copy Git Info", callback: async () => {
Clipboard.copy(`gitHash: ${gitHash}\ngitRemote: ${gitRemote}`);
copyToClipboard(`gitHash: ${gitHash}\ngitRemote: ${gitRemote}`);
Toasts.show({
message: "Copied git info to clipboard!",

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { copyToClipboard } from "@utils/clipboard";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu, Toasts, UserProfileStore } from "@webpack/common";
function getProfileColors(userId) {
try {
const profile = UserProfileStore.getUserProfile(userId);
if (!profile || !profile.themeColors || profile.themeColors.length < 2) {
return null;
}
const primaryColor = profile.themeColors[0].toString(16).padStart(6, "0");
const secondaryColor = profile.themeColors[1].toString(16).padStart(6, "0");
return { primaryColor, secondaryColor };
} catch (e) {
console.error("Failed to get profile colors:", e);
return null;
}
}
function copyProfileColors(userId) {
const colors = getProfileColors(userId);
if (!colors) {
Toasts.show({
type: Toasts.Type.FAILURE,
message: "No profile colors found!",
id: Toasts.genId()
});
return;
}
const { primaryColor, secondaryColor } = colors;
// Formatting
const formattedColors = `Primary-color #${primaryColor}, Secondary-Color #${secondaryColor}`;
try {
copyToClipboard(formattedColors);
Toasts.show({
type: Toasts.Type.SUCCESS,
message: "Profile colors copied to clipboard!",
id: Toasts.genId()
});
} catch (e) {
console.error("Failed to copy to clipboard:", e);
Toasts.show({
type: Toasts.Type.FAILURE,
message: "Error copying profile colors!",
id: Toasts.genId()
});
}
}
export function ColorIcon() {
return (
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="#94b3e4"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17,4H15.82A3,3,0,0,0,13,2H11A3,3,0,0,0,8.18,4H7A3,3,0,0,0,4,7V19a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V7A3,3,0,0,0,17,4ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm8,14a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V7A1,1,0,0,1,7,6H8V7A1,1,0,0,0,9,8h6a1,1,0,0,0,1-1V6h1a1,1,0,0,1,1,1Z" />
</svg>
);
}
// spawn in the context menu
const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }) => {
if (!user) return;
children.push(
<Menu.MenuItem
id="CopyProfileColors"
icon={ColorIcon}
label={<span style={{ color: "rgb(148, 179, 228)" }}>Copy Profile Colors</span>}
action={() => copyProfileColors(user.id)}
/>
);
};
export default definePlugin({
name: "CopyProfileColors",
description: "A plugin to copy people's profile gradient colors to clipboard.",
authors: [EquicordDevs.Crxa, EquicordDevs.Cortex], // Cortex is here because he showed me how to add icons <3
start() {
addContextMenuPatch("user-context", userContextMenuPatch);
addContextMenuPatch("user-profile-actions", userContextMenuPatch);
},
stop() {
// bye bye menu options
removeContextMenuPatch("user-context", userContextMenuPatch);
removeContextMenuPatch("user-profile-actions", userContextMenuPatch);
}
});

View file

@ -0,0 +1,135 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin from "@utils/types";
import { findStoreLazy } from "@webpack";
import { Menu, React } from "@webpack/common";
import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore");
interface Sticker {
t: "Sticker";
format_type: number;
id: string;
type: number;
}
const StickerExt = ["png", "png", "json", "gif"] as const;
function getUrl(data: Sticker) {
if (data.format_type === 4)
return `https:${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.gif?size=4096&lossless=true`;
return `https://${window.GLOBAL_ENV.CDN_HOST}/stickers/${data.id}.${StickerExt[data.format_type]}?size=4096&lossless=true`;
}
function buildMenuItem(Sticker, fetchData: () => Promisable<Omit<Sticker, "t">>) {
return (
<>
<Menu.MenuSeparator></Menu.MenuSeparator>
<Menu.MenuItem
id="copystickerurl"
key="copystickerurl"
label={"Copy URL"}
action={async () => {
const res = await fetchData();
const data = { t: Sticker, ...res } as Sticker;
const url = getUrl(data[0]);
copyWithToast(url, "Link copied!");
}
}
/>
<Menu.MenuItem
id="openstickerlink"
key="openstickerlink"
label={"Open URL"}
action={async () => {
const res = await fetchData();
const data = { t: Sticker, ...res } as Sticker;
const url = getUrl(data[0]);
VencordNative.native.openExternal(url);
}
}
/>
</>
);
}
function buildMenuExpression(Sticker, fetchData: () => Promisable<Omit<Sticker, "t">>) {
return (
<>
<Menu.MenuSeparator></Menu.MenuSeparator>
<Menu.MenuItem
id="copystickerurl"
key="copystickerurl"
label={"Copy URL"}
action={async () => {
const res = await fetchData();
const data = { t: Sticker, ...res } as Sticker;
const url = getUrl(data);
copyWithToast(url, "Link copied!");
}
}
/>
<Menu.MenuItem
id="openstickerlink"
key="openstickerlink"
label={"Open URL"}
action={async () => {
const res = await fetchData();
const data = { t: Sticker, ...res } as Sticker;
const url = getUrl(data);
VencordNative.native.openExternal(url);
}
}
/>
</>
);
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
const { favoriteableId, favoriteableType } = props ?? {};
if (!favoriteableId) return;
const menuItem = (() => {
const sticker = props.message.stickerItems.find(s => s.id === favoriteableId);
if (sticker?.format_type === 3) return;
switch (favoriteableType) {
case "sticker":
return buildMenuItem("Sticker", () => props.message.stickerItems);
}
})();
if (menuItem)
findGroupChildrenByChildId("devmode-copy-id", children, true)?.push(menuItem);
};
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
const { id } = props?.target?.dataset ?? {};
if (!id) return;
if (!props.target.className?.includes("lottieCanvas")) {
const stickerCache = StickersStore.getStickerById(id);
if (stickerCache) {
children.push(buildMenuExpression("Sticker", () => stickerCache));
}
}
};
export default definePlugin({
name: "CopyStickerLinks",
description: "Adds the ability to copy and open sticker links to your browser",
authors: [Devs.Byeoon],
contextMenus: {
"message": messageContextMenuPatch,
"expression-picker": expressionPickerPatch
}
});

View file

@ -5,9 +5,10 @@
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { copyToClipboard } from "@utils/clipboard";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Clipboard, Menu } from "@webpack/common";
import { Menu } from "@webpack/common";
import type { Channel, User } from "discord-types/general";
const MentionIcon = () => (
@ -37,7 +38,7 @@ const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: U
<Menu.MenuItem
id="vc-copy-user-mention"
label="Copy User Mention"
action={() => Clipboard.copy(`<@${user.id}>`)}
action={() => copyToClipboard(`<@${user.id}>`)}
icon={MentionIcon}
/>
);

View file

@ -71,11 +71,11 @@ export default definePlugin({
find: "#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}",
replacement: [
{
match: /(?<=\i=null!=\i\?).{0,25}\((\i),"LT"\):\(0,\i\.\i\)\(\i,!0\)/,
match: /(?<=null!=\i\?).{0,25}\((\i),"LT"\):\(0,\i\.\i\)\(\i,!0\)/,
replace: '$self.format($1,"compactFormat","[calendar]"):$self.format($1,"cozyFormat","LT")',
},
{
match: /(?<=text:)\(0,\i.\i\)\((\i),"LLLL"\)(?=,)/,
match: /(?<=text:)\(\)=>\(0,\i.\i\)\((\i),"LLLL"\)(?=,)/,
replace: '$self.format($1,"tooltipFormat","LLLL")',
},
]

View file

@ -62,12 +62,8 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: {
};
export function getCustomColorString(userId: string, withHash?: boolean): string | undefined {
if (!colors[userId] || !Settings.plugins.CustomUserColors.enabled)
return;
if (withHash)
return `#${colors[userId]}`;
if (!colors[userId] || !Settings.plugins.CustomUserColors.enabled) return;
if (withHash) return `#${colors[userId]}`;
return colors[userId];
}
@ -98,21 +94,23 @@ export default definePlugin({
// this also affects name headers in chats outside of servers
find: '="SYSTEM_TAG"',
replacement: {
match: /\i.gradientClassName]\),style:/,
replace: "$&{color:$self.colorIfServer(arguments[0])},_style:"
// Override colorString with our custom color and disable gradients if applying the custom color.
match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/,
replace: (m, colorString, hasGradientColors) => `${m}` +
`vcCustomUserColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),`
},
predicate: () => !Settings.plugins.IrcColors.enabled
},
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(highlighted:\i,)/,
match: /(subText:\i\(\),)/,
replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`},"
},
predicate: () => settings.store.dmList,
},
{
find: "!1,wrapContent",
find: '"AvatarWithText"',
replacement: [
{
match: /(\}=\i)/,
@ -125,22 +123,40 @@ export default definePlugin({
],
predicate: () => settings.store.dmList,
},
{
find: '"Reply Chain Nudge")',
replacement: {
match: /(,color:)(\i),/,
replace: "$1$self.colorInReplyingTo(arguments[0]) ?? $2,",
},
},
],
colorDMList(a: any): string | undefined {
const userId = a?.user?.id;
if (!userId) return;
const colorString = getCustomColorString(userId, true);
if (colorString) return colorString;
return "inherit";
getMessageColorsVariables(context: any, hasGradientColors: boolean) {
const colorString = this.colorIfServer(context);
const originalColorString = context?.author?.colorString;
return [colorString, hasGradientColors && colorString === originalColorString];
},
colorIfServer(a: any): string | undefined {
const roleColor = a.author?.colorString;
colorDMList(context: any): string | undefined {
const userId = context?.user?.id;
const colorString = getCustomColorString(userId, true);
return colorString ?? "inherit";
},
if (a?.channel?.guild_id && !settings.store.colorInServers) return roleColor;
colorIfServer(context: any): string | undefined {
const userId = context?.message?.author?.id;
const colorString = context?.author?.colorString;
const color = getCustomColorString(a.message.author.id, true);
return color ?? roleColor ?? undefined;
}
if (context?.channel?.guild_id && !settings.store.colorInServers) return colorString;
const color = getCustomColorString(userId, true);
return color ?? colorString ?? undefined;
},
colorInReplyingTo(a: any) {
const { id } = a.reply.message.author;
return getCustomColorString(id, true);
},
});

View file

@ -1,62 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { zipSync } from "fflate";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
// Assuming "privacy" is the correct ID for the group you want to modify.
const group = findGroupChildrenByChildId("privacy", children);
if (group) {
group.push(
<Menu.MenuItem id="emoji.download" label="Download Emojis" action={() => zipServerEmojis(guild)}></Menu.MenuItem>
);
}
};
export default definePlugin({
name: "EmojiDumper",
description: "Context menu to dump and download a server's emojis.",
authors: [EquicordDevs.Cortex, Devs.Samwich, EquicordDevs.Woosh],
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
}
});
async function zipServerEmojis(guild: Guild) {
const emojis = Vencord.Webpack.Common.EmojiStore.getGuilds()[guild.id]?.emojis;
if (!emojis) {
return console.log("Server not found!");
}
const fetchEmojis = async e => {
const filename = e.id + (e.animated ? ".gif" : ".png");
const emoji = await fetch("https://cdn.discordapp.com/emojis/" + filename + "?size=512&quality=lossless").then(res => res.blob());
return { file: new Uint8Array(await emoji.arrayBuffer()), filename };
};
const emojiPromises = emojis.map(e => fetchEmojis(e));
Promise.all(emojiPromises)
.then(results => {
const emojis = zipSync(Object.fromEntries(results.map(({ file, filename }) => [filename, file])));
const blob = new Blob([emojis], { type: "application/zip" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${guild.name}-emojis.zip`;
link.click();
link.remove();
})
.catch(error => {
console.error(error);
});
}

View file

@ -12,7 +12,7 @@ import {
} from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import { sleep } from "@utils/misc";
import definePlugin from "@utils/types";
import {
@ -140,7 +140,7 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
export default definePlugin({
name: "Encryptcord",
description: "End-to-end encryption in Discord!",
authors: [Devs.Inbestigator],
authors: [Devs.Inbestigator, EquicordDevs.ItsAlex],
patches: [
{
find: "INTERACTION_APPLICATION_COMMAND_INVALID_VERSION",
@ -360,16 +360,26 @@ async function handleGroupData(groupData) {
// Handle joining group
async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) {
encryptcordGroupMembers[senderId] = { key: senderKey, parent: UserStore.getCurrentUser().id, child: null };
encryptcordGroupMembers[UserStore.getCurrentUser().id].child = senderId;
const currentUserId = UserStore.getCurrentUser().id;
if (!encryptcordGroupMembers[senderId]) {
encryptcordGroupMembers[senderId] = { key: senderKey, parent: currentUserId, child: null };
}
if (!encryptcordGroupMembers[currentUserId]) {
encryptcordGroupMembers[currentUserId] = { key: "", parent: null, child: null };
}
encryptcordGroupMembers[currentUserId].child = senderId;
await DataStore.set("encryptcordGroupMembers", encryptcordGroupMembers);
const groupChannel = await DataStore.get("encryptcordChannelId");
const newMember = await UserUtils.getUser(senderId).catch(() => null);
if (!newMember) return;
const membersData = {};
Object.entries(encryptcordGroupMembers)
.forEach(([memberId, value]) => {
Object.entries(encryptcordGroupMembers).forEach(([memberId, value]) => {
membersData[memberId] = value;
});
@ -382,6 +392,7 @@ async function handleJoin(senderId: string, senderKey: string, encryptcordGroupM
});
await Promise.all(dmPromises);
await MessageActions.receiveMessage(groupChannel, {
...await createMessage("", senderId, groupChannel, 7), components: [{
type: 1,

View file

@ -1 +0,0 @@
@import url("https://dablulite.github.io/css-snippets/BetterAuthApps/import.css");

View file

@ -1 +0,0 @@
@import url("https://dablulite.github.io/css-snippets/BetterStatusPicker/import.css");

View file

@ -1 +0,0 @@
@import url("https://raw.githubusercontent.com/gold-me/DiscordIcons/master/DiscordIcons.theme.css");

View file

@ -1,128 +0,0 @@
/* stylelint-disable selector-class-pattern */
/* stylelint-disable color-function-notation */
:root {
/* || Gradients */
--gradient-special: 140deg, hsl(245deg, calc(var(--saturaton-factor, 1)*79%), 72%) 0%, hsl(287deg, calc(var(--saturaton-factor, 1)*80%), 70%) 100%;
--gradient-blurple: 140deg, hsl(235deg, calc(var(--saturation-factor, 1)*85%), 72%) 0%, hsl(235deg, calc(var(--saturation-factor, 1)*85%), 60%) 100%;
--gradient-green: 140deg, hsl(139deg, calc(var(--saturaton-factor, 1)*47%), 44%) 0%, hsl(139deg, calc(var(--saturaton-factor, 1)*66%), 24%) 100%;
--gradient-yellow: 140deg, hsl(38deg, calc(var(--saturaton-factor, 1)*96%), 54%) 0%, hsl(38deg, calc(var(--saturaton-factor, 1)*82%), 41%) 100%;
--gradient-red: 140deg, hsl(359deg, calc(var(--saturaton-factor, 1)*83%), 59%) 0%, hsl(359deg, calc(var(--saturaton-factor, 1)*54%), 37%) 100%;
--gradient-grey: 140deg, hsl(214deg, calc(var(--saturaton-factor, 1)*10%), 50%) 0%, hsl(216deg, calc(var(--saturaton-factor, 1)*11%), 26%) 100%;
/* || Transitions */
--button-transition: 0.1s linear;
--font-default: 500;
--font-hover: 525;
--fontsize-hover: 15px;
--transform-normal: scale(1);
--transform-hover: scale(1.15);
--button-transform-hover: scale(1.04);
}
/* || Filled Buttons */
.lookFilled-yCfaCM {
transform: var(--transform-normal);
transition: var(--button-transition);
background: var(--gradient);
}
.lookFilled-yCfaCM:hover {
transform: var(--button-transform-hover);
}
.lookFilled-yCfaCM[disabled] {
transform: none;
}
.lookFilled-yCfaCM.colorBrand-I6CyqQ {
--gradient: linear-gradient(var(--gradient-blurple));
}
.lookFilled-yCfaCM.colorGreen-3y-Z79,
.lookFilled-yCfaCM.button_adcaac.buttonActive_adcaac {
--gradient: linear-gradient(var(--gradient-green));
}
.lookFilled-yCfaCM.colorYellow-Pgtmch {
--gradient: linear-gradient(var(--gradient-yellow));
}
.lookFilled-yCfaCM.colorRed-rQXKgM {
--gradient: linear-gradient(var(--gradient-red));
}
.lookFilled-yCfaCM.colorPrimary-2AuQVo,
.lookFilled-yCfaCM.colorGrey-2iAG-B,
.lookFilled-yCfaCM.buttonColor_adcaac {
--gradient: linear-gradient(var(--gradient-grey));
}
/* || Context Menus */
.menu_d90b3d .item-1OdjEX:not(.hideInteraction-2jPGL_) {
font-weight: var(--font-default);
transition: var(--button-transition);
}
.menu_d90b3d .item-1OdjEX:not(.hideInteraction-2jPGL_).focused-3qFvc8,
.menu_d90b3d .item-1OdjEX:not(.hideInteraction-2jPGL_):active {
font-size: var(--fontsize-hover);
font-weight: var(--font-hover);
background: var(--gradient);
}
.menu_d90b3d .colorDefault-CDqZdO.focused-3qFvc8,
.menu_d90b3d .colorDefault-CDqZdO:active {
--gradient: linear-gradient(var(--gradient-blurple));
}
.menu_d90b3d .colorDanger-3n-KnP.focused-3qFvc8,
.menu_d90b3d .colorDanger-3n-KnP:active,
.menu_d90b3d #status-picker-dnd.focused-3qFvc8,
.menu_d90b3d #status-picker-dnd:active {
--gradient: linear-gradient(var(--gradient-red));
}
.menu_d90b3d .colorPremium-vwmYZQ.focused-3qFvc8,
.menu_d90b3d .colorPremium-vwmYZQ:active {
--gradient: linear-gradient(var(--gradient-special));
}
.menu_d90b3d #status-picker-online.focused-3qFvc8,
.menu_d90b3d #status-picker-online:active {
--gradient: linear-gradient(var(--gradient-green));
}
.menu_d90b3d #status-picker-idle.focused-3qFvc8,
.menu_d90b3d #status-picker-idle:active {
--gradient: linear-gradient(var(--gradient-yellow));
}
.menu_d90b3d #status-picker-invisible.focused-3qFvc8,
.menu_d90b3d #status-picker-invisible:active {
--gradient: linear-gradient(var(--gradient-grey));
}
/* || Message Actions */
.wrapper_f7e168 .button_f7e168 {
background: var(--gradient);
}
.wrapper_f7e168 .button_f7e168 img,
.wrapper_f7e168 .button_f7e168 svg {
transition: var(--button-transition);
transform: var(--transform-normal);
}
.wrapper_f7e168 .button_f7e168:hover {
--gradient: linear-gradient(var(--gradient-blurple));
}
.wrapper_f7e168 .button_f7e168:hover svg {
transform: var(--transform-hover);
color: white;
}
.wrapper_f7e168 .button_f7e168.dangerous_f7e168:hover {
--gradient: linear-gradient(var(--gradient-red));
}

View file

@ -1,487 +0,0 @@
/* stylelint-disable property-no-vendor-prefix */
/* stylelint-disable selector-class-pattern */
:root {
--settingsicons: 1;
--si-size: 18px;
--si-gap: 14px;
--use-si: calc(var(--settingsicons, 1) / (var(--settingsicons, 1)));
--si-myaccount: url("https://minidiscordthemes.github.io/SettingsIcons/svg/myaccount.svg");
--si-profilecustomization: url("https://minidiscordthemes.github.io/SettingsIcons/svg/profilecustomization.svg");
--si-privacysafety: url("https://minidiscordthemes.github.io/SettingsIcons/svg/privacysafety.svg");
--si-familycenter: url("https://minidiscordthemes.github.io/SettingsIcons/svg/familycenter.svg");
--si-authorizedapps: url("https://minidiscordthemes.github.io/SettingsIcons/svg/authorizedapps.svg");
--si-sessions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/sessions.svg");
--si-connections: url("https://minidiscordthemes.github.io/SettingsIcons/svg/connections.svg");
--si-settingsclips: url("https://minidiscordthemes.github.io/SettingsIcons/svg/settingsclips.svg");
--si-friendrequests: url("https://minidiscordthemes.github.io/SettingsIcons/svg/friendrequests.svg");
--si-discordnitro: url("https://minidiscordthemes.github.io/SettingsIcons/svg/discordnitro.svg");
--si-nitroserverboost: url("https://minidiscordthemes.github.io/SettingsIcons/svg/nitroserverboost.svg");
--si-subscriptions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/subscriptions.svg");
--si-libraryinventory: url("https://minidiscordthemes.github.io/SettingsIcons/svg/libraryinventory.svg");
--si-billing: url("https://minidiscordthemes.github.io/SettingsIcons/svg/billing.svg");
--si-appearance: url("https://minidiscordthemes.github.io/SettingsIcons/svg/appearance.svg");
--si-accessibility: url("https://minidiscordthemes.github.io/SettingsIcons/svg/accessibility.svg");
--si-voicevideo: url("https://minidiscordthemes.github.io/SettingsIcons/svg/voicevideo.svg");
--si-textimages: url("https://minidiscordthemes.github.io/SettingsIcons/svg/textimages.svg");
--si-notifications: url("https://minidiscordthemes.github.io/SettingsIcons/svg/notifications.svg");
--si-keybinds: url("https://minidiscordthemes.github.io/SettingsIcons/svg/keybinds.svg");
--si-language: url("https://minidiscordthemes.github.io/SettingsIcons/svg/language.svg");
--si-windows: url("https://minidiscordthemes.github.io/SettingsIcons/svg/windows.svg");
--si-streamermode: url("https://minidiscordthemes.github.io/SettingsIcons/svg/streamermode.svg");
--si-rtcspeedtest: url("https://minidiscordthemes.github.io/SettingsIcons/svg/rtcspeedtest.svg");
--si-advanced: url("https://minidiscordthemes.github.io/SettingsIcons/svg/advanced.svg");
--si-activityprivacy: url("https://minidiscordthemes.github.io/SettingsIcons/svg/activityprivacy.svg");
--si-gameactivity: url("https://minidiscordthemes.github.io/SettingsIcons/svg/gameactivity.svg");
--si-overlay: url("https://minidiscordthemes.github.io/SettingsIcons/svg/overlay.svg");
--si-changelog: url("https://minidiscordthemes.github.io/SettingsIcons/svg/changelog.svg");
--si-merchandise: url("https://minidiscordthemes.github.io/SettingsIcons/svg/merchandise.svg");
--si-hypesquadonline: url("https://minidiscordthemes.github.io/SettingsIcons/svg/hypesquadonline.svg");
--si-powermodesettings: url("https://minidiscordthemes.github.io/SettingsIcons/svg/powermodesettings.svg");
--si-experiments: url("https://minidiscordthemes.github.io/SettingsIcons/svg/experiments.svg");
--si-developeroptions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/developeroptions.svg");
--si-hotspotoptions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/hotspotoptions.svg");
--si-dismissiblecontentoptions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/dismissiblecontentoptions.svg");
--si-startuptimings: url("https://minidiscordthemes.github.io/SettingsIcons/svg/startuptimings.svg");
--si-paymentflowmodals: url("https://minidiscordthemes.github.io/SettingsIcons/svg/paymentflowmodals.svg");
--si-textplayground: url("https://minidiscordthemes.github.io/SettingsIcons/svg/textplayground.svg");
--si-textcomponent: url("https://minidiscordthemes.github.io/SettingsIcons/svg/textcomponent.svg");
--si-logout: url("https://minidiscordthemes.github.io/SettingsIcons/svg/logout.svg");
--si-equicordsettings: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordsettings.svg");
--si-equicordplugins: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordplugins.svg");
--si-equicordthemes: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordthemes.svg");
--si-equicordupdater: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordupdater.svg");
--si-equicordcloud: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordcloud.svg");
--si-equicordsettingssync: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordsettingssync.svg");
--si-equicordpatchhelper: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vencordpatchhelper.svg");
--si-equibop: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vesktop.svg");
--si-vesktop: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vesktop.svg");
--si-overview: url("https://minidiscordthemes.github.io/SettingsIcons/svg/overview.svg");
--si-roles: url("https://minidiscordthemes.github.io/SettingsIcons/svg/roles.svg");
--si-emoji: url("https://minidiscordthemes.github.io/SettingsIcons/svg/emoji.svg");
--si-stickers: url("https://minidiscordthemes.github.io/SettingsIcons/svg/stickers.svg");
--si-soundboard: url("https://minidiscordthemes.github.io/SettingsIcons/svg/soundboard.svg");
--si-widget: url("https://minidiscordthemes.github.io/SettingsIcons/svg/widget.svg");
--si-guildtemplates: url("https://minidiscordthemes.github.io/SettingsIcons/svg/guildtemplates.svg");
--si-vanityurl: url("https://minidiscordthemes.github.io/SettingsIcons/svg/vanityurl.svg");
--si-integrations: url("https://minidiscordthemes.github.io/SettingsIcons/svg/integrations.svg");
--si-appdirectory: url("https://minidiscordthemes.github.io/SettingsIcons/svg/appdirectory.svg");
--si-safety: url("https://minidiscordthemes.github.io/SettingsIcons/svg/safety.svg");
--si-auditlog: url("https://minidiscordthemes.github.io/SettingsIcons/svg/auditlog.svg");
--si-bans: url("https://minidiscordthemes.github.io/SettingsIcons/svg/bans.svg");
--si-community: url("https://minidiscordthemes.github.io/SettingsIcons/svg/community.svg");
--si-onboarding: url("https://minidiscordthemes.github.io/SettingsIcons/svg/onboarding.svg");
--si-analytics: url("https://minidiscordthemes.github.io/SettingsIcons/svg/analytics.svg");
--si-partner: url("https://minidiscordthemes.github.io/SettingsIcons/svg/partner.svg");
--si-discovery: url("https://minidiscordthemes.github.io/SettingsIcons/svg/discovery.svg");
--si-rolesubscriptions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/rolesubscriptions.svg");
--si-guildpremium: url("https://minidiscordthemes.github.io/SettingsIcons/svg/guildpremium.svg");
--si-members: url("https://minidiscordthemes.github.io/SettingsIcons/svg/members.svg");
--si-instantinvites: url("https://minidiscordthemes.github.io/SettingsIcons/svg/instantinvites.svg");
--si-delete: url("https://minidiscordthemes.github.io/SettingsIcons/svg/delete.svg");
--si-permissions: url("https://minidiscordthemes.github.io/SettingsIcons/svg/permissions.svg");
--si-default: url("https://minidiscordthemes.github.io/SettingsIcons/svg/default.svg");
}
.sidebarRegion_c25c6d {
flex-basis: calc(218px + var(--use-si)*(var(--si-size) + var(--si-gap))) !important
}
.sidebar_c25c6d {
width: calc(218px + var(--use-si)*(var(--si-size) + var(--si-gap))) !important
}
.sidebar_c25c6d :is(.item_a0 .icon_f7189e, .premiumLabel_ae3c77>svg, .premiumLabel_ae3c77 img, .tabBarItemContainer_e7c031>svg, .tabBarItemContainer_e7c031 img) {
transform: scaleX(calc(1 - var(--use-si)))
}
.sidebar_c25c6d .side_a0 .item_a0 {
display: flex;
align-items: center
}
.sidebar_c25c6d .side_a0 .item_a0::before {
content: "";
flex: 0 0 auto;
width: calc(var(--use-si)*var(--si-size));
height: calc(var(--use-si)*var(--si-size));
margin-right: calc(var(--use-si)*var(--si-size)/2);
background: currentcolor;
z-index: 2;
-webkit-mask: var(--si-default) center/contain no-repeat;
mask: var(--si-default) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="My Account"]::before {
-webkit-mask: var(--si-myaccount) center/contain no-repeat;
mask: var(--si-myaccount) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Profile Customization"]::before {
-webkit-mask: var(--si-profilecustomization) center/contain no-repeat;
mask: var(--si-profilecustomization) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Privacy & Safety"]::before {
-webkit-mask: var(--si-privacysafety) center/contain no-repeat;
mask: var(--si-privacysafety) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Family Center"]::before {
-webkit-mask: var(--si-familycenter) center/contain no-repeat;
mask: var(--si-familycenter) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Authorized Apps"]::before {
-webkit-mask: var(--si-authorizedapps) center/contain no-repeat;
mask: var(--si-authorizedapps) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Sessions"]::before {
-webkit-mask: var(--si-sessions) center/contain no-repeat;
mask: var(--si-sessions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Connections"]::before {
-webkit-mask: var(--si-connections) center/contain no-repeat;
mask: var(--si-connections) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Settings Clips"]::before {
-webkit-mask: var(--si-settingsclips) center/contain no-repeat;
mask: var(--si-settingsclips) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Friend Requests"]::before {
-webkit-mask: var(--si-friendrequests) center/contain no-repeat;
mask: var(--si-friendrequests) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Discord Nitro"]::before {
-webkit-mask: var(--si-discordnitro) center/contain no-repeat;
mask: var(--si-discordnitro) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Nitro Server Boost"]::before {
-webkit-mask: var(--si-nitroserverboost) center/contain no-repeat;
mask: var(--si-nitroserverboost) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Subscriptions"]::before {
-webkit-mask: var(--si-subscriptions) center/contain no-repeat;
mask: var(--si-subscriptions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Library Inventory"]::before {
-webkit-mask: var(--si-libraryinventory) center/contain no-repeat;
mask: var(--si-libraryinventory) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Billing"]::before {
-webkit-mask: var(--si-billing) center/contain no-repeat;
mask: var(--si-billing) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Appearance"]::before {
-webkit-mask: var(--si-appearance) center/contain no-repeat;
mask: var(--si-appearance) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Accessibility"]::before {
-webkit-mask: var(--si-accessibility) center/contain no-repeat;
mask: var(--si-accessibility) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Voice & Video"]::before {
-webkit-mask: var(--si-voicevideo) center/contain no-repeat;
mask: var(--si-voicevideo) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Text & Images"]::before {
-webkit-mask: var(--si-textimages) center/contain no-repeat;
mask: var(--si-textimages) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Notifications"]::before {
-webkit-mask: var(--si-notifications) center/contain no-repeat;
mask: var(--si-notifications) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Keybinds"]::before {
-webkit-mask: var(--si-keybinds) center/contain no-repeat;
mask: var(--si-keybinds) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Language"]::before {
-webkit-mask: var(--si-language) center/contain no-repeat;
mask: var(--si-language) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Windows"]::before {
-webkit-mask: var(--si-windows) center/contain no-repeat;
mask: var(--si-windows) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Streamer Mode"]::before {
-webkit-mask: var(--si-streamermode) center/contain no-repeat;
mask: var(--si-streamermode) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="RTC Speed Test"]::before {
-webkit-mask: var(--si-rtcspeedtest) center/contain no-repeat;
mask: var(--si-rtcspeedtest) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Advanced"]::before {
-webkit-mask: var(--si-advanced) center/contain no-repeat;
mask: var(--si-advanced) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Activity Privacy"]::before {
-webkit-mask: var(--si-activityprivacy) center/contain no-repeat;
mask: var(--si-activityprivacy) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Game Activity"]::before {
-webkit-mask: var(--si-gameactivity) center/contain no-repeat;
mask: var(--si-gameactivity) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Overlay"]::before {
-webkit-mask: var(--si-overlay) center/contain no-repeat;
mask: var(--si-overlay) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="changelog"]::before {
-webkit-mask: var(--si-changelog) center/contain no-repeat;
mask: var(--si-changelog) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="merchandise"]::before {
-webkit-mask: var(--si-merchandise) center/contain no-repeat;
mask: var(--si-merchandise) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Hypesquad Online"]::before {
-webkit-mask: var(--si-hypesquadonline) center/contain no-repeat;
mask: var(--si-hypesquadonline) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Powermode Settings"]::before {
-webkit-mask: var(--si-powermodesettings) center/contain no-repeat;
mask: var(--si-powermodesettings) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Experiments"]::before {
-webkit-mask: var(--si-experiments) center/contain no-repeat;
mask: var(--si-experiments) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Developer Options"]::before {
-webkit-mask: var(--si-developeroptions) center/contain no-repeat;
mask: var(--si-developeroptions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Hotspot Options"]::before {
-webkit-mask: var(--si-hotspotoptions) center/contain no-repeat;
mask: var(--si-hotspotoptions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Dismissible Content Options"]::before {
-webkit-mask: var(--si-dismissiblecontentoptions) center/contain no-repeat;
mask: var(--si-dismissiblecontentoptions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="StartupTimings"]::before {
-webkit-mask: var(--si-startuptimings) center/contain no-repeat;
mask: var(--si-startuptimings) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Payment Flow Modals"]::before {
-webkit-mask: var(--si-paymentflowmodals) center/contain no-repeat;
mask: var(--si-paymentflowmodals) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Text Playground"]::before {
-webkit-mask: var(--si-textplayground) center/contain no-repeat;
mask: var(--si-textplayground) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Text Component"]::before {
-webkit-mask: var(--si-textcomponent) center/contain no-repeat;
mask: var(--si-textcomponent) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="logout"]::before {
-webkit-mask: var(--si-logout) center/contain no-repeat;
mask: var(--si-logout) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordSettings"]::before {
-webkit-mask: var(--si-equicordsettings) center/contain no-repeat;
mask: var(--si-equicordsettings) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordPlugins"]::before {
-webkit-mask: var(--si-equicordplugins) center/contain no-repeat;
mask: var(--si-equicordplugins) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordThemes"]::before {
-webkit-mask: var(--si-equicordthemes) center/contain no-repeat;
mask: var(--si-equicordthemes) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordUpdater"]::before {
-webkit-mask: var(--si-equicordupdater) center/contain no-repeat;
mask: var(--si-equicordupdater) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordCloud"]::before {
-webkit-mask: var(--si-equicordcloud) center/contain no-repeat;
mask: var(--si-equicordcloud) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordSettingsSync"]::before {
-webkit-mask: var(--si-equicordsettingssync) center/contain no-repeat;
mask: var(--si-equicordsettingssync) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EquicordPatchHelper"]::before {
-webkit-mask: var(--si-equicordpatchhelper) center/contain no-repeat;
mask: var(--si-equicordpatchhelper) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Equibop"]::before {
-webkit-mask: var(--si-equibop) center/contain no-repeat;
mask: var(--si-equibop) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="Vesktop"]::before {
-webkit-mask: var(--si-vesktop) center/contain no-repeat;
mask: var(--si-vesktop) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="OVERVIEW"]::before {
-webkit-mask: var(--si-overview) center/contain no-repeat;
mask: var(--si-overview) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="ROLES"]::before {
-webkit-mask: var(--si-roles) center/contain no-repeat;
mask: var(--si-roles) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="EMOJI"]::before {
-webkit-mask: var(--si-emoji) center/contain no-repeat;
mask: var(--si-emoji) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="STICKERS"]::before {
-webkit-mask: var(--si-stickers) center/contain no-repeat;
mask: var(--si-stickers) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="SOUNDBOARD"]::before {
-webkit-mask: var(--si-soundboard) center/contain no-repeat;
mask: var(--si-soundboard) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="WIDGET"]::before {
-webkit-mask: var(--si-widget) center/contain no-repeat;
mask: var(--si-widget) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="GUILD_TEMPLATES"]::before {
-webkit-mask: var(--si-guildtemplates) center/contain no-repeat;
mask: var(--si-guildtemplates) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="VANITY_URL"]::before {
-webkit-mask: var(--si-vanityurl) center/contain no-repeat;
mask: var(--si-vanityurl) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="INTEGRATIONS"]::before {
-webkit-mask: var(--si-integrations) center/contain no-repeat;
mask: var(--si-integrations) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="APP_DIRECTORY"]::before {
-webkit-mask: var(--si-appdirectory) center/contain no-repeat;
mask: var(--si-appdirectory) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="SAFETY"]::before {
-webkit-mask: var(--si-safety) center/contain no-repeat;
mask: var(--si-safety) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="AUDIT_LOG"]::before {
-webkit-mask: var(--si-auditlog) center/contain no-repeat;
mask: var(--si-auditlog) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="BANS"]::before {
-webkit-mask: var(--si-bans) center/contain no-repeat;
mask: var(--si-bans) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="COMMUNITY"]::before {
-webkit-mask: var(--si-community) center/contain no-repeat;
mask: var(--si-community) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="ONBOARDING"]::before {
-webkit-mask: var(--si-onboarding) center/contain no-repeat;
mask: var(--si-onboarding) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="ANALYTICS"]::before {
-webkit-mask: var(--si-analytics) center/contain no-repeat;
mask: var(--si-analytics) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="PARTNER"]::before {
-webkit-mask: var(--si-partner) center/contain no-repeat;
mask: var(--si-partner) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="DISCOVERY"]::before {
-webkit-mask: var(--si-discovery) center/contain no-repeat;
mask: var(--si-discovery) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="ROLE_SUBSCRIPTIONS"]::before {
-webkit-mask: var(--si-rolesubscriptions) center/contain no-repeat;
mask: var(--si-rolesubscriptions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="GUILD_PREMIUM"]::before {
-webkit-mask: var(--si-guildpremium) center/contain no-repeat;
mask: var(--si-guildpremium) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="MEMBERS"]::before {
-webkit-mask: var(--si-members) center/contain no-repeat;
mask: var(--si-members) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="INSTANT_INVITES"]::before {
-webkit-mask: var(--si-instantinvites) center/contain no-repeat;
mask: var(--si-instantinvites) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="DELETE"]::before {
-webkit-mask: var(--si-delete) center/contain no-repeat;
mask: var(--si-delete) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0[data-tab-id="PERMISSIONS"]::before {
-webkit-mask: var(--si-permissions) center/contain no-repeat;
mask: var(--si-permissions) center/contain no-repeat
}
.sidebar_c25c6d .side_a0 .item_a0>div {
flex: 1 1 auto
}

View file

@ -1 +0,0 @@
@import url("https://raw.githubusercontent.com/coolesding/snippets/main/import/fixnitrothemes.css");

View file

@ -1,9 +0,0 @@
@import url("https://raw.githubusercontent.com/Equicord/Equicord/main/src/equicordplugins/equicordCSS/css/main.min.css");
/* https://github.com/MiniDiscordThemes/SettingsIcons#customisation */
:root {
--settingsicons: 1;
--si-size: 18px;
--si-gap: 14px;
}

View file

@ -1 +0,0 @@
@import url("https://dablulite.github.io/css-snippets/UserReimagined/import.css");

View file

@ -1,124 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Import required modules and components
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
// Importing the style managed fixes on and off switch
import betterauthapps from "./css/betterauthapps.css?managed";
import betterstatuspicker from "./css/betterstatuspicker.css?managed";
import discordicons from "./css/discordicons.css?managed";
import gradientbuttons from "./css/gradientbuttons.css?managed";
import nitrothemesfix from "./css/nitrothemesfix.css?managed";
import settingsicons from "./css/settingsicons.css?managed";
import userreimagined from "./css/userreimagined.css?managed";
// Forcing restartNeeded: true to not overcomplicate the live update of the settings using FluxDispatcher and making it complex
const settings = definePluginSettings({
betterAuthApps: {
type: OptionType.BOOLEAN,
description: "Enable Better Auth Apps CSS",
restartNeeded: true,
default: false
},
betterStatusPicker: {
type: OptionType.BOOLEAN,
description: "Enable Better Status Picker CSS",
restartNeeded: true,
default: false
},
discordicons: {
type: OptionType.BOOLEAN,
description: "Enable Discord Icons CSS",
restartNeeded: true,
default: false
},
gradientButtons: {
type: OptionType.BOOLEAN,
description: "Enable Gradient Buttons CSS",
restartNeeded: true,
default: false
},
nitroThemesFix: {
type: OptionType.BOOLEAN,
description: "Enable Fix Nitro Themes CSS",
restartNeeded: true,
default: false
},
settingsIcons: {
type: OptionType.BOOLEAN,
description: "Enable Settings Icons CSS",
restartNeeded: true,
default: false
},
userReimagined: {
type: OptionType.BOOLEAN,
description: "Enable User Reimagined CSS",
restartNeeded: true,
default: false
}
});
let settingsArray: Array<any> = [];
let cssArray: Array<any> = [];
export default definePlugin({
name: "EquicordCSS",
description: "CSS for Equicord users. You will need to look at the settings.",
authors: [EquicordDevs.thororen, EquicordDevs.Panniku],
dependencies: ["ThemeAttributes"],
settings,
start() {
// Push variables to array to iterate on start() and stop()
settingsArray.push(
settings.store.betterAuthApps,
settings.store.betterStatusPicker,
settings.store.discordicons,
settings.store.gradientButtons,
settings.store.nitroThemesFix,
settings.store.settingsIcons,
settings.store.userReimagined
);
cssArray.push(
betterauthapps,
betterstatuspicker,
discordicons,
gradientbuttons,
nitrothemesfix,
settingsicons,
userreimagined
);
settingsArray.forEach((s, i) => {
if (s) enableStyle(cssArray[i]);
});
},
stop() {
settingsArray.forEach((s, i) => {
if (s) disableStyle(cssArray[i]);
});
settingsArray = [];
cssArray = [];
}
});

View file

@ -24,7 +24,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common";
import { Menu, Popout, useRef, useState } from "@webpack/common";
import type { ReactNode } from "react";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
@ -116,6 +116,7 @@ function VencordPopoutIcon() {
}
function VencordPopoutButton() {
const buttonRef = useRef(null);
const [show, setShow] = useState(false);
return (
@ -126,10 +127,12 @@ function VencordPopoutButton() {
animation={Popout.Animation.NONE}
shouldShow={show}
onRequestClose={() => setShow(false)}
targetElementRef={buttonRef}
renderPopout={() => VencordPopout(() => setShow(false))}
>
{(_, { isShown }) => (
<HeaderBarIcon
ref={buttonRef}
className="vc-toolbox-btn"
onClick={() => setShow(v => !v)}
tooltip={isShown ? null : "Equicord Toolbox"}

View file

@ -19,9 +19,10 @@
import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { copyToClipboard } from "@utils/clipboard";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Clipboard, Toasts } from "@webpack/common";
import { Toasts } from "@webpack/common";
interface User {
id: string;
@ -97,7 +98,7 @@ export default definePlugin({
copyContactToClipboard() {
if (this.contactList) {
Clipboard.copy(JSON.stringify(this.contactList));
copyToClipboard(JSON.stringify(this.contactList));
Toasts.show({
message: "Contacts copied to clipboard successfully.",
type: Toasts.Type.SUCCESS,

View file

@ -5,16 +5,17 @@
*/
import { Text, Tooltip } from "@webpack/common";
import type { ComponentProps } from "react";
import type { ComponentProps, RefObject } from "react";
export interface BuilderButtonProps {
label?: string | undefined;
tooltip?: string | undefined;
selectedStyle?: ComponentProps<"div">["style"];
buttonProps?: ComponentProps<"div"> | undefined;
buttonRef?: RefObject<null>;
}
export const BuilderButton = ({ label, tooltip, selectedStyle, buttonProps }: BuilderButtonProps) => (
export const BuilderButton = ({ buttonRef, label, tooltip, selectedStyle, buttonProps }: BuilderButtonProps) => (
<Tooltip text={tooltip} shouldShow={!!tooltip}>
{tooltipProps => (
<div style={{ width: "60px" }}>
@ -32,6 +33,7 @@ export const BuilderButton = ({ label, tooltip, selectedStyle, buttonProps }: Bu
borderRadius: "4px",
cursor: "pointer"
}}
ref={buttonRef}
>
{!selectedStyle && (
<svg

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Popout } from "@webpack/common";
import { Popout, useRef } from "@webpack/common";
import { BuilderButton, type BuilderButtonProps, CustomColorPicker, type CustomColorPickerProps } from ".";
@ -13,9 +13,12 @@ export interface BuilderColorButtonProps extends Pick<BuilderButtonProps, "label
setColor: (color: number | null) => void;
}
export const BuilderColorButton = ({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) => (
export function BuilderColorButton({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) {
const buttonRef = useRef(null);
return (
<Popout
position="bottom"
targetElementRef={buttonRef}
renderPopout={() => (
<CustomColorPicker
value={color}
@ -34,8 +37,10 @@ export const BuilderColorButton = ({ label, color, setColor, suggestedColors }:
tooltip={hexColor}
selectedStyle={hexColor ? { background: hexColor } : undefined}
buttonProps={popoutProps}
buttonRef={buttonRef}
/>
);
}}
</Popout>
);
}

View file

@ -0,0 +1,83 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Constants, PermissionsBits, PermissionStore, React, RestAPI, useCallback, useEffect, useState } from "@webpack/common";
const showIcon = () => {
const [show, setShow] = useState(false);
const handleKeys = useCallback(e => {
const keysHeld = e.ctrlKey && e.altKey;
setShow(keysHeld);
}, []);
useEffect(() => {
window.addEventListener("keydown", handleKeys);
window.addEventListener("keyup", handleKeys);
return () => {
window.removeEventListener("keydown", handleKeys);
window.removeEventListener("keyup", handleKeys);
};
}, [handleKeys]);
return show;
};
export default definePlugin({
name: "FastDeleteChannels",
description: "Adds a trash icon to delete channels when holding ctrl + alt",
authors: [EquicordDevs.thororen],
patches: [
// TY TypingIndicator
{
find: "UNREAD_IMPORTANT:",
replacement: {
match: /\.name,{.{0,140}\.children.+?:null(?<=,channel:(\i).+?)/,
replace: "$&,$self.TrashIcon($1)"
}
},
{
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
replacement: {
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
replace: "$&,$self.TrashIcon($1)"
}
}
],
TrashIcon: channel => {
const show = showIcon();
if (!show || !PermissionStore.can(PermissionsBits.MANAGE_CHANNELS, channel)) return null;
return (
<span
onClick={() => RestAPI.del({ url: Constants.Endpoints.CHANNEL(channel.id) })}
>
<svg
width="16"
height="16"
fill="none"
viewBox="0 0 24 24"
color="#ed4245"
>
<path
fill="currentColor"
d="M14.25 1c.41 0 .75.34.75.75V3h5.25c.41 0 .75.34.75.75v.5c0 .41-.34.75-.75.75H3.75A.75.75 0 0 1 3 4.25v-.5c0-.41.34-.75.75-.75H9V1.75c0-.41.34-.75.75-.75h4.5Z"
/>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M5.06 7a1 1 0 0 0-1 1.06l.76 12.13a3 3 0 0 0 3 2.81h8.36a3 3 0 0 0 3-2.81l.75-12.13a1 1 0 0 0-1-1.06H5.07ZM11 12a1 1 0 1 0-2 0v6a1 1 0 1 0 2 0v-6Zm3-1a1 1 0 1 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1Z"
/>
</svg>
</span>
);
}
});

View file

@ -21,7 +21,7 @@ import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, MessageStore, ReactDOM, Toasts } from "@webpack/common";
import { ChannelStore, createRoot, MessageStore, Toasts } from "@webpack/common";
import Message from "discord-types/general/Message";
import { Root } from "react-dom/client";
@ -101,7 +101,7 @@ export default definePlugin({
madeComponent = true;
element = document.createElement("div");
document.querySelector("[class^=base_]")!.appendChild(element);
root = ReactDOM.createRoot(element);
root = createRoot(element);
}
root!.render(<ReplyNavigator replies={replies} />);
}

View file

@ -34,11 +34,10 @@ export default definePlugin({
patches: [
// Taken from AnonymiseFileNames
{
find: "instantBatchUpload:",
find: 'type:"UPLOAD_START"',
replacement: {
match: /uploadFiles:(\i),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.fixExt(f)),$1(...args)),",
match: /await \i\.uploadFiles\((\i),/,
replace: "$1.forEach($self.fixExt),$&"
},
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
},

View file

@ -0,0 +1,53 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "discord-types/general";
// Taken From Signature :)
const settings = definePluginSettings({
forwardPreface: {
description: "What should forwarded from be prefaced with",
type: OptionType.SELECT,
options: [
{ label: ">", value: ">", default: true },
{ label: "-#", value: "-#" }
]
}
});
export default definePlugin({
name: "ForwardAnywhere",
description: "If a forward fails send it as a normal message also allows nsfw forwards",
authors: [EquicordDevs.thororen],
settings,
patches: [
{
find: "#{intl::MESSAGE_FORWARDING_NSFW_NOT_ALLOWED}",
replacement: {
match: /if\((\i)\.isNSFW\(\)&&.{0,25}\)\)\)/,
replace: "if(false)",
}
},
{
find: "#{intl::MESSAGE_ACTION_FORWARD_TO}",
replacement: {
match: /(?<=let (\i)=.{0,25}rejected.{0,25}\);)(?=.{0,25}message:(\i))/,
replace: "if ($1) return $self.sendForward($1,$2);",
}
},
],
sendForward(channels: any, message: Message) {
for (const c of channels) {
sendMessage(c.id, {
content: `${message.content}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>`
});
}
}
});

View file

@ -6,8 +6,9 @@
import "./styles.css";
import { copyToClipboard } from "@utils/clipboard";
import { findByPropsLazy } from "@webpack";
import { Button, Clipboard, Flex, Forms, Parser, Text, useEffect, useState } from "@webpack/common";
import { Button, Flex, Forms, Parser, Text, useEffect, useState } from "@webpack/common";
import { FriendInvite } from "./types";
@ -51,7 +52,7 @@ function FriendInviteCard({ invite }: { invite: FriendInvite; }) {
<CopyButton
copyText="Copy"
copiedText="Copied!"
onClick={() => Clipboard.copy(`https://discord.gg/${invite.code}`)}
onClick={() => copyToClipboard(`https://discord.gg/${invite.code}`)}
/>
</Flex>
</Flex>

View file

@ -17,9 +17,10 @@ export default definePlugin({
{
find: "#{intl::ADD_FRIEND})}),(",
replacement: {
match: /\.Fragment[^]*?children:\[[^]*?}\)/,
match: /header,children:\[.*?\{\}\)/,
replace: "$&,$self.FriendCodesPanel"
}
},
noWarn: true,
}
],

View file

@ -10,10 +10,11 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { copyToClipboard } from "@utils/clipboard";
import { Devs, EquicordDevs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Alerts, Button, Clipboard, ContextMenuApi, FluxDispatcher, Forms, Menu, React, showToast, TextInput, Toasts, useCallback, useState } from "@webpack/common";
import { Alerts, Button, ContextMenuApi, FluxDispatcher, Forms, Menu, React, showToast, TextInput, Toasts, useCallback, useState } from "@webpack/common";
import { addToCollection, cache_collections, createCollection, DATA_COLLECTION_NAME, deleteCollection, fixPrefix, getCollections, getGifById, getItemCollectionNameFromId, moveGifToCollection, refreshCacheCollection, removeFromCollection, renameCollection } from "./utils/collectionManager";
import { getFormat } from "./utils/getFormat";
@ -526,7 +527,7 @@ const RemoveItemContextMenu = ({ type, nameOrId, instance }) => (
action={() => {
const gifInfo = getGifById(nameOrId);
if (!gifInfo) return;
Clipboard.copy(gifInfo.url);
copyToClipboard(gifInfo.url);
showToast("URL copied to clipboard", Toasts.Type.SUCCESS);
}}
/>

View file

@ -76,7 +76,7 @@ export default definePlugin({
}
},
{
find: "action:\"PRESS_APP_CONNECTION\"",
find: "#{intl::CONNECTIONS}),scrollIntoView",
replacement: {
match: /(?<=user:(\i).{0,15}displayProfile:(\i).*?CONNECTIONS.{0,100}\}\)\}\))/,
replace: ",$self.ProfilePopoutComponent({ user: $1, displayProfile: $2 })"

View file

@ -5,10 +5,11 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { copyToClipboard } from "@utils/clipboard";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Clipboard, Forms, TextInput, Toasts, useState } from "@webpack/common";
import { Button, Forms, TextInput, Toasts, useState } from "@webpack/common";
import { darkenColorHex, generateRandomColorHex, saturateColorHex } from "./generateTheme";
import { themes } from "./themeDefinitions";
@ -58,10 +59,7 @@ function copyPreset(name: string) {
name: "${name}"
}
`;
if (Clipboard.SUPPORTS_COPY) {
Clipboard.copy(template);
}
copyToClipboard(template);
}
function CopyPresetComponent() {
@ -229,9 +227,7 @@ export function ColorPick({ propertyname }: { propertyname: string; }) {
function copyCSS() {
if (Clipboard.SUPPORTS_COPY) {
Clipboard.copy(getCSS(parseFontContent()));
}
copyToClipboard(getCSS(parseFontContent()));
}
function parseFontContent() {

View file

@ -0,0 +1,75 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { EmojiStore, Menu, StickersStore } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { zipSync } from "fflate";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
// Assuming "privacy" is the correct ID for the group you want to modify.
const group = findGroupChildrenByChildId("privacy", children);
if (group) {
group.push(
<>
<Menu.MenuItem id="emoji.download" label="Download Emojis" action={() => zipGuildAssets(guild, "emojis")}></Menu.MenuItem>
<Menu.MenuItem id="sticker.download" label="Download Stickers" action={() => zipGuildAssets(guild, "stickers")}></Menu.MenuItem>
</>
);
}
};
async function zipGuildAssets(guild: Guild, type: "emojis" | "stickers") {
const isEmojis = type === "emojis";
const items = isEmojis
? EmojiStore.getGuilds()[guild.id]?.emojis
: StickersStore.getStickersByGuildId(guild.id);
if (!items) {
return console.log("Server not found!");
}
const fetchAsset = async e => {
const ext = e.animated ? ".gif" : ".png";
const filename = e.id + ext;
const url = isEmojis
? `https://${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/emojis/${filename}?size=512&quality=lossless`
: `https://${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${filename}?size=4096&lossless=true`;
const response = await fetch(url);
const blob = await response.blob();
return { file: new Uint8Array(await blob.arrayBuffer()), filename };
};
const assetPromises = items.map(e => fetchAsset(e));
Promise.all(assetPromises)
.then(results => {
const zipped = zipSync(Object.fromEntries(results.map(({ file, filename }) => [filename, file])));
const blob = new Blob([zipped], { type: "application/zip" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${guild.name}-${type}.zip`;
link.click();
link.remove();
})
.catch(console.error);
}
migratePluginSettings("GuildPickerDumper", "EmojiDumper");
export default definePlugin({
name: "GuildPickerDumper",
description: "Context menu to dump and download a server's emojis and stickers.",
authors: [EquicordDevs.Cortex, Devs.Samwich, EquicordDevs.Synth, EquicordDevs.thororen],
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
}
});

View file

@ -24,7 +24,7 @@ export const HiddenServersStore = proxyLazyWebpack(() => {
// id try to use .initialize() but i dont know how it works
public async load() {
const data = await DataStore.get(DB_KEY);
if (data) {
if (data && data instanceof Set) {
this._hiddenGuilds = data;
}
}

View file

@ -77,8 +77,8 @@ export default definePlugin({
find: '("guildsnav")',
replacement: [
{
match: /(?<=#{intl::SERVERS}\),children:)(\i)(\)?\.map\(\i\))/g,
replace: "$self.useFilteredGuilds($1)$2",
match: /(?<=#{intl::SERVERS}\),gap:"xs",children:.{0,100}?)(\i)(\.map\(.{5,30}\}\))/,
replace: "$self.useFilteredGuilds($1)$2"
},
// despite my best efforts, the above doesnt trigger a rerender
{

View file

@ -6,7 +6,7 @@
import { classes } from "@utils/misc";
import { findByCode } from "@webpack";
import { Button, Clickable, Menu, Popout, React } from "@webpack/common";
import { Button, Clickable, Menu, Popout, React, useRef } from "@webpack/common";
import { SvgOverFlowIcon } from "../icons/overFlowIcon";
@ -101,6 +101,8 @@ export function NoteBookTabs({ tabs, selectedTabId, onSelectTab }: { tabs: strin
);
}, [tabs, selectedTabId, onSelectTab, overflowedTabs]);
const buttonRef = useRef(null);
return (
<div
className={classes("vc-notebook-tabbar")}
@ -137,9 +139,11 @@ export function NoteBookTabs({ tabs, selectedTabId, onSelectTab }: { tabs: strin
position="bottom"
align="right"
spacing={0}
targetElementRef={buttonRef}
>
{props => (
<Button
ref={buttonRef}
{...props}
className={"vc-notebook-overflow-chevron"}
size={Button.Sizes.ICON}

View file

@ -4,10 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { copyToClipboard } from "@utils/clipboard";
import { classes } from "@utils/misc";
import { ModalProps } from "@utils/modal";
import { findByCode, findByCodeLazy, findByProps, findComponentByCodeLazy } from "@webpack";
import { Clipboard, ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React } from "@webpack/common";
import noteHandler from "../../NoteHandler";
import { HolyNotes } from "../../types";
@ -139,13 +140,13 @@ const NoteContextMenu = (
<Menu.MenuItem
label="Copy Text"
id="copy-text"
action={() => Clipboard.copy(note.content)}
action={() => copyToClipboard(note.content)}
/>
{note?.attachments.length ? (
<Menu.MenuItem
label="Copy Attachment URL"
id="copy-url"
action={() => Clipboard.copy(note.attachments[0].url)}
action={() => copyToClipboard(note.attachments[0].url)}
/>) : null}
<Menu.MenuItem
color="danger"
@ -181,7 +182,7 @@ const NoteContextMenu = (
<Menu.MenuItem
label="Copy ID"
id="copy-id"
action={() => Clipboard.copy(note.id)}
action={() => copyToClipboard(note.id)}
/>
</Menu.Menu>
);

View file

@ -5,6 +5,7 @@
*/
import { CodeBlock } from "@components/CodeBlock";
import { copyToClipboard } from "@utils/clipboard";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import {
@ -41,9 +42,7 @@ function ModalComponent(props: { func: Function; iconName: string; color: number
color={Button.Colors.PRIMARY}
className={"vc-iv-raw-modal-copy-button"}
onClick={() => {
// silly typescript
// @ts-ignore
Clipboard.copy(String(func));
copyToClipboard(String(func));
Toasts.show({
id: Toasts.genId(),
message: `Copied raw \`${iconName}\` to clipboard`,

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { copyToClipboard } from "@utils/clipboard";
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
@ -27,8 +28,7 @@ export type ClickableProps<T extends "a" | "div" | "span" | "li" = "div"> = Prop
export function IconTooltip({ children, copy, className, ...props }: ClickableProps & { children: string; copy: string; }) {
return <TooltipContainer text={"Click to copy"} className={className}>
<Clickable onClick={() => {
// @ts-ignore
Clipboard.copy(copy);
copyToClipboard(copy);
}} {...props}>{children}</Clickable>
</TooltipContainer>;
}

View file

@ -6,7 +6,7 @@
import { saveFile } from "@utils/web";
import { filters, findAll, findByPropsLazy, waitFor } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import { createRoot, React, ReactDOM } from "@webpack/common";
import * as t from "@webpack/types";
export let _cssColors: string[] = [];
export type IconsDef = { [k: string]: t.Icon; };
@ -82,7 +82,7 @@ export function saveIcon(iconName: string, icon: EventTarget & SVGSVGElement | E
export function convertComponentToHtml(component?: React.ReactElement): string {
const container = document.createElement("div");
const root = ReactDOM.createRoot(container);
const root = createRoot(container);
ReactDOM.flushSync(() => root.render(component));
const content = container.innerHTML;

View file

@ -9,14 +9,11 @@ import "./styles.css";
import { EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { findStoreLazy } from "@webpack";
import { StickersStore } from "@webpack/common";
import { getMimeType, isLinkAnImage, settings, stripDiscordParams } from "./settings";
const logger = new Logger("ImagePreview", "#FFFFFF");
const StickerStore = findStoreLazy("StickersStore") as {
getStickerById(id: string): any;
};
let currentPreview: HTMLDivElement | null = null;
let currentPreviewFile: HTMLImageElement | HTMLVideoElement | null = null;
@ -124,7 +121,7 @@ function loadImagePreview(url: string, sticker: boolean) {
if (sticker) {
const stickerId = url.split("/").pop()?.split(".")[0] ?? null;
const stickerData = stickerId ? StickerStore.getStickerById(stickerId) : null;
const stickerData = stickerId ? StickersStore.getStickerById(stickerId) : null;
if (stickerData) {
switch (stickerData.type) {

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common";
const settings = definePluginSettings({
showIcon: {
type: OptionType.BOOLEAN,
default: true,
description: "Show a button to toggle the Ingtoninator plugin",
restartNeeded: true
},
isEnabled: {
type: OptionType.BOOLEAN,
default: true,
description: "Enable or disable the Ingtoninator"
}
});
const isLegal = (word: string) => {
if (word.startsWith("<@")) return false;
if (word.endsWith("ington")) return false;
if (/^https?:\/\//i.test(word)) return false;
if (/[aeouy]$/i.test(word)) return false;
return true;
};
const handleMessage = ((channelId, message) => {
if (!settings.store.isEnabled) return;
if (!message.content || !message.content.trim()) return;
const words = message.content.trim().split(/\s+/);
if (words.length === 0) return;
let index = -1;
let attempts = 0;
do {
index = Math.floor(Math.random() * words.length);
attempts++;
} while (!isLegal(words[index]) && attempts < words.length * 2);
if (isLegal(words[index])) {
const word = words[index];
if (word.endsWith("ing")) {
words[index] = word === word.toUpperCase() ? word + "TON" : word + "ton";
} else if (word.endsWith("i") || word.endsWith("I")) {
words[index] = word === word.toUpperCase() ? word + "NGTON" : word + "ngton";
} else if (word.endsWith("in") || word.endsWith("IN")) {
words[index] = word === word.toUpperCase() ? word + "GTON" : word + "gton";
} else if (word.endsWith("ing") || word.endsWith("ING")) {
words[index] = word === word.toUpperCase() ? word + "TON" : word + "ton";
} else if (word.endsWith("ingt") || word.endsWith("INGT")) {
words[index] = word === word.toUpperCase() ? word + "ON" : word + "on";
} else {
words[index] = word === word.toUpperCase() ? word + "INGTON" : word + "ington";
}
}
message.content = words.join(" ");
});
const IngtoninatorButton: ChatBarButtonFactory = ({ isMainChat }) => {
const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]);
const toggle = () => settings.store.isEnabled = !settings.store.isEnabled;
if (!isMainChat || !showIcon) return null;
return (
<ChatBarButton
tooltip={isEnabled ? "Ingtoninator Enabled" : "Ingtoninator Disabled"}
onClick={toggle}
>
{isEnabled ? (
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<path transform="translate(351 -153)" fill="currentcolor" d="M-177.7,334.5c6.3-2.3,12.6-5.2,19.8-8.6c31.9-16.4,51.7-41.7,51.7-41.7s-32.5,0.6-64.4,17 c-4,1.7-7.5,4-10.9,5.7c5.7-7.5,12.1-16.4,18.7-25c25-37.1,31.3-77.3,31.3-77.3s-34.8,21-59.2,58.6c-5.2,7.5-9.8,14.9-13.8,22.7 c1.1-10.3,1.1-22.1,1.1-33.6c0-50-19.8-91.1-19.8-91.1s-19.8,40.5-19.8,91.1c0,12.1,0.6,23.3,1.1,33.6c-4-7.5-8.6-14.9-13.8-22.7 c-25-37.1-59.2-58.6-59.2-58.6s6.3,40,31.3,77.3c6.3,9.2,12.1,17.5,18.7,25c-3.4-2.3-7.5-4-10.9-5.7c-31.9-16.4-64.4-17-64.4-17 s19.8,25.6,51.7,41.7c6.9,3.4,13.2,6.3,19.8,8.6c-4,0.6-8,1.1-12.1,2.3c-30.5,6.4-53.2,23.9-53.2,23.9s27.3,7.5,58.6,1.1 c9.8-2.3,19.8-4.6,27.3-7.5c-1.1,1.1,15.8-8.6,21.6-14.4v60.4h8.6v-61.8c6.3,6.3,22.7,16.4,22.1,14.9c8,2.9,17.5,5.2,27.3,7.5 c30.8,6.3,58.6-1.1,58.6-1.1s-22.1-17.5-53.4-23.8C-169.6,335.7-173.7,335.1-177.7,334.5z" />
</svg>
) : (
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<path transform="translate(351 -153)" fill="var(--status-danger)" d="M-177.7,334.5c6.3-2.3,12.6-5.2,19.8-8.6c31.9-16.4,51.7-41.7,51.7-41.7s-32.5,0.6-64.4,17 c-4,1.7-7.5,4-10.9,5.7c5.7-7.5,12.1-16.4,18.7-25c25-37.1,31.3-77.3,31.3-77.3s-34.8,21-59.2,58.6c-5.2,7.5-9.8,14.9-13.8,22.7 c1.1-10.3,1.1-22.1,1.1-33.6c0-50-19.8-91.1-19.8-91.1s-19.8,40.5-19.8,91.1c0,12.1,0.6,23.3,1.1,33.6c-4-7.5-8.6-14.9-13.8-22.7 c-25-37.1-59.2-58.6-59.2-58.6s6.3,40,31.3,77.3c6.3,9.2,12.1,17.5,18.7,25c-3.4-2.3-7.5-4-10.9-5.7c-31.9-16.4-64.4-17-64.4-17 s19.8,25.6,51.7,41.7c6.9,3.4,13.2,6.3,19.8,8.6c-4,0.6-8,1.1-12.1,2.3c-30.5,6.4-53.2,23.9-53.2,23.9s27.3,7.5,58.6,1.1 c9.8-2.3,19.8-4.6,27.3-7.5c-1.1,1.1,15.8-8.6,21.6-14.4v60.4h8.6v-61.8c6.3,6.3,22.7,16.4,22.1,14.9c8,2.9,17.5,5.2,27.3,7.5 c30.8,6.3,58.6-1.1,58.6-1.1s-22.1-17.5-53.4-23.8C-169.6,335.7-173.7,335.1-177.7,334.5z" />
</svg>
)}
</ChatBarButton>
);
};
export default definePlugin({
name: "Ingtoninator",
description: "Suffixes 'ington' to a random word in your message",
authors: [EquicordDevs.zyqunix],
settings,
start() {
addChatBarButton("Ingtoninator", IngtoninatorButton);
addMessagePreSendListener(handleMessage);
},
stop() {
removeChatBarButton("Ingtoninator");
removeMessagePreSendListener(handleMessage);
}
});

View file

@ -9,7 +9,7 @@ import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, ReactDOM, useEffect, useState } from "@webpack/common";
import { createRoot, FluxDispatcher, useEffect, useState } from "@webpack/common";
import { Root } from "react-dom/client";
let jumpscareRoot: Root | undefined;
@ -38,7 +38,7 @@ function getJumpscareRoot(): Root {
element.id = "jumpscare-root";
element.classList.add("jumpscare-root");
document.body.append(element);
jumpscareRoot = ReactDOM.createRoot(element);
jumpscareRoot = createRoot(element);
}
return jumpscareRoot;

View file

@ -33,7 +33,7 @@ const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout");
const tabClass = findByPropsLazy("inboxTitle", "tab");
const buttonClass = findByPropsLazy("size36");
const MenuHeader = findByCodeLazy(".getUnseenInviteCount())");
const Popout = findByCodeLazy("#{intl::UNBLOCK_TO_JUMP_TITLE}", "canCloseAllMessages:");
const Popout = findByCodeLazy("getProTip", "canCloseAllMessages:");
const createMessageRecord = findByCodeLazy(".createFromServer(", ".isBlockedForMessage", "messageReference:");
const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries";
const KEYWORD_LOG_KEY = "KeywordNotify_log";

View file

@ -0,0 +1,151 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu, MessageActions, MessageStore, NavigationRouter, Toasts, UserStore } from "@webpack/common";
async function findLastMessageFromUser(channelId: string, userId: string) {
try {
const messageCollection = MessageStore.getMessages(channelId);
let messages = messageCollection?.toArray() || [];
let userMessage = messages.filter(m => m?.author?.id === userId).pop();
if (userMessage) return userMessage.id;
try {
await MessageActions.fetchMessages({
channelId: channelId,
limit: 50
});
const updatedCollection = MessageStore.getMessages(channelId);
messages = updatedCollection?.toArray() || [];
userMessage = messages.filter(m => m?.author?.id === userId).pop();
if (userMessage) return userMessage.id;
} catch (fetchError) {
console.error("Error fetching messages:", fetchError);
}
Toasts.show({
type: Toasts.Type.FAILURE,
message: "Couldn't find any recent messages from this user.",
id: Toasts.genId()
});
return null;
} catch (error) {
console.error("Error finding last message:", error);
Toasts.show({
type: Toasts.Type.FAILURE,
message: "Failed to find messages. Check console for details.",
id: Toasts.genId()
});
return null;
}
}
async function jumpToLastActive(channel: any, targetUserId?: string) {
try {
if (!channel) {
Toasts.show({
type: Toasts.Type.FAILURE,
message: "Channel information not available.",
id: Toasts.genId()
});
return;
}
const guildId = channel.guild_id !== null ? channel.guild_id : "@me";
const channelId = channel.id;
let userId: string;
if (targetUserId) {
userId = targetUserId;
} else {
const currentUser = UserStore.getCurrentUser();
userId = currentUser.id;
}
const messageId = await findLastMessageFromUser(channelId, userId);
if (messageId) {
const url = `/channels/${guildId}/${channelId}/${messageId}`;
NavigationRouter.transitionTo(url);
}
} catch (error) {
console.error("Error in jumpToLastActive:", error);
Toasts.show({
type: Toasts.Type.FAILURE,
message: "Failed to jump to message. Check console for details.",
id: Toasts.genId()
});
}
}
const ChannelContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
children.push(
<Menu.MenuItem
id="LastActive"
label={<span style={{ color: "#aa6746" }}>Your Last Message</span>}
icon={LastActiveIcon}
action={() => {
jumpToLastActive(channel);
}}
/>
);
};
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user, channel }) => {
if (!channel || !user?.id) return;
children.push(
<Menu.MenuItem
id="LastActive"
label={<span style={{ color: "#aa6746" }}>User's Last Message</span>}
icon={UserLastActiveIcon}
action={() => {
jumpToLastActive(channel, user.id);
}}
/>
);
};
export function UserLastActiveIcon() {
return (
<svg
viewBox="0 0 52 52"
width="20"
height="20"
fill="#aa6746"
>
<g>
<path d="M11.4,21.6L24.9,7.9c0.6-0.6,1.6-0.6,2.2,0l13.5,13.7c0.6,0.6,0.6,1.6,0,2.2L38.4,26
c-0.6,0.6-1.6,0.6-2.2,0l-9.1-9.4c-0.6-0.6-1.6-0.6-2.2,0l-9.1,9.3c-0.6,0.6-1.6,0.6-2.2,0l-2.2-2.2C10.9,23.1,10.9,22.2,11.4,21.6
z"/>
<path d="M11.4,39.7L24.9,26c0.6-0.6,1.6-0.6,2.2,0l13.5,13.7c0.6,0.6,0.6,1.6,0,2.2l-2.2,2.2
c-0.6,0.6-1.6,0.6-2.2,0l-9.1-9.4c-0.6-0.6-1.6-0.6-2.2,0L15.8,44c-0.6,0.6-1.6,0.6-2.2,0l-2.2-2.2C10.9,41.2,10.9,40.2,11.4,39.7z
"/>
</g>
</svg>
);
}
export function LastActiveIcon() {
return (
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="#aa6746"
xmlns="http://www.w3.org/2000/svg"
>
<path fillRule="evenodd" d="M12,2 C17.5228475,2 22,6.4771525 22,12 C22,17.5228475 17.5228475,22 12,22 C6.4771525,22 2,17.5228475 2,12 C2,6.4771525 6.4771525,2 12,2 Z M12,4 C7.581722,4 4,7.581722 4,12 C4,16.418278 7.581722,20 12,20 C16.418278,20 20,16.418278 20,12 C20,7.581722 16.418278,4 12,4 Z M12,6 C12.5128358,6 12.9355072,6.38604019 12.9932723,6.88337887 L13,7 L13,11.5857864 L14.7071068,13.2928932 C15.0976311,13.6834175 15.0976311,14.3165825 14.7071068,14.7071068 C14.3466228,15.0675907 13.7793918,15.0953203 13.3871006,14.7902954 L13.2928932,14.7071068 L11.2928932,12.7071068 C11.1366129,12.5508265 11.0374017,12.3481451 11.0086724,12.131444 L11,12 L11,7 C11,6.44771525 11.4477153,6 12,6 Z" />
</svg>
);
}
export default definePlugin({
name: "LastActive",
description: "A plugin to jump to last active message from yourself or another user in a channel/server.",
authors: [EquicordDevs.Crxa],
contextMenus: {
"channel-context": ChannelContextMenuPatch,
"user-context": UserContextMenuPatch,
"thread-context": ChannelContextMenuPatch
}
});

View file

@ -30,7 +30,7 @@ type Spinner = ComponentType<Omit<HTMLAttributes<HTMLDivElement>, "children"> &
};
// https://github.com/Kyuuhachi/VencordPlugins/blob/main/MessageLinkTooltip/index.tsx#L11-L33
export const Spinner = findComponentByCodeLazy('"pulsingEllipsis"') as Spinner;
export const Spinner = findComponentByCodeLazy('"pulsingEllipsis"') as unknown as Spinner;
export const QrCodeIcon = findComponentByCodeLazy("0v3ZM20");

View file

@ -14,7 +14,8 @@ async function handleButtonClick() {
sendMessage(getCurrentChannel().id, { content: "meow" });
}
const ChatBarIcon: ChatBarButtonFactory = () => {
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null;
return (
<ChatBarButton tooltip="Meow" onClick={handleButtonClick}>
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 576 512"><path fill="currentColor" d="M320 192h17.1c22.1 38.3 63.5 64 110.9 64c11 0 21.8-1.4 32-4v228c0 17.7-14.3 32-32 32s-32-14.3-32-32V339.2L280 448h56c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-53 0-96-43-96-96V192.5c0-16.1-12-29.8-28-31.8l-7.9-1c-17.5-2.2-30-18.2-27.8-35.7S50.5 94 68 96.2l7.9 1c48 6 84.1 46.8 84.1 95.3v85.3c34.4-51.7 93.2-85.8 160-85.8m160 26.5c-10 3.5-20.8 5.5-32 5.5c-28.4 0-54-12.4-71.6-32c-3.7-4.1-7-8.5-9.9-13.2C357.3 164 352 146.6 352 128V10.7C352 4.8 356.7.1 362.6 0h.2c3.3 0 6.4 1.6 8.4 4.2v.1l12.8 17l27.2 36.3L416 64h64l4.8-6.4L512 21.3l12.8-17v-.1c2-2.6 5.1-4.2 8.4-4.2h.2c5.9.1 10.6 4.8 10.6 10.7V128c0 17.3-4.6 33.6-12.6 47.6c-11.3 19.8-29.6 35.2-51.4 42.9M432 128a16 16 0 1 0-32 0a16 16 0 1 0 32 0m48 16a16 16 0 1 0 0-32a16 16 0 1 0 0 32" /></svg>

View file

@ -89,7 +89,7 @@ export async function exportLogs() {
const messages = await getAllMessagesIDB();
const data = JSON.stringify({ messages }, null, 2);
if (IS_WEB || IS_VESKTOP) {
if (IS_WEB || IS_VESKTOP || IS_EQUIBOP || !DiscordNative) {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);

View file

@ -449,7 +449,7 @@ export const PickerHeader = ({ onQueryChange }: PickerHeaderProps) => {
<div>
<div className={clPicker("search-box")}>
<TextInput
style={{ height: "30px" }}
style={{ height: "30px", border: "none" }}
placeholder="Search stickers"
autoFocus={true}

View file

@ -35,54 +35,34 @@ export default definePlugin({
find: "ChannelStickerPickerButton",
replacement: [{
match: /(children:\(0,\i\.jsx\)\()(.{0,10})({innerClassName.{10,30}\.stickerButton)/,
replace: (_, head, button, tail) => {
const isMoreStickers = "arguments[0]?.stickersType";
return `${head}${isMoreStickers}?$self.stickerButton:${button}${tail}`;
}
replace: "$1arguments[0]?.stickersType?$self.stickerButton:$2$3"
}, {
match: /(\i=)(\i\.useCallback.{0,25}\.STICKER,.{0,10});/,
replace: (_, decl, cb) => {
const newCb = cb.replace(/(?<=\(\)=>\{\(.*?\)\().+?\.STICKER/, "\"stickers+\"");
return `${decl}arguments[0]?.stickersType?${newCb}:${cb};`;
}
match: /(\i=)((\i\.useCallback\(\(\)=>\{\(.*?\)\().*?\.STICKER,(\i.{0,10}));/,
replace: '$1arguments[0]?.stickersType?$3"stickers+",$4:$2;'
}, {
match: /(\i)=((\i)===\i\.\i\.STICKER)/,
replace: (_, isActive, isStickerTab, currentTab) => {
const c = "arguments[0].stickersType";
return `${isActive}=${c}?(${currentTab}===${c}):(${isStickerTab})`;
}
replace: "$1=arguments[0].stickersType?($3===arguments[0].stickersType):($2)"
}]
},
{
find: ".gifts)",
replacement: {
match: /,.{0,5}\(null==\(\i=\i\.stickers\)\?void 0.*?(\i)\.push\((\(0,\w\.jsx\))\((.+?),{disabled:\i,type:(\i)},"sticker"\)\)\)/,
replace: (m, _, jsx, compo, type) => {
const c = "arguments[0].type";
return `${m},${c}?.submit?.button&&${_}.push(${jsx}(${compo},{disabled:!${c}?.submit?.button,type:${type},stickersType:"stickers+"},"stickers+"))`;
}
match: /(?<=,.{0,5}\(null==\(\i=\i\.stickers\)\?void 0.*?(\i)\.push\((\(0,\i\.jsx\))\((.+?),{disabled:\i,type:(\i)},"sticker"\)\)\))/,
replace: ",arguments[0].type?.submit?.button&&$1.push($2($3,{disabled:!arguments[0].type?.submit?.button,type:$4,stickersType:\"stickers+\"},\"stickers+\"))"
}
},
{
find: "#{intl::EXPRESSION_PICKER_CATEGORIES_A11Y_LABEL}",
replacement: {
match: /role:"tablist",.*?,?"aria-label":.+?\),children:(\[.*?\)\]}\)}\):null,)(.*?closePopout:\w.*?:null)/s,
replace: m => {
const stickerTabRegex = /(\w+?)\?(\([^()]+?\))\((.{1,2}),{.{0,128},isActive:(.{1,2})===.{1,6}\.STICKER.{1,140},children:(.{1,5}\.string\(.+?\)).*?:null/s;
const res = m.replace(stickerTabRegex, (_m, canUseStickers, jsx, tabHeaderComp, currentTab, stickerText) => {
const isActive = `${currentTab}==="stickers+"`;
return (
`${_m},${canUseStickers}?` +
`${jsx}(${tabHeaderComp},{id:"stickers+-picker-tab","aria-controls":"more-stickers-picker-tab-panel","aria-selected":${isActive},isActive:${isActive},autoFocus:true,viewType:"stickers+",children:${jsx}("div",{children:${stickerText}+"+"})})` +
":null"
);
});
return res.replace(/:null,((.{1,200})===.{1,30}\.STICKER&&\w+\?(\([^()]{1,10}\)).{1,15}?(\{.*?,onSelectSticker:.*?\})\):null)/s, (_, _m, currentTab, jsx, props) => {
return `:null,${currentTab}==="stickers+"?${jsx}($self.moreStickersComponent,${props}):null,${_m}`;
});
}
replacement: [
{
match: /(?<=null,(\i)\?(\(.*?\))\((\i),{.{0,128},isActive:(\i)===.{0,200},children:(\i\.intl\.string\(.*?\))\}\)\}\):null,)/s,
replace: '$1?$2($3,{id:"stickers+-picker-tab","aria-controls":"more-stickers-picker-tab-panel","aria-selected":$4==="stickers+",isActive:$4==="stickers+",autoFocus:true,viewType:"stickers+",children:$5+"+"}):null,'
},
{
match: /:null,((.{1,200})===.{1,30}\.STICKER&&\w+\?(\([^()]{1,10}\)).{1,15}?(\{.*?,onSelectSticker:.*?\})\):null)/,
replace: ':null,$2==="stickers+"?$3($self.moreStickersComponent,$4):null,$1'
}
]
},
{
find: '==="remove_text"',

View file

@ -116,7 +116,7 @@
.vc-more-stickers-category-scroller,
.vc-more-stickers-picker-content-scroller {
scrollbar-width: thin;
scrollbar-width: none;
scrollbar-color: var(--scrollbar-thin-thumb) var(--scrollbar-thin-track);
}

View file

@ -11,8 +11,7 @@ import { ChannelStore, UploadHandler } from "@webpack/common";
import { FFmpegState, Sticker } from "./types";
const MessageUpload = findByPropsLazy("instantBatchUpload");
const MessageUpload = findByPropsLazy("uploadFiles");
const CloudUpload = findLazy(m => m.prototype?.trackUploadFinished);
const PendingReplyStore = findByPropsLazy("getPendingReply");
const MessageUtils = findByPropsLazy("sendMessage");

View file

@ -14,7 +14,7 @@ import { FFmpegState } from "./types";
export const cl = classNameFactory("vc-more-stickers-");
export const clPicker = (className: string, ...args: any[]) => cl("picker-" + className, ...args);
const CORS_PROXY = "https://corsproxy.io?";
const CORS_PROXY = "https://corsproxy.io/?url=";
function corsUrl(url: string | URL) {
return CORS_PROXY + encodeURIComponent(url.toString());

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByCodeLazy, findLazy } from "@webpack";
import { GuildStore } from "@webpack/common";
import { RC } from "@webpack/types";
import { Channel, Guild, Message, User } from "discord-types/general";
import type { ITag } from "./types";
export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
export const tags = [
{
name: "WEBHOOK",
displayName: "Webhook",
description: "Messages sent by webhooks",
condition: isWebhook
}, {
name: "OWNER",
displayName: "Owner",
description: "Owns the server",
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
}, {
name: "ADMINISTRATOR",
displayName: "Admin",
description: "Has the administrator permission",
permissions: ["ADMINISTRATOR"]
}, {
name: "MODERATOR_STAFF",
displayName: "Staff",
description: "Can manage the server, channels or roles",
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
}, {
name: "MODERATOR",
displayName: "Mod",
description: "Can manage messages or kick/ban people",
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
}, {
name: "VOICE_MODERATOR",
displayName: "VC Mod",
description: "Can manage voice chats",
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
}, {
name: "CHAT_MODERATOR",
displayName: "Chat Mod",
description: "Can timeout people",
permissions: ["MODERATE_MEMBERS"]
}
] as const satisfies ITag[];
export const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number | null, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
export const computePermissions: (options: {
user?: { id: string; } | string | null;
context?: Guild | Channel | null;
overwrites?: Channel["permissionOverwrites"] | null;
checkElevated?: boolean /* = true */;
excludeGuildPermissions?: boolean /* = false */;
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");

View file

@ -0,0 +1,185 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { classNameFactory } from "@api/Styles";
import { Devs, EquicordDevs } from "@utils/constants";
import { getCurrentChannel, getIntlMessage } from "@utils/discord";
import definePlugin from "@utils/types";
import { ChannelStore, GuildStore, PermissionsBits, SelectedChannelStore, UserStore } from "@webpack/common";
import { Channel, Message, User } from "discord-types/general";
import { computePermissions, Tag, tags } from "./consts";
import { settings } from "./settings";
import { TagSettings } from "./types";
const cl = classNameFactory("vc-mut-");
const genTagTypes = () => {
let i = 100;
const obj = {};
for (const { name } of tags) {
obj[name] = ++i;
obj[i] = name;
}
return obj;
};
export default definePlugin({
name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen],
dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"],
settings,
patches: [
// Make discord actually use our tags
{
find: ".STAFF_ONLY_DM:",
replacement: [
{
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})default:(\i)=/,
replace: "default:$2=$self.getTagText($self.localTags[$1]);",
},
{
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})\.BOT:(?=default:)/,
replace: "$&return null;",
predicate: () => settings.store.dontShowBotTag
},
],
}
],
start() {
const tagSettings = settings.store.tagSettings || {} as TagSettings;
for (const tag of Object.values(tags)) {
tagSettings[tag.name] ??= {
showInChat: true,
showInNotChat: true,
text: tag.displayName
};
}
settings.store.tagSettings = tagSettings;
},
localTags: genTagTypes(),
getChannelId() {
return SelectedChannelStore.getChannelId();
},
renderNicknameIcon(props) {
const tagId = this.getTag({
user: UserStore.getUser(props.userId),
channel: ChannelStore.getChannel(this.getChannelId()),
channelId: this.getChannelId(),
isChat: false
});
return tagId && <Tag
type={tagId}
verified={false}>
</Tag>;
},
renderMessageDecoration(props) {
const tagId = this.getTag({
message: props.message,
user: UserStore.getUser(props.message.author.id),
channelId: props.message.channel_id,
isChat: false
});
return tagId && <Tag
useRemSizes={true}
className={cl("message-tag", props.message.author.isVerifiedBot() && "message-verified")}
type={tagId}
verified={false}>
</Tag>;
},
renderMemberListDecorator(props) {
const tagId = this.getTag({
user: props.user,
channel: getCurrentChannel(),
channelId: this.getChannelId(),
isChat: false
});
return tagId && <Tag
type={tagId}
verified={false}>
</Tag>;
},
getTagText(tagName: string) {
if (!tagName) return getIntlMessage("APP_TAG");
const tag = tags.find(({ name }) => tagName === name);
if (!tag) return tagName || getIntlMessage("APP_TAG");
return settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
},
getTag({
message, user, channelId, isChat, channel
}: {
message?: Message,
user?: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
channelId?: string;
isChat?: boolean;
}): number | null {
const settings = this.settings.store;
if (!user) return null;
if (isChat && user.id === "1") return null;
if (user.isClyde()) return null;
if (user.bot && settings.dontShowForBots) return null;
channel ??= ChannelStore.getChannel(channelId!) as any;
if (!channel) return null;
const perms = this.getPermissions(user, channel);
for (const tag of tags) {
if (isChat && !settings.tagSettings[tag.name].showInChat)
continue;
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
continue;
// If the owner tag is disabled, and the user is the owner of the guild,
// avoid adding other tags because the owner will always match the condition for them
if (
(tag.name !== "OWNER" &&
GuildStore.getGuild(channel?.guild_id)?.ownerId ===
user.id &&
isChat &&
!settings.tagSettings.OWNER.showInChat) ||
(!isChat &&
!settings.tagSettings.OWNER.showInNotChat)
)
continue;
if ("permissions" in tag ?
tag.permissions.some(perm => perms.includes(perm)) :
tag.condition(message!, user, channel)) {
return this.localTags[tag.name];
}
}
return null;
},
getPermissions(user: User, channel: Channel): string[] {
const guild = GuildStore.getGuild(channel?.guild_id);
if (!guild) return [];
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
return Object.entries(PermissionsBits)
.map(([perm, permInt]) =>
permissions & permInt ? perm : ""
)
.filter(Boolean);
},
});

View file

@ -0,0 +1,104 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { OptionType } from "@utils/types";
import { Card, Flex, Forms, Switch, TextInput, Tooltip } from "@webpack/common";
import { Tag, tags } from "./consts";
import { TagSettings } from "./types";
function SettingsComponent() {
const tagSettings = settings.store.tagSettings as TagSettings;
const { localTags } = Vencord.Plugins.plugins.MoreUserTags as any;
return (
<Flex flexDirection="column">
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "16px",
}}
>
{tags.map(t => (
<Card
key={t.name}
style={{
padding: "1em 1em 0",
width: "calc(33.333% - 11px)",
boxSizing: "border-box",
}}
>
<Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{t.displayName} Tag
</div>
)}
</Tooltip>
</Forms.FormTitle>
<div style={{ marginBottom: "10px" }}>
<Forms.FormText style={{ fontSize: "13px" }}>
Example:
</Forms.FormText>
<Tag type={localTags[t.name]} />
</div>
<TextInput
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
</Switch>
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
</Switch>
</Card>
))}
</div>
</Flex>
);
}
export const settings = definePluginSettings({
dontShowForBots: {
description: "Don't show extra tags for bots (excluding webhooks)",
type: OptionType.BOOLEAN,
default: false
},
dontShowBotTag: {
description: "Only show extra tags for bots / Hide [APP] text",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me"
}
});

View file

@ -0,0 +1,27 @@
.vc-message-decorations-wrapper .vc-mut-message-tag {
margin-bottom: 1px;
}
/* stylelint-disable-next-line no-descending-specificity */
.vc-mut-message-tag {
/* Remove default margin from tags in messages */
margin-top: unset !important;
/* Align with Discord default tags in messages */
/* stylelint-disable-next-line length-zero-no-unit */
bottom: 0px;
top: -2px;
margin-right: 3px;
}
.vc-mut-message-verified {
height: 1rem !important;
}
span[class*="botTagCozy"][data-moreTags-darkFg="true"]>svg>path {
fill: #000;
}
span[class*="botTagCozy"][data-moreTags-darkFg="false"]>svg>path {
fill: #fff;
}

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { Permissions } from "@webpack/types";
import type { Channel, Message, User } from "discord-types/general";
import { tags } from "./consts";
export type ITag = {
// name used for identifying, must be alphanumeric + underscores
name: string;
// name shown on the tag itself, can be anything probably; automatically uppercase'd
displayName: string;
description: string;
} & ({
permissions: Permissions[];
} | {
condition?(message: Message | null, user: User, channel: Channel): boolean;
});
export interface TagSetting {
text: string;
showInChat: boolean;
showInNotChat: boolean;
}
export type TagSettings = {
[k in typeof tags[number]["name"]]: TagSetting;
};

View file

@ -0,0 +1,73 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandInputType, ApplicationCommandOptionType } from "@api/Commands";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
const morseMap = {
A: ".-", B: "-...", C: "-.-.", D: "-..", E: ".", F: "..-.",
G: "--.", H: "....", I: "..", J: ".---", K: "-.-", L: ".-..",
M: "--", N: "-.", O: "---", P: ".--.", Q: "--.-", R: ".-.",
S: "...", T: "-", U: "..-", V: "...-", W: ".--", X: "-..-",
Y: "-.--", Z: "--..",
0: "-----", 1: ".----", 2: "..---", 3: "...--", 4: "....-",
5: ".....", 6: "-....", 7: "--...", 8: "---..", 9: "----.",
" ": "/"
};
const toMorse = (text: string) => {
return text.toUpperCase().split("").map(char => morseMap[char] ?? "").join(" ");
};
const fromMorse = (text: string) => {
const reversedMap = Object.fromEntries(Object.entries(morseMap).map(([k, v]) => [v, k]));
const raw = text.split(" ").map(code => reversedMap[code] ?? "").join("").toLowerCase();
return raw.charAt(0).toUpperCase() + raw.slice(1);
};
// boo regex
const isMorse = (text: string) => /^[.\-/ ]+$/.test(text);
export default definePlugin({
name: "Morse",
description: "A slash command to translate to/from morse code.",
authors: [EquicordDevs.zyqunix],
commands: [
{
inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
name: "morse",
description: "Translate to or from Morse code",
options: [
{
name: "text",
description: "Text to convert",
type: ApplicationCommandOptionType.STRING,
required: true
}
],
execute: opts => {
const input = opts.find(o => o.name === "text")?.value as string;
const output = isMorse(input) ? fromMorse(input) : toMorse(input);
return {
content: `${output}`
};
},
}
]
});

Some files were not shown because too many files have changed in this diff Show more