fix some issues (#1021)

* fix betterblockedusers crash

* fix statuspresets

* sidebarchat not opening

* actually fix status presets, hide when there is no presets

* actually fix betterblockedusers

* make sidebarchat patches better

* hide the github button when no github linked

* undo anything statuspresets im not touching this thing

* make it so cleanchannelnames shows the emoji when editing the channel

* fix lint

* cloudsync

* undo last changes from statuspresets i did

* Update index.tsx

* mostly fix it

* fully fix statuspresets

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
creations 2026-04-28 09:04:38 -04:00 • committed by GitHub
parent c2e966e50d
commit 3a1c331102
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 178 additions and 146 deletions

View file

@ -20,17 +20,25 @@ import { ManifestEntry, SyncRequest, SyncResponse } from "./types";
const logger = new Logger("SettingsSync:Cloud", "#39b7e0");
const MANIFEST_STORE_KEY = "Vencord_cloudManifest";
const API_VERSION_STORE_KEY = "Vencord_cloudApiVersions";
type ApiVersion = "v2" | "v1";
let apiVersion: ApiVersion = "v2";
async function loadApiVersionMap(): Promise<Record<string, ApiVersion>> {
return await DataStore.get<Record<string, ApiVersion>>(API_VERSION_STORE_KEY) ?? {};
}
async function getApiVersion(): Promise<ApiVersion> {
return apiVersion;
const map = await loadApiVersionMap();
return map[getCloudUrl().origin] ?? "v2";
}
async function setApiVersion(version: ApiVersion) {
apiVersion = version;
await DataStore.update<Record<string, ApiVersion>>(API_VERSION_STORE_KEY, map => {
map ??= {};
map[getCloudUrl().origin] = version;
return map;
});
}
function toBase64(data: Uint8Array): string {

View file

@ -22,17 +22,18 @@ export default definePlugin({
patches: [
{
find: '"],{numberOfBlockedUsers:',
group: true,
replacement: [
{
match: /(?<=listType:\i,numberOfUsers:\i\}=(\i).{0,800}numberOfIgnoredUsers:\i\}\)\}\)\]\}\))/,
replace: ",$1.listType==='blocked'?$self.renderSearchInput():null"
match: /(?<=\(0,\i\.jsx\)\(\i,\{listType:(\i),numberOfUsers:\i\.length\}\),)/,
replace: "$1==='blocked'?$self.renderSearchInput():null,"
},
{
match: /(?<=\{userId:(\i).*?\.globalName.{0,25}\}\)\]\}\),)(\(.*?loading:\i\}\))/,
replace: "$self.renderUser($1,$2)",
},
{
match: /(?<=\}=(\i).{0,10}(\i).useState.{0,5};)/,
match: /(?<=userIds:\i,listType:\i\}=(\i).{0,30}(\i)\.useState\(\d+\);)/,
replace: "let [searchResults,setSearchResults]=$2.useState([]);$self.setUpdateFunc($1,setSearchResults);"
},
{
@ -50,17 +51,19 @@ export default definePlugin({
updateFunc(searchResults);
}, []);
return <TextInput
placeholder="Search users..."
style={{ width: "200px" }}
onInput={e => {
const search = (e.target as HTMLInputElement).value.toLowerCase().trim();
setValue(search);
lastSearch = search;
const searchResults = this.getFilteredUsers(search);
updateFunc(searchResults);
}} value={value}
></TextInput>;
return <div className="vc-bbu-search">
<TextInput
placeholder="Search users..."
style={{ width: "200px" }}
onInput={e => {
const search = (e.target as HTMLInputElement).value.toLowerCase().trim();
setValue(search);
lastSearch = search;
const searchResults = this.getFilteredUsers(search);
updateFunc(searchResults);
}} value={value}
/>
</div>;
},
renderUser(userId: string, rest: any) {
return (

View file

@ -1,3 +1,8 @@
[class*="usersList"] [class*="text"] {
user-select: text !important;
}
.vc-bbu-search,
.vc-bbu-search * {
border-radius: 0 !important;
}

View file

@ -7,11 +7,32 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "@vencord/discord-types";
import { ChannelStore } from "@webpack/common";
const SMALL_CAPS: Record<string, string> = {
"ᴀ": "a", "ʙ": "b", "ᴄ": "c", "ᴅ": "d", "ᴇ": "e", "ꜰ": "f", "ɢ": "g", "ʜ": "h", "ɪ": "i", "ᴊ": "j",
"ᴋ": "k", "ʟ": "l", "ᴍ": "m", "ɴ": "n", "ᴏ": "o", "ᴘ": "p", "ǫ": "q", "ʀ": "r", "ꜱ": "s", "ᴛ": "t",
"ᴜ": "u", "ᴠ": "v", "ᴡ": "w", "x": "x", "ʏ": "y", "ᴢ": "z",
};
const ORIGINAL_NAME = Symbol("cleanChannelName.original");
let editingChannelId: string | null = null;
function computeClean(name: string, type: number): string {
return name
.normalize("NFKC")
.replace(/[ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ]/g, m => SMALL_CAPS[m])
.replace(/[^ -~]?\p{Extended_Pictographic}[^ -~]?/ug, "")
.replace(/-?[^\p{Letter} -~]-?/ug, [2, 4].includes(type) ? " " : "-")
.replace(/(^-|-$)/g, "")
.replace(/-+/g, "-");
}
export default definePlugin({
name: "CleanChannelName",
authors: [Devs.AutumnVN],
description: "remove all emoji and decor shit from channel names",
description: "Remove emoji and decoration from channel names. Reverts to the original while you're editing the channel.",
tags: ["Appearance", "Customisation", "Chat", "Emotes", "Servers"],
patches: [
{
@ -19,27 +40,41 @@ export default definePlugin({
replacement: {
match: /(?<=getChannel\(\i\)\{if\(null!=\i\)return )\i\(\i\)/,
replace: "$self.cleanChannelName($&)",
}
}
},
},
],
flux: {
CHANNEL_SETTINGS_INIT({ channelId }: { channelId: string; }) {
editingChannelId = channelId;
(ChannelStore as any).emitChange?.();
},
CHANNEL_SETTINGS_CLOSE() {
editingChannelId = null;
(ChannelStore as any).emitChange?.();
},
},
cleanChannelName(channel?: Channel) {
if (channel) {
channel.name = channel.name
.normalize("NFKC")
.replace(/[ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ]/g, match => {
const smallCapsToNormal = {
"ᴀ": "a", "ʙ": "b", "ᴄ": "c", "ᴅ": "d", "ᴇ": "e", "ꜰ": "f", "ɢ": "g", "ʜ": "h", "ɪ": "i", "ᴊ": "j",
"ᴋ": "k", "ʟ": "l", "ᴍ": "m", "ɴ": "n", "ᴏ": "o", "ᴘ": "p", "ǫ": "q", "ʀ": "r", "ꜱ": "s", "ᴛ": "t",
"ᴜ": "u", "ᴠ": "v", "ᴡ": "w", "x": "x", "ʏ": "y", "ᴢ": "z"
};
return smallCapsToNormal[match];
})
.replace(/[^\u0020-\u007E]?\p{Extended_Pictographic}[^\u0020-\u007E]?/ug, "")
.replace(/-?[^\p{Letter}\u0020-\u007E]-?/ug, [2, 4].includes(channel.type) ? " " : "-")
.replace(/(^-|-$)/g, "")
.replace(/-+/g, "-");
}
if (channel == null) return channel;
const c = channel as any;
if (c[ORIGINAL_NAME] !== undefined) return channel;
c[ORIGINAL_NAME] = channel.name;
Object.defineProperty(channel, "name", {
configurable: true,
enumerable: true,
get() {
if (editingChannelId === channel.id) return c[ORIGINAL_NAME];
return computeClean(c[ORIGINAL_NAME], channel.type);
},
set(value: string) {
c[ORIGINAL_NAME] = value;
},
});
return channel;
}
},
});

View file

@ -13,7 +13,7 @@ import { classNameFactory } from "@utils/css";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
import { React } from "@webpack/common";
import { React, UserProfileStore } from "@webpack/common";
import { ProfilePopoutComponent } from "./components/ProfilePopoutComponent";
import { ProfileTabComponent } from "./components/ProfileTabComponent";
@ -48,8 +48,8 @@ export default definePlugin({
{
find: "#{intl::USER_PROFILE_ACTIVITY}",
replacement: {
match: /\.MUTUAL_GUILDS\}\)\)(?=,(\i))/,
replace: '$&,$1.push({text:"GitHub",section:"GITHUB"})',
match: /(\i)\.id!==\i\?\.id&&\i&&\(.{0,300}\.MUTUAL_GUILDS\}\)\)(?=,(\i))/,
replace: '$&,$self.shouldShowGitHub($1.id)&&$2.push({text:"GitHub",section:"GITHUB"})',
}
},
// User Profile Modal v2 tab content
@ -61,6 +61,9 @@ export default definePlugin({
}
}
],
shouldShowGitHub(userId: string) {
return UserProfileStore.getUserProfile(userId)?.connectedAccounts?.some((c: any) => c.type === "github") ?? false;
},
renderProfileCollection: (props: { user: User; displayProfile?: any; }) => {
return (
<ProfilePopoutComponent

View file

@ -310,26 +310,29 @@ export default definePlugin({
tags: ["Appearance", "Chat", "Servers"],
dependencies: ["HeaderBarAPI"],
patches: [
{
find: "this.renderThreadSidebar()",
replacement: {
match: /(this\.renderThreadSidebar\(\))\]/,
replace: "$1,$self.renderSidebar()]"
}
},
{
find: 'case"pendingFriends":',
group: true,
replacement: [
{
match: /ChannelRenderer"\),/,
replace: "$&vc_SidebarChat=$self.renderSidebar(),"
},
{
match: /return(\(0,\i\.jsxs?\)\(\i,{}\))}/,
replace: "return [$1, vc_SidebarChat]}"
replace: "return [$1, $self.renderSidebar()]}"
},
{
match: /(?<=guild_products.{0,1600})(case \i\.\i.{0,50}return)(.+?\}\));(?=.+?params\.messageId)/g,
replace: "$1[$2, vc_SidebarChat];",
replace: "$1[$2, $self.renderSidebar()];",
predicate: () => settings.store.patchCommunity,
},
{
match: /(case \i\.\i\.GAME_SERVERS:.{0,50}\.CHANNEL.{0,25}return)(.*?);/,
replace: "$1[$2, vc_SidebarChat];",
replace: "$1[$2, $self.renderSidebar()];",
predicate: () => settings.store.patchCommunity,
},
],

View file

@ -1,3 +0,0 @@
[class*="outerContainer"] [class*="container"] [class*="actionBar"] {
gap: 0;
}

View file

@ -23,46 +23,30 @@ import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import { classes } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findModuleId, wreq } from "@webpack";
import { Clickable, Menu, OverridePremiumTypeStore, Toasts, useState } from "@webpack/common";
import managedStyle from "./fixActionBar.css?managed";
const settings = definePluginSettings({
StatusPresets: {
type: OptionType.COMPONENT,
description: "Status Presets",
component: () => <></>,
default: {}
}
});
import { Menu, OverridePremiumTypeStore, Toasts } from "@webpack/common";
interface Emoji {
animated: boolean;
id: bigint | null;
id: string | null;
name: string;
}
const PlusSmallIcon = findComponentByCodeLazy("0-2h-5V6Z");
interface DiscordStatus {
emojiInfo: Emoji | null;
text: string;
clearAfter: "TODAY" | number | null;
status: "online" | "dnd" | "idle" | "invisible";
}
// TODO: find clearCustomStatusHint original css/svg or replace
let PMenu;
const PMenu = findComponentByCodeLazy("#{intl::MORE_OPTIONS}", ",renderSubmenu:");
const EmojiComponent = findComponentByCodeLazy(/\.translateSurrogatesToInlineEmoji\(\i\.name\);/);
const CustomStatusSettings = getUserSettingLazy("status", "customStatus")!;
const StatusModule = proxyLazy(() => {
const id = findModuleId(".customStatusInputTitle,");
const id = findModuleId("#{intl::SAVE}", '"custom-status-input"', '"Invalid custom status clear timeout"),');
return wreq(Number(id));
});
@ -77,7 +61,6 @@ const openCustomStatusModalLazy = () => openModalLazy(async () => {
function getExpirationMs(expiration: "TODAY" | number) {
if (expiration !== "TODAY") return Date.now() + expiration;
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime();
}
@ -92,69 +75,64 @@ function setStatus(status: DiscordStatus) {
});
}
const ClearStatusButton = () => <Clickable onClick={e => { e.stopPropagation(); CustomStatusSettings?.updateSetting(null); }}><PlusSmallIcon className={"vc-sp-icon"} /></Clickable>;
function StatusIcon({ isHovering, status }: { isHovering: boolean; status: DiscordStatus; }) {
return <div>{isHovering ?
<PlusSmallIcon className={"vc-sp-icon"} />
: (status.emojiInfo != null ? <EmojiComponent emoji={status.emojiInfo} animate={false} hideTooltip={false} /> : <div />)}</div>;
}
const RenderStatusMenuItem = ({ status, update, disabled }: { status: DiscordStatus; update: () => void; disabled: boolean; }) => {
const [isHovering, setIsHovering] = useState(false);
const handleMouseOver = () => {
setIsHovering(true);
};
const handleMouseOut = () => {
setIsHovering(false);
};
return <div className={classes("vc-sp-item", disabled ? "vc-sp-disabled" : null)}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}>
<Clickable
onClick={e => {
e.stopPropagation();
settings.store.StatusPresets[status.text] = undefined; // setting to undefined to remove it.
update();
}}><StatusIcon isHovering={isHovering} status={status} /></Clickable>
<div style={{ marginLeft: "2px" }}>{status.text}</div>
</div >;
};
const StatusSubMenuComponent = () => {
const premiumType = OverridePremiumTypeStore.getState().premiumTypeActual ?? 0;
const update = useForceUpdater();
return <Menu.Menu navId="sp-custom-status-submenu" onClose={() => { }}>
{Object.entries((settings.store.StatusPresets as { [k: string]: DiscordStatus | undefined; })).map(([index, status]) => status != null ? <Menu.MenuItem
key={"status-presets-" + index}
id={"status-presets-" + index}
label={status.status}
action={() => (status.emojiInfo?.id != null && premiumType > 0 || status.emojiInfo?.id == null) && setStatus(status)}
render={() => <RenderStatusMenuItem
status={status}
update={update}
disabled={status.emojiInfo?.id != null && premiumType === 0}
/>}
/> : null)}
</Menu.Menu>;
return (
<Menu.Menu navId="sp-custom-status-submenu" onClose={() => { }}>
{Object.entries((settings.store.StatusPresets as { [k: string]: DiscordStatus | undefined; })).map(([index, status]) =>
status != null ? (
<Menu.MenuItem
key={"status-presets-" + index}
id={"status-presets-" + index}
label={status.text}
action={() => (status.emojiInfo?.id == null || premiumType > 0) && setStatus(status)}
icon={status.emojiInfo != null
? () => <EmojiComponent emoji={status.emojiInfo} animate={false} hideTooltip={false} />
: undefined
}
disabled={status.emojiInfo?.id != null && premiumType === 0}
>
<Menu.MenuItem
id={"status-presets-delete-" + index}
label="Delete Preset"
action={() => {
const newPresets = JSON.parse(JSON.stringify(settings.store.StatusPresets));
delete newPresets[status.text];
settings.store.StatusPresets = newPresets;
update();
}}
/>
</Menu.MenuItem>
) : null
)}
</Menu.Menu>
);
};
const settings = definePluginSettings({
StatusPresets: {
type: OptionType.COMPONENT,
description: "Status Presets",
component: () => <></>,
default: {}
}
});
export default definePlugin({
name: "StatusPresets",
description: "Allows you to remember your statuses and set them later",
tags: ["Activity", "Utility"],
authors: [EquicordDevs.iamme],
settings: settings,
settings,
dependencies: ["UserSettingsAPI"],
managedStyle,
patches: [
{
find: '="custom-status-input";',
replacement: {
match: /(?<=(\i)\?\.state\?\?.*?)\{text:\i\.\i\.\i\(\i\.\i#{intl::SAVE}\)/,
replace: "$self.renderRememberButton($1),$&"
match: /(?<=\[(\i).{0,6}\.useState\(\i\?\.state\?\?""\),\[(\i).{0,25}\?\?null\),\[(\i).*?)\{text:\i\.\i\.\i\(\i\.\i#{intl::SAVE}\)/,
replace: "$self.renderRememberButton({text:$1,emojiInfo:$2,clearAfter:$3}),$&"
}
},
{
@ -167,14 +145,12 @@ export default definePlugin({
{
find: "#{intl::MORE_OPTIONS}),...",
replacement: {
match: /(?<=targetElementRef:\i,spacing:.{0,150}className:\i\.\i,children:(\i)\(\)\}\)\})/,
replace: "$self.PMenu=$1;"
match: /\i:\(\)=>\i,(?=.*?function (\i).{0,100}renderSubmenu:\i,ref:)/,
replace: "$&PMenu:()=>$1,"
}
},
],
set PMenu(value: any) {
PMenu = value;
},
render() {
const status = CustomStatusSettings.getSetting();
return (
@ -185,9 +161,7 @@ export default definePlugin({
id="sp-custom/presets-status"
action="PRESS_SET_STATUS"
onClick={openCustomStatusModalLazy}
icon={
() => <div />
}
icon={() => <div />}
label="Set Custom Status"
renderSubmenu={StatusSubMenuComponent}
/>
@ -196,19 +170,13 @@ export default definePlugin({
id="sp-edit/presets-status"
action="PRESS_EDIT_CUSTOM_STATUS"
onClick={openCustomStatusModalLazy}
hint={<ClearStatusButton />}
icon={
() => status.emoji != null ? (
<EmojiComponent
emoji={status.emoji}
animate={false}
hideTooltip={false}
/>
) : null
}
icon={() => status.emoji != null ? (
<EmojiComponent emoji={status.emoji} animate={false} hideTooltip={false} />
) : null}
label="Edit Custom Status"
renderSubmenu={StatusSubMenuComponent}
/>}
/>
}
</ErrorBoundary>
);
},

View file

@ -2,12 +2,23 @@
opacity: 0.5;
}
.vc-sp-item:hover {
background-color: var(--menu-item-default-hover-bg) !important;
color: var(--white) !important;
}
.vc-sp-icon {
transform: rotate(45deg);
margin-top: 4px;
width: 16px;
height: 16px;
color: var(--interactive-normal);
opacity: 0;
}
[data-menu-item="true"]:hover .vc-sp-icon {
opacity: 1;
}
[data-menu-item="true"]:hover .vc-sp-icon:hover {
color: var(--status-danger);
}
[data-menu-item="true"][id^="sp-custom-status-submenu"] img.emoji {
width: 16px !important;
height: 16px !important;
}

View file

@ -27,7 +27,7 @@ import { classes } from "@utils/misc";
import { formatDuration } from "@utils/text";
import type { Channel, RoleOrUserPermission } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { cl, settings } from "..";

View file

@ -141,7 +141,6 @@ function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel" | "Mess
const isMessage = name === "Message";
if (isMessage && !settings.store.messageContextMenu) return;
// typescript parser goes crazy if this is inline
const id = `vc-view-${name.toLowerCase()}-raw`;
const action = isMessage