Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
3b593ae3dd
29 changed files with 324 additions and 354 deletions
|
|
@ -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 "./passiveupdatestate";
|
||||
export * from "./utils";
|
||||
|
|
|
|||
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";
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import type { Channel, CustomEmoji, Message } from "@vencord/discord-types";
|
||||
import type { Channel, CloudUpload, CustomEmoji, Message } from "@vencord/discord-types";
|
||||
import { MessageStore } from "@webpack/common";
|
||||
import type { Promisable } from "type-fest";
|
||||
|
||||
|
|
@ -30,32 +30,6 @@ export interface MessageObject {
|
|||
tts: boolean;
|
||||
}
|
||||
|
||||
export interface Upload {
|
||||
classification: string;
|
||||
currentSize: number;
|
||||
description: string | null;
|
||||
filename: string;
|
||||
id: string;
|
||||
isImage: boolean;
|
||||
isRemix?: boolean;
|
||||
isThumbnail: boolean;
|
||||
isVideo: boolean;
|
||||
item: {
|
||||
file: File;
|
||||
platform: number;
|
||||
};
|
||||
loaded: number;
|
||||
mimeType: string;
|
||||
preCompressionSize: number;
|
||||
responseUrl: string;
|
||||
sensitive: boolean;
|
||||
showLargeMessageDialog: boolean;
|
||||
spoiler: boolean;
|
||||
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
|
||||
uniqueId: string;
|
||||
uploadedFilename: string;
|
||||
}
|
||||
|
||||
export interface MessageReplyOptions {
|
||||
messageReference: Message["messageReference"];
|
||||
allowedMentions?: {
|
||||
|
|
@ -68,7 +42,7 @@ export interface MessageReplyOptions {
|
|||
|
||||
export interface MessageOptions {
|
||||
stickers?: string[];
|
||||
uploads?: Upload[];
|
||||
uploads?: CloudUpload[];
|
||||
replyOptions: MessageReplyOptions;
|
||||
content: string;
|
||||
channel: Channel;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@
|
|||
*/
|
||||
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||
import { Upload } from "@api/MessageEvents";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { reverseExtensionMap } from "@equicordplugins/fixFileExtensions";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { CloudUpload } from "@vencord/discord-types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { useState } from "@webpack/common";
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ export default definePlugin({
|
|||
},
|
||||
],
|
||||
|
||||
AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: Upload; }) => {
|
||||
AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: CloudUpload; }) => {
|
||||
const [anonymise, setAnonymise] = useState(upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault);
|
||||
|
||||
function onToggleAnonymise() {
|
||||
|
|
@ -117,8 +117,7 @@ export default definePlugin({
|
|||
);
|
||||
}, { noop: true }),
|
||||
|
||||
anonymise(upload: Upload) {
|
||||
|
||||
anonymise(upload: CloudUpload) {
|
||||
const originalFileName = upload.filename;
|
||||
const tarMatch = tarExtMatcher.exec(originalFileName);
|
||||
const extIdx = tarMatch?.index ?? originalFileName.lastIndexOf(".");
|
||||
|
|
|
|||
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,162 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const defaultRules = [
|
||||
"action_object_map",
|
||||
"action_type_map",
|
||||
"action_ref_map",
|
||||
"spm@*.aliexpress.com",
|
||||
"scm@*.aliexpress.com",
|
||||
"aff_platform",
|
||||
"aff_trace_key",
|
||||
"algo_expid@*.aliexpress.*",
|
||||
"algo_pvid@*.aliexpress.*",
|
||||
"btsid",
|
||||
"ws_ab_test",
|
||||
"pd_rd_*@amazon.*",
|
||||
"_encoding@amazon.*",
|
||||
"psc@amazon.*",
|
||||
"tag@amazon.*",
|
||||
"ref_@amazon.*",
|
||||
"pf_rd_*@amazon.*",
|
||||
"pf@amazon.*",
|
||||
"crid@amazon.*",
|
||||
"keywords@amazon.*",
|
||||
"sprefix@amazon.*",
|
||||
"sr@amazon.*",
|
||||
"ie@amazon.*",
|
||||
"node@amazon.*",
|
||||
"qid@amazon.*",
|
||||
"callback@bilibili.com",
|
||||
"cvid@bing.com",
|
||||
"form@bing.com",
|
||||
"sk@bing.com",
|
||||
"sp@bing.com",
|
||||
"sc@bing.com",
|
||||
"qs@bing.com",
|
||||
"pq@bing.com",
|
||||
"sc_cid",
|
||||
"mkt_tok",
|
||||
"trk",
|
||||
"trkCampaign",
|
||||
"ga_*",
|
||||
"gclid",
|
||||
"gclsrc",
|
||||
"hmb_campaign",
|
||||
"hmb_medium",
|
||||
"hmb_source",
|
||||
"spReportId",
|
||||
"spJobID",
|
||||
"spUserID",
|
||||
"spMailingID",
|
||||
"itm_*",
|
||||
"s_cid",
|
||||
"elqTrackId",
|
||||
"elqTrack",
|
||||
"assetType",
|
||||
"assetId",
|
||||
"recipientId",
|
||||
"campaignId",
|
||||
"siteId",
|
||||
"mc_cid",
|
||||
"mc_eid",
|
||||
"pk_*",
|
||||
"sc_campaign",
|
||||
"sc_channel",
|
||||
"sc_content",
|
||||
"sc_medium",
|
||||
"sc_outcome",
|
||||
"sc_geo",
|
||||
"sc_country",
|
||||
"nr_email_referer",
|
||||
"vero_conv",
|
||||
"vero_id",
|
||||
"yclid",
|
||||
"_openstat",
|
||||
"mbid",
|
||||
"cmpid",
|
||||
"cid",
|
||||
"c_id",
|
||||
"campaign_id",
|
||||
"Campaign",
|
||||
"hash@ebay.*",
|
||||
"fb_action_ids",
|
||||
"fb_action_types",
|
||||
"fb_ref",
|
||||
"fb_source",
|
||||
"fbclid",
|
||||
"refsrc@facebook.com",
|
||||
"hrc@facebook.com",
|
||||
"gs_l",
|
||||
"gs_lcp@google.*",
|
||||
"ved@google.*",
|
||||
"ei@google.*",
|
||||
"sei@google.*",
|
||||
"gws_rd@google.*",
|
||||
"gs_gbg@google.*",
|
||||
"gs_mss@google.*",
|
||||
"gs_rn@google.*",
|
||||
"_hsenc",
|
||||
"_hsmi",
|
||||
"__hssc",
|
||||
"__hstc",
|
||||
"hsCtaTracking",
|
||||
"source@sourceforge.net",
|
||||
"position@sourceforge.net",
|
||||
"t@*.twitter.com",
|
||||
"s@*.twitter.com",
|
||||
"ref_*@*.twitter.com",
|
||||
"t@*.x.com",
|
||||
"s@*.x.com",
|
||||
"ref_*@*.x.com",
|
||||
"t@*.fixupx.com",
|
||||
"s@*.fixupx.com",
|
||||
"ref_*@*.fixupx.com",
|
||||
"t@*.fxtwitter.com",
|
||||
"s@*.fxtwitter.com",
|
||||
"ref_*@*.fxtwitter.com",
|
||||
"t@*.vxtwitter.com",
|
||||
"s@*.vxtwitter.com",
|
||||
"ref_*@*.vxtwitter.com",
|
||||
"t@*.twittpr.com",
|
||||
"s@*.twittpr.com",
|
||||
"ref_*@*.twittpr.com",
|
||||
"t@*.fixvx.com",
|
||||
"s@*.fixvx.com",
|
||||
"ref_*@*.fixvx.com",
|
||||
"tt_medium",
|
||||
"tt_content",
|
||||
"lr@yandex.*",
|
||||
"redircnt@yandex.*",
|
||||
"feature@*.youtube.com",
|
||||
"kw@*.youtube.com",
|
||||
"si@*.youtube.com",
|
||||
"pp@*.youtube.com",
|
||||
"si@*.youtu.be",
|
||||
"wt_zmc",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
"utm_medium",
|
||||
"utm_campaign",
|
||||
"utm_term",
|
||||
"si@open.spotify.com",
|
||||
"igshid",
|
||||
"igsh",
|
||||
"share_id@reddit.com",
|
||||
"si@soundcloud.com",
|
||||
];
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
|
|||
|
||||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers, and use client themes.",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan],
|
||||
description: "Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality",
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
settings,
|
||||
|
|
@ -281,6 +281,14 @@ export default definePlugin({
|
|||
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
||||
}
|
||||
},
|
||||
// Allow users to use custom client themes
|
||||
{
|
||||
find: "customUserThemeSettings:{",
|
||||
replacement: {
|
||||
match: /(?<=\i=)\(0,\i\.\i\)\(\i\.\i\.TIER_2\)(?=,|;)/g,
|
||||
replace: "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
||||
replacement: [
|
||||
|
|
@ -392,24 +400,15 @@ export default definePlugin({
|
|||
if (premiumType !== 2) {
|
||||
proto.appearance ??= AppearanceSettingsActionCreators.create();
|
||||
|
||||
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
||||
const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
|
||||
theme: UserSettingsProtoStore.settings.appearance.theme
|
||||
});
|
||||
const protoStoreAppearenceSettings = UserSettingsProtoStore.settings.appearance;
|
||||
|
||||
proto.appearance.theme = appearanceSettingsDummy.theme;
|
||||
}
|
||||
const appearanceSettingsOverwrite = AppearanceSettingsActionCreators.create({
|
||||
...proto.appearance,
|
||||
theme: protoStoreAppearenceSettings?.theme,
|
||||
clientThemeSettings: protoStoreAppearenceSettings?.clientThemeSettings
|
||||
});
|
||||
|
||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
|
||||
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
|
||||
backgroundGradientPresetId: {
|
||||
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
||||
}
|
||||
});
|
||||
|
||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
|
||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
||||
}
|
||||
proto.appearance = appearanceSettingsOverwrite;
|
||||
}
|
||||
} catch (err) {
|
||||
new Logger("FakeNitro").error(err);
|
||||
|
|
|
|||
|
|
@ -20,13 +20,11 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { useCallback, useEffect, useRef, useState } from "@webpack/common";
|
||||
|
||||
interface SearchBarComponentProps {
|
||||
ref?: React.MutableRefObject<any>;
|
||||
ref?: React.RefObject<any>;
|
||||
autoFocus: boolean;
|
||||
className: string;
|
||||
size: string;
|
||||
onChange: (query: string) => void;
|
||||
onClear: () => void;
|
||||
|
|
@ -59,9 +57,6 @@ interface Instance {
|
|||
forceUpdate: () => void;
|
||||
}
|
||||
|
||||
|
||||
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow");
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
searchOption: {
|
||||
type: OptionType.SELECT,
|
||||
|
|
@ -136,7 +131,7 @@ export default definePlugin({
|
|||
|
||||
function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const ref = useRef<{ containerRef?: React.MutableRefObject<HTMLDivElement>; } | null>(null);
|
||||
const ref = useRef<{ containerRef?: React.RefObject<HTMLDivElement>; } | null>(null);
|
||||
|
||||
const onChange = useCallback((searchQuery: string) => {
|
||||
setQuery(searchQuery);
|
||||
|
|
@ -181,7 +176,6 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
|
|||
<SearchBarComponent
|
||||
ref={ref}
|
||||
autoFocus={true}
|
||||
className={containerClasses.searchBar}
|
||||
size="md"
|
||||
onChange={onChange}
|
||||
onClear={() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
[class^="panels"] [class^="avatarWrapper"] {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.vc-game-activity {
|
||||
margin-right: 4px;
|
||||
}
|
||||
/* make gap smaller since we're adding an extra button */
|
||||
[class^="panels"] [class^="buttons"] {
|
||||
gap: 4px;
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ interface Activity {
|
|||
buttons?: Array<string>;
|
||||
name: string;
|
||||
application_id: string;
|
||||
status_display_type?: number;
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
|
|
@ -137,6 +138,25 @@ const settings = definePluginSettings({
|
|||
type: OptionType.STRING,
|
||||
default: "some music",
|
||||
},
|
||||
statusDisplayType: {
|
||||
description: "Show the track / artist name in the member list",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Don't show (shows generic listening message)",
|
||||
value: "off",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Show artist name",
|
||||
value: "artist"
|
||||
},
|
||||
{
|
||||
label: "Show track name",
|
||||
value: "track"
|
||||
}
|
||||
]
|
||||
},
|
||||
nameFormat: {
|
||||
description: "Show name of song and artist in status name",
|
||||
type: OptionType.SELECT,
|
||||
|
|
@ -354,6 +374,11 @@ export default definePlugin({
|
|||
|
||||
details: trackData.name,
|
||||
state: trackData.artist,
|
||||
status_display_type: {
|
||||
"off": 0,
|
||||
"artist": 1,
|
||||
"track": 2
|
||||
}[settings.store.statusDisplayType],
|
||||
assets,
|
||||
|
||||
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
|
|
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { isObjectEmpty } from "@utils/misc";
|
|||
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common";
|
||||
|
||||
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from ".";
|
||||
import { CircleIcon } from "./CircleIcon";
|
||||
import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
|
||||
import { VoiceIcon } from "./VoiceIcon";
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
<Tooltip text={`${formattedOnlineCount} online in this channel`} position="bottom">
|
||||
{props => (
|
||||
<div {...props} className={cl("container")}>
|
||||
<span className={cl("online-dot")} />
|
||||
<CircleIcon className={cl("online-count")} />
|
||||
<span className={cl("online")}>{formattedOnlineCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -88,7 +89,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
|
||||
{props => (
|
||||
<div {...props} className={cl("container")}>
|
||||
<span className={cl("total-dot")} />
|
||||
<CircleIcon className={cl("total-count")} />
|
||||
<span className={cl("total")}>{numberFormat(totalCount)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as F
|
|||
getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
|
||||
};
|
||||
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
toolTip: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
flex-wrap: wrap;
|
||||
margin-top: 1em;
|
||||
padding-inline: 1em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.vc-membercount-container {
|
||||
|
|
@ -38,20 +39,18 @@
|
|||
color: var(--color-voice);
|
||||
}
|
||||
|
||||
.vc-membercount-online-dot {
|
||||
background-color: var(--color-online);
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
.vc-membercount-online-count {
|
||||
fill: var(--status-online);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.vc-membercount-total-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--color-total);
|
||||
.vc-membercount-total-count {
|
||||
fill: none;
|
||||
stroke: var(--status-offline);
|
||||
stroke-width: 4px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.vc-membercount-voice-icon {
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ export default definePlugin({
|
|||
replacement: {
|
||||
// The two groups inside the first group grab the minified names of the variables,
|
||||
// they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.
|
||||
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\.NONE\?)\1\+\2/,
|
||||
replace: "0"
|
||||
match: /(\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.+?)\2\+\3/,
|
||||
replace: (_, rest) => `${rest}0`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 + "#",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa
|
|||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { chooseFile } from "@utils/web";
|
||||
import { CloudUpload as TCloudUpload } from "@vencord/discord-types";
|
||||
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
|
||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
|
|
@ -38,7 +40,7 @@ import { cl } from "./utils";
|
|||
import { VoicePreview } from "./VoicePreview";
|
||||
import { VoiceRecorderWeb } from "./WebRecorder";
|
||||
|
||||
const CloudUpload = findLazy(m => m.prototype?.trackUploadFinished);
|
||||
const CloudUpload: typeof TCloudUpload = findLazy(m => m.prototype?.trackUploadFinished);
|
||||
const PendingReplyStore = findStoreLazy("PendingReplyStore");
|
||||
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
|
||||
|
||||
|
|
@ -93,8 +95,8 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
|
|||
const upload = new CloudUpload({
|
||||
file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }),
|
||||
isThumbnail: false,
|
||||
platform: 1,
|
||||
}, channelId, false, 0);
|
||||
platform: CloudUploadPlatform.WEB,
|
||||
}, channelId);
|
||||
|
||||
upload.on("complete", () => {
|
||||
RestAPI.post({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,6 +621,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Cootshk",
|
||||
id: 921605971577548820n
|
||||
},
|
||||
thororen: {
|
||||
name: "thororen",
|
||||
id: 848339671629299742n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
export const EquicordDevs = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -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(".useImperativeHandle(", ".SEARCH)", "query:"), m => Menu.MenuSearchControl = m);
|
||||
waitFor(filters.componentByCode(".SEARCH)", ".focus()", "query:"), m => Menu.MenuSearchControl = m);
|
||||
|
||||
export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
|
||||
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue