This commit is contained in:
Heli-o 2024-09-06 21:20:48 +02:00
commit c07d78fa96
54 changed files with 833 additions and 396 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.9.8", "version": "1.10.1",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -70,6 +70,7 @@
"stylelint": "^16.8.1", "stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1", "ts-patch": "^3.2.1",
"ts-pattern": "^5.3.1",
"tsx": "^4.16.5", "tsx": "^4.16.5",
"type-fest": "^4.23.0", "type-fest": "^4.23.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",

View file

@ -116,6 +116,9 @@ importers:
ts-patch: ts-patch:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.2.1 version: 3.2.1
ts-pattern:
specifier: ^5.3.1
version: 5.3.1
tsx: tsx:
specifier: ^4.16.5 specifier: ^4.16.5
version: 4.16.5 version: 4.16.5
@ -2524,6 +2527,9 @@ packages:
resolution: {integrity: sha512-hlR43v+GUIUy8/ZGFP1DquEqPh7PFKQdDMTAmYt671kCCA6AkDQMoeFaFmZ7ObPLYOmpMgyKUqL1C+coFMf30w==} resolution: {integrity: sha512-hlR43v+GUIUy8/ZGFP1DquEqPh7PFKQdDMTAmYt671kCCA6AkDQMoeFaFmZ7ObPLYOmpMgyKUqL1C+coFMf30w==}
hasBin: true hasBin: true
ts-pattern@5.3.1:
resolution: {integrity: sha512-1RUMKa8jYQdNfmnK4jyzBK3/PS/tnjcZ1CW0v1vWDeYe5RBklc/nquw03MEoB66hVBm4BnlCfmOqDVxHyT1DpA==}
tsconfig-paths@3.15.0: tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@ -5158,6 +5164,8 @@ snapshots:
semver: 7.6.3 semver: 7.6.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
ts-pattern@5.3.1: {}
tsconfig-paths@3.15.0: tsconfig-paths@3.15.0:
dependencies: dependencies:
'@types/json5': 0.0.29 '@types/json5': 0.0.29

View file

@ -334,5 +334,6 @@ export const commonRendererPlugins = [
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"), banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"), banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"), banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins ...commonOpts.plugins
]; ];

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers"; import { sendBotMessage } from "./commandHelpers";
@ -46,10 +47,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) { export const _init = function (cmds: Command[]) {
try { try {
BUILT_IN = cmds; BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0]; OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0]; RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
} catch (e) { } catch (e) {
console.error("Failed to load CommandsApi"); new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
} }
return cmds; return cmds;
} as never; } as never;
@ -138,6 +139,8 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`); throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true; command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`; command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN; command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT; command.type ??= ApplicationCommandType.CHAT_INPUT;

View file

@ -93,8 +93,10 @@ export interface Command {
isVencordCommand?: boolean; isVencordCommand?: boolean;
name: string; name: string;
untranslatedName?: string;
displayName?: string; displayName?: string;
description: string; description: string;
untranslatedDescription?: string;
displayDescription?: string; displayDescription?: string;
options?: Option[]; options?: Option[];

View file

@ -65,8 +65,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
} }
/** /**
* Discord's copy icon, as seen in the user popout right of the username when clicking * Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
* your own username in the bottom left user panel
*/ */
export function CopyIcon(props: IconProps) { export function CopyIcon(props: IconProps) {
return ( return (
@ -76,8 +75,9 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="currentColor"> <g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" /> <path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" /> <path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
</g> </g>
</Icon> </Icon>
); );

View file

@ -382,6 +382,7 @@ function PatchHelper() {
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} /> <CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button> <Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</> </>
)} )}
</SettingsTab> </SettingsTab>

View file

@ -77,8 +77,16 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle> <Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText> <Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div> <div>
{themeLinks.map(link => ( {themeLinks.map(rawLink => {
<Card style={{ const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
padding: ".5em", padding: ".5em",
marginBottom: ".5em", marginBottom: ".5em",
marginTop: ".5em" marginTop: ".5em"
@ -86,11 +94,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{ <Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word" overflowWrap: "break-word"
}}> }}>
{link} {label}
</Forms.FormTitle> </Forms.FormTitle>
<Validator link={link} /> <Validator link={link} />
</Card> </Card>;
))} })}
</div> </div>
</> </>
); );
@ -296,6 +304,7 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable"> <Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText> <Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card> </Card>

View file

@ -1,3 +0,0 @@
[class*="profileBadges"] {
flex: none;
}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./fixBadgeOverflow.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -79,7 +77,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??" replace: "...$1.props,$& $1.image??"
}, },
{ {
match: /(?<=text:(\i)\.description,.{0,50})children:/, match: /(?<=text:(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :" replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
}, },
// conditionally override their onClick with badge.onClick if it exists // conditionally override their onClick with badge.onClick if it exists

View file

@ -34,7 +34,7 @@ export default definePlugin({
{ {
find: "Messages.SERVERS,children", find: "Messages.SERVERS,children",
replacement: { replacement: {
match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/, match: /(?<=Messages\.SERVERS,children:)\i\.map\(\i\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)" replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
} }
} }

View file

@ -64,7 +64,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}` replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}, },
{ {
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/, match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
} }
] ]

View file

@ -132,8 +132,8 @@ export default definePlugin({
}, },
// Export the isBetterFolders variable to the folders component // Export the isBetterFolders variable to the folders component
{ {
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/, match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,' replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
} }
] ]
}, },

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, useStateFromStores } from "@webpack/common"; import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -36,7 +36,6 @@ function setTheme(theme: string) {
saveClientTheme({ theme }); saveClientTheme({ theme });
} }
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() { function ThemeSettings() {

View file

@ -0,0 +1,5 @@
# CopyFileContents
Adds a button to text file attachments to copy their contents.
![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca)

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { CopyIcon, NoEntrySignIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin from "@utils/types";
import { Tooltip, useState } from "@webpack/common";
const CheckMarkIcon = () => {
return <svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z"></path>
</svg>;
};
export default definePlugin({
name: "CopyFileContents",
description: "Adds a button to text file attachments to copy their contents",
authors: [Devs.Obsidian, Devs.Nuckyz],
patches: [
{
find: ".Messages.PREVIEW_BYTES_LEFT.format(",
replacement: {
match: /\.footerGap.+?url:\i,fileName:\i,fileSize:\i}\),(?<=fileContents:(\i),bytesLeft:(\i).+?)/g,
replace: "$&$self.addCopyButton({fileContents:$1,bytesLeft:$2}),"
}
}
],
addCopyButton: ErrorBoundary.wrap(({ fileContents, bytesLeft }: { fileContents: string, bytesLeft: number; }) => {
const [recentlyCopied, setRecentlyCopied] = useState(false);
return (
<Tooltip text={recentlyCopied ? "Copied!" : bytesLeft > 0 ? "File too large to copy" : "Copy File Contents"}>
{tooltipProps => (
<div
{...tooltipProps}
className="vc-cfc-button"
role="button"
onClick={() => {
if (!recentlyCopied && bytesLeft <= 0) {
copyWithToast(fileContents);
setRecentlyCopied(true);
setTimeout(() => setRecentlyCopied(false), 2000);
}
}}
>
{recentlyCopied ? <CheckMarkIcon /> : bytesLeft > 0 ? <NoEntrySignIcon color="var(--channel-icon)" /> : <CopyIcon />}
</div>
)}
</Tooltip>
);
}, { noop: true }),
});

View file

@ -0,0 +1,8 @@
.vc-cfc-button {
color: var(--interactive-normal);
cursor: pointer;
}
.vc-cfc-button:hover {
color: var(--interactive-hover);
}

View file

@ -46,7 +46,7 @@ const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) { async function embedDidMount(this: Component<Props>) {
try { try {
const { embed } = this.props; const { embed } = this.props;
const { replaceElements } = settings.store; const { replaceElements, dearrowByDefault } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return; if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
@ -63,18 +63,22 @@ async function embedDidMount(this: Component<Props>) {
if (!hasTitle && !hasThumb) return; if (!hasTitle && !hasThumb) return;
embed.dearrow = { embed.dearrow = {
enabled: true enabled: dearrowByDefault
}; };
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) { if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle; const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
embed.rawTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
}
embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;
if (dearrowByDefault) embed.rawTitle = replacementTitle;
}
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) { if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL; const replacementProxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;
if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;
} }
this.forceUpdate(); this.forceUpdate();
@ -96,6 +100,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")} className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
onClick={() => { onClick={() => {
const { enabled, oldThumb, oldTitle } = embed.dearrow; const { enabled, oldThumb, oldTitle } = embed.dearrow;
settings.store.dearrowByDefault = !enabled;
embed.dearrow.enabled = !enabled; embed.dearrow.enabled = !enabled;
if (oldTitle) { if (oldTitle) {
embed.dearrow.oldTitle = embed.rawTitle; embed.dearrow.oldTitle = embed.rawTitle;
@ -153,6 +158,12 @@ const settings = definePluginSettings({
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly }, { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly }, { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
], ],
},
dearrowByDefault: {
description: "Dearrow videos automatically",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
} }
}); });

View file

@ -7,16 +7,15 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Heading, RelationshipStore, Text } from "@webpack/common"; import { RelationshipStore, Text } from "@webpack/common";
const containerWrapper = findByPropsLazy("memberSinceWrapper"); const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince"); const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"'); const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale"); const locale = findByPropsLazy("getLocale");
const section = findLazy((m: any) => m.section !== void 0 && m.heading !== void 0 && Object.values(m).length === 2); const Section = findComponentByCodeLazy('"auto":"smooth"', ".section");
export default definePlugin({ export default definePlugin({
name: "FriendsSince", name: "FriendsSince",
@ -28,7 +27,7 @@ export default definePlugin({
find: ".PANEL}),nicknameIcons", find: ".PANEL}),nicknameIcons",
replacement: { replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/, match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:true})" replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})"
} }
}, },
// User Profile Modal // User Profile Modal
@ -36,34 +35,19 @@ export default definePlugin({
find: "action:\"PRESS_APP_CONNECTION\"", find: "action:\"PRESS_APP_CONNECTION\"",
replacement: { replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/, match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:false})," replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
} }
} }
], ],
getFriendSince(userId: string) { FriendsSinceComponent: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
try {
if (!RelationshipStore.isFriend(userId)) return null;
return RelationshipStore.getSince(userId);
} catch (err) {
new Logger("FriendsSince").error(err);
return null;
}
},
friendsSinceNew: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
if (!RelationshipStore.isFriend(userId)) return null; if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId); const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null; if (!friendsSince) return null;
return ( return (
<section className={section.section}> <Section heading="Friends Since">
<Heading variant="text-xs/semibold" style={isSidebar ? {} : { color: "var(--header-secondary)" }}>
Friends Since
</Heading>
{ {
isSidebar ? ( isSidebar ? (
<Text variant="text-sm/normal"> <Text variant="text-sm/normal">
@ -91,8 +75,7 @@ export default definePlugin({
</div> </div>
) )
} }
</Section>
</section>
); );
}, { noop: true }), }, { noop: true }),
}); });

View file

@ -0,0 +1,13 @@
# IgnoreActivities
Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings.
![](https://github.com/user-attachments/assets/f0c19060-0ecf-4f1c-8165-a5aa40143c82)
![](https://github.com/user-attachments/assets/73c3fa7a-5b90-41ee-a4d6-91fa76458b74)
![](https://github.com/user-attachments/assets/1ab3fe73-3911-48d1-8a08-e976af614b41)
The activity stays showing as a detected game even if ignored, differently from the stock Toggle Detection button from Discord:
![](https://github.com/user-attachments/assets/08ea60c3-3a31-42de-ae4c-7535fbf1b45a)

View file

@ -26,6 +26,11 @@ interface IgnoredActivity {
type: ActivitiesTypes; type: ActivitiesTypes;
} }
const enum FilterMode {
Whitelist,
Blacklist
}
const RunningGameStore = findStoreLazy("RunningGameStore"); const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!;
@ -70,14 +75,17 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity); if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex); else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation recalculateActivities();
}
function recalculateActivities() {
ShowCurrentGame.updateSetting(old => old); ShowCurrentGame.updateSetting(old => old);
} }
function ImportCustomRPCComponent() { function ImportCustomRPCComponent() {
return ( return (
<Flex flexDirection="column"> <Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText> <Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
<div> <div>
<Button <Button
onClick={() => { onClick={() => {
@ -86,7 +94,7 @@ function ImportCustomRPCComponent() {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE); return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
} }
const isAlreadyAdded = allowedIdsPushID?.(id); const isAlreadyAdded = idsListPushID?.(id);
if (isAlreadyAdded) { if (isAlreadyAdded) {
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE); showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
} }
@ -99,39 +107,39 @@ function ImportCustomRPCComponent() {
); );
} }
let allowedIdsPushID: ((id: string) => boolean) | null = null; let idsListPushID: ((id: string) => boolean) | null = null;
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) { function IdsListComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? ""); const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? "");
allowedIdsPushID = (id: string) => { idsListPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean)); const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false); const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", "); const ids = Array.from(currentIds).join(", ");
setAllowedIds(ids); setIdsList(ids);
props.setValue(ids); props.setValue(ids);
return isAlreadyAdded; return isAlreadyAdded;
}; };
useEffect(() => () => { useEffect(() => () => {
allowedIdsPushID = null; idsListPushID = null;
}, []); }, []);
function handleChange(newValue: string) { function handleChange(newValue: string) {
setAllowedIds(newValue); setIdsList(newValue);
props.setValue(newValue); props.setValue(newValue);
} }
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle> <Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText> <Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<TextInput <TextInput
type="text" type="text"
value={allowedIds} value={idsList}
onChange={handleChange} onChange={handleChange}
placeholder="235834946571337729, 343383572805058560" placeholder="235834946571337729, 343383572805058560"
/> />
@ -145,40 +153,62 @@ const settings = definePluginSettings({
description: "", description: "",
component: () => <ImportCustomRPCComponent /> component: () => <ImportCustomRPCComponent />
}, },
allowedIds: { listMode: {
type: OptionType.SELECT,
description: "Change the mode of the filter list",
options: [
{
label: "Whitelist",
value: FilterMode.Whitelist,
default: true
},
{
label: "Blacklist",
value: FilterMode.Blacklist,
}
],
onChange: recalculateActivities
},
idsList: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "", description: "",
default: "", default: "",
onChange(newValue: string) { onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean)); const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.allowedIds = Array.from(ids).join(", "); settings.store.idsList = Array.from(ids).join(", ");
recalculateActivities();
}, },
component: props => <AllowedIdsComponent setValue={props.setValue} /> component: props => <IdsListComponent setValue={props.setValue} />
}, },
ignorePlaying: { ignorePlaying: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)", description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false default: false,
onChange: recalculateActivities
}, },
ignoreStreaming: { ignoreStreaming: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore all streaming activities", description: "Ignore all streaming activities",
default: false default: false,
onChange: recalculateActivities
}, },
ignoreListening: { ignoreListening: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)", description: "Ignore all listening activities (These are usually spotify activities)",
default: false default: false,
onChange: recalculateActivities
}, },
ignoreWatching: { ignoreWatching: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore all watching activities", description: "Ignore all watching activities",
default: false default: false,
onChange: recalculateActivities
}, },
ignoreCompeting: { ignoreCompeting: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)", description: "Ignore all competing activities (These are normally special game activities)",
default: false default: false,
onChange: recalculateActivities
} }
}).withPrivateSettings<{ }).withPrivateSettings<{
ignoredActivities: IgnoredActivity[]; ignoredActivities: IgnoredActivity[];
@ -189,8 +219,8 @@ function getIgnoredActivities() {
} }
function isActivityTypeIgnored(type: number, id?: string) { function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.allowedIds.includes(id)) { if (id && settings.store.idsList.includes(id)) {
return false; return settings.store.listMode === FilterMode.Blacklist;
} }
switch (type) { switch (type) {
@ -206,8 +236,8 @@ function isActivityTypeIgnored(type: number, id?: string) {
export default definePlugin({ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz, Devs.Kylie],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below",
dependencies: ["UserSettingsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
@ -236,6 +266,7 @@ export default definePlugin({
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
} }
}, },
// Discord has 3 different components for activities. Currently, the last is the one being used
{ {
find: ".activityTitleText,variant", find: ".activityTitleText,variant",
replacement: { replacement: {
@ -249,10 +280,23 @@ export default definePlugin({
match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/, match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
} }
},
{
find: ".promotedLabelWrapperNonBanner,children",
replacement: {
match: /\.appDetailsHeaderContainer.+?children:\i.*?}\),(?<=application:(\i).+?)/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}
} }
], ],
async start() { async start() {
// Migrate allowedIds
if (Settings.plugins.IgnoreActivities.allowedIds) {
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities"); const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) { if (oldIgnoredActivitiesData != null) {
@ -279,7 +323,7 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false; if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) { if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id); return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
} else { } else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) { if (exePath) {

View file

@ -14,7 +14,7 @@ import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) { export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id; const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
const totalCount = useStateFromStores( const totalCount = useStateFromStores(
[GuildMemberCountStore], [GuildMemberCountStore],
@ -33,7 +33,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
const threadGroups = useStateFromStores( const threadGroups = useStateFromStores(
[ThreadMemberListStore], [ThreadMemberListStore],
() => ThreadMemberListStore.getMemberListSections(currentChannel.id) () => ThreadMemberListStore.getMemberListSections(currentChannel?.id)
); );
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) { if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {

View file

@ -15,8 +15,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
const onlineMemberMap = new Map<string, number>(); const onlineMemberMap = new Map<string, number>();
class OnlineMemberCountStore extends Flux.Store { class OnlineMemberCountStore extends Flux.Store {
getCount(guildId: string) { getCount(guildId?: string) {
return onlineMemberMap.get(guildId); return onlineMemberMap.get(guildId!);
} }
async _ensureCount(guildId: string) { async _ensureCount(guildId: string) {
@ -25,8 +25,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id); await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
} }
ensureCount(guildId: string) { ensureCount(guildId?: string) {
if (onlineMemberMap.has(guildId)) return; if (!guildId || onlineMemberMap.has(guildId)) return;
preloadQueue.push(() => preloadQueue.push(() =>
this._ensureCount(guildId) this._ensureCount(guildId)

View file

@ -28,12 +28,12 @@ import { FluxStore } from "@webpack/types";
import { MemberCount } from "./MemberCount"; import { MemberCount } from "./MemberCount";
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId?: string): number | null; };
export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; getProps(guildId?: string, channelId?: string): { groups: { count: number; id: string; }[]; };
}; };
export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as FluxStore & { export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as FluxStore & {
getMemberListSections(channelId: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; }; getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
}; };

View file

@ -9,3 +9,8 @@
.vc-mentionAvatars-role-icon { .vc-mentionAvatars-role-icon {
margin: 0 2px 0.2rem 4px; margin: 0 2px 0.2rem 4px;
} }
/** don't display inside the ServerInfo modal owner mention */
.vc-gp-owner .vc-mentionAvatars-icon {
display: none;
}

View file

@ -20,7 +20,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common"; import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general"; import { Channel, User } from "discord-types/general";
@ -28,6 +28,7 @@ const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const UserUtils = findByPropsLazy("getGlobalName"); const UserUtils = findByPropsLazy("getGlobalName");
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const ExpandableList = findComponentByCodeLazy(".mutualFriendItem]");
const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon"); const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
function getGroupDMName(channel: Channel) { function getGroupDMName(channel: Channel) {
@ -50,6 +51,29 @@ function getMutualGDMCountText(user: User) {
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`; return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
} }
function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
return mutualDms.map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(c.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));
}
const IS_PATCHED = Symbol("MutualGroupDMs.Patched"); const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({ export default definePlugin({
@ -70,6 +94,13 @@ export default definePlugin({
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&" replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
} }
] ]
},
{
find: 'section:"MUTUAL_FRIENDS"',
replacement: {
match: /\.openUserProfileModal.+?\)}\)}\)(?<=(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
replace: "$&,$self.renderDMPageList({user: arguments[0].user, Divider: $1, listStyle: $2.list})"
}
} }
], ],
@ -84,28 +115,9 @@ export default definePlugin({
}, },
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const mutualDms = useMemo(() => getMutualGroupDms(user.id), [user.id]); const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = mutualDms.map(c => ( const entries = renderClickableGDMs(mutualGDms, onClose);
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(c.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));
return ( return (
<ScrollerThin <ScrollerThin
@ -124,5 +136,24 @@ export default definePlugin({
} }
</ScrollerThin> </ScrollerThin>
); );
}),
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: User, Divider: JSX.Element, listStyle: string; }) => {
const mutualGDms = getMutualGroupDms(user.id);
if (mutualGDms.length === 0) return null;
const header = getMutualGDMCountText(user);
return (
<>
{Divider}
<ExpandableList
className={listStyle}
header={header}
isLoadingHeader={false}
children={renderClickableGDMs(mutualGDms, () => { })}
/>
</>
);
}) })
}); });

View file

@ -36,7 +36,7 @@ export default definePlugin({
} }
], ],
shouldSkip(guildId: string, emoji: any) { shouldSkip(guildId: string, emoji: any) {
if (emoji.type !== "GUILD_EMOJI") { if (emoji.type !== 1) {
return false; return false;
} }
if (settings.store.shownEmojis === "onlyUnicode") { if (settings.store.shownEmojis === "onlyUnicode") {

View file

@ -33,7 +33,7 @@ interface URLReplacementRule {
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant // Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = { const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: { spotify: {
match: /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/, match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`, replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app", description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/, shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,

View file

@ -21,8 +21,10 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord"; import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import { findByCodeLazy } from "@webpack";
import type { Guild } from "discord-types/general"; import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
import { settings } from ".."; import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils"; import { cl, getPermissionDescription, getPermissionString } from "../utils";
@ -42,15 +44,15 @@ export interface RoleOrUserPermission {
overwriteDeny?: bigint; overwriteDeny?: bigint;
} }
function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) { type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; };
return openModal(modalProps => ( const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji");
<RolesAndUsersPermissions
modalProps={modalProps} function getRoleIconSrc(role: Role) {
permissions={permissions} const icon = getRoleIconData(role, 20);
guild={guild} if (!icon) return;
header={header}
/> const { customIconSrc, unicodeEmoji } = icon;
)); return customIconSrc ?? unicodeEmoji?.url;
} }
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) { function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {
@ -86,31 +88,34 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
size={ModalSize.LARGE} size={ModalSize.LARGE}
> >
<ModalHeader> <ModalHeader>
<Text className={cl("perms-title")} variant="heading-lg/semibold">{header} permissions:</Text> <Text className={cl("modal-title")} variant="heading-lg/semibold">{header} permissions:</Text>
<ModalCloseButton onClick={modalProps.onClose} /> <ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent className={cl("modal-content")}>
{!selectedItem && ( {!selectedItem && (
<div className={cl("perms-no-perms")}> <div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text> <Text variant="heading-lg/normal">No permissions to display!</Text>
</div> </div>
)} )}
{selectedItem && ( {selectedItem && (
<div className={cl("perms-container")}> <div className={cl("modal-container")}>
<div className={cl("perms-list")}> <ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => { {permissions.map((permission, index) => {
const user = UserStore.getUser(permission.id ?? ""); const user: User | undefined = UserStore.getUser(permission.id ?? "");
const role = roles[permission.id ?? ""]; const role: Role | undefined = roles[permission.id ?? ""];
const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined;
return ( return (
<button <div
className={cl("perms-list-item-btn")} className={cl("modal-list-item-btn")}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
role="button"
tabIndex={0}
> >
<div <div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} className={cl("modal-list-item", { "modal-list-item-active": selectedItemIndex === index })}
onContextMenu={e => { onContextMenu={e => {
if (permission.type === PermissionType.Role) if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => ( ContextMenuApi.openContextMenu(e, () => (
@ -124,7 +129,6 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
ContextMenuApi.openContextMenu(e, () => ( ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu <UserContextMenu
userId={permission.id!} userId={permission.id!}
onClose={modalProps.onClose}
/> />
)); ));
} }
@ -132,13 +136,19 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
> >
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
<span <span
className={cl("perms-role-circle")} className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }} style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/> />
)} )}
{permission.type === PermissionType.User && user !== undefined && ( {permission.type === PermissionType.Role && roleIconSrc != null && (
<img <img
className={cl("perms-user-img")} className={cl("modal-role-image")}
src={roleIconSrc}
/>
)}
{permission.type === PermissionType.User && user != null && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL(void 0, void 0, false)} src={user.getAvatarURL(void 0, void 0, false)}
/> />
)} )}
@ -147,28 +157,25 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
permission.type === PermissionType.Role permission.type === PermissionType.Role
? role?.name ?? "Unknown Role" ? role?.name ?? "Unknown Role"
: permission.type === PermissionType.User : permission.type === PermissionType.User
? (user && getUniqueUsername(user)) ?? "Unknown User" ? (user != null && getUniqueUsername(user)) ?? "Unknown User"
: ( : (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}> <Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner @owner
<OwnerCrownIcon <OwnerCrownIcon height={18} width={18} aria-hidden="true" />
height={18}
width={18}
aria-hidden="true"
/>
</Flex> </Flex>
) )
} }
</Text> </Text>
</div> </div>
</button> </div>
); );
})} })}
</div> </ScrollerThin>
<div className={cl("perms-perms")}> <div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.entries(PermissionsBits).map(([permissionName, bit]) => ( {Object.entries(PermissionsBits).map(([permissionName, bit]) => (
<div className={cl("perms-perms-item")}> <div className={cl("modal-perms-item")}>
<div className={cl("perms-perms-item-icon")}> <div className={cl("modal-perms-item-icon")}>
{(() => { {(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem; const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
@ -192,7 +199,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</Tooltip> </Tooltip>
</div> </div>
))} ))}
</div> </ScrollerThin>
</div> </div>
)} )}
</ModalContent> </ModalContent>
@ -208,7 +215,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options" aria-label="Role Options"
> >
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-role-id" id={cl("copy-role-id")}
label={i18n.Messages.COPY_ID_ROLE} label={i18n.Messages.COPY_ID_ROLE}
action={() => { action={() => {
Clipboard.copy(roleId); Clipboard.copy(roleId);
@ -217,14 +224,13 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
{(settings.store as any).unsafeViewAsRole && ( {(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem <Menu.MenuItem
id="vc-pw-view-as-role" id={cl("view-as-role")}
label={i18n.Messages.VIEW_AS_ROLE} label={i18n.Messages.VIEW_AS_ROLE}
action={() => { action={() => {
const role = GuildStore.getRole(guild.id, roleId); const role = GuildStore.getRole(guild.id, roleId);
if (!role) return; if (!role) return;
onClose(); onClose();
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE", type: "IMPERSONATE_UPDATE",
guildId: guild.id, guildId: guild.id,
@ -235,15 +241,14 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
} }
} }
}); });
} }}
}
/> />
)} )}
</Menu.Menu> </Menu.Menu>
); );
} }
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) { function UserContextMenu({ userId }: { userId: string; }) {
return ( return (
<Menu.Menu <Menu.Menu
navId={cl("user-context-menu")} navId={cl("user-context-menu")}
@ -251,7 +256,7 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
aria-label="User Options" aria-label="User Options"
> >
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-user-id" id={cl("copy-user-id")}
label={i18n.Messages.COPY_ID_USER} label={i18n.Messages.COPY_ID_USER}
action={() => { action={() => {
Clipboard.copy(userId); Clipboard.copy(userId);
@ -263,4 +268,13 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent); const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);
export default openRolesAndUsersPermissionsModal; export default function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
}

View file

@ -29,6 +29,7 @@ import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermi
interface UserPermission { interface UserPermission {
permission: string; permission: string;
roleName: string;
roleColor: string; roleColor: string;
rolePosition: number; rolePosition: number;
} }
@ -45,8 +46,48 @@ const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(()
return { RoleRootClasses, RoleClasses, RoleBorderClasses }; return { RoleRootClasses, RoleClasses, RoleBorderClasses };
}); });
interface FakeRoleProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
color: string;
}
function FakeRole({ text, color, ...props }: FakeRoleProps) {
return (
<div {...props} className={classes(RoleClasses.role)}>
<div className={RoleClasses.roleRemoveButton}>
<span
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)}
style={{ backgroundColor: color }}
/>
</div>
<div className={RoleClasses.roleName}>
<Text
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
>
{text}
</Text>
</div>
</div>
);
}
interface GrantedByTooltipProps {
roleName: string;
roleColor: string;
}
function GrantedByTooltip({ roleName, roleColor }: GrantedByTooltipProps) {
return (
<>
<Text variant="text-sm/medium">Granted By</Text>
<FakeRole text={roleName} color={roleColor} />
</>
);
}
function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { guild: Guild; guildMember: GuildMember; forceOpen?: boolean; }) { function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { guild: Guild; guildMember: GuildMember; forceOpen?: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]); const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => { const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermissions = []; const userPermissions: UserPermissions = [];
@ -67,6 +108,7 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner"; const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner";
userPermissions.push({ userPermissions.push({
permission: OWNER, permission: OWNER,
roleName: "Owner",
roleColor: "var(--primary-300)", roleColor: "var(--primary-300)",
rolePosition: Infinity rolePosition: Infinity
}); });
@ -75,10 +117,11 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
sortUserRoles(userRoles); sortUserRoles(userRoles);
for (const [permission, bit] of Object.entries(PermissionsBits)) { for (const [permission, bit] of Object.entries(PermissionsBits)) {
for (const { permissions, colorString, position } of userRoles) { for (const { permissions, colorString, position, name } of userRoles) {
if ((permissions & bit) === bit) { if ((permissions & bit) === bit) {
userPermissions.push({ userPermissions.push({
permission: getPermissionString(permission), permission: getPermissionString(permission),
roleName: name,
roleColor: colorString || "var(--primary-300)", roleColor: colorString || "var(--primary-300)",
rolePosition: position rolePosition: position
}); });
@ -91,7 +134,7 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
userPermissions.sort((a, b) => b.rolePosition - a.rolePosition); userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);
return [rolePermissions, userPermissions]; return [rolePermissions, userPermissions];
}, [stns.permissionsSortOrder]); }, [permissionsSortOrder]);
return ( return (
<ExpandableHeader <ExpandableHeader
@ -108,46 +151,41 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state} onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state}
defaultState={settings.store.defaultPermissionsDropdownState} defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[ buttons={[
(<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}> <Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => ( {tooltipProps => (
<button <div
{...tooltipProps} {...tooltipProps}
className={cl("userperms-sortorder-btn")} className={cl("user-sortorder-btn")}
role="button"
tabIndex={0}
onClick={() => { onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole; settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
}} }}
> >
<svg <svg
width="20" width="20"
height="20" height="20"
viewBox="0 96 960 960" viewBox="0 96 960 960"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"} transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
> >
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" /> <path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg> </svg>
</button> </div>
)} )}
</Tooltip>) </Tooltip>
]}> ]}>
{userPermissions.length > 0 && ( {userPermissions.length > 0 && (
<div className={classes(RoleRootClasses.root)}> <div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor }) => ( {userPermissions.map(({ permission, roleColor, roleName }) => (
<div className={classes(RoleClasses.role)}> <Tooltip
<div className={RoleClasses.roleRemoveButton}> text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
<span tooltipClassName={cl("granted-by-container")}
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)} tooltipContentClassName={cl("granted-by-content")}
style={{ backgroundColor: roleColor }}
/>
</div>
<div className={RoleClasses.roleName}>
<Text
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
> >
{permission} {tooltipProps => (
</Text> <FakeRole {...tooltipProps} text={permission} color={roleColor} />
</div> )}
</div> </Tooltip>
))} ))}
</div> </div>
)} )}

View file

@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common"; import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general"; import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
@ -54,12 +54,12 @@ export const settings = definePluginSettings({
options: [ options: [
{ label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true }, { label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true },
{ label: "Lowest Role", value: PermissionsSortOrder.LowestRole } { label: "Lowest Role", value: PermissionsSortOrder.LowestRole }
], ]
}, },
defaultPermissionsDropdownState: { defaultPermissionsDropdownState: {
description: "Whether the permissions dropdown on user popouts should be open by default", description: "Whether the permissions dropdown on user popouts should be open by default",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: false, default: false
} }
}); });
@ -73,14 +73,12 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
action={() => { action={() => {
const guild = GuildStore.getGuild(guildId); const guild = GuildStore.getGuild(guildId);
let permissions: RoleOrUserPermission[]; const { permissions, header } = match(type)
let header: string; .returnType<{ permissions: RoleOrUserPermission[], header: string; }>()
.with(MenuItemParentType.User, () => {
switch (type) {
case MenuItemParentType.User: {
const member = GuildMemberStore.getMember(guildId, id!); const member = GuildMemberStore.getMember(guildId, id!);
permissions = getSortedRoles(guild, member) const permissions: RoleOrUserPermission[] = getSortedRoles(guild, member)
.map(role => ({ .map(role => ({
type: PermissionType.Role, type: PermissionType.Role,
...role ...role
@ -93,37 +91,37 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
}); });
} }
header = member.nick ?? UserStore.getUser(member.userId).username; return {
permissions,
break; header: member.nick ?? UserStore.getUser(member.userId).username
} };
})
case MenuItemParentType.Channel: { .with(MenuItemParentType.Channel, () => {
const channel = ChannelStore.getChannel(id!); const channel = ChannelStore.getChannel(id!);
permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
type: type as PermissionType, type: type as PermissionType,
id, id,
overwriteAllow: allow, overwriteAllow: allow,
overwriteDeny: deny overwriteDeny: deny
})), guildId); })), guildId);
header = channel.name; return {
permissions,
break; header: channel.name
} };
})
default: { .otherwise(() => {
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({ const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role, type: PermissionType.Role,
...role ...role
})); }));
header = guild.name; return {
permissions,
break; header: guild.name
} };
} });
openRolesAndUsersPermissionsModal(permissions, guild, header); openRolesAndUsersPermissionsModal(permissions, guild, header);
}} }}
@ -133,32 +131,34 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
return (children, props) => { return (children, props) => {
if (!props) return; if (
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) !props ||
(type === MenuItemParentType.User && !props.user) ||
(type === MenuItemParentType.Guild && !props.guild) ||
(type === MenuItemParentType.Channel && (!props.channel || !props.guild))
) {
return; return;
}
const group = findGroupChildrenByChildId(childId, children); const group = findGroupChildrenByChildId(childId, children);
const item = (() => { const item = match(type)
switch (type) { .with(MenuItemParentType.User, () => MenuItem(props.guildId, props.user.id, type))
case MenuItemParentType.User: .with(MenuItemParentType.Channel, () => MenuItem(props.guild.id, props.channel.id, type))
return MenuItem(props.guildId, props.user.id, type); .with(MenuItemParentType.Guild, () => MenuItem(props.guild.id))
case MenuItemParentType.Channel: .otherwise(() => null);
return MenuItem(props.guild.id, props.channel.id, type);
case MenuItemParentType.Guild:
return MenuItem(props.guild.id);
default:
return null;
}
})();
if (item == null) return; if (item == null) return;
if (group) if (group) {
group.push(item); return group.push(item);
else if (childId === "roles" && props.guildId) }
// "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID" // "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID"
if (childId === "roles" && props.guildId) {
children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>); children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>);
}
}; };
} }

View file

@ -1,20 +1,6 @@
/* User Permissions Component */ /* User Permissions Component */
.vc-permviewer-userperms-title-container { .vc-permviewer-user-sortorder-btn {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
margin-bottom: 6px;
}
.vc-permviewer-userperms-btns-container {
display: flex;
align-items: center;
}
.vc-permviewer-userperms-sortorder-btn {
all: unset;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -23,27 +9,17 @@
height: 24px; height: 24px;
} }
.vc-permviewer-userperms-permdetails-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
.vc-permviewer-userperms-toggleperms-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
/* RolesAndUsersPermissions Component */ /* RolesAndUsersPermissions Component */
.vc-permviewer-perms-title { .vc-permviewer-modal-content {
padding: 16px 4px 16px 16px;
}
.vc-permviewer-modal-title {
flex-grow: 1; flex-grow: 1;
} }
.vc-permviewer-perms-no-perms { .vc-permviewer-modal-no-perms {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
@ -52,101 +28,103 @@
text-align: center; text-align: center;
} }
.vc-permviewer-perms-container { .vc-permviewer-modal-container {
display: grid; width: 100%;
grid-template-columns: 1fr 2fr; height: 100%;
grid-template-areas: "list permissions"; display: flex;
padding: 16px 0; gap: 8px;
} }
.vc-permviewer-perms-list { .vc-permviewer-modal-list {
grid-area: list;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
border-right: 2px solid var(--background-modifier-active); padding-right: 8px;
width: 200px;
} }
.vc-permviewer-perms-list-item-btn { .vc-permviewer-modal-list-item-btn {
all: unset;
cursor: pointer; cursor: pointer;
} }
.vc-permviewer-perms-list-item { .vc-permviewer-modal-list-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 5px; gap: 8px;
cursor: pointer; padding: 8px;
width: 230px;
border-radius: 5px; border-radius: 5px;
} }
.vc-permviewer-perms-list-item:hover { .vc-permviewer-modal-list-item:hover {
background-color: var(--background-modifier-hover); background-color: var(--background-modifier-hover);
} }
.vc-permviewer-perms-list-item-active { .vc-permviewer-modal-list-item-active {
background-color: var(--background-modifier-selected); background-color: var(--background-modifier-selected);
} }
.vc-permviewer-perms-list-item > div { .vc-permviewer-modal-list-item > div {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.vc-permviewer-perms-role-circle { .vc-permviewer-modal-role-circle {
border-radius: 50%; border-radius: 50%;
width: 12px; width: 12px;
height: 12px; height: 12px;
margin-left: 3px;
margin-right: 11px;
flex-shrink: 0; flex-shrink: 0;
} }
.vc-permviewer-perms-user-img { .vc-permviewer-modal-role-image {
width: 20px;
height: 20px;
object-fit: contain;
}
.vc-permviewer-modal-user-img {
border-radius: 50%; border-radius: 50%;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 6px;
} }
.vc-permviewer-perms-perms { .vc-permviewer-modal-divider {
grid-area: permissions; width: 2px;
background-color: var(--background-modifier-active);
}
.vc-permviewer-modal-perms {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 5px; padding-right: 8px;
} }
.vc-permviewer-perms-perms-item { .vc-permviewer-modal-perms-item {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px; gap: 5px;
padding: 10px 2px 10px 10px;
border-bottom: 2px solid var(--background-modifier-active); border-bottom: 2px solid var(--background-modifier-active);
} }
.vc-permviewer-perms-perms-item:last-child { .vc-permviewer-modal-perms-item:last-child {
border: 0; border: 0;
} }
.vc-permviewer-perms-perms-item-icon { .vc-permviewer-modal-perms-item-icon {
border: 1px solid var(--background-modifier-selected); border: 1px solid var(--background-modifier-selected);
width: 24px; width: 24px;
height: 24px; height: 24px;
margin-right: 5px;
} }
.vc-permviewer-perms-perms-item .vc-info-icon { .vc-permviewer-modal-perms-item .vc-info-icon {
color: var(--interactive-muted); color: var(--interactive-muted);
margin-left: auto;
cursor: pointer; cursor: pointer;
position: absolute;
right: 0;
scale: 0.9;
transition: color ease-in 0.1s; transition: color ease-in 0.1s;
} }
.vc-permviewer-perms-perms-item .vc-info-icon:hover { .vc-permviewer-modal-perms-item .vc-info-icon:hover {
color: var(--interactive-active); color: var(--interactive-active);
} }
@ -167,3 +145,14 @@
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6)); background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
border-color: var(--profile-body-border-color) border-color: var(--profile-body-border-color)
} }
.vc-permviewer-granted-by-container {
max-width: 300px;
width: auto;
}
.vc-permviewer-granted-by-content {
display: flex;
align-items: center;
gap: 4px;
}

View file

@ -57,7 +57,7 @@ export default definePlugin({
}, },
{ {
match: /text:\i\.\i.Messages.USER_PROFILE_PRONOUNS/, match: /text:\i\.\i.Messages.USER_PROFILE_PRONOUNS/,
replace: '$&+vcHasPendingPronouns?"":` (${vcPronounSource})`' replace: '$&+(vcHasPendingPronouns?"":` (${vcPronounSource})`)'
}, },
{ {
match: /(\.pronounsText.+?children:)(\i)/, match: /(\.pronounsText.+?children:)(\i)/,

View file

@ -22,12 +22,13 @@ import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common"; import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Auth } from "../auth"; import { Auth } from "../auth";
import { ReviewType } from "../entities";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { cl } from "../utils"; import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; modalKey: string, discordId: string; name: string; }) { function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; name: string; type: ReviewType; }) {
const [data, setData] = useState<Response>(); const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true); const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -58,6 +59,7 @@ function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; mod
onFetchReviews={setData} onFetchReviews={setData}
scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })} scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
hideOwnReview hideOwnReview
type={type}
/> />
</div> </div>
</ModalContent> </ModalContent>
@ -95,7 +97,7 @@ function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; mod
); );
} }
export function openReviewsModal(discordId: string, name: string) { export function openReviewsModal(discordId: string, name: string, type: ReviewType) {
const modalKey = "vc-rdb-modal-" + Date.now(); const modalKey = "vc-rdb-modal-" + Date.now();
openModal(props => ( openModal(props => (
@ -104,6 +106,7 @@ export function openReviewsModal(discordId: string, name: string) {
modalProps={props} modalProps={props}
discordId={discordId} discordId={discordId}
name={name} name={name}
type={type}
/> />
), { modalKey }); ), { modalKey });
} }

View file

@ -21,7 +21,7 @@ import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpa
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth"; import { Auth, authorize } from "../auth";
import { Review } from "../entities"; import { Review, ReviewType } from "../entities";
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings"; import { settings } from "../settings";
import { cl, showToast } from "../utils"; import { cl, showToast } from "../utils";
@ -45,6 +45,7 @@ interface Props extends UserProps {
page?: number; page?: number;
scrollToTop?(): void; scrollToTop?(): void;
hideOwnReview?: boolean; hideOwnReview?: boolean;
type: ReviewType;
} }
export default function ReviewsView({ export default function ReviewsView({
@ -56,6 +57,7 @@ export default function ReviewsView({
page = 1, page = 1,
showInput = false, showInput = false,
hideOwnReview = false, hideOwnReview = false,
type,
}: Props) { }: Props) {
const [signal, refetch] = useForceUpdater(true); const [signal, refetch] = useForceUpdater(true);
@ -80,6 +82,7 @@ export default function ReviewsView({
reviews={reviewData!.reviews} reviews={reviewData!.reviews}
hideOwnReview={hideOwnReview} hideOwnReview={hideOwnReview}
profileId={discordId} profileId={discordId}
type={type}
/> />
{showInput && ( {showInput && (
@ -94,7 +97,7 @@ export default function ReviewsView({
); );
} }
function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) { function ReviewList({ refetch, reviews, hideOwnReview, profileId, type }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; type: ReviewType; }) {
const myId = UserStore.getCurrentUser().id; const myId = UserStore.getCurrentUser().id;
return ( return (
@ -111,7 +114,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch():
{reviews?.length === 0 && ( {reviews?.length === 0 && (
<Forms.FormText className={cl("placeholder")}> <Forms.FormText className={cl("placeholder")}>
Looks like nobody reviewed this user yet. You could be the first! Looks like nobody reviewed this {type === ReviewType.User ? "user" : "server"} yet. You could be the first!
</Forms.FormText> </Forms.FormText>
)} )}
</div> </div>

View file

@ -30,7 +30,7 @@ import { Guild, User } from "discord-types/general";
import { Auth, initAuth, updateAuth } from "./auth"; import { Auth, initAuth, updateAuth } from "./auth";
import { openReviewsModal } from "./components/ReviewModal"; import { openReviewsModal } from "./components/ReviewModal";
import { NotificationType } from "./entities"; import { NotificationType, ReviewType } from "./entities";
import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings"; import { settings } from "./settings";
import { showToast } from "./utils"; import { showToast } from "./utils";
@ -44,7 +44,7 @@ const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { gu
label="View Reviews" label="View Reviews"
id="vc-rdb-server-reviews" id="vc-rdb-server-reviews"
icon={OpenExternalIcon} icon={OpenExternalIcon}
action={() => openReviewsModal(guild.id, guild.name)} action={() => openReviewsModal(guild.id, guild.name, ReviewType.Server)}
/> />
); );
}; };
@ -56,7 +56,7 @@ const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { use
label="View Reviews" label="View Reviews"
id="vc-rdb-user-reviews" id="vc-rdb-user-reviews"
icon={OpenExternalIcon} icon={OpenExternalIcon}
action={() => openReviewsModal(user.id, user.username)} action={() => openReviewsModal(user.id, user.username, ReviewType.User)}
/> />
); );
}; };
@ -157,7 +157,7 @@ export default definePlugin({
return ( return (
<TooltipContainer text="View Reviews"> <TooltipContainer text="View Reviews">
<Button <Button
onClick={() => openReviewsModal(user.id, user.username)} onClick={() => openReviewsModal(user.id, user.username, ReviewType.User)}
look={Button.Looks.FILLED} look={Button.Looks.FILLED}
size={Button.Sizes.NONE} size={Button.Sizes.NONE}
color={RoleButtonClasses.bannerColor} color={RoleButtonClasses.bannerColor}

View file

@ -57,7 +57,7 @@ export default definePlugin({
patches: [ patches: [
// Chat Mentions // Chat Mentions
{ {
find: 'location:"UserMention', find: ".USER_MENTION)",
replacement: [ replacement: [
{ {
match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/, match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,

View file

@ -4,21 +4,38 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
onlySnow: {
type: OptionType.BOOLEAN,
description: "Only play the Snow Halation Theme",
default: false,
restartNeeded: true
}
});
// NOTE - Ultimately should probably be turned into a ringtone picker plugin
export default definePlugin({ export default definePlugin({
name: "SecretRingToneEnabler", name: "SecretRingToneEnabler",
description: "Always play the secret version of the discord ringtone (except during special ringtone events)", description: "Always play the secret version of the discord ringtone (except during special ringtone events)",
authors: [Devs.AndrewDLO, Devs.FieryFlames], authors: [Devs.AndrewDLO, Devs.FieryFlames, Devs.RamziAH],
settings,
patches: [ patches: [
{ {
find: '"call_ringing_beat"', find: '"call_ringing_beat"',
replacement: { replacement: [
{
match: /500!==\i\(\)\.random\(1,1e3\)/, match: /500!==\i\(\)\.random\(1,1e3\)/,
replace: "false", replace: "false"
}
}, },
], {
predicate: () => settings.store.onlySnow,
match: /"call_ringing_beat",/,
replace: ""
}
]
}
]
}); });

View file

@ -0,0 +1,3 @@
# StickerPaste
Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending.

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "StickerPaste",
description: "Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending",
authors: [Devs.ImBanana],
patches: [
{
find: ".stickers,previewSticker:",
replacement: {
match: /if\(\i\.\i\.getUploadCount/,
replace: "return true;$&",
}
}
]
});

View file

@ -22,10 +22,10 @@ export const settings = definePluginSettings({
}, },
superReactionPlayingLimit: { superReactionPlayingLimit: {
description: "Max Super Reactions to play at once", description: "Max Super Reactions to play at once. 0 to disable playing Super Reactions",
type: OptionType.SLIDER, type: OptionType.SLIDER,
default: 20, default: 20,
markers: [5, 10, 20, 40, 60, 80, 100], markers: [0, 5, 10, 20, 40, 60, 80, 100],
stickToMarkers: true, stickToMarkers: true,
}, },
}, { }, {
@ -58,6 +58,7 @@ export default definePlugin({
shouldPlayBurstReaction(playingCount: number) { shouldPlayBurstReaction(playingCount: number) {
if (settings.store.unlimitedSuperReactionPlaying) return true; if (settings.store.unlimitedSuperReactionPlaying) return true;
if (settings.store.superReactionPlayingLimit === 0) return false;
if (playingCount <= settings.store.superReactionPlayingLimit) return true; if (playingCount <= settings.store.superReactionPlayingLimit) return true;
return false; return false;
}, },

View file

@ -0,0 +1,5 @@
# TimeBarAllActivities
Adds the Spotify time bar to all activities if they have start and end timestamps.
![](https://github.com/user-attachments/assets/9fbbe33c-8218-43c9-8b8d-f907a4e809fe)

View file

@ -1,35 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn],
patches: [
{
find: "}renderTimeBar(",
replacement: {
match: /renderTimeBar\((.{1,3})\){.{0,50}?let/,
replace: "renderTimeBar($1){let"
}
}
],
});

View file

@ -0,0 +1,76 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
interface Activity {
timestamps?: ActivityTimestamps;
}
interface ActivityTimestamps {
start?: string;
end?: string;
}
const ActivityTimeBar = findComponentByCodeLazy<ActivityTimestamps>(".Millis.HALF_SECOND", ".bar", ".progress");
export const settings = definePluginSettings({
hideActivityDetailText: {
type: OptionType.BOOLEAN,
description: "Hide the large title text next to the activity",
default: true,
},
hideActivityTimerBadges: {
type: OptionType.BOOLEAN,
description: "Hide the timer badges next to the activity",
default: true,
}
});
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn, Devs.niko],
settings,
patches: [
{
find: ".Messages.USER_ACTIVITY_PLAYING",
replacement: [
// Insert Spotify time bar component
{
match: /\(0,.{0,30}activity:(\i),className:\i\.badges\}\)/g,
replace: "$&,$self.getTimeBar($1)"
},
// Hide the large title on listening activities, to make them look more like Spotify (also visible from hovering over the large icon)
{
match: /(\i).type===(\i\.\i)\.WATCHING/,
replace: "($self.settings.store.hideActivityDetailText&&$self.isActivityTimestamped($1)&&$1.type===$2.LISTENING)||$&"
}
]
},
// Hide the "badge" timers that count the time since the activity starts
{
find: ".TvIcon).otherwise",
replacement: {
match: /null!==\(\i=null===\(\i=(\i)\.timestamps\).{0,50}created_at/,
replace: "($self.settings.store.hideActivityTimerBadges&&$self.isActivityTimestamped($1))?null:$&"
}
}
],
isActivityTimestamped(activity: Activity) {
return activity.timestamps != null && activity.timestamps.start != null && activity.timestamps.end != null;
},
getTimeBar(activity: Activity) {
if (this.isActivityTimestamped(activity)) {
return <ActivityTimeBar start={activity.timestamps!.start} end={activity.timestamps!.end} />;
}
}
});

View file

@ -0,0 +1,9 @@
# Volume Booster
Allows you to boost the volume over 200% on desktop and over 100% on other clients.
Works on users, bots, and streams!
![the volume being moved up to 270% on vesktop](https://github.com/user-attachments/assets/793e012e-c069-4fa4-a3d5-61c2f55edd3e)
![the volume being moved up to 297% on a stream](https://github.com/user-attachments/assets/77463eb9-2537-4821-a3ab-82f60633ccbc)

View file

@ -31,10 +31,27 @@ const settings = definePluginSettings({
} }
}); });
interface StreamData {
audioContext: AudioContext,
audioElement: HTMLAudioElement,
emitter: any,
// added by this plugin
gainNode?: GainNode,
id: string,
levelNode: AudioWorkletNode,
sinkId: string | "default",
stream: MediaStream,
streamSourceNode?: MediaStreamAudioSourceNode,
videoStreamId: string,
_mute: boolean,
_speakingFlags: number,
_volume: number;
}
export default definePlugin({ export default definePlugin({
name: "VolumeBooster", name: "VolumeBooster",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz, Devs.sadan],
description: "Allows you to set the user and stream volume above the default maximum.", description: "Allows you to set the user and stream volume above the default maximum",
settings, settings,
patches: [ patches: [
@ -45,12 +62,28 @@ export default definePlugin({
].map(find => ({ ].map(find => ({
find, find,
replacement: { replacement: {
match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/, match: /(?<=maxValue:)\i\.\i\?(\d+?):(\d+?)(?=,)/,
replace: (_, higherMaxVolume, minorMaxVolume) => "" replace: (_, higherMaxVolume, minorMaxVolume) => `${higherMaxVolume}*$self.settings.store.multiplier`
+ `?${higherMaxVolume}*$self.settings.store.multiplier`
+ `:${minorMaxVolume}*$self.settings.store.multiplier`
} }
})), })),
// Patches needed for web/vesktop
{
find: "streamSourceNode",
predicate: () => !IS_DISCORD_DESKTOP,
group: true,
replacement: [
// Remove rounding algorithm
{
match: /Math\.max.{0,30}\)\)/,
replace: "arguments[0]"
},
// Patch the volume
{
match: /\.volume=this\._volume\/100;/,
replace: ".volume=0.00;$self.patchVolume(this);"
}
]
},
// Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord
{ {
find: "AudioContextSettingsMigrated", find: "AudioContextSettingsMigrated",
@ -83,4 +116,26 @@ export default definePlugin({
] ]
} }
], ],
patchVolume(data: StreamData) {
if (data.stream.getAudioTracks().length === 0) return;
data.streamSourceNode ??= data.audioContext.createMediaStreamSource(data.stream);
if (!data.gainNode) {
const gain = data.gainNode = data.audioContext.createGain();
data.streamSourceNode.connect(gain);
gain.connect(data.audioContext.destination);
}
// @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.gainNode.gain.value = data._mute
? 0
: data._volume / 100;
}
}); });

View file

@ -86,7 +86,7 @@ interface NotificationObject {
title: string; title: string;
content: string; content: string;
useBase64Icon: boolean; useBase64Icon: boolean;
icon: ArrayBuffer | string; icon: string;
sourceApp: string; sourceApp: string;
} }
@ -320,7 +320,13 @@ function shouldIgnoreForChannelType(channel: Channel) {
} }
function sendMsgNotif(titleString: string, content: string, message: Message) { function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => { fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`)
.then(response => response.blob())
.then(blob => new Promise<string>(resolve => {
const r = new FileReader();
r.onload = () => resolve((r.result as string).split(",")[1]);
r.readAsDataURL(blob);
})).then(result => {
const msgData: NotificationObject = { const msgData: NotificationObject = {
type: 1, type: 1,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
@ -331,7 +337,7 @@ function sendMsgNotif(titleString: string, content: string, message: Message) {
title: titleString, title: titleString,
content: content, content: content,
useBase64Icon: true, useBase64Icon: true,
icon: new TextDecoder().decode(result), icon: result,
sourceApp: "Vencord" sourceApp: "Vencord"
}; };

View file

@ -534,6 +534,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Joona", name: "Joona",
id: 297410829589020673n id: 297410829589020673n
}, },
sadan: {
name: "sadan",
id: 521819891141967883n,
},
Kylie: {
name: "Cookie",
id: 721853658941227088n
},
AshtonMemer: { AshtonMemer: {
name: "AshtonMemer", name: "AshtonMemer",
id: 373657230530052099n id: 373657230530052099n
@ -546,10 +554,22 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Lumap", name: "Lumap",
id: 585278686291427338n, id: 585278686291427338n,
}, },
Obsidian: {
name: "Obsidian",
id: 683171006717755446n,
},
SerStars: { SerStars: {
name: "SerStars", name: "SerStars",
id: 861631850681729045n, id: 861631850681729045n,
}, },
niko: {
name: "niko",
id: 341377368075796483n,
},
RamziAH: {
name: "RamziAH",
id: 1279957227612147747n,
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

View file

@ -18,7 +18,7 @@
import { MessageObject } from "@api/MessageEvents"; import { MessageObject } from "@api/MessageEvents";
import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general"; import { Channel, Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@ -54,12 +54,12 @@ export async function openInviteModal(code: string) {
}); });
} }
export function getCurrentChannel() { export function getCurrentChannel(): Channel | undefined {
return ChannelStore.getChannel(SelectedChannelStore.getChannelId()); return ChannelStore.getChannel(SelectedChannelStore.getChannelId());
} }
export function getCurrentGuild(): Guild | undefined { export function getCurrentGuild(): Guild | undefined {
return GuildStore.getGuild(getCurrentChannel()?.guild_id); return GuildStore.getGuild(getCurrentChannel()?.guild_id!);
} }
export function openPrivateChannel(userId: string) { export function openPrivateChannel(userId: string) {

View file

@ -17,6 +17,7 @@
*/ */
import { Settings, SettingsStore } from "@api/Settings"; import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
let style: HTMLStyleElement; let style: HTMLStyleElement;
@ -59,7 +60,18 @@ async function initThemes() {
const { themeLinks, enabledThemes } = Settings; const { themeLinks, enabledThemes } = Settings;
const links: string[] = [...themeLinks]; // "darker" and "midnight" both count as dark
const activeTheme = ThemeStore.theme === "light" ? "light" : "dark";
const links = themeLinks
.map(rawLink => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return rawLink;
const [, mode, link] = match;
return mode === activeTheme ? link : null;
})
.filter(link => link !== null);
if (IS_WEB) { if (IS_WEB) {
for (const theme of enabledThemes) { for (const theme of enabledThemes) {
@ -85,6 +97,7 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes); SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes); SettingsStore.addChangeListener("enabledThemes", initThemes);
ThemeStore.addChangeListener(initThemes);
if (!IS_WEB) if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes); VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -53,6 +53,7 @@ export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & {
}; };
export let EmojiStore: t.EmojiStore; export let EmojiStore: t.EmojiStore;
export let ThemeStore: t.ThemeStore;
export let WindowStore: t.WindowStore; export let WindowStore: t.WindowStore;
export let DraftStore: t.DraftStore; export let DraftStore: t.DraftStore;
@ -84,3 +85,4 @@ waitForStore("GuildChannelStore", m => GuildChannelStore = m);
waitForStore("MessageStore", m => MessageStore = m); waitForStore("MessageStore", m => MessageStore = m);
waitForStore("WindowStore", m => WindowStore = m); waitForStore("WindowStore", m => WindowStore = m);
waitForStore("EmojiStore", m => EmojiStore = m); waitForStore("EmojiStore", m => EmojiStore = m);
waitForStore("ThemeStore", m => ThemeStore = m);

View file

@ -91,7 +91,7 @@ export type Tooltip = ComponentType<{
/** Tooltip.Colors.BLACK */ /** Tooltip.Colors.BLACK */
color?: string; color?: string;
/** TooltipPositions.TOP */ /** TooltipPositions.TOP */
position?: string; position?: PopoutPosition;
tooltipClassName?: string; tooltipClassName?: string;
tooltipContentClassName?: string; tooltipContentClassName?: string;
@ -110,7 +110,7 @@ export type TooltipContainer = ComponentType<PropsWithChildren<{
/** Tooltip.Colors.BLACK */ /** Tooltip.Colors.BLACK */
color?: string; color?: string;
/** TooltipPositions.TOP */ /** TooltipPositions.TOP */
position?: string; position?: PopoutPosition;
spacing?: number; spacing?: number;
className?: string; className?: string;
@ -252,7 +252,7 @@ export type Select = ComponentType<PropsWithChildren<{
look?: 0 | 1; look?: 0 | 1;
className?: string; className?: string;
popoutClassName?: string; popoutClassName?: string;
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center"; popoutPosition?: PopoutPosition;
optionClassName?: string; optionClassName?: string;
autoFocus?: boolean; autoFocus?: boolean;
@ -293,7 +293,7 @@ export type SearchableSelect = ComponentType<PropsWithChildren<{
className?: string; className?: string;
popoutClassName?: string; popoutClassName?: string;
wrapperClassName?: string; wrapperClassName?: string;
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center"; popoutPosition?: PopoutPosition;
optionClassName?: string; optionClassName?: string;
autoFocus?: boolean; autoFocus?: boolean;
@ -376,6 +376,8 @@ declare enum PopoutAnimation {
FADE = "4" FADE = "4"
} }
type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
export type Popout = ComponentType<{ export type Popout = ComponentType<{
children( children(
thing: { thing: {
@ -387,7 +389,7 @@ export type Popout = ComponentType<{
}, },
data: { data: {
isShown: boolean; isShown: boolean;
position: string; position: PopoutPosition;
} }
): ReactNode; ): ReactNode;
shouldShow?: boolean; shouldShow?: boolean;
@ -395,7 +397,7 @@ export type Popout = ComponentType<{
closePopout(): void; closePopout(): void;
isPositioned: boolean; isPositioned: boolean;
nudge: number; nudge: number;
position: string; position: PopoutPosition;
setPopoutRef(ref: any): void; setPopoutRef(ref: any): void;
updatePosition(): void; updatePosition(): void;
}): ReactNode; }): ReactNode;
@ -404,13 +406,13 @@ export type Popout = ComponentType<{
onRequestClose?(): void; onRequestClose?(): void;
/** "center" and others */ /** "center" and others */
align?: string; align?: "left" | "right" | "center";
/** Popout.Animation */ /** Popout.Animation */
animation?: PopoutAnimation; animation?: PopoutAnimation;
autoInvert?: boolean; autoInvert?: boolean;
nudgeAlignIntoViewport?: boolean; nudgeAlignIntoViewport?: boolean;
/** "bottom" and others */ /** "bottom" and others */
position?: string; position?: PopoutPosition;
positionKey?: string; positionKey?: string;
spacing?: number; spacing?: number;
}> & { }> & {
@ -459,7 +461,7 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
style?: CSSProperties; style?: CSSProperties;
dir?: "ltr"; dir?: "ltr";
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical" | "auto";
paddingFix?: boolean; paddingFix?: boolean;
fade?: boolean; fade?: boolean;

View file

@ -220,6 +220,14 @@ export class GuildStore extends FluxStore {
getAllGuildRoles(): Record<string, Record<string, Role>>; getAllGuildRoles(): Record<string, Record<string, Role>>;
} }
export class ThemeStore extends FluxStore {
theme: "light" | "dark" | "darker" | "midnight";
darkSidebar: boolean;
isSystemThemeAvailable: boolean;
systemPrefersColorScheme: "light" | "dark";
systemTheme: null;
}
export type useStateFromStores = <T>( export type useStateFromStores = <T>(
stores: t.FluxStore[], stores: t.FluxStore[],
mapper: () => T, mapper: () => T,

View file

@ -49,6 +49,11 @@ export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYea
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage"); export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage");
export const { match, P }: Pick<typeof import("ts-pattern"), "match" | "P"> = mapMangledModuleLazy("@ts-pattern/matcher", {
match: filters.byCode("return new"),
P: filters.byProps("when")
});
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
export const i18n: t.i18n = findLazy(m => m.Messages?.["en-US"]); export const i18n: t.i18n = findLazy(m => m.Messages?.["en-US"]);