Merge branch 'dev'

This commit is contained in:
thororen1234 2025-09-08 21:59:43 -04:00
commit 3b56cb94a6
No known key found for this signature in database
67 changed files with 451 additions and 443 deletions

View file

@ -1,3 +1,4 @@
export * from "./channel";
export * from "./commands";
export * from "./messages";
export * from "./channel";
export * from "./misc";

View file

@ -0,0 +1,4 @@
export const enum CloudUploadPlatform {
REACT_NATIVE = 0,
WEB = 1,
}

View file

@ -4,6 +4,7 @@ export * from "./components";
export * from "./flux";
export * from "./fluxEvents";
export * from "./menu";
export * from "./modules";
export * from "./stores";
export * from "./passiveupdatestate";
export * from "./utils";

View file

@ -0,0 +1,74 @@
import EventEmitter from "events";
import { CloudUploadPlatform } from "../../enums";
interface BaseUploadItem {
platform: CloudUploadPlatform;
id?: string;
origin?: string;
isThumbnail?: boolean;
clip?: unknown;
}
export interface ReactNativeUploadItem extends BaseUploadItem {
platform: CloudUploadPlatform.REACT_NATIVE;
uri: string;
filename?: string;
mimeType?: string;
durationSecs?: number;
waveform?: string;
isRemix?: boolean;
}
export interface WebUploadItem extends BaseUploadItem {
platform: CloudUploadPlatform.WEB;
file: File;
}
export type CloudUploadItem = ReactNativeUploadItem | WebUploadItem;
export class CloudUpload extends EventEmitter {
constructor(item: CloudUploadItem, channelId: string, showLargeMessageDialog?: boolean, reactNativeFileIndex?: number);
channelId: string;
classification: string;
clip: unknown;
contentHash: unknown;
currentSize: number;
description: string | null;
durationSecs: number | undefined;
etag: string | undefined;
error: unknown;
filename: string;
id: string;
isImage: boolean;
isRemix: boolean | undefined;
isThumbnail: boolean;
isVideo: boolean;
item: {
file: File;
platform: CloudUploadPlatform;
origin: string;
};
loaded: number;
mimeType: string;
origin: string;
postCompressionSize: number | undefined;
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
startTime: number;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED" | "REMOVED_FROM_MSG_DRAFT";
uniqueId: string;
uploadedFilename: string;
waveform: string | undefined;
// there are many more methods than just these but I didn't find them particularly useful
upload(): Promise<void>;
cancel(): void;
delete(): Promise<void>;
getSize(): number;
maybeConvertToWebP(): Promise<void>;
removeFromMsgDraft(): void;
}

View file

@ -0,0 +1 @@
export * from "./CloudUpload";

View file

@ -17,7 +17,7 @@
*/
import { Logger } from "@utils/Logger";
import type { Channel, CustomEmoji, Message } from "@vencord/discord-types";
import type { Channel, CloudUpload, CustomEmoji, Message } from "@vencord/discord-types";
import { MessageStore } from "@webpack/common";
import type { Promisable } from "type-fest";
@ -30,32 +30,6 @@ export interface MessageObject {
tts: boolean;
}
export interface Upload {
classification: string;
currentSize: number;
description: string | null;
filename: string;
id: string;
isImage: boolean;
isRemix?: boolean;
isThumbnail: boolean;
isVideo: boolean;
item: {
file: File;
platform: number;
};
loaded: number;
mimeType: string;
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
uniqueId: string;
uploadedFilename: string;
}
export interface MessageReplyOptions {
messageReference: Message["messageReference"];
allowedMentions?: {
@ -68,7 +42,7 @@ export interface MessageReplyOptions {
export interface MessageOptions {
stickers?: string[];
uploads?: Upload[];
uploads?: CloudUpload[];
replyOptions: MessageReplyOptions;
content: string;
channel: Channel;

View file

@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { isPrimitiveReactNode } from "@utils/react";
import { waitFor } from "@webpack";
import { ReactNode } from "react";
let NoticesModule: any;
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
@ -36,7 +39,11 @@ export function nextNotice() {
}
}
export function showNotice(message: string, buttonText: string, onOkClick: () => void) {
noticesQueue.push(["GENERIC", message, buttonText, onOkClick]);
export function showNotice(message: ReactNode, buttonText: string, onOkClick: () => void) {
const notice = isPrimitiveReactNode(message)
? message
: <ErrorBoundary fallback={() => "Error Showing Notice"}>{message}</ErrorBoundary>;
noticesQueue.push(["GENERIC", notice, buttonText, onOkClick]);
if (!currentNotice) nextNotice();
}

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { initWs } from "@plugins/devCompanion.dev/initWs";
import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
import { initWs } from "plugins/devCompanion.dev/initWs";
import { getBuildNumber, patchTimings } from "webpack/patchWebpack";
import { addPatch, patches } from "../plugins";

View file

@ -5,7 +5,7 @@
*/
import { migratePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
import definePlugin from "@utils/types";
import { User } from "@vencord/discord-types";
@ -14,7 +14,7 @@ migratePluginSettings("AlwaysExpandProfiles", "AlwaysExpandProfile");
export default definePlugin({
name: "AlwaysExpandProfiles",
description: "Always expands profile popouts to the full modal",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
patches: [
{
find: '"view-profile"',

View file

@ -7,7 +7,7 @@
import "./styles.css";
import { migratePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { patchActivityList } from "./patch-helpers/activityList";
@ -19,12 +19,7 @@ migratePluginSettings("BetterActivities", "MemberListActivities");
export default definePlugin({
name: "BetterActivities",
description: "Shows activity icons in the member list and allows showing all activities",
authors: [
Devs.D3SOX,
Devs.Arjix,
Devs.AutumnVN,
EquicordDevs.thororen
],
authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN, Devs.thororen],
tags: ["activity"],
settings,

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
@ -19,7 +19,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "BetterCommands",
description: "Enhances the command system with miscellaneous improvements.",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
patches: [
{

View file

@ -4,13 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "BypassPinPrompt",
description: "Bypass the pin prompt when using the pin functions",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
patches: [
{
find: '"Channel Pins"',

View file

@ -6,7 +6,7 @@
import "./style.css";
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "@vencord/discord-types";
import { GuildStore, React, SelectedGuildStore } from "@webpack/common";
@ -32,7 +32,7 @@ function renderBadge(id: number, title: string) {
export default definePlugin({
name: "ChannelBadges",
description: "Adds badges to channels based on their type",
authors: [EquicordDevs.creations, EquicordDevs.thororen],
authors: [EquicordDevs.creations, Devs.thororen],
settings,
patches: [
// TY TypingIndicator

View file

@ -7,7 +7,7 @@
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { UserStore } from "@webpack/common";
@ -22,7 +22,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "CharacterCounter",
description: "Adds a character counter to the chat input",
authors: [EquicordDevs.creations, EquicordDevs.Panniku, EquicordDevs.thororen],
authors: [EquicordDevs.creations, EquicordDevs.Panniku, Devs.thororen],
settings,
patches: [
{

View file

@ -6,7 +6,7 @@
import "./styles.css";
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { makeDevBanner, settings } from "./components";
@ -14,7 +14,7 @@ import { makeDevBanner, settings } from "./components";
export default definePlugin({
name: "DiscordDevBanner",
description: "Enables the Discord developer banner, in which displays the build-ID",
authors: [EquicordDevs.KrystalSkull, EquicordDevs.thororen],
authors: [EquicordDevs.KrystalSkull, Devs.thororen],
settings,
patches: [
{

View file

@ -18,7 +18,7 @@
import "@equicordplugins/_misc/styles.css";
import { EQUICORD_HELPERS, EquicordDevs, GUILD_ID } from "@utils/constants";
import { Devs, EQUICORD_HELPERS, EquicordDevs, GUILD_ID } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Button, ChannelStore, Flex, Forms, GuildMemberStore, showToast, Toasts } from "@webpack/common";
@ -30,7 +30,7 @@ import { toggleEnabled } from "./utils";
export default definePlugin({
name: "EquicordHelper",
description: "Fixes some misc issues with discord",
authors: [EquicordDevs.thororen, EquicordDevs.nyx, EquicordDevs.Naibuu],
authors: [Devs.thororen, EquicordDevs.nyx, EquicordDevs.Naibuu],
settingsAboutComponent: () => <>
<Forms.FormText className="plugin-warning" style={{ textAlign: "left" }}>
This plugin was created to allow us as the Equicord Team & Contributors

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
let clickCount = 0;
@ -33,7 +33,7 @@ function play() {
export default definePlugin({
name: "Equissant",
description: "Crossant every 10 clicks :trolley:",
authors: [EquicordDevs.SomeAspy, EquicordDevs.thororen],
authors: [EquicordDevs.SomeAspy, Devs.thororen],
start() {
document.addEventListener("click", play);
},

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Constants, PermissionsBits, PermissionStore, React, RestAPI, useCallback, useEffect, useState } from "@webpack/common";
@ -74,7 +74,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "FastDeleteChannels",
description: "Adds a trash icon to delete channels",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
patches: [
// TY TypingIndicator

View file

@ -7,7 +7,7 @@
import { Upload } from "@api/MessageEvents";
import { Settings } from "@api/Settings";
import { tarExtMatcher } from "@plugins/anonymiseFileNames";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { ReporterTestable } from "@utils/types";
const extensionMap = {
@ -26,7 +26,7 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
export default definePlugin({
name: "FixFileExtensions",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
description: "Fixes file extensions by renaming them to a compatible supported format if possible",
reporterTestable: ReporterTestable.None,
patches: [

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
@ -25,7 +25,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "ForwardAnywhere",
description: "If a forward fails send it as a normal message also allows nsfw forwards",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
patches: [
{

View file

@ -67,7 +67,7 @@ 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],
authors: [EquicordDevs.Cortex, Devs.Samwich, EquicordDevs.Synth, Devs.thororen],
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch

View file

@ -20,7 +20,7 @@ import "@equicordplugins/_misc/styles.css";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import clanBadges from "../_misc/clanBadges.css?managed";
@ -46,7 +46,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "GuildTagSettings",
description: "Settings for Guild Tags",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
patches: [
{

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs, EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { VoiceState } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
@ -38,7 +38,7 @@ async function autoStartStream() {
export default definePlugin({
name: "InstantScreenshare",
description: "Instantly screenshare when joining a voice channel",
authors: [Devs.HAHALOSAH, EquicordDevs.thororen],
authors: [Devs.HAHALOSAH, Devs.thororen],
settings,
flux: {
async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {

View file

@ -30,7 +30,7 @@ migratePluginSettings("MoreCommands", "CuteAnimeBoys", "CuteNekos", "CutePats",
export default definePlugin({
name: "MoreCommands",
description: "Adds various fun and useful commands",
authors: [Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, EquicordDevs.thororen, Devs.Korbo],
authors: [Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, Devs.thororen, Devs.Korbo],
commands: [
...choice,
...system,

View file

@ -34,7 +34,7 @@ import { YtmPlayer } from "./youtubeMusic/PlayerComponent";
export default definePlugin({
name: "MusicControls",
description: "Music Controls and Lyrics for multiple services ",
authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev, EquicordDevs.thororen, EquicordDevs.vmohammad, Devs.Joona],
authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev, Devs.thororen, EquicordDevs.vmohammad, Devs.Joona],
settings,
tags: [
// Spotify

View file

@ -21,7 +21,7 @@ import "./spotifyStyles.css";
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@shared/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
@ -76,27 +76,28 @@ function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
);
}
function CopyContextMenu({ name, path }: { name: string; path: string; }) {
const copyId = `spotify-copy-${name}`;
const openId = `spotify-open-${name}`;
function CopyContextMenu({ name, type, path }: { type: string; name: string; path: string; }) {
return (
<Menu.Menu
navId={`spotify-${name}-menu`}
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={`Spotify ${name} Menu`}
navId="vc-spotify-menu"
onClose={ContextMenuApi.closeContextMenu}
aria-label={`Spotify ${type} Menu`}
>
<Menu.MenuItem
key={copyId}
id={copyId}
label={`Copy ${name} Link`}
id="vc-spotify-copy-name"
label={`Copy ${type} Name`}
action={() => copyWithToast(name)}
icon={CopyIcon}
/>
<Menu.MenuItem
id="vc-spotify-copy-link"
label={`Copy ${type} Link`}
action={() => copyWithToast("https://open.spotify.com" + path)}
icon={LinkIcon}
/>
<Menu.MenuItem
key={openId}
id={openId}
label={`Open ${name} in Spotify`}
id="vc-spotify-open"
label={`Open ${type} in Spotify`}
action={() => SpotifyStore.openExternal(path)}
icon={OpenExternalIcon}
/>
@ -104,11 +105,6 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) {
);
}
function makeContextMenu(name: string, path: string) {
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
ContextMenuApi.openContextMenu(e, () => <CopyContextMenu name={name} path={path} />);
}
function Controls() {
const [isPlaying, shuffle, repeat] = useStateFromStores(
[SpotifyStore],
@ -259,13 +255,14 @@ function AlbumContextMenu({ track }: { track: Track; }) {
);
}
function makeLinkProps(name: string, condition: unknown, path: string) {
function makeLinkProps(type: "Song" | "Artist" | "Album", condition: unknown, name: string, path: string) {
if (!condition) return {};
return {
role: "link",
onClick: () => SpotifyStore.openExternal(path),
onContextMenu: makeContextMenu(name, path)
onContextMenu: e =>
ContextMenuApi.openContextMenu(e, () => <CopyContextMenu type={type} name={name} path={path} />)
} satisfies React.HTMLAttributes<HTMLElement>;
}
@ -306,7 +303,7 @@ function Info({ track }: { track: Track; }) {
id={cl("song-title")}
className={cl("ellipoverflow")}
title={track.name}
{...makeLinkProps("Song", track.id, `/track/${track.id}`)}
{...makeLinkProps("Song", track.id, track.name, `/track/${track.id}`)}
>
{track.name}
</Forms.FormText>
@ -319,7 +316,7 @@ function Info({ track }: { track: Track; }) {
className={cl("artist")}
style={{ fontSize: "inherit" }}
title={a.name}
{...makeLinkProps("Artist", a.id, `/artist/${a.id}`)}
{...makeLinkProps("Artist", a.id, a.name, `/artist/${a.id}`)}
>
{a.name}
</span>
@ -336,7 +333,7 @@ function Info({ track }: { track: Track; }) {
className={cl("album")}
style={{ fontSize: "inherit" }}
title={track.album.name}
{...makeLinkProps("Album", track.album.id, `/album/${track.album.id}`)}
{...makeLinkProps("Album", track.album.id, track.album.name, `/album/${track.album.id}`)}
>
{track.album.name}
</span>

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { User } from "@vencord/discord-types";
import { UserStore } from "@webpack/common";
@ -36,7 +36,7 @@ function ready(user: ModifiedUser) {
export default definePlugin({
name: "NoNitroUpsell",
description: "Removes ALL of Discord's nitro upsells by tricking the client into thinking you have nitro.",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
patches: [
{
find: "#{intl::USER_PROFILE_ENTRY_POINTS_AMP_UP_YOUR_PROFILE}",

View file

@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { debounce } from "@shared/debounce";
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { makeRange, OptionType } from "@utils/types";
import { findByCode, findByProps, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, MediaEngineStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore, VoiceActions, VoiceStateStore } from "@webpack/common";
@ -162,7 +162,7 @@ interface VoiceState {
export default definePlugin({
name: "RandomVoice",
description: "Adds a Button near the Mute button to join a random voice call.",
authors: [EquicordDevs.xijexo, EquicordDevs.omaw, EquicordDevs.thororen],
authors: [EquicordDevs.xijexo, EquicordDevs.omaw, Devs.thororen],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",

View file

@ -6,7 +6,7 @@
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { showNotification } from "@api/Notifications";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { saveFile } from "@utils/web";
import { findComponentByCodeLazy } from "@webpack";
@ -56,6 +56,6 @@ const button: ChatBarButtonFactory = ({ isMainChat }) => {
export default definePlugin({
name: "SaveFavoriteGIFs",
description: "Save favorite GIF urls to a file",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
renderChatBarButton: button,
});

View file

@ -20,7 +20,7 @@ import { getListeners } from "./utils";
export default definePlugin({
name: "SoundBoardLogger",
authors: [Devs.Moxxie, EquicordDevs.Fres, Devs.amy, EquicordDevs.thororen],
authors: [Devs.Moxxie, EquicordDevs.Fres, Devs.amy, Devs.thororen],
description: "Logs all soundboards that are played in a voice chat and allows you to download them",
dependencies: ["ChatInputButtonAPI"],
patches: [

View file

@ -5,7 +5,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Constants, React, RestAPI, useEffect, useState } from "@webpack/common";
@ -81,7 +81,7 @@ function SpotifyActivityToggleButton() {
export default definePlugin({
name: "SpotifyActivityToggle",
description: "Adds a toggle button for Spotify activity visibility.",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",

View file

@ -6,7 +6,7 @@
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { PresenceStore, UserStore } from "@webpack/common";
@ -41,7 +41,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "StatusWhilePlaying",
description: "Automatically updates your online status (online, idle, dnd) when launching games",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
flux: {
RUNNING_GAMES_CHANGE(event) {

View file

@ -17,7 +17,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
@ -32,7 +32,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "UnlimitedAccounts",
description: "Increases the amount of accounts you can add.",
authors: [EquicordDevs.thororen],
authors: [Devs.thororen],
settings,
patches: [
{

View file

@ -6,7 +6,7 @@
import { definePluginSettings } from "@api/Settings";
import { Link } from "@components/Link";
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
@ -29,7 +29,7 @@ export default definePlugin({
data,
name: "UserPFP",
description: "Allows you to use an animated avatar without Nitro",
authors: [EquicordDevs.nexpid, EquicordDevs.thororen],
authors: [EquicordDevs.nexpid, Devs.thororen],
settings,
settingsAboutComponent: () => (
<>

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import { isEquicordPluginDev, isPluginDev } from "@utils/misc";
import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
@ -12,7 +12,7 @@ import { UserStore } from "@webpack/common";
export default definePlugin({
name: "VCSupport",
description: "Wumpus Dance + Support Warnings",
authors: [EquicordDevs.thororen, EquicordDevs.coolesding],
authors: [Devs.thororen, EquicordDevs.coolesding],
required: true,
start() {
const selfId = UserStore.getCurrentUser()?.id;

View file

@ -108,7 +108,7 @@ let clientOldChannelId: string | undefined;
export default definePlugin({
name: "VoiceChannelLog",
description: "Logs who joins and leaves voice channels",
authors: [Devs.Sqaaakoi, EquicordDevs.thororen, EquicordDevs.nyx],
authors: [Devs.Sqaaakoi, Devs.thororen, EquicordDevs.nyx],
contextMenus: {
"channel-context": patchChannelContextMenu
},

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import { humanFriendlyJoin } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types";
import { Message, User } from "@vencord/discord-types";
@ -104,7 +104,7 @@ let clientOldChannelId: string | undefined;
export default definePlugin({
name: "VoiceJoinMessages",
description: "Recieve client-side ephemeral messages when your friends join voice channels",
authors: [Devs.Sqaaakoi, EquicordDevs.thororen],
authors: [Devs.Sqaaakoi, Devs.thororen],
settings,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {

1
src/globals.d.ts vendored
View file

@ -65,6 +65,7 @@ declare global {
*/
export var DiscordNative: any;
export var Vesktop: any;
export var Equibop: any;
export var VesktopNative: any;
interface Window extends Record<PropertyKey, any> {

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
match: /(?<=\]\}\)),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
}

View file

@ -165,7 +165,7 @@ function generatePluginList() {
}
if (enabledPlugins.length > 100 && !(isPluginDev(UserStore.getCurrentUser()?.id) || isEquicordPluginDev(UserStore.getCurrentUser()?.id))) {
return Alerts.show({
Alerts.show({
title: "You are attempting to get support!",
body: <div>
<style>
@ -180,6 +180,8 @@ function generatePluginList() {
<Forms.FormText>try removing some plugins and see if it fixes!</Forms.FormText>
</div>
});
return "You have more than 100 plugins enabled, please reduce the number of enabled plugins to get support.";
}
return content;

View file

@ -17,12 +17,12 @@
*/
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { Upload } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { reverseExtensionMap } from "@equicordplugins/fixFileExtensions";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { CloudUpload } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
@ -96,7 +96,7 @@ export default definePlugin({
},
],
AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: Upload; }) => {
AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: CloudUpload; }) => {
const [anonymise, setAnonymise] = useState(upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault);
function onToggleAnonymise() {
@ -117,8 +117,7 @@ export default definePlugin({
);
}, { noop: true }),
anonymise(upload: Upload) {
anonymise(upload: CloudUpload) {
const originalFileName = upload.filename;
const tarMatch = tarExtMatcher.exec(originalFileName);
const extIdx = tarMatch?.index ?? originalFileName.lastIndexOf(".");

View file

@ -0,0 +1,11 @@
# ClearURLs
Automatically removes tracking elements from URLs you send.
Uses data from the [ClearURLs browser extension](https://clearurls.xyz/).
## Example
**Before:** `https://www.amazon.com/dp/exampleProduct/ref=sxin_0_pb?__mk_de_DE=ÅMÅŽÕÑ&keywords=tea&pd_rd_i=exampleProduct&pd_rd_r=8d39e4cd-1e4f-43db-b6e7-72e969a84aa5&pd_rd_w=1pcKM&pd_rd_wg=hYrNl&pf_rd_p=50bbfd25-5ef7-41a2-68d6-74d854b30e30&pf_rd_r=0GMWD0YYKA7XFGX55ADP&qid=1517757263&rnid=2914120011`
**After:** `https://www.amazon.com/dp/exampleProduct/`

View file

@ -1,162 +0,0 @@
/*
* 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/>.
*/
export const defaultRules = [
"action_object_map",
"action_type_map",
"action_ref_map",
"spm@*.aliexpress.com",
"scm@*.aliexpress.com",
"aff_platform",
"aff_trace_key",
"algo_expid@*.aliexpress.*",
"algo_pvid@*.aliexpress.*",
"btsid",
"ws_ab_test",
"pd_rd_*@amazon.*",
"_encoding@amazon.*",
"psc@amazon.*",
"tag@amazon.*",
"ref_@amazon.*",
"pf_rd_*@amazon.*",
"pf@amazon.*",
"crid@amazon.*",
"keywords@amazon.*",
"sprefix@amazon.*",
"sr@amazon.*",
"ie@amazon.*",
"node@amazon.*",
"qid@amazon.*",
"callback@bilibili.com",
"cvid@bing.com",
"form@bing.com",
"sk@bing.com",
"sp@bing.com",
"sc@bing.com",
"qs@bing.com",
"pq@bing.com",
"sc_cid",
"mkt_tok",
"trk",
"trkCampaign",
"ga_*",
"gclid",
"gclsrc",
"hmb_campaign",
"hmb_medium",
"hmb_source",
"spReportId",
"spJobID",
"spUserID",
"spMailingID",
"itm_*",
"s_cid",
"elqTrackId",
"elqTrack",
"assetType",
"assetId",
"recipientId",
"campaignId",
"siteId",
"mc_cid",
"mc_eid",
"pk_*",
"sc_campaign",
"sc_channel",
"sc_content",
"sc_medium",
"sc_outcome",
"sc_geo",
"sc_country",
"nr_email_referer",
"vero_conv",
"vero_id",
"yclid",
"_openstat",
"mbid",
"cmpid",
"cid",
"c_id",
"campaign_id",
"Campaign",
"hash@ebay.*",
"fb_action_ids",
"fb_action_types",
"fb_ref",
"fb_source",
"fbclid",
"refsrc@facebook.com",
"hrc@facebook.com",
"gs_l",
"gs_lcp@google.*",
"ved@google.*",
"ei@google.*",
"sei@google.*",
"gws_rd@google.*",
"gs_gbg@google.*",
"gs_mss@google.*",
"gs_rn@google.*",
"_hsenc",
"_hsmi",
"__hssc",
"__hstc",
"hsCtaTracking",
"source@sourceforge.net",
"position@sourceforge.net",
"t@*.twitter.com",
"s@*.twitter.com",
"ref_*@*.twitter.com",
"t@*.x.com",
"s@*.x.com",
"ref_*@*.x.com",
"t@*.fixupx.com",
"s@*.fixupx.com",
"ref_*@*.fixupx.com",
"t@*.fxtwitter.com",
"s@*.fxtwitter.com",
"ref_*@*.fxtwitter.com",
"t@*.vxtwitter.com",
"s@*.vxtwitter.com",
"ref_*@*.vxtwitter.com",
"t@*.twittpr.com",
"s@*.twittpr.com",
"ref_*@*.twittpr.com",
"t@*.fixvx.com",
"s@*.fixvx.com",
"ref_*@*.fixvx.com",
"tt_medium",
"tt_content",
"lr@yandex.*",
"redircnt@yandex.*",
"feature@*.youtube.com",
"kw@*.youtube.com",
"si@*.youtube.com",
"pp@*.youtube.com",
"si@*.youtu.be",
"wt_zmc",
"utm_source",
"utm_content",
"utm_medium",
"utm_campaign",
"utm_term",
"si@open.spotify.com",
"igshid",
"igsh",
"share_id@reddit.com",
"si@soundcloud.com",
];

View file

@ -22,77 +22,74 @@ import {
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { defaultRules } from "./defaultRules";
const CLEAR_URLS_JSON_URL = "https://raw.githubusercontent.com/ClearURLs/Rules/master/data.min.json";
// From lodash
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
interface Provider {
urlPattern: string;
completeProvider: boolean;
rules?: string[];
rawRules?: string[];
referralMarketing?: string[];
exceptions?: string[];
redirections?: string[];
forceRedirection?: boolean;
}
interface ClearUrlsData {
providers: Record<string, Provider>;
}
interface RuleSet {
name: string;
urlPattern: RegExp;
rules?: RegExp[];
rawRules?: RegExp[];
exceptions?: RegExp[];
}
export default definePlugin({
name: "ClearURLs",
description: "Removes tracking garbage from URLs",
authors: [Devs.adryd],
description: "Automatically removes tracking elements from URLs you send",
authors: [Devs.adryd, Devs.thororen],
start() {
this.createRules();
rules: [] as RuleSet[],
async start() {
await this.createRules();
},
stop() {
this.rules = [];
},
onBeforeMessageSend(_, msg) {
return this.onSend(msg);
return this.cleanMessage(msg);
},
onBeforeMessageEdit(_cid, _mid, msg) {
return this.onSend(msg);
return this.cleanMessage(msg);
},
escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str))
? str.replace(reRegExpChar, "\\$&")
: (str || "");
},
async createRules() {
const res = await fetch(CLEAR_URLS_JSON_URL)
.then(res => res.json()) as ClearUrlsData;
createRules() {
// Can be extended upon once user configs are available
// Eg. (useDefaultRules: boolean, customRules: Array[string])
const rules = defaultRules;
this.rules = [];
this.universalRules = new Set();
this.rulesByHost = new Map();
this.hostRules = new Map();
for (const [name, provider] of Object.entries(res.providers)) {
const urlPattern = new RegExp(provider.urlPattern, "i");
for (const rule of rules) {
const splitRule = rule.split("@");
const paramRule = new RegExp(
"^" +
this.escapeRegExp(splitRule[0]).replace(/\\\*/, ".+?") +
"$"
);
const rules = provider.rules?.map(rule => new RegExp(rule, "i"));
const rawRules = provider.rawRules?.map(rule => new RegExp(rule, "i"));
const exceptions = provider.exceptions?.map(ex => new RegExp(ex, "i"));
if (!splitRule[1]) {
this.universalRules.add(paramRule);
continue;
}
const hostRule = new RegExp(
"^(www\\.)?" +
this.escapeRegExp(splitRule[1])
.replace(/\\\./, "\\.")
.replace(/^\\\*\\\./, "(.+?\\.)?")
.replace(/\\\*/, ".+?") +
"$"
);
const hostRuleIndex = hostRule.toString();
this.hostRules.set(hostRuleIndex, hostRule);
if (this.rulesByHost.get(hostRuleIndex) == null) {
this.rulesByHost.set(hostRuleIndex, new Set());
}
this.rulesByHost.get(hostRuleIndex).add(paramRule);
}
},
removeParam(rule: string | RegExp, param: string, parent: URLSearchParams) {
if (param === rule || rule instanceof RegExp && rule.test(param)) {
parent.delete(param);
this.rules.push({
name,
urlPattern,
rules,
rawRules,
exceptions,
});
}
},
@ -106,34 +103,40 @@ export default definePlugin({
}
// Cheap way to check if there are any search params
if (url.searchParams.entries().next().done) {
// If there are none, we don't need to modify anything
return match;
}
if (url.searchParams.entries().next().done) return match;
// Check all universal rules
this.universalRules.forEach(rule => {
url.searchParams.forEach((_value, param, parent) => {
this.removeParam(rule, param, parent);
});
});
// Check rules for each provider that matches
this.rules.forEach(({ urlPattern, exceptions, rawRules, rules }) => {
if (!urlPattern.test(url.href) || exceptions?.some(ex => ex.test(url.href))) return;
// Check rules for each hosts that match
this.hostRules.forEach((regex, hostRuleName) => {
if (!regex.test(url.hostname)) return;
this.rulesByHost.get(hostRuleName).forEach(rule => {
url.searchParams.forEach((_value, param, parent) => {
this.removeParam(rule, param, parent);
const toDelete: string[] = [];
if (rules) {
// Add matched params to delete list
url.searchParams.forEach((_, param) => {
if (rules.some(rule => rule.test(param))) {
toDelete.push(param);
}
});
}
// Delete matched params from list
toDelete.forEach(param => url.searchParams.delete(param));
// Match and remove any raw rules
let cleanedUrl = url.href;
rawRules?.forEach(rawRule => {
cleanedUrl = cleanedUrl.replace(rawRule, "");
});
url = new URL(cleanedUrl);
});
return url.toString();
},
onSend(msg: MessageObject) {
cleanMessage(msg: MessageObject) {
// Only run on messages that contain URLs
if (msg.content.match(/http(s)?:\/\//)) {
if (/http(s)?:\/\//.test(msg.content)) {
msg.content = msg.content.replace(
/(https?:\/\/[^\s<]+[^<.,:;"'>)|\]\s])/g,
match => this.replacer(match)

View file

@ -180,6 +180,13 @@ function makeShortcuts() {
experimentBucket: bucket,
});
},
switchBranch: (branch: string) => {
if (!IS_VESKTOP && !IS_EQUIBOP) throw new Error("This function only works on vesktop and equibop.");
const target = IS_VESKTOP ? Vesktop : Equibop;
if (target.Settings.store.discordBranch === branch) throw new Error(`Already on ${branch}.`);
target.Settings.store.discordBranch = branch;
VesktopNative.app.relaunch();
},
};
}

View file

@ -22,8 +22,8 @@ import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { initWs, socket, stopWs } from "./initWs";
console.log("imported");
export const PORT = 8485;
export const CLIENT_VERSION: readonly [major: number, minor: number, patch: number] = [0, 1, 1];
export const logger = new Logger("DevCompanion");
@ -67,4 +67,3 @@ export default definePlugin({
stop: stopWs,
});

View file

@ -14,7 +14,7 @@ import { loadLazyChunks } from "debug/loadLazyChunks";
import { reporterData } from "debug/reporterData";
import { Settings } from "Vencord";
import { logger, PORT, settings } from ".";
import { CLIENT_VERSION, logger, PORT, settings } from ".";
import { Recieve } from "./types";
import { FullOutgoingMessage, OutgoingMessage } from "./types/send";
import { extractModule, extractOrThrow, findModuleId, getModulePatchedBy, mkRegexFind, parseNode, toggleEnabled, } from "./util";
@ -63,7 +63,6 @@ export function initWs(isManual = false) {
}));
}
try {
if (settings.store.notifyOnAutoConnect || isManual) {
Toasts.show({
@ -449,6 +448,16 @@ export function initWs(isManual = false) {
});
break;
}
case "version": {
replyData({
type: "version",
ok: true,
data: {
clientVersion: CLIENT_VERSION
}
});
break;
}
default:
// @ts-expect-error should be never
reply("Unknown Type " + d?.type);

View file

@ -62,7 +62,7 @@ export type FindData = {
args: FindNode[];
};
export type IncomingMessage = DisablePlugin | RawId | DiffPatch | Reload | ExtractModule | TestPatch | TestFind | AllModules | I18nLookup;
export type IncomingMessage = DisablePlugin | RawId | DiffPatch | Reload | ExtractModule | TestPatch | TestFind | AllModules | I18nLookup | Version;
export type FullIncomingMessage = IncomingMessage & { nonce: number; };
export type DisablePlugin = {
@ -109,6 +109,14 @@ export type ExtractModule = {
data: FindOrSearchData;
};
export type Version = {
type: "version";
data: {
// major minor patch
server_version: [number, number, number];
};
};
export type TestPatch = {
type: "testPatch";
data: (

View file

@ -4,20 +4,19 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// should be the same types as src/server/types/recieve.ts in the companion
import { ReporterData as IReporterData } from "debug/reporterData";
export type ReporterData = IReporterData;
import { ReporterData } from "debug/reporterData";
export type OutgoingMessage = (Report | DiffModule | ExtractModule | ModuleList | RawId | I18nValue) & Base;
export type OutgoingMessage = Base<DiffModule | ExtractModule | ModuleList | RawId | I18nValue | VersionResponse>;
export type FullOutgoingMessage = OutgoingMessage & Nonce;
export type Base = {
export type Base<T> = ({
ok: true;
} | {
} & T) | ({
ok: false;
data?: any;
data: null;
error: string;
};
} & Omit<T, "data">);
export type Nonce = {
nonce: number;
};
@ -81,4 +80,11 @@ export type RawId = {
type: "rawId";
data: string;
};
export type VersionResponse = {
type: "version";
data: {
clientVersion: readonly [number, number, number];
};
};
// #endregion

View file

@ -182,8 +182,8 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers, and use client themes.",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan],
description: "Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality",
dependencies: ["MessageEventsAPI"],
settings,
@ -281,6 +281,14 @@ export default definePlugin({
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
}
},
// Allow users to use custom client themes
{
find: "customUserThemeSettings:{",
replacement: {
match: /(?<=\i=)\(0,\i\.\i\)\(\i\.\i\.TIER_2\)(?=,|;)/g,
replace: "true"
}
},
{
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
replacement: [
@ -392,24 +400,15 @@ export default definePlugin({
if (premiumType !== 2) {
proto.appearance ??= AppearanceSettingsActionCreators.create();
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
theme: UserSettingsProtoStore.settings.appearance.theme
});
const protoStoreAppearenceSettings = UserSettingsProtoStore.settings.appearance;
proto.appearance.theme = appearanceSettingsDummy.theme;
}
const appearanceSettingsOverwrite = AppearanceSettingsActionCreators.create({
...proto.appearance,
theme: protoStoreAppearenceSettings?.theme,
clientThemeSettings: protoStoreAppearenceSettings?.clientThemeSettings
});
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
backgroundGradientPresetId: {
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
}
});
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
}
proto.appearance = appearanceSettingsOverwrite;
}
} catch (err) {
new Logger("FakeNitro").error(err);

View file

@ -20,18 +20,17 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { useCallback, useEffect, useRef, useState } from "@webpack/common";
interface SearchBarComponentProps {
ref?: React.MutableRefObject<any>;
ref?: React.RefObject<any>;
autoFocus: boolean;
className: string;
size: string;
onChange: (query: string) => void;
onClear: () => void;
query: string;
placeholder: string;
className?: string;
}
type TSearchBarComponent =
@ -59,9 +58,6 @@ interface Instance {
forceUpdate: () => void;
}
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow");
export const settings = definePluginSettings({
searchOption: {
type: OptionType.SELECT,
@ -136,7 +132,7 @@ export default definePlugin({
function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) {
const [query, setQuery] = useState("");
const ref = useRef<{ containerRef?: React.MutableRefObject<HTMLDivElement>; } | null>(null);
const ref = useRef<{ containerRef?: React.RefObject<HTMLDivElement>; } | null>(null);
const onChange = useCallback((searchQuery: string) => {
setQuery(searchQuery);
@ -152,7 +148,7 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
// scroll back to top
ref.current?.containerRef?.current
.closest("#gif-picker-tab-panel")
?.closest("#gif-picker-tab-panel")
?.querySelector("[class|=\"content\"]")
?.firstElementChild?.scrollTo(0, 0);
@ -181,8 +177,8 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
<SearchBarComponent
ref={ref}
autoFocus={true}
className={containerClasses.searchBar}
size="md"
className=""
onChange={onChange}
onClear={() => {
setQuery("");

View file

@ -1,7 +1,4 @@
[class^="panels"] [class^="avatarWrapper"] {
min-width: 88px;
}
.vc-game-activity {
margin-right: 4px;
}
/* make gap smaller since we're adding an extra button */
[class^="panels"] [class^="buttons"] {
gap: 4px;
}

View file

@ -48,6 +48,7 @@ interface Activity {
buttons?: Array<string>;
name: string;
application_id: string;
status_display_type?: number;
metadata?: {
button_urls?: Array<string>;
};
@ -137,6 +138,25 @@ const settings = definePluginSettings({
type: OptionType.STRING,
default: "some music",
},
statusDisplayType: {
description: "Show the track / artist name in the member list",
type: OptionType.SELECT,
options: [
{
label: "Don't show (shows generic listening message)",
value: "off",
default: true
},
{
label: "Show artist name",
value: "artist"
},
{
label: "Show track name",
value: "track"
}
]
},
nameFormat: {
description: "Show name of song and artist in status name",
type: OptionType.SELECT,
@ -354,6 +374,11 @@ export default definePlugin({
details: trackData.name,
state: trackData.artist,
status_display_type: {
"off": 0,
"artist": 1,
"track": 2
}[settings.store.statusDisplayType],
assets,
buttons: buttons.length ? buttons.map(v => v.label) : undefined,

View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function CircleIcon({ className }: { className?: string; }) {
return (
<svg viewBox="0 0 24 24" className={className}>
<circle
cx="12"
cy="12"
r="8"
/>
</svg>
);
}

View file

@ -9,6 +9,7 @@ import { isObjectEmpty } from "@utils/misc";
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from ".";
import { CircleIcon } from "./CircleIcon";
import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
import { VoiceIcon } from "./VoiceIcon";
@ -80,7 +81,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
<Tooltip text={`${formattedOnlineCount} online in this channel`} position="bottom">
{props => (
<div {...props} className={cl("container")}>
<span className={cl("online-dot")} />
<CircleIcon className={cl("online-count")} />
<span className={cl("online")}>{formattedOnlineCount}</span>
</div>
)}
@ -88,7 +89,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
{props => (
<div {...props} className={cl("container")}>
<span className={cl("total-dot")} />
<CircleIcon className={cl("total-count")} />
<span className={cl("total")}>{numberFormat(totalCount)}</span>
</div>
)}

View file

@ -36,7 +36,6 @@ export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as F
getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
};
export const settings = definePluginSettings({
toolTip: {
type: OptionType.BOOLEAN,

View file

@ -18,6 +18,7 @@
flex-wrap: wrap;
margin-top: 1em;
padding-inline: 1em;
line-height: 1.2em;
}
.vc-membercount-container {
@ -38,20 +39,18 @@
color: var(--color-voice);
}
.vc-membercount-online-dot {
background-color: var(--color-online);
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
.vc-membercount-online-count {
fill: var(--status-online);
width: 18px;
height: 18px;
}
.vc-membercount-total-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
border: 3px solid var(--color-total);
.vc-membercount-total-count {
fill: none;
stroke: var(--status-offline);
stroke-width: 4px;
width: 15px;
height: 15px;
}
.vc-membercount-voice-icon {

View file

@ -87,8 +87,8 @@ export default definePlugin({
replacement: {
// The two groups inside the first group grab the minified names of the variables,
// they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\.NONE\?)\1\+\2/,
replace: "0"
match: /(\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.+?)\2\+\3/,
replace: (_, rest) => `${rest}0`
}
}
],

View file

@ -20,7 +20,7 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { generateId, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { MessageAttachment } from "@vencord/discord-types";
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common";
@ -45,7 +45,7 @@ const getImageBox = (url: string): Promise<{ width: number, height: number; } |
const getAttachments = async (channelId: string) =>
await Promise.all(
UploadStore.getUploads(channelId, DraftType.ChannelMessage)
.map(async (upload: any) => {
.map(async (upload: CloudUpload) => {
const { isImage, filename, spoiler, item: { file } } = upload;
const url = URL.createObjectURL(file);
const attachment: MessageAttachment = {
@ -53,7 +53,7 @@ const getAttachments = async (channelId: string) =>
filename: spoiler ? "SPOILER_" + filename : filename,
// weird eh? if i give it the normal content type the preview doenst work
content_type: undefined,
size: await upload.getSize(),
size: upload.getSize(),
spoiler,
// discord adds query params to the url, so we need to add a hash to prevent that
url: url + "#",

View file

@ -635,12 +635,26 @@ function renderUsername(
const hoveringMessageSet = new Set<string>();
const hoveringReactionPopoutSet = new Set<string>();
function handleHoveringMessage(message: any, animate: boolean) {
if (!message) return;
if (animate) {
addHoveringMessage(message.id);
addHoveringMessage(message.showMeYourNameGroupId);
} else {
removeHoveringMessage(message.id);
removeHoveringMessage(message.showMeYourNameGroupId);
}
}
function addHoveringMessage(id: string) {
if (!id) return;
hoveringMessageSet.add(id);
settings.store.triggerNameRerender = !settings.store.triggerNameRerender;
}
function removeHoveringMessage(id: string) {
if (!id) return;
hoveringMessageSet.delete(id);
settings.store.triggerNameRerender = !settings.store.triggerNameRerender;
}
@ -783,14 +797,21 @@ export default definePlugin({
]
},
{
// Track hovering on messages to animate mentions.
find: /setAnimate.{0,50}\.ANIMATE_CHAT_AVATAR,/,
replacement: {
// Track hovering on messages to animate names since Discord's
// built-in hover tracking is buggy. Used by a patch below.
match: /(let{setAnimate:\i}=(\i);)/,
replace: "$1if(arguments[0].message){if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)}};"
replace: "$1$self.handleHoveringMessage(arguments[0].message,$2.animate);"
}
},
{
// Attach the group ID to their messages to allow animating mentions within a group.
find: "CUSTOM_GIFT?\"\":",
replacement: {
match: /(\(\i,\i,\i\);)(let \i=\i.id===\i(?:.{0,500}?)hovering:(\i))/,
replace: "$1arguments[0].message.showMeYourNameGroupId=arguments[0].groupId;$2"
},
},
{
// Replace names in mentions.
find: ".USER_MENTION)",
@ -815,19 +836,6 @@ export default definePlugin({
replace: "$1props:arguments[2],"
}
},
{
// Track hovering over second-level messages to animate gradients.
// Tack on the groupId, which is how the client groups messages from
// the same author together, to the message temporarily so that mentions
// across a group can animate together. By default, Discord handles this,
// but moving between messages can cause a race condition where the animation
// stops on one of the messages in the group. This patch fixes that.
find: "CUSTOM_GIFT?\"\":",
replacement: {
match: /(\(\i,\i,\i\);)(let \i=\i.id===\i(?:.{0,500}?)hovering:(\i))/,
replace: "$1arguments[0].message.showMeYourNameGroupId=arguments[0].groupId;if($3){$self.addHoveringMessage(arguments[0].groupId)}else{$self.removeHoveringMessage(arguments[0].groupId)};$2"
},
},
{
// Replace names in the member list.
find: "let{colorRoleName:",
@ -936,6 +944,7 @@ export default definePlugin({
addHoveringMessage,
removeHoveringMessage,
handleHoveringMessage,
addHoveringReactionPopout,
removeHoveringReactionPopout,
getMessageNameText,

View file

@ -27,6 +27,8 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa
import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { CloudUpload as TCloudUpload } from "@vencord/discord-types";
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
@ -38,7 +40,7 @@ import { cl } from "./utils";
import { VoicePreview } from "./VoicePreview";
import { VoiceRecorderWeb } from "./WebRecorder";
const CloudUpload = findLazy(m => m.prototype?.trackUploadFinished);
const CloudUpload: typeof TCloudUpload = findLazy(m => m.prototype?.trackUploadFinished);
const PendingReplyStore = findStoreLazy("PendingReplyStore");
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
@ -93,8 +95,8 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
const upload = new CloudUpload({
file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }),
isThumbnail: false,
platform: 1,
}, channelId, false, 0);
platform: CloudUploadPlatform.WEB,
}, channelId);
upload.on("complete", () => {
RestAPI.post({

View file

@ -24,7 +24,7 @@ const settings = definePluginSettings({
multiplier: {
description: "Volume Multiplier",
type: OptionType.SLIDER,
markers: makeRange(1, 5, 1),
markers: makeRange(1, 5, 0.5),
default: 2,
stickToMarkers: true,
}

View file

@ -621,6 +621,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Cootshk",
id: 921605971577548820n
},
thororen: {
name: "thororen",
id: 848339671629299742n
},
} satisfies Record<string, Dev>);
export const EquicordDevs = Object.freeze({

View file

@ -17,7 +17,7 @@
*/
import { MessageObject } from "@api/MessageEvents";
import { Channel, Guild, GuildFeatures, Message, User } from "@vencord/discord-types";
import { Channel, CloudUpload, Guild, GuildFeatures, Message, User } from "@vencord/discord-types";
import { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, IconUtils, InviteActions, MessageActions, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Except } from "type-fest";
@ -117,6 +117,20 @@ interface MessageOptions {
replied_user: boolean;
};
stickerIds: string[];
attachmentsToUpload: CloudUpload[];
poll: {
allow_multiselect: boolean;
answers: Array<{
poll_media: {
text: string;
attachment_ids?: unknown;
emoji?: { name: string; id?: string; };
};
}>;
duration: number;
layout_type: number;
question: { text: string; };
};
}
export function sendMessage(

View file

@ -17,7 +17,7 @@
*/
import { React, useEffect, useMemo, useReducer, useState } from "@webpack/common";
import { ActionDispatch } from "react";
import { ActionDispatch, ReactNode } from "react";
import { checkIntersecting } from "./misc";
@ -25,6 +25,14 @@ export * from "./lazyReact";
export const NoopComponent = () => null;
/**
* Check if a React node is a primitive (string, number, bigint, boolean, undefined)
*/
export function isPrimitiveReactNode(node: ReactNode): boolean {
const t = typeof node;
return t === "string" || t === "number" || t === "bigint" || t === "boolean" || t === "undefined";
}
/**
* Check if an element is on screen
* @param intersectOnly If `true`, will only update the state when the element comes into view

View file

@ -21,7 +21,8 @@ import { moment } from "@webpack/common";
// Utils for readable text transformations eg: `toTitle(fromKebab())`
// Case style to words
export const wordsFromCamel = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());
export const wordsFromCamel = (text: string) =>
text.split(/(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])/).map(w => /^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase());
export const wordsFromSnake = (text: string) => text.toLowerCase().split("_");
export const wordsFromKebab = (text: string) => text.toLowerCase().split("-");
export const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase());

View file

@ -42,7 +42,7 @@ waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
waitFor(filters.componentByCode('path:["empty"]'), m => Menu.Menu = m);
waitFor(filters.componentByCode("sliderContainer", "slider", "handleSize:16", "=100"), m => Menu.MenuSliderControl = m);
waitFor(filters.componentByCode('role:"searchbox', "top:2", "query:"), m => Menu.MenuSearchControl = m);
waitFor(filters.componentByCode(".SEARCH)", ".focus()", "query:"), m => Menu.MenuSearchControl = m);
export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),