merge unconflict
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./channel";
|
||||
export * from "./commands";
|
||||
export * from "./messages";
|
||||
export * from "./channel";
|
||||
export * from "./misc";
|
||||
|
||||
4
packages/discord-types/enums/misc.ts
Normal file
4
packages/discord-types/enums/misc.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const enum CloudUploadPlatform {
|
||||
REACT_NATIVE = 0,
|
||||
WEB = 1,
|
||||
}
|
||||
1
packages/discord-types/src/index.d.ts
vendored
1
packages/discord-types/src/index.d.ts
vendored
@@ -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";
|
||||
|
||||
74
packages/discord-types/src/modules/CloudUpload.d.ts
vendored
Normal file
74
packages/discord-types/src/modules/CloudUpload.d.ts
vendored
Normal 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;
|
||||
}
|
||||
1
packages/discord-types/src/modules/index.d.ts
vendored
Normal file
1
packages/discord-types/src/modules/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./CloudUpload";
|
||||
11
packages/discord-types/src/stores/StreamerModeStore.d.ts
vendored
Normal file
11
packages/discord-types/src/stores/StreamerModeStore.d.ts
vendored
Normal 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;
|
||||
}
|
||||
42
packages/discord-types/src/stores/VoiceStateStore.d.ts
vendored
Normal file
42
packages/discord-types/src/stores/VoiceStateStore.d.ts
vendored
Normal 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;
|
||||
}
|
||||
2
packages/discord-types/src/stores/index.d.ts
vendored
2
packages/discord-types/src/stores/index.d.ts
vendored
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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},`
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
11
src/plugins/clearURLs/README.md
Normal file
11
src/plugins/clearURLs/README.md
Normal 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/`
|
||||
@@ -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",
|
||||
];
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
17
src/plugins/memberCount/CircleIcon.tsx
Normal file
17
src/plugins/memberCount/CircleIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
14
src/plugins/memberCount/VoiceIcon.tsx
Normal file
14
src/plugins/memberCount/VoiceIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 })
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 + "#",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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),"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user