merge unconflict

This commit is contained in:
2025-09-09 18:44:49 +02:00
47 changed files with 536 additions and 401 deletions

View File

@@ -19,12 +19,9 @@
- More Plugins
- Utility (ex.: ToneIndicators)
- Fun (ex.: DecTalk, MoreMarkdown)
- In-built core functions (ex.: Server Themes)
- Customization (ex.: Server Themes)
- Alien Cats
> [!NOTE]
> Server themes are currently disabled, as they'll be rewritten soon.
---
> [!IMPORTANT]

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 "./utils";
export * as Webpack from "../webpack";

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

@@ -0,0 +1,11 @@
import { FluxStore } from "@vencord/discord-types";
export class StreamerModeStore extends FluxStore {
get autoToggle(): boolean;
get disableNotifications(): boolean;
get disableSounds(): boolean;
get enableContentProtection(): boolean;
get enabled(): boolean;
get hideInstantInvites(): boolean;
get hidePersonalInformation(): boolean;
}

View File

@@ -0,0 +1,42 @@
import { DiscordRecord } from "../common";
import { FluxStore } from "./FluxStore";
export type UserVoiceStateRecords = Record<string, VoiceState>;
export type VoiceStates = Record<string, UserVoiceStateRecords>;
export interface VoiceState extends DiscordRecord {
userId: string;
channelId: string | null | undefined;
sessionId: string | null | undefined;
mute: boolean;
deaf: boolean;
selfMute: boolean;
selfDeaf: boolean;
selfVideo: boolean;
selfStream: boolean | undefined;
suppress: boolean;
requestToSpeakTimestamp: string | null | undefined;
discoverable: boolean;
isVoiceMuted(): boolean;
isVoiceDeafened(): boolean;
}
export class VoiceStateStore extends FluxStore {
getAllVoiceStates(): VoiceStates;
getVoiceStates(guildId?: string | null): UserVoiceStateRecords;
getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
getVoiceState(guildId: string | null, userId: string): VoiceState | undefined;
getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined;
getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined;
getVoiceStateForUser(userId: string): VoiceState | undefined;
getCurrentClientVoiceChannelId(guildId: string | null): string | undefined;
isCurrentClientInVoiceChannel(): boolean;
isInChannel(channelId: string, userId?: string): boolean;
hasVideo(channelId: string): boolean;
}

View File

@@ -12,10 +12,12 @@ export * from "./RelationshipStore";
export * from "./SelectedChannelStore";
export * from "./SelectedGuildStore";
export * from "./StickersStore";
export * from "./StreamerModeStore";
export * from "./ThemeStore";
export * from "./TypingStore";
export * from "./UserProfileStore";
export * from "./UserStore";
export * from "./VoiceStateStore";
export * from "./WindowStore";
/**

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,30 +30,6 @@ export interface MessageObject {
tts: boolean;
}
export interface Upload {
classification: string;
currentSize: number;
description: string | null;
filename: string;
id: string;
isImage: 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?: {
@@ -64,7 +40,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

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

@@ -16,11 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Upload } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
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";
@@ -89,7 +89,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() {
@@ -110,7 +110,7 @@ export default definePlugin({
);
}, { noop: true }),
anonymise(upload: Upload) {
anonymise(upload: CloudUpload) {
if ((upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault) === false) {
return;
}

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,159 +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@*.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

@@ -80,7 +80,7 @@ export default definePlugin({
},
// Change top right chat toolbar button from the help one to the dev one
{
find: '"M9 3v18"',
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
match: /hasBugReporterAccess:(\i)/,
replace: "_hasBugReporterAccess:$1=true"

View File

@@ -159,7 +159,6 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
{ func: "canUseHighVideoUploadQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canStreamQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canUseClientThemes" },
{ func: "canUseCustomNotificationSounds" },
{ func: "canUsePremiumAppIcons" }
];
@@ -175,8 +174,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, use client themes and custom Discord notifications.",
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,
@@ -274,6 +273,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: [
@@ -387,24 +394,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,3 +1,4 @@
[class^="panels"] [class^="avatarWrapper"] {
min-width: 88px;
}
/* make gap smaller since we're adding an extra button */
[class^="panels"] [class^="buttons"] {
gap: 4px;
}

View File

@@ -47,6 +47,7 @@ interface Activity {
buttons?: Array<string>;
name: string;
application_id: string;
status_display_type?: number;
metadata?: {
button_urls?: Array<string>;
};
@@ -134,6 +135,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,
@@ -346,6 +366,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

@@ -6,16 +6,38 @@
import { getCurrentChannel } from "@utils/discord";
import { isObjectEmpty } from "@utils/misc";
import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common";
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, ThreadMemberListStore } from ".";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from ".";
import { CircleIcon } from "./CircleIcon";
import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
import { VoiceIcon } from "./VoiceIcon";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const { voiceActivity } = settings.use(["voiceActivity"]);
const includeVoice = voiceActivity && !isTooltip;
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
const voiceActivityCount = useStateFromStores(
[VoiceStateStore],
() => {
if (!includeVoice) return 0;
const voiceStates = VoiceStateStore.getVoiceStates(guildId);
if (!voiceStates) return 0;
return Object.values(voiceStates)
.filter(({ channelId }) => {
if (!channelId) return false;
const channel = ChannelStore.getChannel(channelId);
return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel);
})
.length;
}
);
const totalCount = useStateFromStores(
[GuildMemberCountStore],
() => GuildMemberCountStore.getMemberCount(guildId)
@@ -51,26 +73,37 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
if (totalCount == null)
return null;
const formattedVoiceCount = numberFormat(voiceActivityCount ?? 0);
const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?";
return (
<div className={cl("widget", { tooltip: isTooltip, "member-list": !isTooltip })}>
<Tooltip text={`${formattedOnlineCount} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span className={cl("online-dot")} />
<div {...props} className={cl("container")}>
<CircleIcon className={cl("online-count")} />
<span className={cl("online")}>{formattedOnlineCount}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
{props => (
<div {...props}>
<span className={cl("total-dot")} />
<div {...props} className={cl("container")}>
<CircleIcon className={cl("total-count")} />
<span className={cl("total")}>{numberFormat(totalCount)}</span>
</div>
)}
</Tooltip>
{includeVoice && voiceActivityCount > 0 &&
<Tooltip text={`${formattedVoiceCount} members in voice`} position="bottom">
{props => (
<div {...props} className={cl("container")}>
<VoiceIcon className={cl("voice-icon")} />
<span className={cl("voice")}>{formattedVoiceCount}</span>
</div>
)}
</Tooltip>
}
</div>
);
}

View File

@@ -0,0 +1,14 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function VoiceIcon({ className }: { className?: string; }) {
return (
<svg viewBox="0 0 32 32" fill="currentColor" className={className}>
<path d="M15.6668 3C14.2523 3 12.8958 3.5619 11.8956 4.5621C10.8954 5.56229 10.3335 6.91884 10.3335 8.33333V13.6666C10.3335 15.0811 10.8954 16.4378 11.8956 17.438C12.8958 18.4381 14.2523 19 15.6668 19C17.0813 19 18.4378 18.4381 19.438 17.438C20.4382 16.4378 21.0001 15.0811 21.0001 13.6666V8.33333C21.0001 6.91884 20.4382 5.56229 19.438 4.5621C18.4378 3.5619 17.0813 3 15.6668 3Z" />
<path d="M7.66667 13.6666C7.66667 13.313 7.52619 12.9739 7.27614 12.7238C7.02609 12.4738 6.68695 12.3333 6.33333 12.3333C5.97971 12.3333 5.64057 12.4738 5.39052 12.7238C5.14047 12.9739 5 13.313 5 13.6666C4.99911 16.2653 5.94692 18.7749 7.66545 20.7243C9.38399 22.6736 11.7551 23.9285 14.3334 24.2533V27H11.6667C11.3131 27 10.9739 27.1404 10.7239 27.3905C10.4738 27.6405 10.3334 27.9797 10.3334 28.3333C10.3334 28.6869 10.4738 29.0261 10.7239 29.2761C10.9739 29.5262 11.3131 29.6666 11.6667 29.6666H19.6667C20.0203 29.6666 20.3595 29.5262 20.6095 29.2761C20.8596 29.0261 21 28.6869 21 28.3333C21 27.9797 20.8596 27.6405 20.6095 27.3905C20.3595 27.1404 20.0203 27 19.6667 27H17V24.2533C19.5783 23.9285 21.9494 22.6736 23.6679 20.7243C25.3864 18.7749 26.3343 16.2653 26.3334 13.6666C26.3334 13.313 26.1929 12.9739 25.9428 12.7238C25.6928 12.4738 25.3536 12.3333 25 12.3333C24.6464 12.3333 24.3073 12.4738 24.0572 12.7238C23.8072 12.9739 23.6667 13.313 23.6667 13.6666C23.6667 15.7884 22.8238 17.8232 21.3235 19.3235C19.8233 20.8238 17.7884 21.6666 15.6667 21.6666C13.545 21.6666 11.5101 20.8238 10.0098 19.3235C8.50952 17.8232 7.66667 15.7884 7.66667 13.6666Z" />
</svg>
);
}

View File

@@ -36,19 +36,23 @@ export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as F
getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
};
const settings = definePluginSettings({
export const settings = definePluginSettings({
toolTip: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the server tooltip",
description: "Show member count on the server tooltip",
default: true,
restartNeeded: true
},
memberList: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the member list",
description: "Show member count in the member list",
default: true,
restartNeeded: true
},
voiceActivity: {
type: OptionType.BOOLEAN,
description: "Show voice activity with member count in the member list",
default: true
}
});
@@ -58,8 +62,8 @@ export const cl = classNameFactory("vc-membercount-");
export default definePlugin({
name: "MemberCount",
description: "Shows the amount of online & total members in the server member list and tooltip",
authors: [Devs.Ven, Devs.Commandtechno],
description: "Shows the number of online members, total members, and users in voice channels on the server — in the member list and tooltip.",
authors: [Devs.Ven, Devs.Commandtechno, Devs.Apexo],
settings,
patches: [
@@ -82,6 +86,6 @@ export default definePlugin({
predicate: () => settings.store.toolTip
}
],
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
render: ErrorBoundary.wrap(() => <MemberCount />, { noop: true }),
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
});

View File

@@ -1,9 +1,11 @@
.vc-membercount-widget {
gap: 0.85em;
display: flex;
align-content: center;
--color-online: var(--green-360);
--color-total: var(--primary-400);
--color-voice: var(--primary-400);
}
.vc-membercount-tooltip {
@@ -13,8 +15,16 @@
.vc-membercount-member-list {
justify-content: center;
flex-wrap: wrap;
margin-top: 1em;
padding-inline: 1em;
line-height: 1.2em;
}
.vc-membercount-container {
display: flex;
align-items: center;
gap: 0.5em;
}
.vc-membercount-online {
@@ -25,20 +35,26 @@
color: var(--color-total);
}
.vc-membercount-online-dot {
background-color: var(--color-online);
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 0.5em;
.vc-membercount-voice {
color: var(--color-voice);
}
.vc-membercount-total-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
border: 3px solid var(--color-total);
margin: 0 0.5em 0 1em;
.vc-membercount-online-count {
fill: var(--status-online);
width: 18px;
height: 18px;
}
.vc-membercount-total-count {
fill: none;
stroke: var(--status-offline);
stroke-width: 4px;
width: 15px;
height: 15px;
}
.vc-membercount-voice-icon {
color: var(--color-voice);
width: 15px;
height: 15px;
}

View File

@@ -98,7 +98,7 @@ export default definePlugin({
if (!isNonNullish(nonce)) return null;
// Bots basically never send a nonce, and if someone does do it then it's usually not a snowflake
if (message.bot) return null;
if (message.author.bot) return null;
let isDiscordKotlin = false;
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds

View File

@@ -128,8 +128,10 @@ const HTMLReact = (data, _1, _2, _3) => {
console.error(data.content);
}
}
// eslint-disable-next-line react/no-children-prop
return <ShadowDomComponent className="HTMLMessageContent" children={{ __html: trueContent }} />;
return (
// eslint-disable-next-line react/no-children-prop
<ShadowDomComponent className="HTMLMessageContentWrapper" children={{ __html: `<span class="HTMLMessageContent">${trueContent}</span>` }} />
);
};
function escapeRegex(str: string): string {

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}\i=)\1\+\2/,
replace: "0"
match: /(\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.+?)\2\+\3/,
replace: (_, rest) => `${rest}0`
}
}
],

View File

@@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import type { Message } from "@vencord/discord-types";
import { ChannelStore, GuildMemberStore } from "@webpack/common";
const settings = definePluginSettings({
userList: {
@@ -28,16 +29,22 @@ const settings = definePluginSettings({
type: OptionType.STRING,
default: "1234567890123445,1234567890123445",
},
roleList: {
description:
"List of roles to allow or exempt pings for (separated by commas or spaces)",
type: OptionType.STRING,
default: "1234567890123445,1234567890123445",
},
shouldPingListed: {
description: "Behaviour",
type: OptionType.SELECT,
options: [
{
label: "Do not ping the listed users",
label: "Do not ping the listed users / roles",
value: false,
},
{
label: "Only ping the listed users",
label: "Only ping the listed users / roles",
value: true,
default: true,
},
@@ -57,7 +64,14 @@ export default definePlugin({
settings,
shouldMention(message: Message, isHoldingShift: boolean) {
const isListed = settings.store.userList.includes(message.author.id);
let isListed = settings.store.userList.includes(message.author.id);
const channel = ChannelStore.getChannel(message.channel_id);
if (channel?.guild_id && !isListed) {
const roles = GuildMemberStore.getMember(channel.guild_id, message.author.id)?.roles;
isListed = !!roles && roles.some(role => settings.store.roleList.includes(role));
}
const isExempt = settings.store.shouldPingListed ? isListed : !isListed;
return settings.store.inverseShiftReply ? isHoldingShift !== isExempt : !isHoldingShift && isExempt;
},

View File

@@ -38,17 +38,21 @@ export default definePlugin({
description: "If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit",
authors: [Devs.ProffDea],
settings,
patches: [{
find: ".getDesktopType()===",
replacement: [{
match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/,
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
},
patches: [
{
match: /sound:(\i\?\i:void 0,soundpack:\i,volume:\i,onClick)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}]
}],
find: ".getDesktopType()===",
replacement: [
{
match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/,
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
},
{
match: /sound:(\i\?\i:void 0,volume:\i,onClick)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}
]
}
],
isPrivateChannelRead(message: MessageJSON) {
const channelType = ChannelStore.getChannel(message.channel_id)?.type;
if (

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

@@ -28,6 +28,7 @@ const Engines = {
Yandex: "https://yandex.com/images/search?rpt=imageview&url=",
SauceNAO: "https://saucenao.com/search.php?url=",
IQDB: "https://iqdb.org/?url=",
Bing: "https://www.bing.com/images/search?view=detailv2&iss=sbi&q=imgurl:",
TinEye: "https://www.tineye.com/search?url=",
ImgOps: "https://imgops.com/start?url="
} as const;

View File

@@ -34,7 +34,13 @@ export default definePlugin({
if (settings.store.serverBlockList.includes(guildId)) return;
const res = await fetch(`https://api.zoid.one/nexulien/servercss/${guildId}`);
if (!res.ok) return;
if (!res.ok) {
const styleEl = document.getElementById("server-styling");
if (styleEl) {
styleEl.remove();
}
return;
}
const css = await res.text();
let styleEl = document.getElementById("server-styling");
if (!styleEl) {

View File

@@ -11,7 +11,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Channel, Message, User } from "@vencord/discord-types";
import { RelationshipStore } from "@webpack/common";
import { RelationshipStore, StreamerModeStore } from "@webpack/common";
interface UsernameProps {
author: { nick: string; authorId: string; };
@@ -74,7 +74,9 @@ export default definePlugin({
const { mode, friendNicknames, displayNames, inReplies } = settings.store;
const user = userOverride ?? message.author;
let { username } = user;
let username = StreamerModeStore.enabled
? user.username[0] + "…"
: user.username;
if (displayNames)
username = user.globalName || username;

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

@@ -8,8 +8,8 @@ import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { Channel } from "@vencord/discord-types";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy, mapMangledModuleLazy } from "@webpack";
import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common";
import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, UserSummaryItem, useStateFromStores, VoiceStateStore } from "@webpack/common";
const cl = classNameFactory("vc-uvs-");
@@ -18,7 +18,6 @@ const { useChannelName } = mapMangledModuleLazy("#{intl::GROUP_DM_ALONE}", {
useChannelName: filters.byCode("()=>null==")
});
const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({");
const VoiceStateStore = findStoreLazy("VoiceStateStore");
const Avatar = findComponentByCodeLazy(".status)/2):0");
const GroupDMAvatars = findComponentByCodeLazy("frontSrc:", "getAvatarURL");
@@ -84,7 +83,7 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) {
const voiceStates = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStatesForChannel(channel.id));
const users = useMemo(
() => Object.values<any>(voiceStates).map(voiceState => UserStore.getUser(voiceState.userId)).filter(user => user != null),
() => Object.values(voiceStates).map(voiceState => UserStore.getUser(voiceState.userId)).filter(user => user != null),
[voiceStates]
);
@@ -139,7 +138,7 @@ export interface VoiceChannelIndicatorProps {
const clickTimers = {} as Record<string, any>;
export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isProfile, isActionButton, shouldHighlight }: VoiceChannelIndicatorProps) => {
const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId as string | undefined);
const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId);
const channel = channelId == null ? undefined : ChannelStore.getChannel(channelId);
if (channel == null) return null;

View File

@@ -22,13 +22,12 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
import definePlugin, { ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore, VoiceStateStore } from "@webpack/common";
import { ReactElement } from "react";
import { getCurrentVoice, settings } from "./settings";
interface VoiceState {
interface VoiceStateChangeEvent {
userId: string;
channelId?: string;
oldChannelId?: string;
@@ -38,8 +37,6 @@ interface VoiceState {
selfMute: boolean;
}
const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId");
// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying
// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would
// not say the second mute, which would lead you to believe they're unmuted
@@ -88,7 +85,7 @@ let StatusMap = {} as Record<string, {
// for some ungodly reason
let myLastChannelId: string | undefined;
function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) {
function getTypeAndChannelId({ channelId, oldChannelId }: VoiceStateChangeEvent, isMe: boolean) {
if (isMe && channelId !== myLastChannelId) {
oldChannelId = myLastChannelId;
myLastChannelId = channelId;
@@ -163,7 +160,7 @@ export default definePlugin({
settings,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceStateChangeEvent[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id;
@@ -195,7 +192,7 @@ export default definePlugin({
AUDIO_TOGGLE_SELF_MUTE() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
const s = VoiceStateStore.getVoiceStateForChannel(chanId);
if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute";
@@ -204,7 +201,7 @@ export default definePlugin({
AUDIO_TOGGLE_SELF_DEAF() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
const s = VoiceStateStore.getVoiceStateForChannel(chanId);
if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen";

View File

@@ -129,9 +129,10 @@ export default definePlugin({
patches: [
{
find: '"M9 3v18"',
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
match: /focusSectionProps:"HELP".{0,20},className:(\i\.button)\}\),/,
// TODO: (?:\.button) is for stable compat and should be removed soon:tm:
match: /focusSectionProps:"HELP".{0,20},className:(\i(?:\.button)?)\}\),/,
replace: "$& $self.renderVencordPopoutButton($1),"
}
}

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";
@@ -37,7 +39,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");
@@ -92,8 +94,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

@@ -55,6 +55,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "V",
id: 343383572805058560n,
},
Apexo: {
name: "Apexo",
id: 228548952687902720n
},
Arjix: {
name: "ArjixWasTaken",
id: 674710789138939916n,
@@ -640,7 +644,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
iamme: {
name: "i am me",
id: 984392761929256980n,
}
},
thororen: {
name: "thororen",
id: 848339671629299742n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

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"),

View File

@@ -47,12 +47,14 @@ export let SelectedGuildStore: t.SelectedGuildStore;
export let ChannelStore: t.ChannelStore;
export let TypingStore: t.TypingStore;
export let RelationshipStore: t.RelationshipStore;
export let VoiceStateStore: t.VoiceStateStore;
export let EmojiStore: t.EmojiStore;
export let StickersStore: t.StickersStore;
export let ThemeStore: t.ThemeStore;
export let WindowStore: t.WindowStore;
export let DraftStore: t.DraftStore;
export let StreamerModeStore: t.StreamerModeStore;
/**
* @see jsdoc of {@link t.useStateFromStores}
@@ -79,6 +81,8 @@ waitForStore("WindowStore", m => WindowStore = m);
waitForStore("EmojiStore", m => EmojiStore = m);
waitForStore("StickersStore", m => StickersStore = m);
waitForStore("TypingStore", m => TypingStore = m);
waitForStore("VoiceStateStore", m => VoiceStateStore = m);
waitForStore("StreamerModeStore", m => StreamerModeStore = m);
waitForStore("ThemeStore", m => {
ThemeStore = m;
// Importing this directly can easily cause circular imports. For this reason, use a non import access here.