Manage Blocked Users
diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css
index 190b8f62..c62c300e 100644
--- a/src/plugins/reviewDB/style.css
+++ b/src/plugins/reviewDB/style.css
@@ -16,16 +16,11 @@
border: 1px solid var(--profile-message-input-border-color);
}
-.vc-rdb-modal-footer > div {
+.vc-rdb-modal-footer-wrapper {
width: 100%;
margin: 6px 16px;
}
-/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */
-.vc-rdb-input > div > div {
- padding-left: 0 !important;
-}
-
.vc-rdb-placeholder {
margin-bottom: 4px;
font-weight: bold;
@@ -69,7 +64,7 @@
border-radius: 8px;
}
-.vc-rdb-review-comment img {
+.vc-rdb-review-comment [class*="avatar"] {
vertical-align: text-top;
}
@@ -117,13 +112,13 @@
align-items: center;
}
-.vc-rdb-block-modal-row img {
+.vc-rdb-block-modal-avatar {
border-radius: 50%;
height: 2em;
width: 2em;
}
-.vc-rdb-block-modal img::before {
+.vc-rdb-block-modal-avatar::before {
content: "";
display: block;
width: 100%;
diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx
index 7b811943..ffa2b5a2 100644
--- a/src/plugins/roleColorEverywhere/index.tsx
+++ b/src/plugins/roleColorEverywhere/index.tsx
@@ -84,8 +84,14 @@ export default definePlugin({
find: ".USER_MENTION)",
replacement: [
{
+ // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,
- replace: "$&,color:$self.getColorInt($1?.id,$2?.id)"
+ replace: "$&,color:$self.getColorInt($1?.id,$2?.id)",
+ noWarn: true
+ },
+ {
+ match: /(?<=onContextMenu:\i,color:)\i(?=\},\i\),\{children)(?<=user:(\i),channel:(\i).{0,500}?)/,
+ replace: "$self.getColorInt($1?.id,$2?.id)",
}
],
predicate: () => settings.store.chatMentions
@@ -124,11 +130,11 @@ export default definePlugin({
},
// Voice Users
{
- find: "renderPrioritySpeaker(){",
+ find: ".usernameSpeaking]:",
replacement: [
{
- match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
- replace: "$&style:$self.getColorStyle(this?.props?.user?.id,this?.props?.guildId),"
+ match: /\.usernameSpeaking\]:.+?,(?=children)(?<=guildId:(\i),.+?user:(\i).+?)/,
+ replace: "$&style:$self.getColorStyle($2.id,$1),"
}
],
predicate: () => settings.store.voiceUsers
diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx
index 437df1a5..6bfac736 100644
--- a/src/plugins/sendTimestamps/index.tsx
+++ b/src/plugins/sendTimestamps/index.tsx
@@ -68,15 +68,16 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
return (
-
+
Timestamp Picker
-
+
setValue(e.currentTarget.value)}
@@ -86,23 +87,25 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
/>
Timestamp Format
- ({
- label: m,
- value: m
- }))
- }
- isSelected={v => v === format}
- select={v => setFormat(v)}
- serialize={v => v}
- renderOptionLabel={o => (
-
- {Parser.parse(formatTimestamp(time, o.value))}
-
- )}
- renderOptionValue={() => rendered}
- />
+
+
({
+ label: m,
+ value: m
+ }))
+ }
+ isSelected={v => v === format}
+ select={v => setFormat(v)}
+ serialize={v => v}
+ renderOptionLabel={o => (
+
+ {Parser.parse(formatTimestamp(time, o.value))}
+
+ )}
+ renderOptionValue={() => rendered}
+ />
+
Preview
@@ -141,8 +144,8 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
diff --git a/src/plugins/sendTimestamps/styles.css b/src/plugins/sendTimestamps/styles.css
index 033d5c9d..e7efbe59 100644
--- a/src/plugins/sendTimestamps/styles.css
+++ b/src/plugins/sendTimestamps/styles.css
@@ -1,4 +1,4 @@
-.vc-st-modal-content input {
+.vc-st-date-picker {
background-color: var(--input-background);
color: var(--text-normal);
width: 95%;
@@ -12,35 +12,28 @@
font-size: 100%;
}
-.vc-st-format-label,
-.vc-st-format-label span {
- background-color: transparent;
-}
-
-.vc-st-modal-content [class|="select"] {
+.vc-st-format-select {
margin-bottom: 1em;
+
+ --background-modifier-accent: transparent;
}
-.vc-st-modal-content [class|="select"] span {
- background-color: var(--input-background);
+.vc-st-format-label {
+ --background-modifier-accent: transparent;
}
.vc-st-modal-header {
place-content: center space-between;
}
-.vc-st-modal-header h1 {
+.vc-st-modal-title {
margin: 0;
}
-.vc-st-modal-header button {
+.vc-st-modal-close-button {
padding: 0;
}
.vc-st-preview-text {
margin-bottom: 1em;
}
-
-.vc-st-button svg {
- transform: scale(1.1) translateY(1px);
-}
diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx
index be77ca1c..9f2d3008 100644
--- a/src/plugins/serverInfo/GuildInfoModal.tsx
+++ b/src/plugins/serverInfo/GuildInfoModal.tsx
@@ -31,7 +31,8 @@ export function openGuildInfoModal(guild: Guild) {
const enum Tabs {
ServerInfo,
Friends,
- BlockedUsers
+ BlockedUsers,
+ IgnoredUsers
}
interface GuildProps {
@@ -44,7 +45,8 @@ interface RelationshipProps extends GuildProps {
const fetched = {
friends: false,
- blocked: false
+ blocked: false,
+ ignored: false
};
function renderTimestamp(timestamp: number) {
@@ -56,10 +58,12 @@ function renderTimestamp(timestamp: number) {
function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState();
const [blockedCount, setBlockedCount] = useState();
+ const [ignoredCount, setIgnoredCount] = useState();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
+ fetched.ignored = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
@@ -90,6 +94,7 @@ function GuildInfoModal({ guild }: GuildProps) {
{iconUrl
?
openImageModal({
@@ -132,12 +137,19 @@ function GuildInfoModal({ guild }: GuildProps) {
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
+
+ Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : ""}
+
{currentTab === Tabs.ServerInfo && }
{currentTab === Tabs.Friends && }
{currentTab === Tabs.BlockedUsers && }
+ {currentTab === Tabs.IgnoredUsers && }
);
@@ -159,6 +171,7 @@ function Owner(guildId: string, owner: User) {
return (
openImageModal({
@@ -211,7 +224,13 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
return UserList("blocked", guild, blockedIds, setCount);
}
-function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
+function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
+ const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id));
+ return UserList("ignored", guild, ignoredIds, setCount);
+}
+
+
+function UserList(type: "friends" | "blocked" | "ignored", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] as string[];
const members = [] as string[];
diff --git a/src/plugins/serverInfo/styles.css b/src/plugins/serverInfo/styles.css
index 8c88e4f4..274b7d13 100644
--- a/src/plugins/serverInfo/styles.css
+++ b/src/plugins/serverInfo/styles.css
@@ -21,7 +21,7 @@
margin: 0.5em;
}
-.vc-gp-header img {
+.vc-gp-icon {
width: 48px;
height: 48px;
cursor: pointer;
@@ -82,7 +82,7 @@
gap: 0.2em;
}
-.vc-gp-owner img {
+.vc-gp-owner-avatar {
height: 20px;
border-radius: 50%;
cursor: pointer;
diff --git a/src/plugins/shikiCodeblocks.desktop/components/Code.tsx b/src/plugins/shikiCodeblocks.desktop/components/Code.tsx
index 8deca588..2794234d 100644
--- a/src/plugins/shikiCodeblocks.desktop/components/Code.tsx
+++ b/src/plugins/shikiCodeblocks.desktop/components/Code.tsx
@@ -84,9 +84,9 @@ export const Code = ({
}
const codeTableRows = lines.map((line, i) => (
-
- {i + 1}
- {line}
+
+ {i + 1}
+ {line}
));
diff --git a/src/plugins/shikiCodeblocks.desktop/components/Highlighter.tsx b/src/plugins/shikiCodeblocks.desktop/components/Highlighter.tsx
index dd140193..2d62af6e 100644
--- a/src/plugins/shikiCodeblocks.desktop/components/Highlighter.tsx
+++ b/src/plugins/shikiCodeblocks.desktop/components/Highlighter.tsx
@@ -102,7 +102,7 @@ export const Highlighter = ({
color: themeBase.plainColor,
}}
>
-
+
);
}
diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx
index 46629c77..f99c0be9 100644
--- a/src/plugins/showConnections/index.tsx
+++ b/src/plugins/showConnections/index.tsx
@@ -125,7 +125,7 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
{connection.name}
{connection.verified && }
-
+
}
key={connection.id}
diff --git a/src/plugins/showConnections/styles.css b/src/plugins/showConnections/styles.css
index cead5201..5bb16e0f 100644
--- a/src/plugins/showConnections/styles.css
+++ b/src/plugins/showConnections/styles.css
@@ -14,6 +14,6 @@
word-break: break-all;
}
-.vc-sc-tooltip svg {
+.vc-sc-tooltip-icon {
min-width: 16px;
}
diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx
index 15cd17c4..bbe286af 100644
--- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx
+++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx
@@ -18,6 +18,7 @@
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
+import { classes } from "@utils/misc";
import { formatDuration } from "@utils/text";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
@@ -25,7 +26,7 @@ import type { Channel } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "../../permissionsViewer/components/RolesAndUsersPermissions";
import { sortPermissionOverwrites } from "../../permissionsViewer/utils";
-import { settings } from "..";
+import { cl, settings } from "..";
const enum SortOrderTypes {
LATEST_ACTIVITY = 0,
@@ -168,19 +169,19 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
}, [channelId]);
return (
-
-
-
+
+
+
-
-
This is a {!PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) ? "hidden" : "locked"} {ChannelTypesToChannelNames[type]} channel.
+
+
This is a {!PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) ? "hidden" : "locked"} {ChannelTypesToChannelNames[type]} channel
{channel.isNSFW() &&
{({ onMouseLeave, onMouseEnter }) => (
0 && (
-
+
{Parser.parseTopic(topic, false, { channelId })}
)}
@@ -213,7 +214,6 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
}
-
{lastPinTimestamp &&
Last message pin:
}
@@ -247,7 +247,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
Default sort order: {SortOrderTypesToNames[defaultSortOrder]}
}
{defaultReactionEmoji != null &&
-
+
Default reaction emoji:
{Parser.defaultRules[defaultReactionEmoji.emojiName ? "emoji" : "customEmoji"].react({
name: defaultReactionEmoji.emojiName
@@ -258,29 +258,29 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
src: defaultReactionEmoji.emojiName
? EmojiUtils.getURL(defaultReactionEmoji.emojiName)
: void 0
- }, void 0, { key: "0" })}
+ }, void 0, { key: 0 })}
}
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&
Posts on this forum require a tag to be set.
}
{availableTags && availableTags.length > 0 &&
-
+
Available tags:
-
+
{availableTags.map(tag => )}
}
-
-
- {Settings.plugins.PermissionsViewer.enabled && (
+
+
+ {Vencord.Plugins.isPluginEnabled("PermissionsViewer") && (
{({ onMouseLeave, onMouseEnter }) => (
openRolesAndUsersPermissionsModal(permissions, GuildStore.getGuild(channel.guild_id), channel.name)}
>
settings.store.defaultAllowedUsersAndRolesDropdownState = !defaultAllowedUsersAndRolesDropdownState}
>
`&&!$self.isHiddenChannel(${channel})`
+ // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
+ match: /(?=(\|\||&&)\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
+ replace: (_, condition, channel) => condition === "||"
+ ? `||$self.isHiddenChannel(${channel})`
+ : `&&!$self.isHiddenChannel(${channel})`
},
{
// Make Discord show inside the channel if clicking on a hidden or locked channel
@@ -122,8 +129,11 @@ export default definePlugin({
{
find: ".AUDIENCE),{isSubscriptionGated",
replacement: {
- match: /!(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
- replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})`
+ // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
+ match: /(!)?(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
+ replace: (m, not, channel) => not
+ ? `${m}&&!$self.isHiddenChannel(${channel})`
+ : `${m}||$self.isHiddenChannel(${channel})`
}
},
{
@@ -163,7 +173,7 @@ export default definePlugin({
replacement: [
// Make the channel appear as muted if it's hidden
{
- match: /{channel:(\i),name:\i,muted:(\i).+?;/,
+ match: /\.subtitle,.+?;(?=return\(0,\i\.jsxs?\))(?<={channel:(\i),name:\i,muted:(\i).+?;)/,
replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};`
},
// Add the hidden eye icon if the channel is hidden
@@ -173,8 +183,11 @@ export default definePlugin({
},
// Make voice channels also appear as muted if they are muted
{
- match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)if\((\i)\)return (\i\.MUTED);/,
- replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return "";`
+ // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
+ match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)(if\()?(\i)(?:\)return |\?)(\i\.MUTED)/,
+ replace: (_, otherClasses, isIf, isMuted, mutedClassExpression) => isIf
+ ? `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
+ : `${isMuted}?${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
}
]
},
@@ -184,13 +197,14 @@ export default definePlugin({
{
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
- match: /\.LOCKED;if\((?<={channel:(\i).+?)/,
- replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
+ // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
+ match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
+ replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
},
{
// Hide unreads
predicate: () => settings.store.hideUnreads === true,
- match: /{channel:(\i),name:\i,.+?unread:(\i).+?;/,
+ match: /\.subtitle,.+?;(?=return\(0,\i\.jsxs?\))(?<={channel:(\i),name:\i,.+?unread:(\i).+?)/,
replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};`
}
]
@@ -471,7 +485,7 @@ export default definePlugin({
}
},
{
- find: '="NowPlayingViewStore",',
+ find: '"NowPlayingViewStore"',
replacement: {
// Make active now voice states on hidden channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,
@@ -539,7 +553,7 @@ export default definePlugin({
aria-hidden={true}
role="img"
>
-
+
), { noop: true }),
@@ -549,14 +563,14 @@ export default definePlugin({
-
+
)}
diff --git a/src/plugins/showHiddenChannels/style.css b/src/plugins/showHiddenChannels/style.css
index 301ad75d..56e50a2f 100644
--- a/src/plugins/showHiddenChannels/style.css
+++ b/src/plugins/showHiddenChannels/style.css
@@ -1,43 +1,31 @@
-.shc-lock-screen-outer-container {
- overflow: hidden scroll;
- flex: 1 1 auto;
- height: 100%;
- width: 100%;
-}
-
-.shc-lock-screen-container {
+.vc-shc-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
+ gap: 0.65em;
+ margin: 0.5em 0;
min-height: 100%;
}
-.shc-lock-screen-container > * {
- margin: 5px;
+.vc-shc-logo {
+ width: 12em;
+ height: 12em;
}
-.shc-lock-screen-logo {
- width: 180px;
- height: 180px;
-}
-
-.shc-lock-screen-heading-container {
+.vc-shc-heading-container {
display: flex;
flex-direction: row;
align-items: center;
+ gap: 0.5em;
}
-.shc-lock-screen-heading-container > * {
- margin: inherit;
-}
-
-.shc-lock-screen-heading-nsfw-icon {
+.vc-shc-heading-nsfw-icon {
color: var(--text-normal);
}
-.shc-lock-screen-topic-container {
+.vc-shc-topic-container {
color: var(--text-normal);
background: var(--bg-overlay-3, var(--background-secondary));
border-radius: 5px;
@@ -45,91 +33,75 @@
max-width: 70vw;
}
-.shc-lock-screen-tags-container {
+.vc-shc-default-emoji-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background: var(--bg-overlay-3, var(--background-secondary));
+ border-radius: 8px;
+ padding: 0.75em;
+ margin-left: 0.75em;
+}
+
+.vc-shc-tags-container {
+ display: flex;
+ flex-direction: column;
background: var(--bg-overlay-3, var(--background-secondary));
border-radius: 5px;
- padding: 10px;
+ padding: 0.75em;
+ gap: 0.75em;
max-width: 70vw;
}
-.shc-lock-screen-tags-container > * {
- margin: inherit;
-}
-
-.shc-lock-screen-tags {
+.vc-shc-tags {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
- gap: 8px;
+ gap: 0.35em;
}
-.shc-evenodd-fill-current-color {
- fill-rule: evenodd;
- fill: currentcolor;
-}
-
-.shc-hidden-channel-icon {
- margin-left: 6px;
- z-index: 0;
- cursor: not-allowed;
-}
-
-.shc-lock-screen-default-emoji-container {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-
-.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] {
- background: var(--bg-overlay-3, var(--background-secondary));
- border-radius: 8px;
- padding: 5px 6px;
- margin-left: 5px;
-}
-
-.shc-lock-screen-allowed-users-and-roles-container {
+.vc-shc-allowed-users-and-roles-container {
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-overlay-3, var(--background-secondary));
border-radius: 5px;
- padding: 10px;
+ padding: 0.75em;
max-width: 70vw;
}
-.shc-lock-screen-allowed-users-and-roles-container-title {
+.vc-shc-allowed-users-and-roles-container-title {
display: flex;
flex-direction: row;
align-items: center;
+ gap: 0.5em;
}
-.shc-lock-screen-allowed-users-and-roles-container-toggle-btn {
+.vc-shc-allowed-users-and-roles-container-toggle-btn {
all: unset;
- margin-left: 5px;
cursor: pointer;
display: flex;
align-items: center;
-}
-
-.shc-lock-screen-allowed-users-and-roles-container-toggle-btn > svg {
color: var(--text-normal);
}
-.shc-lock-screen-allowed-users-and-roles-container-permdetails-btn {
+.vc-shc-allowed-users-and-roles-container-permdetails-btn {
all: unset;
- margin-right: 5px;
cursor: pointer;
display: flex;
align-items: center;
-}
-
-.shc-lock-screen-allowed-users-and-roles-container-permdetails-btn > svg {
color: var(--text-normal);
}
-.shc-lock-screen-allowed-users-and-roles-container > [class^="members"] {
- margin-left: 10px;
+.vc-shc-allowed-users-and-roles-container > [class^="members"] {
+ margin-left: 12px;
flex-wrap: wrap;
justify-content: center;
}
+
+.vc-shc-hidden-channel-icon {
+ cursor: not-allowed;
+ margin-left: 6px;
+ z-index: 0;
+}
diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx
index bbafb58d..1f04f1f3 100644
--- a/src/plugins/showMeYourName/index.tsx
+++ b/src/plugins/showMeYourName/index.tsx
@@ -50,8 +50,8 @@ export default definePlugin({
{
find: '?"@":""',
replacement: {
- match: /(?<=onContextMenu:\i,children:).*?\)}/,
- replace: "$self.renderUsername(arguments[0])}"
+ match: /(?<=onContextMenu:\i,children:)\i\+\i/,
+ replace: "$self.renderUsername(arguments[0])"
}
},
],
diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx
index 1395cf80..2c7c8a85 100644
--- a/src/plugins/showTimeoutDuration/index.tsx
+++ b/src/plugins/showTimeoutDuration/index.tsx
@@ -76,8 +76,8 @@ export default definePlugin({
find: "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}",
replacement: [
{
- match: /(\i)\.Tooltip,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/,
- replace: "$self.TooltipWrapper,{message:arguments[0].message,$2"
+ match: /\i\.\i,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/,
+ replace: "$self.TooltipWrapper,{message:arguments[0].message,$1"
}
]
}
diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx
index 00e60509..04ab8706 100644
--- a/src/plugins/silentMessageToggle/index.tsx
+++ b/src/plugins/silentMessageToggle/index.tsx
@@ -69,8 +69,8 @@ const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
onClick={() => setEnabledValue(!enabled)}
>
diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx
index 92bdbc49..e2d99ec0 100644
--- a/src/plugins/silentTyping/index.tsx
+++ b/src/plugins/silentTyping/index.tsx
@@ -54,7 +54,7 @@ const SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => {
tooltip={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}
onClick={toggle}
>
-
+
{isEnabled && (
<>
diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx
index d708c279..4184931f 100644
--- a/src/plugins/spotifyControls/PlayerComponent.tsx
+++ b/src/plugins/spotifyControls/PlayerComponent.tsx
@@ -17,8 +17,10 @@
*/
import "./spotifyStyles.css";
+import "./visualRefreshSpotifyStyles.css"; // TODO: merge with spotifyStyles.css and remove when old UI is discontinued
import { Settings } from "@api/Settings";
+import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@shared/debounce";
@@ -28,7 +30,7 @@ import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState
import { SpotifyStore, Track } from "./SpotifyStore";
-const cl = (className: string) => `vc-spotify-${className}`;
+const cl = classNameFactory("vc-spotify-");
function msToHuman(ms: number) {
const minutes = ms / 1000 / 60;
@@ -40,7 +42,7 @@ function msToHuman(ms: number) {
function Svg(path: string, label: string) {
return () => (
SpotifyStore.setShuffle(!shuffle)}
>
@@ -143,7 +145,7 @@ function Controls() {
SpotifyStore.setRepeat(nextRepeat)}
style={{ position: "relative" }}
>
@@ -285,11 +287,12 @@ function Info({ track }: { track: Track; }) {
>
);
- if (coverExpanded && img) return (
-
- {i}
-
- );
+ if (coverExpanded && img)
+ return (
+
+ {i}
+
+ );
return (
@@ -305,8 +308,8 @@ function Info({ track }: { track: Track; }) {
{track.name}
{track.artists.some(a => a.name) && (
-
- by
+
+ by
{track.artists.map((a, i) => (
)}
{track.album.name && (
-
- on
+
+ on
{
class SpotifyStore extends Store {
public mPosition = 0;
- private start = 0;
+ public _start = 0;
public track: Track | null = null;
public device: Device | null = null;
@@ -100,26 +100,26 @@ export const SpotifyStore = proxyLazyWebpack(() => {
public get position(): number {
let pos = this.mPosition;
if (this.isPlaying) {
- pos += Date.now() - this.start;
+ pos += Date.now() - this._start;
}
return pos;
}
public set position(p: number) {
this.mPosition = p;
- this.start = Date.now();
+ this._start = Date.now();
}
prev() {
- this.req("post", "/previous");
+ this._req("post", "/previous");
}
next() {
- this.req("post", "/next");
+ this._req("post", "/next");
}
setVolume(percent: number) {
- this.req("put", "/volume", {
+ this._req("put", "/volume", {
query: {
volume_percent: Math.round(percent)
}
@@ -131,17 +131,17 @@ export const SpotifyStore = proxyLazyWebpack(() => {
}
setPlaying(playing: boolean) {
- this.req("put", playing ? "/play" : "/pause");
+ this._req("put", playing ? "/play" : "/pause");
}
setRepeat(state: Repeat) {
- this.req("put", "/repeat", {
+ this._req("put", "/repeat", {
query: { state }
});
}
setShuffle(state: boolean) {
- this.req("put", "/shuffle", {
+ this._req("put", "/shuffle", {
query: { state }
}).then(() => {
this.shuffle = state;
@@ -154,7 +154,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
this.isSettingPosition = true;
- return this.req("put", "/seek", {
+ return this._req("put", "/seek", {
query: {
position_ms: Math.round(ms)
}
@@ -164,7 +164,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
});
}
- private req(method: "post" | "get" | "put", route: string, data: any = {}) {
+ _req(method: "post" | "get" | "put", route: string, data: any = {}) {
if (this.device?.is_active)
(data.query ??= {}).device_id = this.device.id;
diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx
index b4f8e904..c7e8c91f 100644
--- a/src/plugins/spotifyControls/index.tsx
+++ b/src/plugins/spotifyControls/index.tsx
@@ -32,7 +32,7 @@ function toggleHoverControls(value: boolean) {
export default definePlugin({
name: "SpotifyControls",
description: "Adds a Spotify player above the account panel",
- authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000],
+ authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev],
options: {
hoverControls: {
description: "Show controls on hover",
diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css
index 893dc817..0b760377 100644
--- a/src/plugins/spotifyControls/spotifyStyles.css
+++ b/src/plugins/spotifyControls/spotifyStyles.css
@@ -2,7 +2,9 @@
padding: 0.375rem 0.5rem;
border-bottom: 1px solid var(--background-modifier-accent);
- --vc-spotify-green: #1db954; /* so custom themes can easily change it */
+ --vc-spotify-green: var(--spotify, #1db954); /* so custom themes can easily change it */
+ --vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%);
+ --vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%);
}
.theme-light #vc-spotify-player {
@@ -30,22 +32,17 @@
background-color: var(--background-modifier-selected);
}
-.vc-spotify-button svg {
+.vc-spotify-button-icon {
height: 24px;
width: 24px;
}
-[class*="vc-spotify-shuffle"] > svg,
-[class*="vc-spotify-repeat"] > svg {
+.vc-spotify-shuffle .vc-spotify-button-icon,
+.vc-spotify-repeat .vc-spotify-button-icon {
width: 22px;
height: 22px;
}
-.vc-spotify-button svg path {
- width: 100%;
- height: 100%;
-}
-
/* .vc-spotify-button:hover {
filter: brightness(1.3);
} */
@@ -87,12 +84,19 @@
gap: 0.5em;
}
-#vc-spotify-info-wrapper img {
+#vc-spotify-album-image {
height: 90%;
object-fit: contain;
+ border-radius: 3px;
+ transition: filter 0.2s;
}
-#vc-spotify-album-expanded-wrapper img {
+#vc-spotify-album-image:hover {
+ filter: brightness(1.2);
+ cursor: pointer;
+}
+
+#vc-spotify-album-expanded-wrapper #vc-spotify-album-image {
width: 100%;
object-fit: contain;
}
@@ -137,16 +141,6 @@
cursor: pointer;
}
-#vc-spotify-album-image {
- border-radius: 3px;
- transition: filter 0.2s;
-}
-
-#vc-spotify-album-image:hover {
- filter: brightness(1.2);
- cursor: pointer;
-}
-
#vc-spotify-progress-bar {
position: relative;
color: var(--text-normal);
diff --git a/src/plugins/spotifyControls/visualRefreshSpotifyStyles.css b/src/plugins/spotifyControls/visualRefreshSpotifyStyles.css
new file mode 100644
index 00000000..3a140a17
--- /dev/null
+++ b/src/plugins/spotifyControls/visualRefreshSpotifyStyles.css
@@ -0,0 +1,77 @@
+/* TODO: merge with spotifyStyles.css and remove when old UI is discontinued */
+.visual-refresh {
+ #vc-spotify-player {
+ padding: 12px;
+ background: var(--bg-overlay-floating, var(--background-base-low, var(--background-secondary-alt)));
+ margin: 0;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ }
+
+ .vc-spotify-song-info-prefix {
+ display: none;
+ }
+
+ .vc-spotify-artist, .vc-spotify-album {
+ color: var(--header-primary);
+ }
+
+ .vc-spotify-secondary-song-info {
+ font-size: 12px;
+ }
+
+ #vc-spotify-progress-bar {
+ position: relative;
+ color: var(--text-normal);
+ width: 100%;
+ }
+
+ #vc-spotify-progress-bar > [class^="slider"] {
+ flex-grow: 1;
+ width: 100%;
+ padding: 0 !important;
+ }
+
+ #vc-spotify-progress-bar > [class^="slider"] [class^="bar"] {
+ height: 3px !important;
+ top: calc(12px - 4px / 2 + var(--bar-offset));
+ }
+
+ #vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] {
+ background-color: var(--interactive-active);
+ }
+
+ #vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] {
+ background-color: var(--vc-spotify-green);
+ }
+
+ #vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
+ background-color: var(--interactive-active);
+ width: 16px !important;
+ height: 16px !important;
+ margin-top: calc(17px/-2 + var(--bar-offset)/2);
+ margin-left: -0.5px;
+ }
+
+ .vc-spotify-progress-time {
+ margin-top: 8px;
+ font-family: var(--font-code);
+ }
+
+ .vc-spotify-button-row {
+ margin-top: 14px;
+ }
+
+ .vc-spotify-button {
+ margin: 0 2px;
+ border-radius: var(--radius-sm);
+ }
+
+ .vc-spotify-repeat-context, .vc-spotify-repeat-track, .vc-spotify-shuffle-on {
+ background-color: var(--vc-spotify-green-90);
+ }
+
+ .vc-spotify-repeat-context:hover, .vc-spotify-repeat-track:hover, .vc-spotify-shuffle-on:hover {
+ background-color: var(--vc-spotify-green-80);
+ }
+}
diff --git a/src/plugins/spotifyShareCommands/index.ts b/src/plugins/spotifyShareCommands/index.ts
index 8c485666..ed793963 100644
--- a/src/plugins/spotifyShareCommands/index.ts
+++ b/src/plugins/spotifyShareCommands/index.ts
@@ -16,8 +16,9 @@
* along with this program. If not, see .
*/
-import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
+import { ApplicationCommandInputType, Command, findOption, OptionalMessageOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
+import { sendMessage } from "@utils/discord";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, MessageActions } from "@webpack/common";
@@ -55,21 +56,36 @@ interface Track {
const Spotify = findByPropsLazy("getPlayerState");
const PendingReplyStore = findByPropsLazy("getPendingReply");
-function sendMessage(channelId, message) {
- message = {
- // The following are required to prevent Discord from throwing an error
- invalidEmojis: [],
- tts: false,
- validNonShortcutEmojis: [],
- ...message
- };
- const reply = PendingReplyStore.getPendingReply(channelId);
- MessageActions.sendMessage(channelId, message, void 0, MessageActions.getSendMessageOptionsForReply(reply))
- .then(() => {
- if (reply) {
- FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId });
+function makeCommand(name: string, formatUrl: (track: Track) => string): Command {
+ return {
+ name,
+ description: `Share your current Spotify ${name} in chat`,
+ inputType: ApplicationCommandInputType.BUILT_IN,
+ options: [OptionalMessageOption],
+ execute(options, { channel }) {
+ const track: Track | null = Spotify.getTrack();
+ if (!track) {
+ return sendBotMessage(channel.id, {
+ content: "You're not listening to any music."
+ });
}
- });
+
+ const data = formatUrl(track);
+ const message = findOption(options, "message");
+
+ // Note: Due to how Discord handles commands, we need to manually create and send the message
+
+ sendMessage(
+ channel.id,
+ { content: message ? `${message} ${data}` : data },
+ false,
+ MessageActions.getSendMessageOptionsForReply(PendingReplyStore.getPendingReply(channel.id))
+ ).then(() => {
+ FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId: channel.id });
+ });
+
+ }
+ };
}
export default definePlugin({
@@ -77,60 +93,8 @@ export default definePlugin({
description: "Share your current Spotify track, album or artist via slash command (/track, /album, /artist)",
authors: [Devs.katlyn],
commands: [
- {
- name: "track",
- description: "Send your current Spotify track to chat",
- inputType: ApplicationCommandInputType.BUILT_IN,
- options: [],
- execute: (_, ctx) => {
- const track: Track | null = Spotify.getTrack();
- if (track === null) {
- sendBotMessage(ctx.channel.id, {
- content: "You're not listening to any music."
- });
- return;
- }
- // Note: Due to how Discord handles commands, we need to manually create and send the message
- sendMessage(ctx.channel.id, {
- content: `https://open.spotify.com/track/${track.id}`
- });
- }
- },
- {
- name: "album",
- description: "Send your current Spotify album to chat",
- inputType: ApplicationCommandInputType.BUILT_IN,
- options: [],
- execute: (_, ctx) => {
- const track: Track | null = Spotify.getTrack();
- if (track === null) {
- sendBotMessage(ctx.channel.id, {
- content: "You're not listening to any music."
- });
- return;
- }
- sendMessage(ctx.channel.id, {
- content: `https://open.spotify.com/album/${track.album.id}`
- });
- }
- },
- {
- name: "artist",
- description: "Send your current Spotify artist to chat",
- inputType: ApplicationCommandInputType.BUILT_IN,
- options: [],
- execute: (_, ctx) => {
- const track: Track | null = Spotify.getTrack();
- if (track === null) {
- sendBotMessage(ctx.channel.id, {
- content: "You're not listening to any music."
- });
- return;
- }
- sendMessage(ctx.channel.id, {
- content: track.artists[0].external_urls.spotify
- });
- }
- }
+ makeCommand("track", track => `https://open.spotify.com/track/${track.id}`),
+ makeCommand("album", track => `https://open.spotify.com/album/${track.album.id}`),
+ makeCommand("artist", track => track.artists[0].external_urls.spotify)
]
});
diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx
index aabc786a..ac7f3f0c 100644
--- a/src/plugins/startupTimings/index.tsx
+++ b/src/plugins/startupTimings/index.tsx
@@ -27,12 +27,22 @@ export default definePlugin({
authors: [Devs.Megu],
patches: [{
find: "#{intl::ACTIVITY_SETTINGS}",
- replacement: {
- match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
- replace: (_, commaOrSemi, settings, elements) => "" +
- `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
- `&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
- }
+ replacement: [
+ {
+ // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
+ match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
+ replace: (_, commaOrSemi, settings, elements) => "" +
+ `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
+ `&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`,
+ noWarn: true
+ },
+ {
+ match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+\)\)\}\))(?=\)\})/,
+ replace: (_, commaOrSemi, settings, elements) => "" +
+ `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
+ `&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`,
+ },
+ ]
}],
StartupTimingPage
});
diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx
index 3d1e891d..d5d6f4dc 100644
--- a/src/plugins/textReplace/index.tsx
+++ b/src/plugins/textReplace/index.tsx
@@ -45,7 +45,6 @@ const makeEmptyRuleArray = () => [makeEmptyRule()];
const settings = definePluginSettings({
replace: {
type: OptionType.COMPONENT,
- description: "",
component: () => {
const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]);
@@ -245,7 +244,7 @@ export default definePlugin({
},
async start() {
- // TODO: Remove DataStore rules migrations once enough time has passed
+ // TODO(OptionType.CUSTOM Related): Remove DataStore rules migrations once enough time has passed
const oldStringRules = await DataStore.get(STRING_RULES_KEY);
if (oldStringRules != null) {
settings.store.stringRules = oldStringRules;
diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts
index 8e1e022b..7d904e7e 100644
--- a/src/plugins/themeAttributes/index.ts
+++ b/src/plugins/themeAttributes/index.ts
@@ -54,8 +54,8 @@ export default definePlugin({
}
],
- getAvatarStyles(src: string) {
- if (src.startsWith("data:")) return {};
+ getAvatarStyles(src: string | null) {
+ if (!src || src.startsWith("data:")) return {};
return Object.fromEntries(
[128, 256, 512, 1024, 2048, 4096].map(size => [
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx
index 1b77fb94..bc0e335e 100644
--- a/src/plugins/translate/TranslateIcon.tsx
+++ b/src/plugins/translate/TranslateIcon.tsx
@@ -25,7 +25,7 @@ import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal";
import { cl } from "./utils";
-export function TranslateIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) {
+export function TranslateIcon({ height = 20, width = 20, className }: { height?: number; width?: number; className?: string; }) {
return (
-
+
Translate
diff --git a/src/plugins/translate/TranslationAccessory.tsx b/src/plugins/translate/TranslationAccessory.tsx
index 8e8f4c17..9b6393cc 100644
--- a/src/plugins/translate/TranslationAccessory.tsx
+++ b/src/plugins/translate/TranslationAccessory.tsx
@@ -55,7 +55,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
return (
-
+
{Parser.parse(translation.text)}
{" "}
(translated from {translation.sourceLanguage} - setTranslation(undefined)} />)
diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css
index 64b6c9b9..c07c9e36 100644
--- a/src/plugins/translate/styles.css
+++ b/src/plugins/translate/styles.css
@@ -6,7 +6,7 @@
place-content: center space-between;
}
-.vc-trans-modal-header h1 {
+.vc-trans-modal-title {
margin: 0;
}
@@ -17,7 +17,7 @@
font-weight: 400;
}
-.vc-trans-accessory svg {
+.vc-trans-accessory-icon {
margin-right: 0.25em;
}
diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx
index e6a1b3b4..e6903bcd 100644
--- a/src/plugins/typingIndicator/index.tsx
+++ b/src/plugins/typingIndicator/index.tsx
@@ -23,12 +23,12 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
-import { findComponentByCodeLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
+import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks";
-const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots");
+const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const TypingStore = findStoreLazy("TypingStore");
@@ -100,16 +100,24 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
{props => (
{((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && (
-
UserStore.getUser(id))}
- guildId={guildId}
- renderIcon={false}
- max={3}
- showDefaultAvatarsForNullUsers
- showUserPopout
- size={16}
- className="vc-typing-indicator-avatars"
- />
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onKeyPress={e => e.stopPropagation()}
+ >
+ UserStore.getUser(id))}
+ guildId={guildId}
+ renderIcon={false}
+ max={3}
+ showDefaultAvatarsForNullUsers
+ showUserPopout
+ size={16}
+ className="vc-typing-indicator-avatars"
+ />
+
)}
{((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (
diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx
index ff68a486..22013992 100644
--- a/src/plugins/typingTweaks/index.tsx
+++ b/src/plugins/typingTweaks/index.tsx
@@ -25,6 +25,8 @@ import { Avatar, GuildMemberStore, React, RelationshipStore } from "@webpack/com
import { User } from "discord-types/general";
import { PropsWithChildren } from "react";
+import managedStyle from "./style.css?managed";
+
const settings = definePluginSettings({
showAvatars: {
type: OptionType.BOOLEAN,
@@ -60,24 +62,19 @@ interface Props {
const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) {
return (
{
openUserProfile(user.id);
}}
style={{
- display: "grid",
- gridAutoFlow: "column",
- gap: "4px",
color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined,
- cursor: "pointer"
}}
>
{settings.store.showAvatars && (
-
+
)}
{GuildMemberStore.getNick(guildId!, user.id)
|| (!guildId && RelationshipStore.getNickname(user.id))
@@ -94,6 +91,8 @@ export default definePlugin({
authors: [Devs.zt],
settings,
+ managedStyle,
+
patches: [
{
find: "#{intl::THREE_USERS_TYPING}",
@@ -101,7 +100,7 @@ export default definePlugin({
{
// Style the indicator and add function call to modify the children before rendering
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i(?<=guildId:(\i).+?)/,
- replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& }),style:$self.TYPING_TEXT_STYLE"
+ replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& })"
},
{
// Changes the indicator to keep the user object when creating the list of typing users
@@ -118,12 +117,6 @@ export default definePlugin({
}
],
- TYPING_TEXT_STYLE: {
- display: "grid",
- gridAutoFlow: "column",
- gridGap: "0.25em"
- },
-
buildSeveralUsers,
renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => {
diff --git a/src/plugins/typingTweaks/style.css b/src/plugins/typingTweaks/style.css
new file mode 100644
index 00000000..28d0042a
--- /dev/null
+++ b/src/plugins/typingTweaks/style.css
@@ -0,0 +1,5 @@
+.vc-typing-user [class^="wrapper"] {
+ display: inline-block;
+ margin-right: 0.25em;
+ vertical-align: -4px;
+}
diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx
index 16debf71..2df64b72 100644
--- a/src/plugins/unsuppressEmbeds/index.tsx
+++ b/src/plugins/unsuppressEmbeds/index.tsx
@@ -21,12 +21,18 @@ import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
+import { MessageSnapshot } from "@webpack/types";
+
const EMBED_SUPPRESSED = 1 << 2;
-const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => {
+const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, messageSnapshots, embeds, flags, id: messageId } }) => {
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
- if (!isEmbedSuppressed && !embeds.length) return;
+ const hasEmbedsInSnapshots = messageSnapshots.some(
+ (snapshot: MessageSnapshot) => snapshot?.message.embeds.length
+ );
+
+ if (!isEmbedSuppressed && !embeds.length && !hasEmbedsInSnapshots) return;
const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS);
if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return;
diff --git a/src/plugins/userMessagesPronouns/index.ts b/src/plugins/userMessagesPronouns/index.ts
index 27b162b9..1699251e 100644
--- a/src/plugins/userMessagesPronouns/index.ts
+++ b/src/plugins/userMessagesPronouns/index.ts
@@ -41,11 +41,20 @@ export default definePlugin({
},
{
find: '="SYSTEM_TAG"',
- replacement: {
- // Add next to username (compact mode)
- match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\),(?=\i)/g,
- replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),"
- }
+ replacement: [
+ {
+ // Add next to username (compact mode)
+ // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
+ match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\),(?=\i)/g,
+ replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),",
+ noWarn: true
+ },
+ {
+ // Add next to username (compact mode)
+ match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\)\),(?=\i)/g,
+ replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),",
+ },
+ ]
}
],
diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx
index e8924f82..9029cdc5 100644
--- a/src/plugins/userVoiceShow/components.tsx
+++ b/src/plugins/userVoiceShow/components.tsx
@@ -22,7 +22,7 @@ const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const Avatar = findComponentByCodeLazy(".status)/2):0");
-const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL");
+const GroupDMAvatars = findComponentByCodeLazy("frontSrc:", "getAvatarURL");
const ActionButtonClasses = findByPropsLazy("actionButton", "highlight");
@@ -128,17 +128,15 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) {
);
}
-interface VoiceChannelIndicatorProps {
+export interface VoiceChannelIndicatorProps {
userId: string;
- isMessageIndicator?: boolean;
- isProfile?: boolean;
isActionButton?: boolean;
shouldHighlight?: boolean;
}
const clickTimers = {} as Record;
-export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isMessageIndicator, isProfile, isActionButton, shouldHighlight }: VoiceChannelIndicatorProps) => {
+export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isActionButton, shouldHighlight }: VoiceChannelIndicatorProps) => {
const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId as string | undefined);
const channel = channelId == null ? undefined : ChannelStore.getChannel(channelId);
@@ -182,7 +180,7 @@ export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isMessageIndi
{props => {
const iconProps: IconProps = {
...props,
- className: classes(isMessageIndicator && cl("message-indicator"), (!isProfile && !isActionButton) && cl("speaker-margin"), isActionButton && ActionButtonClasses.actionButton, shouldHighlight && ActionButtonClasses.highlight),
+ className: classes(isActionButton && ActionButtonClasses.actionButton, shouldHighlight && ActionButtonClasses.highlight),
size: isActionButton ? 20 : undefined,
onClick
};
diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx
index f3063f59..3d119c43 100644
--- a/src/plugins/userVoiceShow/index.tsx
+++ b/src/plugins/userVoiceShow/index.tsx
@@ -60,7 +60,7 @@ export default definePlugin({
find: "#{intl::USER_PROFILE_LOAD_ERROR}",
replacement: {
match: /(\.fetchError.+?\?)null/,
- replace: (_, rest) => `${rest}$self.VoiceChannelIndicator({userId:arguments[0]?.userId,isProfile:true})`
+ replace: (_, rest) => `${rest}$self.VoiceChannelIndicator({userId:arguments[0]?.userId})`
},
predicate: () => settings.store.showInUserProfileModal
},
@@ -99,7 +99,7 @@ export default definePlugin({
addMemberListDecorator("UserVoiceShow", ({ user }) => user == null ? null : );
}
if (settings.store.showInMessages) {
- addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : );
+ addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : );
}
},
diff --git a/src/plugins/userVoiceShow/style.css b/src/plugins/userVoiceShow/style.css
index d172975b..f9fd56fb 100644
--- a/src/plugins/userVoiceShow/style.css
+++ b/src/plugins/userVoiceShow/style.css
@@ -13,16 +13,6 @@
color: var(--interactive-hover);
}
-.vc-uvs-speaker-margin {
- margin-left: 4px;
-}
-
-.vc-uvs-message-indicator {
- display: inline-flex;
- top: 2.5px;
- position: relative;
-}
-
.vc-uvs-tooltip-container {
max-width: 300px;
}
diff --git a/src/plugins/usrbg/index.css b/src/plugins/usrbg/index.css
deleted file mode 100644
index 69c5b185..00000000
--- a/src/plugins/usrbg/index.css
+++ /dev/null
@@ -1,12 +0,0 @@
-:is([class*="userProfile"], [class*="userPopout"]) [class*="bannerPremium"] {
- background: center / cover no-repeat;
-}
-
-[class*="NonPremium"]:has([class*="bannerPremium"]) [class*="avatarPositionNormal"],
-[class*="PremiumWithoutBanner"]:has([class*="bannerPremium"]) [class*="avatarPositionPremiumNoBanner"] {
- top: 76px;
-}
-
-[style*="background-image"] [class*="background_"] {
- background-color: transparent !important;
-}
diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx
index 788a79ae..df823102 100644
--- a/src/plugins/usrbg/index.tsx
+++ b/src/plugins/usrbg/index.tsx
@@ -17,13 +17,10 @@
*/
import { definePluginSettings } from "@api/Settings";
-import { enableStyle } from "@api/Styles";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
-import style from "./index.css?managed";
-
const API_URL = "https://usrbg.is-hardly.online/users";
interface UsrbgApiReturn {
@@ -115,8 +112,6 @@ export default definePlugin({
},
async start() {
- enableStyle(style);
-
const res = await fetch(API_URL);
if (res.ok) {
this.data = await res.json();
diff --git a/src/plugins/vcNarrator/VoiceSetting.tsx b/src/plugins/vcNarrator/VoiceSetting.tsx
new file mode 100644
index 00000000..fb5c739c
--- /dev/null
+++ b/src/plugins/vcNarrator/VoiceSetting.tsx
@@ -0,0 +1,126 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Forms, SearchableSelect, useMemo, useState } from "@webpack/common";
+
+import { getCurrentVoice, settings } from "./settings";
+
+// TODO: replace by [Object.groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) once it has more maturity
+
+function groupBy(arr: T[], fn: (obj: T) => K) {
+ return arr.reduce((acc, obj) => {
+ const value = fn(obj);
+ acc[value] ??= [];
+ acc[value].push(obj);
+ return acc;
+ }, {} as Record);
+}
+
+interface PickerProps {
+ voice: string | undefined;
+ voices: SpeechSynthesisVoice[];
+}
+
+function SimplePicker({ voice, voices }: PickerProps) {
+ const options = voices.map(voice => ({
+ label: voice.name,
+ value: voice.voiceURI,
+ default: voice.default,
+ }));
+
+ return (
+ o.value === voice)}
+ onChange={v => settings.store.voice = v}
+ closeOnSelect
+ />
+ );
+}
+
+const languageNames = new Intl.DisplayNames(["en"], { type: "language" });
+
+function ComplexPicker({ voice, voices }: PickerProps) {
+ const groupedVoices = useMemo(() => groupBy(voices, voice => voice.lang), [voices]);
+
+ const languageNameMapping = useMemo(() => {
+ const list = [] as Record<"name" | "friendlyName", string>[];
+
+ for (const name in groupedVoices) {
+ try {
+ const friendlyName = languageNames.of(name);
+ if (friendlyName) {
+ list.push({ name, friendlyName });
+ }
+ } catch { }
+ }
+
+ return list;
+ }, [groupedVoices]);
+
+ const [selectedLanguage, setSelectedLanguage] = useState(() => getCurrentVoice()?.lang ?? languageNameMapping[0].name);
+
+ if (languageNameMapping.length === 1) {
+ return (
+
+ );
+ }
+
+ const voicesForLanguage = groupedVoices[selectedLanguage];
+
+ const languageOptions = languageNameMapping.map(l => ({
+ label: l.friendlyName,
+ value: l.name
+ }));
+
+ return (
+ <>
+ Language
+ l.value === selectedLanguage)}
+ onChange={v => setSelectedLanguage(v)}
+ maxVisibleItems={5}
+ closeOnSelect
+ />
+ Voice
+
+ >
+ );
+}
+
+
+function VoiceSetting() {
+ const voices = useMemo(() => window.speechSynthesis?.getVoices() ?? [], []);
+ const { voice } = settings.use(["voice"]);
+
+ if (!voices.length)
+ return No voices found. ;
+
+ // espeak on Linux has a ridiculous amount of voices (26k for me).
+ // If there are more than 20 voices, we split it up into two pickers, one for language, then one with only the voices for that language.
+ // This way, there are around 200-ish options per language
+ const Picker = voices.length > 20 ? ComplexPicker : SimplePicker;
+ return ;
+}
+
+export function VoiceSettingSection() {
+ return (
+
+ Voice
+
+
+ );
+}
diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx
index 95be33be..02eb216c 100644
--- a/src/plugins/vcNarrator/index.tsx
+++ b/src/plugins/vcNarrator/index.tsx
@@ -16,17 +16,18 @@
* along with this program. If not, see .
*/
-import { Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
-import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types";
+import definePlugin, { ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
import { ReactElement } from "react";
+import { getCurrentVoice, settings } from "./settings";
+
interface VoiceState {
userId: string;
channelId?: string;
@@ -43,25 +44,19 @@ const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentC
// 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
-function speak(text: string, settings: any = Settings.plugins.VcNarrator) {
+function speak(text: string, { volume, rate } = settings.store) {
if (!text) return;
const speech = new SpeechSynthesisUtterance(text);
- let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.voice);
- if (!voice) {
- new Logger("VcNarrator").error(`Voice "${settings.voice}" not found. Resetting to default.`);
- voice = speechSynthesis.getVoices().find(v => v.default);
- settings.voice = voice?.voiceURI;
- if (!voice) return; // This should never happen
- }
+ const voice = getCurrentVoice();
speech.voice = voice!;
- speech.volume = settings.volume;
- speech.rate = settings.rate;
+ speech.volume = volume;
+ speech.rate = rate;
speechSynthesis.speak(speech);
}
function clean(str: string) {
- const replacer = Settings.plugins.VcNarrator.latinOnly
+ const replacer = settings.store.latinOnly
? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu
: /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu;
@@ -145,11 +140,11 @@ function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId,
*/
function playSample(tempSettings: any, type: string) {
- const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
+ const s = Object.assign({}, settings.plain, tempSettings);
const currentUser = UserStore.getCurrentUser();
const myGuildId = SelectedGuildStore.getGuildId();
- speak(formatText(settings[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username, GuildMemberStore.getNick(myGuildId, currentUser.id) ?? currentUser.username), settings);
+ speak(formatText(s[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username, GuildMemberStore.getNick(myGuildId, currentUser.id) ?? currentUser.username), s);
}
export default definePlugin({
@@ -158,6 +153,8 @@ export default definePlugin({
authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
+ settings,
+
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
@@ -177,8 +174,8 @@ export default definePlugin({
const [type, id] = getTypeAndChannelId(state, isMe);
if (!type) continue;
- const template = Settings.plugins.VcNarrator[type + "Message"];
- const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username;
+ const template = settings.store[type + "Message"];
+ const user = isMe && !settings.store.sayOwnName ? "" : UserStore.getUser(userId).username;
const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);
const nickname = user && (GuildMemberStore.getNick(myGuildId, userId) ?? user);
const channel = ChannelStore.getChannel(id).name;
@@ -195,7 +192,7 @@ export default definePlugin({
if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute";
- speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
+ speak(formatText(settings.store[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
},
AUDIO_TOGGLE_SELF_DEAF() {
@@ -204,7 +201,7 @@ export default definePlugin({
if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen";
- speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
+ speak(formatText(settings.store[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
}
},
@@ -218,81 +215,6 @@ export default definePlugin({
},
- optionsCache: null as Record | null,
-
- get options() {
- return this.optionsCache ??= {
- voice: {
- type: OptionType.SELECT,
- description: "Narrator Voice",
- options: window.speechSynthesis?.getVoices().map(v => ({
- label: v.name,
- value: v.voiceURI,
- default: v.default
- })) ?? []
- },
- volume: {
- type: OptionType.SLIDER,
- description: "Narrator Volume",
- default: 1,
- markers: [0, 0.25, 0.5, 0.75, 1],
- stickToMarkers: false
- },
- rate: {
- type: OptionType.SLIDER,
- description: "Narrator Speed",
- default: 1,
- markers: [0.1, 0.5, 1, 2, 5, 10],
- stickToMarkers: false
- },
- sayOwnName: {
- description: "Say own name",
- type: OptionType.BOOLEAN,
- default: false
- },
- latinOnly: {
- description: "Strip non latin characters from names before saying them",
- type: OptionType.BOOLEAN,
- default: false
- },
- joinMessage: {
- type: OptionType.STRING,
- description: "Join Message",
- default: "{{USER}} joined"
- },
- leaveMessage: {
- type: OptionType.STRING,
- description: "Leave Message",
- default: "{{USER}} left"
- },
- moveMessage: {
- type: OptionType.STRING,
- description: "Move Message",
- default: "{{USER}} moved to {{CHANNEL}}"
- },
- muteMessage: {
- type: OptionType.STRING,
- description: "Mute Message (only self for now)",
- default: "{{USER}} Muted"
- },
- unmuteMessage: {
- type: OptionType.STRING,
- description: "Unmute Message (only self for now)",
- default: "{{USER}} unmuted"
- },
- deafenMessage: {
- type: OptionType.STRING,
- description: "Deafen Message (only self for now)",
- default: "{{USER}} deafened"
- },
- undeafenMessage: {
- type: OptionType.STRING,
- description: "Undeafen Message (only self for now)",
- default: "{{USER}} undeafened"
- }
- } satisfies Record;
- },
-
settingsAboutComponent({ tempSettings: s }) {
const [hasVoices, hasEnglishVoices] = useMemo(() => {
const voices = speechSynthesis.getVoices();
@@ -300,7 +222,7 @@ export default definePlugin({
}, []);
const types = useMemo(
- () => Object.keys(Vencord.Plugins.plugins.VcNarrator.options!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)),
+ () => Object.keys(settings.def).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)),
[],
);
diff --git a/src/plugins/vcNarrator/settings.ts b/src/plugins/vcNarrator/settings.ts
new file mode 100644
index 00000000..ee3aee0e
--- /dev/null
+++ b/src/plugins/vcNarrator/settings.ts
@@ -0,0 +1,97 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Logger } from "@utils/Logger";
+import { OptionType } from "@utils/types";
+
+import { VoiceSettingSection } from "./VoiceSetting";
+
+export const getDefaultVoice = () => window.speechSynthesis?.getVoices().find(v => v.default);
+
+export function getCurrentVoice(voices = window.speechSynthesis?.getVoices()) {
+ if (!voices) return undefined;
+
+ if (settings.store.voice) {
+ const voice = voices.find(v => v.voiceURI === settings.store.voice);
+ if (voice) return voice;
+
+ new Logger("VcNarrator").error(`Voice "${settings.store.voice}" not found. Resetting to default.`);
+ }
+
+ const voice = voices.find(v => v.default);
+ settings.store.voice = voice?.voiceURI;
+ return voice;
+}
+
+export const settings = definePluginSettings({
+ voice: {
+ type: OptionType.COMPONENT,
+ component: VoiceSettingSection,
+ get default() {
+ return getDefaultVoice()?.voiceURI;
+ }
+ },
+ volume: {
+ type: OptionType.SLIDER,
+ description: "Narrator Volume",
+ default: 1,
+ markers: [0, 0.25, 0.5, 0.75, 1],
+ stickToMarkers: false
+ },
+ rate: {
+ type: OptionType.SLIDER,
+ description: "Narrator Speed",
+ default: 1,
+ markers: [0.1, 0.5, 1, 2, 5, 10],
+ stickToMarkers: false
+ },
+ sayOwnName: {
+ description: "Say own name",
+ type: OptionType.BOOLEAN,
+ default: false
+ },
+ latinOnly: {
+ description: "Strip non latin characters from names before saying them",
+ type: OptionType.BOOLEAN,
+ default: false
+ },
+ joinMessage: {
+ type: OptionType.STRING,
+ description: "Join Message",
+ default: "{{USER}} joined"
+ },
+ leaveMessage: {
+ type: OptionType.STRING,
+ description: "Leave Message",
+ default: "{{USER}} left"
+ },
+ moveMessage: {
+ type: OptionType.STRING,
+ description: "Move Message",
+ default: "{{USER}} moved to {{CHANNEL}}"
+ },
+ muteMessage: {
+ type: OptionType.STRING,
+ description: "Mute Message (only self for now)",
+ default: "{{USER}} muted"
+ },
+ unmuteMessage: {
+ type: OptionType.STRING,
+ description: "Unmute Message (only self for now)",
+ default: "{{USER}} unmuted"
+ },
+ deafenMessage: {
+ type: OptionType.STRING,
+ description: "Deafen Message (only self for now)",
+ default: "{{USER}} deafened"
+ },
+ undeafenMessage: {
+ type: OptionType.STRING,
+ description: "Undeafen Message (only self for now)",
+ default: "{{USER}} undeafened"
+ }
+});
diff --git a/src/plugins/vencordToolbox/index.css b/src/plugins/vencordToolbox/index.css
index a1c85ad5..642375b4 100644
--- a/src/plugins/vencordToolbox/index.css
+++ b/src/plugins/vencordToolbox/index.css
@@ -1,16 +1,16 @@
.vc-toolbox-btn,
-.vc-toolbox-btn>svg {
+.vc-toolbox-icon {
-webkit-app-region: no-drag;
}
-.vc-toolbox-btn>svg {
+.vc-toolbox-icon {
color: var(--interactive-normal);
}
-.vc-toolbox-btn[class*="selected"]>svg {
+.vc-toolbox-btn[class*="selected"] .vc-toolbox-icon {
color: var(--interactive-active);
}
-.vc-toolbox-btn:hover>svg {
+.vc-toolbox-btn:hover .vc-toolbox-icon {
color: var(--interactive-hover);
}
diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx
index 00805fbd..c59df00a 100644
--- a/src/plugins/vencordToolbox/index.tsx
+++ b/src/plugins/vencordToolbox/index.tsx
@@ -23,11 +23,11 @@ import { Settings, useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
-import { findExportedComponentLazy } from "@webpack";
+import { findComponentByCodeLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react";
-const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
+const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
function VencordPopout(onClose: () => void) {
const { useQuickCss } = useSettings(["useQuickCss"]);
@@ -88,7 +88,7 @@ function VencordPopout(onClose: () => void) {
function VencordPopoutIcon(isShown: boolean) {
return (
-
+
);
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index c53116b4..afd9d48c 100644
--- a/src/plugins/viewIcons/index.tsx
+++ b/src/plugins/viewIcons/index.tsx
@@ -193,10 +193,18 @@ export default definePlugin({
// Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp
{
find: ".overlay:void 0,status:",
- replacement: {
- match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
- replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},"
- },
+ replacement: [
+ {
+ // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
+ match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
+ replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",
+ noWarn: true
+ },
+ {
+ match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",.{0,100}className:\i,/,
+ replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",
+ }
+ ],
all: true
},
// Banners
@@ -220,16 +228,16 @@ export default definePlugin({
{
find: ".cursorPointer:null,children",
replacement: {
- match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
- replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})`
+ match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
+ replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`
}
},
// User Dms top large icon
{
find: 'experimentLocation:"empty_messages"',
replacement: {
- match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
- replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})`
+ match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
+ replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
}
}
]
diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx
index b45919a2..ddcbd3b4 100644
--- a/src/plugins/viewRaw/index.tsx
+++ b/src/plugins/viewRaw/index.tsx
@@ -22,12 +22,12 @@ import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
-import { getIntlMessage } from "@utils/discord";
+import { getCurrentGuild, getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
-import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
+import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general";
@@ -118,7 +118,7 @@ const settings = definePluginSettings({
}
});
-function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
+function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => {
const value = props[name.toLowerCase()];
if (!value) return;
@@ -144,6 +144,23 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenu
};
}
+const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {
+ const guild = getCurrentGuild();
+ if (!guild) return;
+
+ const role = GuildStore.getRole(guild.id, id);
+ if (!role) return;
+
+ children.push(
+ openViewRawModal(JSON.stringify(role, null, 4), "Role")}
+ icon={CopyIcon}
+ />
+ );
+};
+
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild",
@@ -152,10 +169,12 @@ export default definePlugin({
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
+ "guild-settings-role-context": MakeContextCallback("Role"),
"channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"),
"gdm-context": MakeContextCallback("Channel"),
- "user-context": MakeContextCallback("User")
+ "user-context": MakeContextCallback("User"),
+ "dev-context": devContextCallback
},
renderMessagePopoverButton(msg) {
diff --git a/src/plugins/voiceMessages/styles.css b/src/plugins/voiceMessages/styles.css
index 1e2b1433..4f2e1d57 100644
--- a/src/plugins/voiceMessages/styles.css
+++ b/src/plugins/voiceMessages/styles.css
@@ -9,10 +9,6 @@
margin-bottom: 1em;
}
-.vc-vmsg-modal audio {
- width: 100%;
-}
-
.vc-vmsg-preview {
color: var(--text-normal);
border-radius: 24px;
diff --git a/src/plugins/volumeBooster/index.ts b/src/plugins/volumeBooster/index.ts
index c9a08bbc..62d2a2c1 100644
--- a/src/plugins/volumeBooster/index.ts
+++ b/src/plugins/volumeBooster/index.ts
@@ -136,7 +136,7 @@ export default definePlugin({
// @ts-expect-error
if (data.sinkId != null && data.sinkId !== data.audioContext.sinkId && "setSinkId" in AudioContext.prototype) {
// @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId
- data.audioContext.setSinkId(data.sinkId);
+ data.audioContext.setSinkId(data.sinkId === "default" ? "" : data.sinkId);
}
data.gainNode.gain.value = data._mute
diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts
index 66123805..07eb4a3e 100644
--- a/src/plugins/webContextMenus.web/index.ts
+++ b/src/plugins/webContextMenus.web/index.ts
@@ -20,10 +20,13 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web";
-import { findByPropsLazy } from "@webpack";
+import { filters, mapMangledModuleLazy } from "@webpack";
import { Clipboard, ComponentDispatch } from "@webpack/common";
-const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
+const ctxMenuCallbacks = mapMangledModuleLazy('.tagName)==="TEXTAREA"||', {
+ contextMenuCallbackWeb: filters.byCode('.tagName)==="INPUT"||'),
+ contextMenuCallbackNative: filters.byCode('.tagName)==="TEXTAREA"||')
+});
async function fetchImage(url: string) {
const res = await fetch(url);
@@ -39,13 +42,16 @@ const settings = definePluginSettings({
addBack: {
type: OptionType.BOOLEAN,
description: "Add back the Discord context menus for images, links and the chat input bar",
+ default: false,
+ restartNeeded: true,
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,
- // so disable this by default. Vesktop just doesn't, so enable by default
- default: IS_VESKTOP,
- restartNeeded: true
+ // so disable this by default. Vesktop just doesn't, so we force enable it there
+ hidden: IS_VESKTOP,
}
});
+const shouldAddBackMenus = () => IS_VESKTOP || settings.store.addBack;
+
const MEDIA_PROXY_URL = "https://media.discordapp.net";
const CDN_URL = "cdn.discordapp.com";
@@ -78,7 +84,7 @@ export default definePlugin({
settings,
start() {
- if (settings.store.addBack) {
+ if (shouldAddBackMenus()) {
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
this.changedListeners = true;
@@ -141,7 +147,7 @@ export default definePlugin({
{
find: 'navId:"image-context"',
all: true,
- predicate: () => settings.store.addBack,
+ predicate: shouldAddBackMenus,
replacement: {
// return IS_DESKTOP ? React.createElement(Menu, ...)
match: /return \i\.\i(?=\?|&&)/,
@@ -152,7 +158,7 @@ export default definePlugin({
// Add back link context menu
{
find: '"interactionUsernameProfile"',
- predicate: () => settings.store.addBack,
+ predicate: shouldAddBackMenus,
replacement: {
match: /if\((?="A"===\i\.tagName&&""!==\i\.textContent)/,
replace: "if(false&&"
@@ -162,7 +168,7 @@ export default definePlugin({
// Add back slate / text input context menu
{
find: 'getElementById("slate-toolbar"',
- predicate: () => settings.store.addBack,
+ predicate: shouldAddBackMenus,
replacement: {
match: /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\)/,
replace: "||true)"
@@ -170,7 +176,7 @@ export default definePlugin({
},
{
find: ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{",
- predicate: () => settings.store.addBack,
+ predicate: shouldAddBackMenus,
replacement: [
{
// if (!IS_DESKTOP) return null;
@@ -186,7 +192,7 @@ export default definePlugin({
},
{
find: '"add-to-dictionary"',
- predicate: () => settings.store.addBack,
+ predicate: shouldAddBackMenus,
replacement: {
match: /let\{text:\i=""/,
replace: "return [null,null];$&"
diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx
index 803cc715..aea57fef 100644
--- a/src/plugins/whoReacted/index.tsx
+++ b/src/plugins/whoReacted/index.tsx
@@ -93,7 +93,7 @@ function makeRenderMoreUsers(users: User[]) {
};
}
-function handleClickAvatar(event: React.MouseEvent) {
+function handleClickAvatar(event: React.UIEvent) {
event.stopPropagation();
}
@@ -165,7 +165,7 @@ export default definePlugin({
-
+
{
win.webContents.on("frame-created", (_, { frame }) => {
- frame.once("dom-ready", () => {
+ frame?.once("dom-ready", () => {
if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;
if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) {
diff --git a/src/shared/SettingsStore.ts b/src/shared/SettingsStore.ts
index 25dd05b1..0b6aa25b 100644
--- a/src/shared/SettingsStore.ts
+++ b/src/shared/SettingsStore.ts
@@ -167,6 +167,8 @@ export class SettingsStore {
this.globalListeners.forEach(cb => cb(root, settingPathStr));
this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));
+ } else {
+ this.globalListeners.forEach(cb => cb(root, pathStr));
}
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts
index 22a38136..aec7292a 100644
--- a/src/utils/Logger.ts
+++ b/src/utils/Logger.ts
@@ -32,7 +32,7 @@ export class Logger {
constructor(public name: string, public color: string = "white") { }
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
- if (IS_REPORTER && IS_WEB) {
+ if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
console[level]("[Vencord]", this.name + ":", ...args);
return;
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index e7582591..65c73bd8 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -16,9 +16,15 @@
* along with this program. If not, see .
*/
-export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
+export const VENBOT_USER_ID = "1017176847865352332";
+export const VENCORD_GUILD_ID = "1015060230222131221";
+export const DONOR_ROLE_ID = "1042507929485586532";
+export const CONTRIB_ROLE_ID = "1026534353167208489";
+export const REGULAR_ROLE_ID = "1026504932959977532";
export const SUPPORT_CHANNEL_ID = "1026515880080842772";
+export const SUPPORT_CATEGORY_ID = "1108135649699180705";
+export const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
export interface Dev {
name: string;
@@ -579,6 +585,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n,
},
+ samsam: {
+ name: "samsam",
+ id: 836452332387565589n,
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts
index d8c361e8..6db180a4 100644
--- a/src/utils/dependencies.ts
+++ b/src/utils/dependencies.ts
@@ -69,8 +69,8 @@ export interface ApngFrameData {
// The below code is only used on the Desktop (electron) build of Vencord.
// Browser (extension) builds do not contain these remote imports.
-export const shikiWorkerSrc = `https://unpkg.com/@vap/shiki-worker@0.0.8/dist/${IS_DEV ? "index.js" : "index.min.js"}`;
-export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wasm";
+export const shikiWorkerSrc = `https://cdn.jsdelivr.net/npm/@vap/shiki-worker@0.0.8/dist/${IS_DEV ? "index.js" : "index.min.js"}`;
+export const shikiOnigasmSrc = "https://cdn.jsdelivr.net/npm/@vap/shiki@0.10.3/dist/onig.wasm";
// @ts-expect-error
-export const getStegCloak = /* #__PURE__*/ makeLazy(() => import("https://unpkg.com/stegcloak-dist@1.0.0/index.js"));
+export const getStegCloak = /* #__PURE__*/ makeLazy(() => import("https://cdn.jsdelivr.net/npm/stegcloak-dist@1.0.0/index.js"));
diff --git a/src/utils/discord.css b/src/utils/discord.css
index 12d15694..746fb564 100644
--- a/src/utils/discord.css
+++ b/src/utils/discord.css
@@ -17,7 +17,6 @@
@media(width <= 485px) {
.vc-image-modal {
- display: relative;
overflow: visible;
overflow: initial;
}
diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts
index e46e44ad..526c5514 100644
--- a/src/utils/lazy.ts
+++ b/src/utils/lazy.ts
@@ -20,9 +20,9 @@ export function makeLazy(factory: () => T, attempts = 5): () => T {
let tries = 0;
let cache: T;
return () => {
- if (!cache && attempts > tries++) {
+ if (cache === undefined && attempts > tries++) {
cache = factory();
- if (!cache && attempts === tries)
+ if (cache === undefined && attempts === tries)
console.error("Lazy factory failed:", factory);
}
return cache;
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index 28c371c5..adca15d3 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -100,6 +100,11 @@ export function pluralise(amount: number, singular: string, plural = singular +
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
}
+export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {
+ if (args.some(arg => arg == null)) return "";
+ return String.raw({ raw: strings }, ...args);
+}
+
export function tryOrElse(func: () => T, fallback: T): T {
try {
const res = func();
diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx
index 83b2f055..d06e5803 100644
--- a/src/utils/modal.tsx
+++ b/src/utils/modal.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { findByPropsLazy, findModuleId, proxyLazyWebpack, wreq } from "@webpack";
+import { filters, findModuleId, mapMangledModuleLazy, proxyLazyWebpack, wreq } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react";
@@ -49,7 +49,7 @@ export interface ModalOptions {
type RenderFunction = (props: ModalProps) => ReactNode | Promise;
-export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
+interface Modals {
ModalRoot: ComponentType;
-};
+}
+
+export const Modals: Modals = mapMangledModuleLazy(':"thin")', {
+ ModalRoot: filters.componentByCode('.MODAL,"aria-labelledby":'),
+ ModalHeader: filters.componentByCode(",id:"),
+ ModalContent: filters.componentByCode(".content,"),
+ ModalFooter: filters.componentByCode(".footer,"),
+ ModalCloseButton: filters.componentByCode(".close]:")
+});
+
+export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
+export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
+export const ModalContent = LazyComponent(() => Modals.ModalContent);
+export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
+export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export type MediaModalItem = {
url: string;
@@ -135,38 +149,33 @@ export const openMediaModal: (props: MediaModalProps) => void = proxyLazyWebpack
return Object.values(openMediaModalModule).find(v => String(v).includes("modalKey:"));
});
-export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
-export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
-export const ModalContent = LazyComponent(() => Modals.ModalContent);
-export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
-export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
+interface ModalAPI {
+ /**
+ * Wait for the render promise to resolve, then open a modal with it.
+ * This is equivalent to render().then(openModal)
+ * You should use the Modal components exported by this file
+ */
+ openModalLazy: (render: () => Promise, options?: ModalOptions & { contextKey?: string; }) => Promise;
+ /**
+ * Open a Modal with the given render function.
+ * You should use the Modal components exported by this file
+ */
+ openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string;
+ /**
+ * Close a modal by its key
+ */
+ closeModal: (modalKey: string, contextKey?: string) => void;
+ /**
+ * Close all open modals
+ */
+ closeAllModals: () => void;
+}
-export const ModalAPI = findByPropsLazy("openModalLazy");
-
-/**
- * Wait for the render promise to resolve, then open a modal with it.
- * This is equivalent to render().then(openModal)
- * You should use the Modal components exported by this file
- */
-export const openModalLazy: (render: () => Promise, options?: ModalOptions & { contextKey?: string; }) => Promise
- = proxyLazyWebpack(() => ModalAPI.openModalLazy);
-
-/**
- * Open a Modal with the given render function.
- * You should use the Modal components exported by this file
- */
-export const openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string
- = proxyLazyWebpack(() => ModalAPI.openModal);
-
-/**
- * Close a modal by its key
- */
-export const closeModal: (modalKey: string, contextKey?: string) => void
- = proxyLazyWebpack(() => ModalAPI.closeModal);
-
-/**
- * Close all open modals
- */
-export const closeAllModals: () => void
- = proxyLazyWebpack(() => ModalAPI.closeAllModals);
+export const ModalAPI: ModalAPI = mapMangledModuleLazy(".modalKey?", {
+ openModalLazy: filters.byCode(".modalKey?"),
+ openModal: filters.byCode(",instant:"),
+ closeModal: filters.byCode(".onCloseCallback()"),
+ closeAllModals: filters.byCode(".getState();for")
+});
+export const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
index 097c6456..b212e624 100644
--- a/src/utils/patches.ts
+++ b/src/utils/patches.ts
@@ -41,16 +41,17 @@ export function canonicalizeMatch(match: T): T {
}
const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`);
- return new RegExp(canonSource, match.flags) as T;
+ const canonRegex = new RegExp(canonSource, match.flags);
+ canonRegex.toString = match.toString.bind(match);
+
+ return canonRegex as T;
}
-export function canonicalizeReplace(replace: T, pluginName: string): T {
- const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
-
+export function canonicalizeReplace(replace: T, pluginPath: string): T {
if (typeof replace !== "function")
- return replace.replaceAll("$self", self) as T;
+ return replace.replaceAll("$self", pluginPath) as T;
- return ((...args) => replace(...args).replaceAll("$self", self)) as T;
+ return ((...args) => replace(...args).replaceAll("$self", pluginPath)) as T;
}
export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor, canonicalize: (value: T) => T) {
@@ -65,12 +66,12 @@ export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor
return descriptor;
}
-export function canonicalizeReplacement(replacement: Pick, plugin: string) {
+export function canonicalizeReplacement(replacement: Pick, pluginPath: string) {
const descriptors = Object.getOwnPropertyDescriptors(replacement);
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
descriptors.replace = canonicalizeDescriptor(
descriptors.replace,
- replace => canonicalizeReplace(replace, plugin),
+ replace => canonicalizeReplace(replace, pluginPath),
);
Object.defineProperties(replacement, descriptors);
}
diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts
index 6a18948d..c1e11759 100644
--- a/src/utils/quickCss.ts
+++ b/src/utils/quickCss.ts
@@ -97,7 +97,14 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
- ThemeStore.addChangeListener(initThemes);
+
+ let currentTheme = ThemeStore.theme;
+ ThemeStore.addChangeListener(() => {
+ if (currentTheme === ThemeStore.theme) return;
+
+ currentTheme = ThemeStore.theme;
+ initThemes();
+ });
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);
diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts
index f19928ac..6ec3e527 100644
--- a/src/utils/settingsSync.ts
+++ b/src/utils/settingsSync.ts
@@ -60,7 +60,7 @@ export async function downloadSettingsBackup() {
}
}
-const toast = (type: number, message: string) =>
+const toast = (type: string, message: string) =>
Toasts.show({
type,
message,
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 54de59e3..3ad63ea1 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -26,7 +26,7 @@ import { MessageDecorationFactory } from "@api/MessageDecorations";
import { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
import { MessagePopoverButtonFactory } from "@api/MessagePopover";
import { FluxEvents } from "@webpack/types";
-import { JSX } from "react";
+import { ReactNode } from "react";
import { Promisable } from "type-fest";
// exists to export default definePlugin({...})
@@ -41,8 +41,17 @@ export interface PatchReplacement {
match: string | RegExp;
/** The replacement string or function which returns the string for the patch replacement */
replace: string | ReplaceFn;
- /** A function which returns whether this patch replacement should be applied */
+ /** Do not warn if this replacement did no changes */
+ noWarn?: boolean;
+ /**
+ * A function which returns whether this patch replacement should be applied.
+ * This is ran before patches are registered, so if this returns false, the patch will never be registered.
+ */
predicate?(): boolean;
+ /** The minimum build number for this patch to be applied */
+ fromBuild?: number;
+ /** The maximum build number for this patch to be applied */
+ toBuild?: number;
}
export interface Patch {
@@ -57,8 +66,15 @@ export interface Patch {
noWarn?: boolean;
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
group?: boolean;
- /** A function which returns whether this patch should be applied */
+ /**
+ * A function which returns whether this patch replacement should be applied.
+ * This is ran before patches are registered, so if this returns false, the patch will never be registered.
+ */
predicate?(): boolean;
+ /** The minimum build number for this patch to be applied */
+ fromBuild?: number;
+ /** The maximum build number for this patch to be applied */
+ toBuild?: number;
}
export interface PluginAuthor {
@@ -150,6 +166,11 @@ export interface PluginDef {
tags?: string[];
+ /**
+ * Managed style to automatically enable and disable when the plugin is enabled or disabled
+ */
+ managedStyle?: string;
+
userProfileBadge?: ProfileBadge;
onMessageClick?: MessageClickListener;
@@ -181,6 +202,10 @@ export const enum ReporterTestable {
FluxEvents = 1 << 4
}
+export function defineDefault(value: T) {
+ return value;
+}
+
export const enum OptionType {
STRING,
NUMBER,
@@ -198,15 +223,16 @@ export type SettingsChecks = {
(IsDisabled> & IsValid, DefinedSettings>);
};
-export type PluginSettingDef = (PluginSettingCustomDef & Pick) | ((
- | PluginSettingStringDef
- | PluginSettingNumberDef
- | PluginSettingBooleanDef
- | PluginSettingSelectDef
- | PluginSettingSliderDef
- | PluginSettingComponentDef
- | PluginSettingBigIntDef
-) & PluginSettingCommon);
+export type PluginSettingDef =
+ (PluginSettingCustomDef & Pick) |
+ (PluginSettingComponentDef & Omit) | ((
+ | PluginSettingStringDef
+ | PluginSettingNumberDef
+ | PluginSettingBooleanDef
+ | PluginSettingSelectDef
+ | PluginSettingSliderDef
+ | PluginSettingBigIntDef
+ ) & PluginSettingCommon);
export interface PluginSettingCommon {
description: string;
@@ -226,12 +252,14 @@ export interface PluginSettingCommon {
*/
target?: "WEB" | "DESKTOP" | "BOTH";
}
+
interface IsDisabled {
/**
* Checks if this setting should be disabled
*/
disabled?(this: D): boolean;
}
+
interface IsValid {
/**
* Prevents the user from saving settings if this is false or a string
@@ -310,7 +338,8 @@ export interface IPluginOptionComponentProps {
export interface PluginSettingComponentDef {
type: OptionType.COMPONENT;
- component: (props: IPluginOptionComponentProps) => JSX.Element;
+ component: (props: IPluginOptionComponentProps) => ReactNode | Promise;
+ default?: any;
}
/** Maps a `PluginSettingDef` to its value type */
@@ -320,7 +349,7 @@ type PluginSettingType = O extends PluginSettingStri
O extends PluginSettingBooleanDef ? boolean :
O extends PluginSettingSelectDef ? O["options"][number]["value"] :
O extends PluginSettingSliderDef ? number :
- O extends PluginSettingComponentDef ? any :
+ O extends PluginSettingComponentDef ? O extends { default: infer Default; } ? Default : any :
O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any :
never;
@@ -382,7 +411,7 @@ export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDe
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid;
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid;
-export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
+export type PluginOptionComponent = PluginSettingComponentDef & Omit;
export type PluginOptionCustom = PluginSettingCustomDef & Pick;
export type PluginNative any>> = {
diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts
index 11526c0f..a772c98d 100644
--- a/src/webpack/common/components.ts
+++ b/src/webpack/common/components.ts
@@ -16,76 +16,88 @@
* along with this program. If not, see .
*/
-import { filters, findByPropsLazy, waitFor } from "@webpack";
+import { LazyComponent } from "@utils/lazyReact";
+import { filters, mapMangledModuleLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal";
import * as t from "./types/components";
-export let Forms = {} as {
- FormTitle: t.FormTitle,
- FormSection: t.FormSection,
- FormDivider: t.FormDivider,
- FormText: t.FormText,
+
+const FormTitle = waitForComponent("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"'));
+const FormText = waitForComponent("FormText", filters.componentByCode(".SELECTABLE),", ".DISABLED:"));
+const FormSection = waitForComponent("FormSection", filters.componentByCode(".titleId)"));
+const FormDivider = waitForComponent("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/));
+
+export const Forms = {
+ FormTitle,
+ FormText,
+ FormSection,
+ FormDivider
};
-export let Icons = {} as t.Icons;
+export const Card = waitForComponent("Card", filters.componentByCode(".editable),", ".outline:"));
+export const Button = waitForComponent("Button", filters.componentByCode("#{intl::A11Y_LOADING_STARTED}))),!1"));
+export const Switch = waitForComponent("Switch", filters.componentByCode(".labelRow,ref:", ".disabledText"));
+export const Checkbox = waitForComponent("Checkbox", filters.componentByCode(".checkboxWrapperDisabled:"));
+
+const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
+ Tooltip: filters.componentByCode("this.renderTooltip()]"),
+ TooltipContainer: filters.componentByCode('="div"')
+}) as {
+ Tooltip: t.Tooltip,
+ TooltipContainer: t.TooltipContainer;
+};
+
+export const Tooltip = LazyComponent(() => Tooltips.Tooltip);
+export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer);
+
+export const TextInput = waitForComponent("TextInput", filters.componentByCode(".error]:this.hasError()"));
+export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:"));
+export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"'));
+export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:"));
+export const Select = waitForComponent("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"==='));
+export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:"));
+export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat('));
+export const Popout = waitForComponent