Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2025-09-08 21:45:09 -04:00
commit 3b593ae3dd
No known key found for this signature in database
29 changed files with 324 additions and 354 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

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

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

@ -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,13 +20,11 @@ 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;
@ -59,9 +57,6 @@ interface Instance {
forceUpdate: () => void;
}
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow");
export const settings = definePluginSettings({
searchOption: {
type: OptionType.SELECT,
@ -136,7 +131,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);
@ -181,7 +176,6 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
<SearchBarComponent
ref={ref}
autoFocus={true}
className={containerClasses.searchBar}
size="md"
onChange={onChange}
onClear={() => {

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

@ -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(".useImperativeHandle(", ".SEARCH)", "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"),