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

@ -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

@ -252,6 +252,24 @@
}
.visual-refresh .button-danger-background:hover {
background-color: var(--status-danger-background) !important;
color: var(--status-danger-text) !important;
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;"
}
]
},
{
find: '"Slate: Unable to find syntax characters"',
replacement: {
match: /((let )(\i)=\i\.indexOf\(\i,(\i)\)),/,
replace: "$1;if ($3 === -1) {return $4;}$2"
}
}
]
],
start() {
if (settings.store.hideClanBadges) enableStyle(clanBadges);
},
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
</Button>
<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,18 +360,28 @@ 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]) => {
membersData[memberId] = value;
});
Object.entries(encryptcordGroupMembers).forEach(([memberId, value]) => {
membersData[memberId] = value;
});
const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel });
@ -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,29 +13,34 @@ export interface BuilderColorButtonProps extends Pick<BuilderButtonProps, "label
setColor: (color: number | null) => void;
}
export const BuilderColorButton = ({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) => (
<Popout
position="bottom"
renderPopout={() => (
<CustomColorPicker
value={color}
onChange={setColor}
showEyeDropper={true}
suggestedColors={suggestedColors}
/>
)}
>
{popoutProps => {
const hexColor = color ? "#" + color.toString(16).padStart(6, "0") : undefined;
return (
<BuilderButton
label={label}
tooltip={hexColor}
selectedStyle={hexColor ? { background: hexColor } : undefined}
buttonProps={popoutProps}
export function BuilderColorButton({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) {
const buttonRef = useRef(null);
return (
<Popout
position="bottom"
targetElementRef={buttonRef}
renderPopout={() => (
<CustomColorPicker
value={color}
onChange={setColor}
showEyeDropper={true}
suggestedColors={suggestedColors}
/>
);
}}
</Popout>
);
)}
>
{popoutProps => {
const hexColor = color ? "#" + color.toString(16).padStart(6, "0") : undefined;
return (
<BuilderButton
label={label}
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"',
@ -114,8 +94,8 @@ export default definePlugin({
>
<path d="M18.5 11c-4.136 0-7.5 3.364-7.5 7.5c0 .871.157 1.704.432 2.482l9.551-9.551A7.462 7.462 0 0 0 18.5 11z" />
<path d="M12 2C6.486 2 2 6.486 2 12c0 4.583 3.158 8.585 7.563 9.69A9.431 9.431 0 0 1 9 18.5C9 13.262 13.262 9 18.5 9c1.12 0 2.191.205 3.19.563C20.585 5.158 16.583 2 12 2z" />
</svg>
</button>
</svg >
</button >
);
},
moreStickersComponent({

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}`
};
},
}
]
});

View file

@ -0,0 +1,62 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { RestAPI } from "@webpack/common";
export default definePlugin({
name: "NoOnboarding",
description: "Bypasses Discord's onboarding process for quicker server entry.",
authors: [EquicordDevs.omaw, Devs.Glitch],
patches: [
{
find: ",acceptInvite(",
replacement: {
match: /INVITE_ACCEPT_SUCCESS.+?,(\i)=null!=.+?;/,
replace: (m, guildId) => `${m}$self.bypassOnboard(${guildId});`
}
},
{
find: "{joinGuild:",
replacement: {
match: /guildId:(\i),lurker:(\i).{0,20}}\)\);/,
replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.bypassOnboard(${guildId});`
}
}
],
bypassOnboard(guild_id: string) {
RestAPI.get({ url: `/guilds/${guild_id}/onboarding` }).then(res => {
const data = res.body;
if (!data?.prompts?.length) return;
const now = Math.floor(Date.now() / 1000);
const prompts_seen: Record<string, number> = {};
const responses_seen: Record<string, number> = {};
const responses: string[] = [];
for (const prompt of data.prompts) {
const options = prompt.options || [];
if (!options.length) continue;
prompts_seen[prompt.id] = now;
for (const opt of options) responses_seen[opt.id] = now;
responses.push(options[options.length - 1].id);
}
const payload = {
onboarding_responses: responses,
onboarding_prompts_seen: prompts_seen,
onboarding_responses_seen: responses_seen,
};
RestAPI.post({
url: `/guilds/${guild_id}/onboarding-responses`,
body: payload
}).catch(() => { });
}).catch(() => { });
}
});

View file

@ -28,9 +28,9 @@ export default definePlugin({
authors: [EquicordDevs.iamme],
patches: [
{
find: "#{intl::MESSAGE_EDITED}),",
find: "isUnsupported})",
replacement: {
match: /#{intl::MESSAGE_EDITED}\),(?:[^}]*[}]){3}\)/,
match: /WITH_CONTENT\}\)/,
replace: "$&,$self.PinnedIcon(arguments[0].message)"
}
}

View file

@ -21,12 +21,9 @@ import "@equicordplugins/_misc/styles.css";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Forms, MessageStore, UserStore } from "@webpack/common";
import { Forms, MessageActions, MessageStore, UserStore } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
async function deleteMessages(amount: number, channel: Channel, delay: number = 1500): Promise<number> {
let deleted = 0;
const userId = UserStore.getCurrentUser().id;

View file

@ -17,38 +17,16 @@
*/
import "@equicordplugins/_misc/styles.css";
import { showNotification } from "@api/Notifications";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getTheme, Theme } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findComponentByCodeLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, NavigationRouter, RestAPI, Tooltip, UserStore } from "@webpack/common";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const isApp = navigator.userAgent.includes("Electron/");
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { showNotification } from "@api/Notifications";
import { Devs } from "@utils/constants";
import { getTheme, Theme } from "@utils/discord";
import definePlugin from "@utils/types";
import { findByProps, findComponentByCodeLazy } from "@webpack";
import { Button, ChannelStore, FluxDispatcher, GuildChannelStore, NavigationRouter, RestAPI, Tooltip, UserStore } from "@webpack/common";
const ToolBarQuestsIcon = findComponentByCodeLazy("1 0 1 1.73Z");
function ToolBarHeader() {
return (
<ErrorBoundary noop={true}>
<HeaderBarIcon
tooltip="Complete Quest"
position="bottom"
className="vc-quest-completer"
icon={ToolBarQuestsIcon}
onClick={openCompleteQuestUI}
>
</HeaderBarIcon>
</ErrorBoundary>
);
}
const QuestIcon = findComponentByCodeLazy("10.47a.76.76");
async function openCompleteQuestUI() {
const ApplicationStreamingStore = findByProps("getStreamerActiveStreamMetadata");
@ -72,7 +50,7 @@ async function openCompleteQuestUI() {
const applicationId = quest.config.application.id;
const applicationName = quest.config.application.name;
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP"].find(x => quest.config.taskConfig.tasks[x] != null);
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
// @ts-ignore
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
// @ts-ignore
@ -108,18 +86,9 @@ async function openCompleteQuestUI() {
});
console.log(`Spoofing video for ${applicationName}.`);
} else if (taskName === "PLAY_ON_DESKTOP") {
if (!isApp) {
showNotification({
title: `${applicationName} - Quest Completer`,
body: `${applicationName}'s quest requires the desktop app.`,
icon: icon,
});
}
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
const appData = res.body[0];
const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", "");
const games = RunningGameStore.getRunningGames();
const fakeGame = {
cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`,
exeName,
@ -133,8 +102,15 @@ async function openCompleteQuestUI() {
processName: appData.name,
start: Date.now(),
};
games.push(fakeGame);
FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [], added: [fakeGame], games: games });
const realGames = RunningGameStore.getRunningGames();
const fakeGames = [fakeGame];
const realGetRunningGames = RunningGameStore.getRunningGames;
const realGetGameForPID = RunningGameStore.getGameForPID;
RunningGameStore.getRunningGames = () => fakeGames;
RunningGameStore.getGameForPID = pid => fakeGames.find(x => x.pid === pid);
FluxDispatcher.dispatch({
type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames
});
const fn = data => {
const progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value);
@ -151,11 +127,9 @@ async function openCompleteQuestUI() {
icon: icon,
});
const idx = games.indexOf(fakeGame);
if (idx > -1) {
games.splice(idx, 1);
FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] });
}
RunningGameStore.getRunningGames = realGetRunningGames;
RunningGameStore.getGameForPID = realGetGameForPID;
FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] });
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
}
};
@ -163,13 +137,6 @@ async function openCompleteQuestUI() {
console.log(`Spoofed your game to ${applicationName}.`);
});
} else if (taskName === "STREAM_ON_DESKTOP") {
if (!isApp) {
showNotification({
title: `${applicationName} - Quest Completer`,
body: `${applicationName}'s quest requires the desktop app.`,
icon: icon,
});
}
const stream = ApplicationStreamingStore.getAnyStreamForUser(UserStore.getCurrentUser()?.id);
if (!stream) {
showNotification({
@ -211,84 +178,68 @@ async function openCompleteQuestUI() {
};
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
console.log(`Spoofed your stream to ${applicationName}.`);
} else if (taskName === "PLAY_ACTIVITY") {
const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds() as any[]).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id;
const streamKey = `call:${channelId}:1`;
const fn = async () => {
while (true) {
const res = await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: false } });
const progress = res.body.progress.PLAY_ACTIVITY.value;
showNotification({
title: `${applicationName} - Quest Completer`,
body: `Current progress: ${progress}/${secondsNeeded} seconds.`,
icon: icon,
});
await new Promise(resolve => setTimeout(resolve, 20 * 1000));
if (progress >= secondsNeeded) {
await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: true } });
break;
}
}
showNotification({
title: `${applicationName} - Quest Completer`,
body: "Quest Completed.",
icon: icon,
});
};
fn();
}
return;
}
}
const settings = definePluginSettings({
clickableQuestDiscovery: {
type: OptionType.BOOLEAN,
description: "Makes the quest button in discovery clickable",
restartNeeded: true,
default: false
}
});
export default definePlugin({
name: "QuestCompleter",
description: "A plugin to complete quests without having the game installed.",
authors: [Devs.amia],
settingsAboutComponent: () => <>
<Forms.FormText className="plugin-warning">
Game Quests do not work on Equibop/Web Platforms. Only Video Quests do.
</Forms.FormText>
</>,
settings,
patches: [
{
find: "\"invite-button\"",
find: "AppTitleBar",
replacement: {
match: /\i&&(\i\i\.push).{0,50}"current-speaker"/,
replace: "$1($self.renderQuestButton()),$&"
match: /(?<=trailing:.{0,70}\(\i\.Fragment,{children:\[)/,
replace: "$self.renderQuestButton(),"
}
},
{
find: "toolbar:function",
replacement: {
match: /(function \i\(\i\){)(.{1,500}toolbar.{1,500}mobileToolbar)/,
replace: "$1$self.toolbarAction(arguments[0]);$2"
}
},
{
find: "M7.5 21.7a8.95 8.95 0 0 1 9 0 1 1 0 0 0 1-1.73c",
replacement: {
match: /(?<=className:\i\}\))/,
replace: ",onClick:()=>$self.openCompleteQuestUI()"
},
predicate: () => settings.store.clickableQuestDiscovery
}
],
renderQuestButton() {
return (
<Tooltip text="Complete Quest">
{tooltipProps => (
<Button style={{ backgroundColor: "transparent" }}
<Button style={{ backgroundColor: "transparent", border: "none" }}
{...tooltipProps}
size={"25"}
size={Button.Sizes.SMALL}
className={"vc-quest-completer-icon"}
onClick={openCompleteQuestUI}
>
<ToolBarQuestsIcon />
<QuestIcon width={20} height={20} size={Button.Sizes.SMALL} />
</Button>
)}
</Tooltip>
);
},
openCompleteQuestUI,
toolbarAction(e) {
if (Array.isArray(e.toolbar))
return e.toolbar.push(
<ErrorBoundary noop={true}>
<ToolBarHeader />
</ErrorBoundary>
);
e.toolbar = [
<ErrorBoundary noop={true} key={"QuestCompleter"} >
<ToolBarHeader />
</ErrorBoundary>,
e.toolbar,
];
}
});

View file

@ -0,0 +1,12 @@
.vc-quest-completer-icon {
bottom: -2px;
padding: 0;
width: var(--space-32);
min-width: 0;
height: var(--space-32);
color: var(--interactive-normal) !important;
&:hover{
filter: brightness(2);
}
}

View file

@ -1,3 +0,0 @@
.vc-quest-completer-icon:hover{
filter: brightness(10);
}

View file

@ -10,7 +10,7 @@ import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Menu, Select, Switch, Text, TextInput, UploadHandler, useEffect, UserStore, useState } from "@webpack/common";
import { Button, Menu, Select, Switch, Text, UploadHandler, useEffect, useState } from "@webpack/common";
import { Message } from "discord-types/general";
import { QuoteIcon } from "./components";
@ -49,7 +49,6 @@ let recentmessage: Message;
let grayscale;
let setStyle: ImageStyle = ImageStyle.inspirational;
let customMessage: string = "";
let isUserCustomCapable = false;
enum userIDOptions {
displayName,
@ -89,14 +88,7 @@ const preparingSentence: string[] = [];
const lines: string[] = [];
async function createQuoteImage(avatarUrl: string, quoteOld: string, grayScale: boolean): Promise<Blob> {
let quote;
if (isUserCustomCapable && customMessage.length > 0) {
quote = FixUpQuote(customMessage);
}
else {
quote = FixUpQuote(quoteOld);
}
const quote = FixUpQuote(quoteOld);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
@ -141,7 +133,7 @@ async function createQuoteImage(avatarUrl: string, quoteOld: string, grayScale:
ctx.fillRect(0, 0, canvas.width, canvas.height);
const avatarBlob = await fetchImageAsBlob(avatarUrl);
const fadeBlob = await fetchImageAsBlob("https://github.com/Equicord/Equibored/raw/main/misc/quoter.png");
const fadeBlob = await fetchImageAsBlob("https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/icons/quoter/quoter.png");
const avatar = new Image();
const fade = new Image();
@ -194,13 +186,7 @@ function registerStyleChange(style) {
GeneratePreview();
}
async function setIsUserCustomCapable() {
const allowList: string[] = await fetch("https://equicord.org/quoter").then(e => e.json()); // Override for memes - IF THIS IS ABUSED WILL WE TAKEN AWAY
isUserCustomCapable = allowList.includes(UserStore.getCurrentUser().id);
}
function QuoteModal(props: ModalProps) {
setIsUserCustomCapable();
const [gray, setGray] = useState(true);
useEffect(() => {
grayscale = gray;
@ -226,13 +212,6 @@ function QuoteModal(props: ModalProps) {
<ModalContent scrollbarType="none">
<img alt="" src="" id={"quoterPreview"} style={{ borderRadius: "20px", width: "100%" }}></img>
<br></br><br></br>
{isUserCustomCapable &&
(
<>
<TextInput onChange={setCustom} value={custom} placeholder="Custom Message"></TextInput>
<br />
</>
)}
<Switch value={gray} onChange={setGray}>Grayscale</Switch>
<Select look={1}
options={Object.keys(ImageStyle).filter(key => isNaN(parseInt(key, 10))).map(key => ({
@ -278,14 +257,7 @@ async function GeneratePreview() {
}
function generateFileNamePreview(message) {
let words;
if (isUserCustomCapable && customMessage.length) {
words = customMessage.split(" ");
}
else {
words = message.split(" ");
}
const words = message.split(" ");
let preview;
if (words.length >= 6) {
preview = words.slice(0, 6).join(" ");

View file

@ -11,8 +11,8 @@ import { makeRange } from "@components/PluginSettings/components";
import { debounce } from "@shared/debounce";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { ChannelStore, ContextMenuApi, GuildStore, Menu, NavigationRouter, PermissionStore, React, SelectedChannelStore, Toasts, UserStore } from "@webpack/common";
import { findByCode, findByProps, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore } from "@webpack/common";
import style from "./styles.css?managed";
@ -32,6 +32,7 @@ const valueOperation = [
const CONNECT = 1n << 20n;
const SPEAK = 1n << 21n;
const STREAM = 1n << 9n;
const VIDEO = 1 << 21;
const settings = definePluginSettings({
UserAmountOperation: {
@ -85,6 +86,11 @@ const settings = definePluginSettings({
description: "Automatically turns on camera",
default: false,
},
autoStream: {
type: OptionType.BOOLEAN,
description: "Automatically turns on stream",
default: false,
},
selfMute: {
type: OptionType.BOOLEAN,
description: "Automatically mutes your mic when joining voice-channel.",
@ -95,6 +101,11 @@ const settings = definePluginSettings({
description: "Automatically deafems your mic when joining voice-channel.",
default: false,
},
leaveEmpty: {
type: OptionType.BOOLEAN,
description: "Finds a random-call, when the voice chat is empty.",
default: false,
},
avoidStages: {
type: OptionType.BOOLEAN,
description: "Avoids joining stage voice-channels.",
@ -137,11 +148,25 @@ const settings = definePluginSettings({
},
});
interface VoiceState {
userId: string;
channelId?: string;
oldChannelId?: string;
deaf: boolean;
mute: boolean;
selfDeaf: boolean;
selfMute: boolean;
selfStream: boolean;
selfVideo: boolean;
sessionId: string;
suppress: boolean;
requestToSpeakTimestamp: string | null;
}
export default definePlugin({
name: "RandomVoice",
description: "Adds a Button near the Mute button to join a random voice call.",
authors: [EquicordDevs.xijexo, EquicordDevs.omaw],
authors: [EquicordDevs.xijexo, EquicordDevs.omaw, EquicordDevs.thororen],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
@ -151,6 +176,22 @@ export default definePlugin({
}
}
],
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const currentUserId = UserStore.getCurrentUser().id;
const myChannelId = VoiceStateStore.getVoiceStateForUser(currentUserId)?.channelId;
if (!myChannelId || !settings.store.leaveEmpty) return;
const voiceStatesMap = VoiceStateStore.getVoiceStates() as Record<string, VoiceState>;
const othersInChannel = Object.values(voiceStatesMap).filter(vs =>
vs.channelId === myChannelId && vs.userId !== currentUserId
);
if (othersInChannel.length === 0) {
randomVoice();
}
},
},
start() {
enableStyle(style);
},
@ -194,8 +235,7 @@ function ContextMenu() {
});
ServerList = Array.from(new Set(ServerList));
const Servers = ServerList.map(server => GuildStore.getGuild(server)).filter(guild => guild !== null);
const Servers = ServerList.map(server => GuildStore.getGuild(server)).filter(guild => guild && guild.id);
const [servers, setServers] = React.useState(settings.store.Servers);
const [SpacesLeftOperation, setSpacesLeftOperation] = React.useState(settings.store.spacesLeftOperation);
const [userAmount, setuserAmount] = React.useState(settings.store.UserAmountOperation);
@ -204,12 +244,13 @@ function ContextMenu() {
const [stage, setStage] = React.useState(settings.store.avoidStages);
const [afk, setAfk] = React.useState(settings.store.avoidAfk);
const [camera, setCamera] = React.useState(settings.store.autoCamera);
const [stream, setStream] = React.useState(settings.store.autoStream);
const [empty, setEmpty] = React.useState(settings.store.leaveEmpty);
const [muteself, setSelfMute] = React.useState(settings.store.selfMute);
const [deafenself, setSelfDeafen] = React.useState(settings.store.selfDeafen);
const [mute, setMute] = React.useState(settings.store.mute);
const [deafen, setDeafen] = React.useState(settings.store.deafen);
const [video, setVideo] = React.useState(settings.store.video);
const [stream, setStream] = React.useState(settings.store.stream);
const [state, setState] = React.useState(settings.store.includeStates);
const [notstate, avoidState] = React.useState(settings.store.avoidStates);
@ -219,6 +260,8 @@ function ContextMenu() {
onClose={() => { }}
aria-label="Voice state modifier"
>
<Menu.MenuItem
id="servers"
label="Select Servers"
@ -318,7 +361,7 @@ function ContextMenu() {
<Menu.MenuCheckboxItem
key="video"
id="video"
label="Video"
label="Camera"
action={() => {
setVideo(!video);
settings.store.video = !video;
@ -545,7 +588,7 @@ function ContextMenu() {
<Menu.MenuSeparator />
<Menu.MenuGroup
label="SETTINGS"
label="SELF SETTINGS"
>
<Menu.MenuItem id="voiceOptions" label="Voice Options" action={() => { }} >
<>
@ -577,6 +620,24 @@ function ContextMenu() {
settings.store.autoCamera = !camera;
}}
checked={camera} />
<Menu.MenuCheckboxItem
key="autoStream"
id="autoStream"
label="Auto Stream"
action={() => {
setStream(!stream);
settings.store.autoStream = !stream;
}}
checked={stream} />
<Menu.MenuCheckboxItem
key="leaveEmpty"
id="leaveEmpty"
label="Leave when Empty"
action={() => {
setEmpty(!empty);
settings.store.leaveEmpty = !empty;
}}
checked={empty} />
</>
</Menu.MenuItem>
@ -704,15 +765,35 @@ function getChannels() {
function JoinVc(channelID) {
const channel = ChannelStore.getChannel(channelID);
const channel_link = `/channels/${channel.guild_id}/${channel.id}`;
ChannelActions.selectVoiceChannel(channelID);
if (settings.store.autoNavigate) NavigationRouter.transitionTo(channel_link);
if (settings.store.autoCamera && PermissionStore.can(STREAM, channel)) autoCamera();
if (settings.store.autoCamera && PermissionStore.can(STREAM, channel)) autoCamera();
if (settings.store.autoNavigate) ChannelRouter.transitionToChannel(channel.id);
if (settings.store.autoCamera && PermissionStore.can(VIDEO, channel)) autoCamera();
if (settings.store.autoStream && PermissionStore.can(STREAM, channel)) autoStream();
if (settings.store.selfMute && !MediaEngineStore.isSelfMute() && SelectedChannelStore.getVoiceChannelId()) toggleSelfMute();
if (settings.store.selfDeafen && !MediaEngineStore.isSelfDeaf() && SelectedChannelStore.getVoiceChannelId()) toggleSelfDeaf();
}
async function autoStream() {
const startStream = findByCode('type:"STREAM_START"');
const mediaEngine = findByProps("getMediaEngine").getMediaEngine();
const getDesktopSources = findByCode("desktop sources");
const selected = SelectedChannelStore.getVoiceChannelId();
if (!selected) return;
const channel = ChannelStore.getChannel(selected);
const sources = await getDesktopSources(mediaEngine, ["screen"], null);
if (!sources || sources.length === 0) return;
const source = sources[0];
if (channel.type === 13 || !PermissionStore.can(PermissionsBits.STREAM, channel)) return;
startStream(channel.guild_id, selected, {
"pid": null,
"sourceId": source.id,
"sourceName": source.name,
"audioSourceId": null,
"sound": true,
"previewDisabled": false
});
}
function autoCamera() {
const checkExist = setInterval(() => {
const cameraOFF = document.querySelector('[aria-label="Turn off Camera" i]') as HTMLButtonElement;

View file

@ -55,7 +55,7 @@ interface ContextMenuProps {
}
const ArrowsLeftRightIcon = findComponentByCodeLazy("18.58V3a1");
const XSmallIcon = findComponentByCodeLazy("12l4.94-4.94a1.5")
const XSmallIcon = findComponentByCodeLazy("12l4.94-4.94a1.5");
function MakeContextCallback(name: "user" | "channel"): NavContextMenuPatchCallback {
return (children, { user, channel, guildId }: ContextMenuProps) => {
@ -149,7 +149,7 @@ export default definePlugin({
}}
/>
<HeaderBarIcon
icon={() => (<XSmallIcon style={{width: "24px",height: "24px"}} />)}
icon={() => (<XSmallIcon style={{ width: "24px",height: "24px" }} />)}
tooltip="Close Sidebar Chat"
onClick={() => {
FluxDispatcher.dispatch({

View file

@ -22,6 +22,14 @@ const settings = definePluginSettings(
description: "The signature that will be added to the end of your messages",
default: "a chronic discord user"
},
textHeader: {
description: "What header to preface text with",
type: OptionType.SELECT,
options: [
{ label: ">", value: ">", default: true },
{ label: "-#", value: "-#" }
]
},
showIcon: {
type: OptionType.BOOLEAN,
default: true,
@ -141,7 +149,7 @@ export default definePlugin({
// text processing injection processor
function textProcessing(input: string) {
return `${input}\n> ${settings.store.name}`;
return `${input}\n${settings.store.textHeader} ${settings.store.name}`;
}

View file

@ -0,0 +1,168 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { ModalProps, ModalRoot, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { React } from "@webpack/common";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
let preloadSong, preloadBoopSound, song, boopSound;
function SoggyModal(props: ModalProps) {
if (settings.store.songVolume !== 0) {
React.useEffect(() => {
song = new Audio(preloadSong.src);
song.volume = settings.store.songVolume;
song.play();
return () => {
song.pause();
song.remove();
};
}, []);
}
const boop = (e: React.MouseEvent<HTMLImageElement>) => {
const { offsetX, offsetY } = e.nativeEvent;
const region = { x: 155, y: 220, width: 70, height: 70 };
if (
settings.store.boopVolume !== 0 &&
offsetX >= region.x &&
offsetX <= region.x + region.width &&
offsetY >= region.y &&
offsetY <= region.y + region.height
) {
boopSound = new Audio(preloadBoopSound.src);
boopSound.volume = settings.store.boopVolume;
boopSound.play();
}
};
return (
<ModalRoot {...props}>
<img
src={settings.store.imageLink}
onClick={boop}
style={{ display: "block" }}
/>
</ModalRoot >
);
}
function buildSoggyModal(): any {
openModal(props => <SoggyModal {...props} />);
}
function SoggyButton() {
return (
<HeaderBarIcon
className="soggy-button"
tooltip={settings.store.tooltipText}
icon={() => (
<img
alt=""
src={settings.store.imageLink}
width={24}
height={24}
draggable={false}
style={{ pointerEvents: "none" }}
/>
)}
onClick={() => buildSoggyModal()}
selected={false}
/>
);
}
const settings = definePluginSettings({
songVolume: {
description: "Volume of the song. 0 to disable",
type: OptionType.SLIDER,
default: 0.25,
markers: [0, 0.25, 0.5, 0.75, 1],
stickToMarkers: false,
},
boopVolume: {
description: "Volume of the boop sound",
type: OptionType.SLIDER,
default: 0.2,
markers: [0, 0.25, 0.5, 0.75, 1],
stickToMarkers: false,
},
tooltipText: {
description: "The text shown when hovering over the button",
type: OptionType.STRING,
default: "the soggy",
},
imageLink: {
description: "URL for the image (button and modal)",
type: OptionType.STRING,
default: "https://soggy.cat/img/soggycat.webp",
},
songLink: {
description: "URL for the song to play",
type: OptionType.STRING,
default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/song.mp3?raw=true",
onChange: (value: string) => {
song = new Audio(value);
}
},
boopLink: {
description: "URL for the boop sound",
type: OptionType.STRING,
default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/honk.wav?raw=true",
onChange: (value: string) => {
boopSound = new Audio(value);
}
}
});
export default definePlugin({
name: "Soggy",
description: "Adds a soggy button to the toolbox",
authors: [EquicordDevs.sliwka],
settings,
patches: [
{
find: "toolbar:function",
replacement: {
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,450}mobileToolbar)/,
replace: "$1$self.addIconToToolBar(arguments[0]);$2"
}
}
],
start: () => {
preloadSong = new Audio(settings.store.songLink);
preloadBoopSound = new Audio(settings.store.boopLink);
},
// taken from message logger lol
addIconToToolBar(e: { toolbar: React.ReactNode[] | React.ReactNode; }) {
if (Array.isArray(e.toolbar))
return e.toolbar.push(
<ErrorBoundary noop={true}>
<SoggyButton />
</ErrorBoundary>
);
e.toolbar = [
<ErrorBoundary noop={true} key={"MessageLoggerEnhanced"} >
<SoggyButton />
</ErrorBoundary>,
e.toolbar,
];
},
});

View file

@ -58,7 +58,8 @@ export function IconWithTooltip({ text, icon, onClick }) {
</Tooltip>;
}
export const ChatBarIcon: ChatBarButtonFactory = () => {
export const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null;
return (
<ChatBarButton tooltip="Open SoundBoard Log"
onClick={openSoundBoardLog}>

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