Compare commits

...

34 commits

Author SHA1 Message Date
f33e0dcb73
offline edition 2024-12-11 16:51:17 +09:00
5b12fd1d45
katex 2 katex harder 2024-12-11 16:51:17 +09:00
10addb295c
katex left align 2024-12-11 16:51:17 +09:00
4a20dd4278
katex 2024-12-11 16:51:17 +09:00
a7f41f0608
asdf 2024-12-11 16:51:17 +09:00
66cc6754e4
owo 2024-12-11 16:51:13 +09:00
sadan4
464c4a9b61
TypingTweaks: Fix usernames not being colored (#3070) 2024-12-11 01:06:34 -03:00
sadan4
dcfddcbc21
ConsoleJanitor: Add HLJS deprecations (#3062) 2024-12-10 21:37:21 +00:00
Nuckyz
9d3c91e9df
TypingTweaks: Fix plugin 2024-12-10 18:29:15 -03:00
Nuckyz
8d65bcf743
BetterFolders: Fix folder icon setting 2024-12-10 18:09:54 -03:00
Nuckyz
4a5f0838e2
Remove workaround for Devtools theme 2024-12-07 20:32:16 -03:00
Nuckyz
99dc65fe4e
Fix TypingIndicator & CallTimer 2024-12-07 20:31:08 -03:00
Nuckyz
3a339636d1
Remove old plugin migrations 2024-12-07 19:55:37 -03:00
Nuckyz
cea0a3c9d9
NoScreensharePreview: Allow plugin to be turned on/off 2024-12-07 19:15:05 -03:00
Vendicated
a3f5dc39a0
CallTimer: fix crashing on canary 2024-12-07 00:14:08 +01:00
sadan4
df44edd41b
BetterFolders: Fix including open folders in main sidebar (#3064) 2024-12-05 21:07:30 -03:00
Nuckyz
cdfc89b819
NoScreensharePreview: Migrate to stock Discord feature 2024-12-03 22:39:36 -03:00
Nuckyz
8711dd9a4b
WebContextMenus: Fix input bar menu 2024-12-03 21:51:41 -03:00
Sqaaakoi
df454ca952
MutualGroupDMs: Fix in DM sidebar when no mutual friends/servers (#2976) 2024-12-02 23:30:56 -03:00
Etorix
6628624082
CommandHelpers: Make findOption use nullish coalescing (#3047) 2024-12-03 02:16:13 +00:00
Etorix
dd87f360d7
CommandsAPI: Fix spread overwriting omitted subcommand options (#3057) 2024-12-03 02:13:27 +00:00
Nuckyz
3f61fe722d
AlwaysTrust: Fix disabling suspicious file popup 2024-12-02 21:51:29 -03:00
Nuckyz
d70e0f27dc
ServerListIndicators: Account for pending clans count 2024-12-02 20:39:54 -03:00
Nuckyz
0ac80ce9d1
MessagePopoverAPI: Add buttons after quick reactions 2024-12-01 00:10:08 -03:00
Nuckyz
fcece61995
Bump to 1.10.8 2024-11-29 19:26:55 -03:00
sadan4
02f50b161b
ImageZoom: Fix zoom level not saving (#3054) 2024-11-29 22:26:10 +00:00
Vendicated
1150a50355
Badges: fix overflow in Discord's css 2024-11-29 22:46:28 +01:00
v
11321eb693
Update CONTRIBUTING.md
Plugins interacting with specific Discord bots are not allowed.
2024-11-29 16:11:55 +01:00
Nuckyz
60b776669b
WebContextMenus: Fix copy context menu 2024-11-25 20:03:34 -03:00
sadan4
d8df96d1e3
BetterFolders: Fix dedicated sidebar (#3037) 2024-11-25 19:44:29 -03:00
Sqaaakoi
a9d44e3341
PermissionsViewer: Fix permission description tooltip & cleanup (#3040) 2024-11-25 01:14:25 -03:00
Mia Rodriguez
e7a54b0587
SilentTyping: Improve button visual look (#3026) 2024-11-25 00:35:12 +00:00
sadan4
23c9e2ce22
ShowHiddenThings: Allow opening mod view on yourself (#3045) 2024-11-24 21:30:27 -03:00
Etorix
5fb63246ca
Add support for onAuxClick on ChatBarButton (#3043) 2024-11-24 21:25:30 -03:00
47 changed files with 1198 additions and 393 deletions

View file

@ -31,6 +31,7 @@ Before starting your plugin:
- No FakeDeafen or FakeMute
- No StereoMic
- No plugins that simply hide or redesign ui elements. This can be done with CSS
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
- No plugins that require the user to enter their own API key

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.10.7",
"version": "1.10.8",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -39,8 +39,10 @@
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
"dompurify": "^3.1.7",
"fflate": "^0.8.2",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"katex": "^0.16.11",
"monaco-editor": "^0.50.0",
"nanoid": "^5.0.7",
"virtual-merge": "^1.0.1"
@ -49,6 +51,8 @@
"@stylistic/eslint-plugin": "^2.6.1",
"@types/chrome": "^0.0.269",
"@types/diff": "^5.2.1",
"@types/dompurify": "^3.0.5",
"@types/katex": "^0.16.7",
"@types/lodash": "^4.17.7",
"@types/node": "^22.0.3",
"@types/react": "^18.3.3",
@ -107,6 +111,6 @@
},
"engines": {
"node": ">=18",
"pnpm": ">=9"
"pnpm": "*"
}
}

View file

@ -28,12 +28,18 @@ importers:
'@vap/shiki':
specifier: 0.10.5
version: 0.10.5
dompurify:
specifier: ^3.1.7
version: 3.1.7
fflate:
specifier: ^0.8.2
version: 0.8.2
gifenc:
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3
katex:
specifier: ^0.16.11
version: 0.16.11
monaco-editor:
specifier: ^0.50.0
version: 0.50.0
@ -53,6 +59,12 @@ importers:
'@types/diff':
specifier: ^5.2.1
version: 5.2.1
'@types/dompurify':
specifier: ^3.0.5
version: 3.0.5
'@types/katex':
specifier: ^0.16.7
version: 0.16.7
'@types/lodash':
specifier: ^4.17.7
version: 4.17.7
@ -622,6 +634,9 @@ packages:
'@types/diff@5.2.1':
resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==}
'@types/dompurify@3.0.5':
resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==}
'@types/eslint@9.6.0':
resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==}
@ -649,6 +664,9 @@ packages:
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/lodash@4.14.194':
resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
@ -688,6 +706,9 @@ packages:
'@types/scheduler@0.16.3':
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -995,6 +1016,10 @@ packages:
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
@ -1127,6 +1152,9 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dompurify@3.1.7:
resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==}
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
@ -1945,6 +1973,10 @@ packages:
jszip@2.7.0:
resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==}
katex@0.16.11:
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
hasBin: true
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -3052,6 +3084,10 @@ snapshots:
'@types/diff@5.2.1': {}
'@types/dompurify@3.0.5':
dependencies:
'@types/trusted-types': 2.0.7
'@types/eslint@9.6.0':
dependencies:
'@types/estree': 1.0.5
@ -3080,6 +3116,8 @@ snapshots:
dependencies:
'@types/node': 18.16.3
'@types/katex@0.16.7': {}
'@types/lodash@4.14.194': {}
'@types/lodash@4.17.7': {}
@ -3124,6 +3162,8 @@ snapshots:
'@types/scheduler@0.16.3': {}
'@types/trusted-types@2.0.7': {}
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.0.3
@ -3494,6 +3534,8 @@ snapshots:
commander@2.20.3: {}
commander@8.3.0: {}
component-emitter@1.3.0: {}
concat-map@0.0.1: {}
@ -3612,6 +3654,8 @@ snapshots:
dependencies:
esutils: 2.0.3
dompurify@3.1.7: {}
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
@ -4503,6 +4547,10 @@ snapshots:
dependencies:
pako: 1.0.11
katex@0.16.11:
dependencies:
commander: 8.3.0
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1

View file

@ -99,7 +99,8 @@ export interface ChatBarButtonProps {
tooltip: string;
onClick: MouseEventHandler<HTMLButtonElement>;
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">;
onAuxClick?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
}
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
@ -115,6 +116,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
onClick={props.onClick}
onContextMenu={props.onContextMenu}
onAuxClick={props.onAuxClick}
{...props.buttonProps}
>
<div className={ButtonWrapperClasses.buttonWrapper}>

View file

@ -54,5 +54,5 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
}

View file

@ -110,6 +110,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
const subCmd = {
...cmd,
...o,
options: o.options !== undefined ? o.options : undefined,
type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`,

View file

@ -1,11 +0,0 @@
.vc-expandableheader-center-flex {
display: flex;
place-items: center;
}
.vc-expandableheader-btn {
all: unset;
cursor: pointer;
width: 24px;
height: 24px;
}

View file

@ -1,121 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 "./ExpandableHeader.css";
import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
moreTooltipText?: string;
onDropDownClick?: (state: boolean) => void;
defaultState?: boolean;
headerText: string;
children: React.ReactNode;
buttons?: React.ReactNode[];
forceOpen?: boolean;
}
export function ExpandableHeader({
children,
onMoreClick,
buttons,
moreTooltipText,
onDropDownClick,
headerText,
defaultState = false,
forceOpen = false,
}: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState || forceOpen);
return (
<>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px"
}}>
<Text
tag="h2"
variant="eyebrow"
style={{
color: "var(--header-primary)",
display: "inline"
}}
>
{headerText}
</Text>
<div className={cl("center-flex")}>
{
buttons ?? null
}
{
onMoreClick && // only show more button if callback is provided
<Tooltip text={moreTooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={onMoreClick}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</button>
)}
</Tooltip>
}
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={() => {
setShowContent(v => !v);
onDropDownClick?.(showContent);
}}
disabled={forceOpen}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
</button>
)}
</Tooltip>
</div>
</div>
{showContent && children}
</>
);
}

View file

@ -10,7 +10,6 @@ export * from "./CodeBlock";
export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./ExpandableHeader";
export * from "./Flex";
export * from "./Heart";
export * from "./Icons";

View file

@ -17,7 +17,7 @@
*/
import { onceDefined } from "@shared/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu, nativeTheme } from "electron";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
@ -100,19 +100,6 @@ if (!IS_VANILLA) {
super(options);
initIpc(this);
// Workaround for https://github.com/electron/electron/issues/43367. Vesktop also has its own workaround
// @TODO: Remove this when the issue is fixed
if (IS_DISCORD_DESKTOP) {
this.webContents.on("devtools-opened", () => {
if (!nativeTheme.shouldUseDarkColors) return;
nativeTheme.themeSource = "light";
setTimeout(() => {
nativeTheme.themeSource = "dark";
}, 100);
});
}
} else super(options);
}
}

View file

@ -0,0 +1,5 @@
/* the profile popout badge container(s) */
[class*="biteSize_"] [class*="tags_"] [class*="container_"] {
/* Discord has padding set to 2px instead of 1px, which causes the 12th badge to wrap to a new line. */
padding: 0 1px;
}

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./fixDiscordBadgePadding.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";

View file

@ -23,11 +23,14 @@ export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.string\(\i\.\i#{intl::MESSAGE_ACTION_REPLY}.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
patches: [
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\))\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,`
}
}
}],
]
});

View file

@ -51,7 +51,7 @@ export default definePlugin({
{
find: "bitbucket.org",
replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
match: /function \i\(\i\){(?=.{0,30}pathname:\i)/,
replace: "$&return null;"
},
predicate: () => settings.store.file

43
src/plugins/badge.ts Normal file
View file

@ -0,0 +1,43 @@
/* eslint-disable header/header */
import { BadgePosition, ProfileBadge } from "@api/Badges";
import { Badges } from "@api/index";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
const SHIGGY_BADGE = "https://cdn.discordapp.com/emojis/1101838344146665502.gif?size=240&quality=lossless";
const BLOBFOXBOX_BADGE = "https://cdn.discordapp.com/emojis/1036216552736952350.webp?size=240&quality=lossless";
const ShiggyBadge: ProfileBadge = {
description: "true shiggy fan",
image: SHIGGY_BADGE,
position: BadgePosition.START,
props: {
style: { transform: "scale(0.9)" }
},
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
link: "https://ryanccn.dev/"
};
const BlobfoxBoxBadge: ProfileBadge = {
description: "blobfox",
image: BLOBFOXBOX_BADGE,
position: BadgePosition.START,
props: {
style: { transform: "scale(0.9)" }
},
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
link: "https://ryanccn.dev/"
};
export default definePlugin({
name: "Ryan's Extra Badges",
description: "shiggy",
authors: [Devs.RyanCaoDev],
dependencies: ["BadgeAPI"],
start() {
Badges.addBadge(ShiggyBadge);
Badges.addBadge(BlobfoxBoxBadge);
},
});

View file

@ -123,7 +123,7 @@ export default definePlugin({
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
match: /lastTargetNode:\i\[\i\.length-1\].+?}\)\](?=}\))/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
@ -159,7 +159,7 @@ export default definePlugin({
]
},
{
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
find: ".expandedFolderBackground,",
predicate: () => settings.store.sidebar,
replacement: [
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
@ -185,7 +185,7 @@ export default definePlugin({
{
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.wrapper,children:\[)/,
match: /(?<=\.isExpanded\),children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
},
{
@ -275,24 +275,30 @@ export default definePlugin({
},
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
try {
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
} catch (e) {
console.error(e);
}
}
return true;
};
return child => {
if (!isBetterFolders) return true;
try {
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
} catch (e) {
console.error(e);
}
return true;
};
},
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
return child?.props?.onScroll != null;
if (!isBetterFolders) return true;
if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) {
// Filter out everything but the scroller for the guild list
child.props.children = child.props.children.filter(child => child?.props?.onScroll != null);
return true;
}
return true;
return false;
};
},

View file

@ -0,0 +1,63 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
/*
* This plugin was modified from code licensed under the following license:
*
* MIT License
*
* Copyright (c) 2021-present Sebastian Law
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
export default function Indicator({ layers, bottom }: { layers: number; bottom: boolean; }) {
return (
<Tooltip color="black" position="top" text={layers <= 1 ? "🥺" : `Decoded from ${layers} nested bottom messages`}>
{({ onMouseLeave, onMouseEnter }) => (
<span
className={`power-bottom-indicator ${findByPropsLazy("edited").edited}`}
style={{ color: "var(--text-muted)" }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{bottom ? "(bottom)" : "(original)"}
</span>
)}
</Tooltip>
);
}

View file

@ -0,0 +1,73 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
/*
* This file was adapted from https://github.com/bottom-software-foundation/bottom-js
* Which is, hopefully, licensed under MIT.
*/
const CHARACTER_VALUES: [number, string][] = [
[200, "🫂"],
[50, "💖"],
[10, "✨"],
[5, "🥺"],
[1, ","],
[0, "❤️"],
];
const SECTION_SEPERATOR = "👉👈";
const FINAL_TERMINATOR = new RegExp(`(${SECTION_SEPERATOR})?$`);
function encodeChar(charValue: number): string {
if (charValue === 0) return "";
const [val, currentCase]: [number, string] =
CHARACTER_VALUES.find(([val]) => charValue >= val) || CHARACTER_VALUES[-1];
return `${currentCase}${encodeChar(charValue - val)}`;
}
export function encode(value: string): string {
return Array.from(new TextEncoder().encode(value))
.map((v: number) => encodeChar(v) + SECTION_SEPERATOR)
.join("");
}
export function decode(value: string): string {
return new TextDecoder().decode(Uint8Array.from(
value
.trim()
.replace(FINAL_TERMINATOR, "")
.split(SECTION_SEPERATOR)
.map(letters => {
return Array.from(letters)
.map(character => {
const [value, emoji]: [number, string] = CHARACTER_VALUES.find(
([_, em]) => em === character
) || [-1, ""];
if (!emoji) {
throw new TypeError(`Invalid bottom text: '${character}'`);
}
return value;
})
.reduce((p, c) => p + c);
})
));
}
export default {
encode: encode,
decode: decode
};

View file

@ -0,0 +1,158 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
/*
* This plugin was modified from code licensed under the following license:
*
* MIT License
*
* Copyright (c) 2021-present Sebastian Law
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { FluxDispatcher, MessageStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import Bottom from "./encoding";
class BottomHandler {
cache: Record<string, Record<string, { originalContent: string; top?: boolean; layers?: number; }>>;
re: RegExp;
constructor() {
this.cache = {};
this.re = /((?:((?:\uD83E\uDEC2)?(?:💖)*(?:✨)*(?:🥺)*(?:,)*(❤️)?)(?:👉👈|\u200b))+)/gm;
}
isTranslated(message) {
if (
!this.cache[message.channel_id] ||
!this.cache[message.channel_id][message.id]
) { return false; }
return this.cache[message.channel_id][message.id].originalContent !== message.content;
}
translate(text: string, notNested: boolean) {
var original = text;
var translated = text;
var layers = 0;
while (original.match(this.re)) {
translated = original.replace(this.re, (str, p1, offset, s) => Bottom.decode(p1) || p1);
// the regex can sometimes pick up invalid bottom in which case we want to return to avoid an infinite loop
if (translated === original || notNested) break;
else {
original = translated;
layers++;
}
}
return {
translated: translated,
layers: layers,
};
}
translateMessage(message: Message, decodeLayers: boolean) {
if (!message.content || message.content.length === 0) {
return "";
}
// Build cache if it doesn't exist
if (!this.cache[message.channel_id]) {
this.cache[message.channel_id] = {};
}
if (!this.cache[message.channel_id][message.id]) {
this.cache[message.channel_id][message.id] = {
originalContent: message.content,
};
}
const cached = this.cache[message.channel_id][message.id];
if (this.isTranslated(message)) {
// if we're reverting back to original, just set the content back to original
message.content = cached.originalContent;
this.updateMessage(message);
} else {
// the message hasn't been edited, let's try to decode it
const { translated, layers } = this.translate(message.content, !decodeLayers);
if (translated === message.content) {
// we don't want to do anything if there is no bottom
// since the translation fails, mark this message to not show the indicator
cached.top = true;
throw new Error("No Bottom detected 🥺");
} else {
// let the indicator show how many layers of decoding we did
cached.layers = layers;
message.content = translated;
this.updateMessage(message);
}
}
}
updateMessage(message: Message) {
console.log({
bottomTranslation: true,
type: "MESSAGE_UPDATE",
message,
});
FluxDispatcher.dispatch({
bottomTranslation: true,
type: "MESSAGE_UPDATE",
message,
});
}
clearCache() {
for (const channelID in this.cache) {
for (const messageID in this.cache[channelID]) {
this.removeMessage(channelID, messageID);
}
}
this.cache = {};
}
removeMessage(channelID: string, messageID: string, reset = true) {
const message = MessageStore.getMessage(channelID, messageID);
if (reset) {
message.content = this.cache[channelID][messageID].originalContent;
this.updateMessage(message);
}
delete this.cache[channelID][messageID];
}
}
export default BottomHandler;

View file

@ -0,0 +1,262 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
/*
* This plugin was modified from code licensed under the following license:
*
* MIT License
*
* Copyright (c) 2021-present Sebastian Law
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { findOption, RequiredMessageOption } from "@api/Commands";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, Toasts } from "@webpack/common";
import Indicator from "./components/Indicator";
import Bottom from "./encoding";
import BottomHandler from "./handler";
const Handler = new BottomHandler();
const settings = definePluginSettings({
"decode-layers": {
description: "Decode Layers",
type: OptionType.BOOLEAN,
default: true,
},
"auto-encode-send": {
description: "Automatically encode outgoing messages",
type: OptionType.BOOLEAN,
default: false,
},
"encode-send-type": {
description: "Automatic Encode Behavior",
type: OptionType.SELECT,
options:
[
{
label: "All",
default: true,
value: 0,
},
{
label: "Inline (Greedy)",
value: 1,
},
{
label: "Inline (Parsed)",
value: 2,
}
],
},
"inline-bottom-prefix": {
description: "Inline bottom prefix",
type: OptionType.STRING,
default: "👉",
},
"inline-bottom-suffix": {
description: "Inline bottom suffix",
type: OptionType.STRING,
default: "👈",
},
});
const escapeRegex: (string: string) => string = string => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
function count(string: string, subString: string): number {
var n = 0;
var pos = 0;
const step = subString.length;
while (true) {
pos = string.indexOf(subString, pos);
if (pos >= 0) {
n++;
pos += step;
} else break;
}
return n;
}
function inlineEncode(p: string, s: string, text: string): string {
var np = count(text, p);
var ns = count(text, s);
if (np === 0 || ns === 0) return text;
var pl = p.length;
var sl = s.length;
const result: string[] = [];
let idx = 0;
while (true) {
var startIndex = text.indexOf(p, idx);
if (startIndex < 0) {
result.push(text.slice(idx));
break;
}
var endIndex = text.indexOf(s, startIndex + pl);
if (endIndex < 0) {
result.push(text.slice(idx));
break;
}
result.push(text.slice(idx, startIndex));
startIndex += pl;
result.push(Bottom.encode(text.slice(startIndex, endIndex)));
endIndex += sl;
idx = endIndex;
}
return result.join("");
}
export default definePlugin({
name: "Bottom",
description: "The Vencord plugin for bottom 🥺",
authors: [
{
id: 1038096782963507210n,
name: "skyevg",
},
],
dependencies: ["MessagePopoverAPI", "CommandsAPI", "MessageEventsAPI", "MessageAccessoriesAPI"],
settings,
start() {
addButton("bottom", msg => {
return {
label: "Translate Bottom",
icon: () => (
<svg x="0" y="0" aria-hidden="false" width="22" height="22" viewBox="0 0 36 36" fill="currentColor" className="icon">
<circle fill="#FFCC4D" cx="18" cy="18" r="18" />
<path fill="#65471B" d="M20.996 27c-.103 0-.206-.016-.309-.049-1.76-.571-3.615-.571-5.375 0-.524.169-1.089-.117-1.26-.642-.171-.525.117-1.089.643-1.26 2.162-.702 4.447-.702 6.609 0 .525.171.813.735.643 1.26-.137.421-.529.691-.951.691z" />
<path fill="#FFF" d="M30.335 12.068c-.903 2.745-3.485 4.715-6.494 4.715-.144 0-.289-.005-.435-.014-1.477-.093-2.842-.655-3.95-1.584.036.495.076.997.136 1.54.152 1.388.884 2.482 2.116 3.163.82.454 1.8.688 2.813.752 1.734.109 3.57-.28 4.873-.909 1.377-.665 2.272-1.862 2.456-3.285.183-1.415-.354-2.924-1.515-4.378z" />
<path fill="#65471B" d="M21.351 7.583c-1.297.55-1.947 2.301-1.977 5.289l.039.068c.897 1.319 2.373 2.224 4.088 2.332.114.007.228.011.341.011 2.634 0 4.849-1.937 5.253-4.524-.115-.105-.221-.212-.343-.316-3.715-3.17-6.467-3.257-7.401-2.86z" />
<path fill="#F4900C" d="M23.841 16.783c3.009 0 5.591-1.97 6.494-4.715-.354-.443-.771-.88-1.241-1.309-.404 2.587-2.619 4.524-5.253 4.524-.113 0-.227-.004-.341-.011-1.715-.108-3.191-1.013-4.088-2.332l-.039-.068c-.007.701.021 1.473.083 2.313 1.108.929 2.473 1.491 3.95 1.584.146.01.291.014.435.014z" />
<circle fill="#FFF" cx="21.413" cy="10.705" r="1.107" />
<path fill="#FFF" d="M12.159 16.783c-3.009 0-5.591-1.97-6.494-4.715-1.161 1.454-1.697 2.963-1.515 4.377.185 1.423 1.079 2.621 2.456 3.285 1.303.629 3.138 1.018 4.873.909 1.013-.064 1.993-.297 2.813-.752 1.231-.681 1.963-1.775 2.116-3.163.06-.542.1-1.042.136-1.536-1.103.923-2.47 1.487-3.95 1.58-.146.011-.291.015-.435.015z" />
<path fill="#65471B" d="M12.159 15.283c.113 0 .227-.004.341-.011 1.715-.108 3.191-1.013 4.088-2.332l.039-.068c-.031-2.988-.68-4.739-1.977-5.289-.934-.397-3.687-.31-7.401 2.859-.122.104-.227.211-.343.316.404 2.588 2.619 4.525 5.253 4.525z" />
<path fill="#F4900C" d="M16.626 12.872l-.039.068c-.897 1.319-2.373 2.224-4.088 2.332-.114.007-.228.011-.341.011-2.634 0-4.849-1.937-5.253-4.524-.47.429-.887.866-1.241 1.309.903 2.745 3.485 4.715 6.494 4.715.144 0 .289-.005.435-.014 1.48-.093 2.847-.657 3.95-1.58.062-.841.091-1.614.083-2.317z" />
<path fill="#FFF" d="M9.781 11.81c.61-.038 1.074-.564 1.035-1.174-.038-.61-.564-1.074-1.174-1.036-.61.038-1.074.564-1.036 1.174.039.61.565 1.074 1.175 1.036z" />
</svg>
),
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: async () => {
try {
Handler.translateMessage(msg, settings.store["decode-layers"]);
} catch (e) {
console.error(e);
Toasts.show(
{
id: Toasts.genId(),
message: e.message,
type: Toasts.Type.MESSAGE
}
);
}
}
};
});
addAccessory("bottom", props => {
try {
if (!Handler.cache[props.message.channel_id][props.message.id].top) {
try {
return (
<ErrorBoundary>
<Indicator layers={Handler.cache[props.message.channel_id][props.message.id].layers ?? 0} bottom={!Handler.isTranslated(props.message)} />
</ErrorBoundary>
);
} catch { }
}
}
catch { }
return null;
});
this.preSend = addPreSendListener((_, msg) => {
if (settings.store["auto-encode-send"]) {
const sendType = settings.store["encode-send-type"];
var { content } = msg;
switch (sendType) {
case 0: // all
content = Bottom.encode(content);
break;
case 1: // inline greedy
var prefix = escapeRegex(settings.store["inline-bottom-prefix"]);
var suffix = escapeRegex(settings.store["inline-bottom-suffix"]);
var reg = new RegExp(`${prefix}(.+)${suffix}`, "gm");
content = content.replace(reg, (str, p1, o, s) => Bottom.encode(p1));
break;
case 2: // inline parsed
var prefix = settings.store["inline-bottom-prefix"];
var suffix = settings.store["inline-bottom-prefix"];
content = inlineEncode(prefix, suffix, content);
break;
}
msg.content = content;
}
});
},
stop() {
removeButton("bottom");
removeAccessory("bottom");
removePreSendListener(this.preSend);
},
commands: [
{
name: "bottom",
description: "Translate and send text as bottom 🥺",
options: [RequiredMessageOption],
execute: opts => ({
content: Bottom.encode(findOption(opts, "message", "")),
})
}
]
});

View file

@ -75,10 +75,11 @@ export default definePlugin({
patches: [{
find: "renderConnectionStatus(){",
replacement: {
match: /(?<=renderConnectionStatus\(\)\{.+\.channel,children:)\i/,
match: /(?<=renderConnectionStatus\(\){.+\.channel,children:).+?}\):\i(?=}\))/,
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
}
}],
renderTimer(channelId: string) {
return <ErrorBoundary noop>
<this.Timer channelId={channelId} />

View file

@ -66,6 +66,13 @@ export default definePlugin({
},
patches: [
{
find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: {
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/,
replace: ""
}
},
{
find: 'react-spring: The "interpolate" function',
replacement: {

View file

@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("DisableCallIdle", "DisableDMCallIdle");
export default definePlugin({
name: "DisableCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.",

View file

@ -18,7 +18,7 @@
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { FluxDispatcher, useLayoutEffect, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
import { settings } from "../index";
@ -55,7 +55,7 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
const imageRef = useRef<HTMLImageElement | null>(null);
// since we accessing document im gonna use useLayoutEffect
React.useLayoutEffect(() => {
useLayoutEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Shift") {
isShiftDown.current = true;
@ -135,12 +135,14 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
setReady(true);
});
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
document.addEventListener("mousemove", updateMousePosition);
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("wheel", onWheel);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
@ -148,14 +150,16 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("wheel", onWheel);
if (settings.store.saveZoomValues) {
settings.store.zoom = zoom.current;
settings.store.size = size.current;
}
};
}, []);
useLayoutEffect(() => () => {
if (settings.store.saveZoomValues) {
settings.store.zoom = zoom.current;
settings.store.size = size.current;
}
});
if (!ready) return null;
const box = element.current?.getBoundingClientRect();

View file

@ -0,0 +1,63 @@
import "./katex.css";
import { Devs } from "@utils/constants";
import definePlugin, { ReporterTestable } from "@utils/types";
import { React, useMemo } from "@webpack/common";
import katex from "katex";
import domPurify from "dompurify";
export interface HighlighterProps {
lang?: string;
content: string;
isPreview: boolean;
tempSettings?: Record<string, any>;
}
export default definePlugin({
name: "KaTeX",
description: "TeX typesetting in discord",
authors: [Devs.skyevg],
reporterTestable: ReporterTestable.Patches,
patches: [
{
find: "codeBlock:{react(",
replacement: {
match: /codeBlock:\{react\((\i),(\i),(\i)\)\{/,
replace: "$&if($1.lang == 'latex') return $self.createBlock($1,$2,$3);"
}
},
{
find: "inlineCode:{react:(",
replacement: {
match: /inlineCode:\{react:\((\i),(\i),(\i)\)=>/,
replace: "$&($1.content.startsWith('$$') && $1.content.endsWith('$$'))?$self.createInline($1,$2,$3):"
}
}
],
start: async () => {
},
stop: () => {
},
createBlock: (props: HighlighterProps) => (
<Latex displayMode formula={props.content} />
),
createInline: (props: HighlighterProps) => (
<Latex formula={props.content.substring(1, props.content.length - 1)} />
),
});
function Latex({ formula, displayMode }: { formula: string, displayMode?: boolean; }) {
const result = useMemo(() => {
const html = katex.renderToString(formula, {
displayMode,
throwOnError: false
});
return domPurify.sanitize(html);
}, [formula, displayMode]);
return displayMode
? <div className="tex" dangerouslySetInnerHTML={{ __html: result }} />
: <span className="tex" dangerouslySetInnerHTML={{ __html: result }} />;
}

View file

@ -0,0 +1,6 @@
@import url("https://unpkg.com/katex@0.16.9/dist/katex.min.css");
.katex-display {
width: min-content;
margin: 1em;
}

View file

@ -19,6 +19,7 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
@ -87,7 +88,7 @@ export default definePlugin({
replacement: [
{
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
replace: "$self.pushSection($1, arguments[0].user);$&"
replace: "$self.pushSection($1,arguments[0].user);$&"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
@ -97,26 +98,46 @@ export default definePlugin({
},
{
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})"
}
replacement: [
{
match: /\i\|\|\i(?=\?\(0,\i\.jsxs?\)\(\i\.\i\.Overlay,)/,
replace: "$&||$self.getMutualGroupDms(arguments[0].user.id).length>0"
},
{
match: /\.openUserProfileModal.+?\)}\)}\)(?<=,(\i)&&(\i)&&(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
replace: (m, hasMutualGuilds, hasMutualFriends, Divider, classes) => "" +
`${m},$self.renderDMPageList({user:arguments[0].user,hasDivider:${hasMutualGuilds}||${hasMutualFriends},Divider:${Divider},listStyle:${classes}.list})`
}
]
}
],
pushSection(sections: any[], user: User) {
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
getMutualGroupDms(userId: string) {
try {
return getMutualGroupDms(userId);
} catch (e) {
new Logger("MutualGroupDMs").error("Failed to get mutual group dms:", e);
}
sections[IS_PATCHED] = true;
sections.push({
section: "MUTUAL_GDMS",
text: getMutualGDMCountText(user)
});
return [];
},
pushSection(sections: any[], user: User) {
try {
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
sections[IS_PATCHED] = true;
sections.push({
section: "MUTUAL_GDMS",
text: getMutualGDMCountText(user)
});
} catch (e) {
new Logger("MutualGroupDMs").error("Failed to push mutual group dms section:", e);
}
},
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = renderClickableGDMs(mutualGDms, onClose);
return (
@ -138,14 +159,13 @@ export default definePlugin({
);
}),
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: User, Divider: JSX.Element, listStyle: string; }) => {
renderDMPageList: ErrorBoundary.wrap(({ user, hasDivider, Divider, listStyle }: { user: User, hasDivider: boolean, Divider: JSX.Element, listStyle: string; }) => {
const mutualGDms = getMutualGroupDms(user.id);
if (mutualGDms.length === 0) return null;
return (
<>
{Divider}
{hasDivider && Divider}
<ExpandableList
listClassName={listStyle}
header={"Mutual Groups"}

View file

@ -20,7 +20,7 @@ import {
findGroupChildrenByChildId,
NavContextMenuPatchCallback
} from "@api/ContextMenu";
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { CogWheel } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -115,8 +115,6 @@ function applyDefaultSettings(guildId: string | null) {
}
}
migratePluginSettings("NewGuildSettings", "MuteNewGuild");
export default definePlugin({
name: "NewGuildSettings",
description: "Automatically mute new servers and change various other settings upon joining",

View file

@ -16,20 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getUserSettingLazy } from "@api/UserSettings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
const DisableStreamPreviews = getUserSettingLazy<boolean>("voiceAndVideo", "disableStreamPreviews")!;
// @TODO: Delete this plugin in the future
export default definePlugin({
name: "NoScreensharePreview",
description: "Disables screenshare previews from being sent.",
authors: [Devs.Nuckyz],
patches: [
{
find: '"ApplicationStreamPreviewUploadManager"',
replacement: {
match: /await \i\.\i\.(makeChunkedRequest\(|post\(\{url:)\i\.\i\.STREAM_PREVIEW.+?\}\)/g,
replace: "0"
}
start() {
if (!DisableStreamPreviews.getSetting()) {
DisableStreamPreviews.updateSetting(true);
}
]
},
stop() {
if (DisableStreamPreviews.getSetting()) {
DisableStreamPreviews.updateSetting(false);
}
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
@ -41,7 +41,6 @@ const settings = definePluginSettings({
},
});
migratePluginSettings("PartyMode", "Party mode 🎉");
export default definePlugin({
name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨",

View file

@ -22,12 +22,12 @@ import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByCodeLazy } from "@webpack";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils";
import { cl, getGuildPermissionSpecMap } from "../utils";
import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons";
export const enum PermissionType {
@ -56,7 +56,7 @@ function getRoleIconSrc(role: Role) {
}
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {
permissions.sort((a, b) => a.type - b.type);
const guildPermissionSpecMap = useMemo(() => getGuildPermissionSpecMap(guild), [guild.id]);
useStateFromStores(
[GuildMemberStore],
@ -65,6 +65,10 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
(old, current) => old.length === current.length
);
useEffect(() => {
permissions.sort((a, b) => a.type - b.type);
}, [permissions]);
useEffect(() => {
const usersToRequest = permissions
.filter(p => p.type === PermissionType.User && !GuildMemberStore.isMember(guild.id, p.id!))
@ -173,7 +177,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</ScrollerThin>
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.entries(PermissionsBits).map(([permissionName, bit]) => (
{Object.values(PermissionsBits).map(bit => (
<div className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
@ -192,9 +196,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
return PermissionDefaultIcon();
})()}
</div>
<Text variant="text-md/normal">{getPermissionString(permissionName)}</Text>
<Text variant="text-md/normal">{guildPermissionSpecMap[String(bit)].title}</Text>
<Tooltip text={getPermissionDescription(permissionName) || "No Description"}>
<Tooltip text={
(() => {
const { description } = guildPermissionSpecMap[String(bit)];
return typeof description === "function" ? i18n.intl.format(description, {}) : description;
})()
}>
{props => <InfoIcon {...props} />}
</Tooltip>
</div>

View file

@ -17,7 +17,6 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { ExpandableHeader } from "@components/ExpandableHeader";
import { getIntlMessage } from "@utils/discord";
import { classes } from "@utils/misc";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
@ -25,7 +24,7 @@ import { PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/com
import type { Guild, GuildMember } from "discord-types/general";
import { PermissionsSortOrder, settings } from "..";
import { cl, getPermissionString, getSortedRoles, sortUserRoles } from "../utils";
import { cl, getGuildPermissionSpecMap, getSortedRoles, sortUserRoles } from "../utils";
import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./RolesAndUsersPermissions";
interface UserPermission {
@ -87,9 +86,11 @@ function GrantedByTooltip({ roleName, roleColor }: GrantedByTooltipProps) {
);
}
function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { guild: Guild; guildMember: GuildMember; forceOpen?: boolean; }) {
function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild: Guild; guildMember: GuildMember; closePopout: () => void; }) {
const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const guildPermissionSpecMap = useMemo(() => getGuildPermissionSpecMap(guild), [guild.id]);
const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermissions = [];
@ -106,7 +107,7 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n)
});
const OWNER = getIntlMessage("GUILD_OWNER") || "Server Owner";
const OWNER = getIntlMessage("GUILD_OWNER") ?? "Server Owner";
userPermissions.push({
permission: OWNER,
roleName: "Owner",
@ -117,11 +118,11 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
sortUserRoles(userRoles);
for (const [permission, bit] of Object.entries(PermissionsBits)) {
for (const bit of Object.values(PermissionsBits)) {
for (const { permissions, colorString, position, name } of userRoles) {
if ((permissions & bit) === bit) {
userPermissions.push({
permission: getPermissionString(permission),
permission: guildPermissionSpecMap[String(bit)].title,
roleName: name,
roleColor: colorString || "var(--primary-300)",
rolePosition: position
@ -137,26 +138,15 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
return [rolePermissions, userPermissions];
}, [permissionsSortOrder]);
return (
<ExpandableHeader
forceOpen={forceOpen}
headerText="Permissions"
moreTooltipText="Role Details"
onMoreClick={() =>
openRolesAndUsersPermissionsModal(
rolePermissions,
guild,
guildMember.nick || UserStore.getUser(guildMember.userId).username
)
}
onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state}
defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[
return <div>
<div className={cl("user-header-container")}>
<Text variant="eyebrow">Permissions</Text>
<div className={cl("user-header-btns")}>
<Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => (
<div
{...tooltipProps}
className={cl("user-sortorder-btn")}
className={cl("user-header-btn")}
role="button"
tabIndex={0}
onClick={() => {
@ -164,8 +154,8 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
}}
>
<svg
width="20"
height="20"
width="24"
height="24"
viewBox="0 96 960 960"
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
@ -174,24 +164,46 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
</div>
)}
</Tooltip>
]}>
{userPermissions.length > 0 && (
<div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor, roleName }) => (
<Tooltip
text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
tooltipClassName={cl("granted-by-container")}
tooltipContentClassName={cl("granted-by-content")}
<Tooltip text="Role Details">
{tooltipProps => (
<div
{...tooltipProps}
className={cl("user-header-btn")}
role="button"
tabIndex={0}
onClick={() => {
closePopout();
openRolesAndUsersPermissionsModal(rolePermissions, guild, guildMember.nick || UserStore.getUser(guildMember.userId).username);
}}
>
{tooltipProps => (
<FakeRole {...tooltipProps} text={permission} color={roleColor} />
)}
</Tooltip>
))}
</div>
)}
</ExpandableHeader>
);
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</div>
)}
</Tooltip>
</div>
</div>
{userPermissions.length > 0 && (
<div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor, roleName }) => (
<Tooltip
text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
tooltipClassName={cl("granted-by-container")}
tooltipContentClassName={cl("granted-by-content")}
>
{tooltipProps => (
<FakeRole {...tooltipProps} text={permission} color={roleColor} />
)}
</Tooltip>
))}
</div>
)}
</div>;
}
export default ErrorBoundary.wrap(UserPermissionsComponent, { noop: true });

View file

@ -56,11 +56,6 @@ export const settings = definePluginSettings({
{ label: "Lowest Role", value: PermissionsSortOrder.LowestRole }
]
},
defaultPermissionsDropdownState: {
description: "Whether the permissions dropdown on user popouts should be open by default",
type: OptionType.BOOLEAN,
default: false
}
});
function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
@ -182,9 +177,9 @@ export default definePlugin({
<Popout
position="bottom"
align="center"
renderPopout={() => (
renderPopout={({ closePopout }) => (
<Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
<UserPermissions guild={guild} guildMember={guildMember} forceOpen />
<UserPermissions guild={guild} guildMember={guildMember} closePopout={closePopout} />
</Dialog>
)}
>

View file

@ -1,12 +1,22 @@
/* User Permissions Component */
.vc-permviewer-user-sortorder-btn {
.vc-permviewer-user-header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.vc-permviewer-user-header-btns {
display: flex;
gap: 8px;
}
.vc-permviewer-user-header-btn {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
/* RolesAndUsersPermissions Component */

View file

@ -17,57 +17,17 @@
*/
import { classNameFactory } from "@api/Styles";
import { getIntlMessage } from "@utils/discord";
import { wordsToTitle } from "@utils/text";
import { GuildStore, Parser } from "@webpack/common";
import { findByPropsLazy } from "@webpack";
import { GuildStore } from "@webpack/common";
import { Guild, GuildMember, Role } from "discord-types/general";
import type { ReactNode } from "react";
import { PermissionsSortOrder, settings } from ".";
import { PermissionType } from "./components/RolesAndUsersPermissions";
export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermissionSpecMap");
export const cl = classNameFactory("vc-permviewer-");
function formatPermissionWithoutMatchingString(permission: string) {
return wordsToTitle(permission.toLowerCase().split("_"));
}
// because discord is unable to be consistent with their names
const PermissionKeyMap = {
MANAGE_GUILD: "MANAGE_SERVER",
MANAGE_GUILD_EXPRESSIONS: "MANAGE_EXPRESSIONS",
CREATE_GUILD_EXPRESSIONS: "CREATE_EXPRESSIONS",
MODERATE_MEMBERS: "MODERATE_MEMBER", // HELLOOOO ??????
STREAM: "VIDEO",
SEND_VOICE_MESSAGES: "ROLE_PERMISSIONS_SEND_VOICE_MESSAGE",
} as const;
export function getPermissionString(permission: string) {
permission = PermissionKeyMap[permission] || permission;
return getIntlMessage(permission) ||
// shouldn't get here but just in case
formatPermissionWithoutMatchingString(permission);
}
export function getPermissionDescription(permission: string): ReactNode {
// DISCORD PLEEEEEEEEAAAAASE IM BEGGING YOU :(
if (permission === "USE_APPLICATION_COMMANDS")
permission = "USE_APPLICATION_COMMANDS_GUILD";
else if (permission === "SEND_VOICE_MESSAGES")
permission = "SEND_VOICE_MESSAGE_GUILD";
else if (permission !== "STREAM")
permission = PermissionKeyMap[permission] || permission;
const msg = getIntlMessage(`ROLE_PERMISSIONS_${permission}_DESCRIPTION`) as any;
if (msg?.hasMarkdown)
return Parser.parse(msg.message);
if (typeof msg === "string") return msg;
return "";
}
export function getSortedRoles({ id }: Guild, member: GuildMember) {
const roles = GuildStore.getRoles(id);

View file

@ -5,7 +5,6 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
@ -25,7 +24,6 @@ const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild;
);
};
migratePluginSettings("ServerInfo", "ServerProfile"); // what was I thinking with this name lmao
export default definePlugin({
name: "ServerInfo",
description: "Allows you to view info about a server",

View file

@ -20,9 +20,9 @@ import { addServerListElement, removeServerListElement, ServerListRenderPosition
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { GuildStore, PresenceStore, RelationshipStore } from "@webpack/common";
import { findStoreLazy } from "@webpack";
import { GuildStore, PresenceStore, RelationshipStore, useStateFromStores } from "@webpack/common";
const enum IndicatorType {
SERVER = 1 << 0,
@ -30,13 +30,24 @@ const enum IndicatorType {
BOTH = SERVER | FRIEND,
}
let onlineFriends = 0;
let guildCount = 0;
let forceUpdateFriendCount: () => void;
let forceUpdateGuildCount: () => void;
const UserGuildJoinRequestStore = findStoreLazy("UserGuildJoinRequestStore");
function FriendsIndicator() {
forceUpdateFriendCount = useForceUpdater();
const onlineFriendsCount = useStateFromStores([RelationshipStore, PresenceStore], () => {
let count = 0;
const friendIds = RelationshipStore.getFriendIDs();
for (const id of friendIds) {
const status = PresenceStore.getStatus(id) ?? "offline";
if (status === "offline") {
continue;
}
count++;
}
return count;
});
return (
<span id="vc-friendcount" style={{
@ -48,13 +59,19 @@ function FriendsIndicator() {
textTransform: "uppercase",
textAlign: "center",
}}>
{onlineFriends} online
{onlineFriendsCount} online
</span>
);
}
function ServersIndicator() {
forceUpdateGuildCount = useForceUpdater();
const guildCount = useStateFromStores([GuildStore, UserGuildJoinRequestStore], () => {
const guildJoinRequests: string[] = UserGuildJoinRequestStore.computeGuildIds();
const guilds = GuildStore.getGuilds();
// Filter only pending guild join requests
return GuildStore.getGuildCount() + guildJoinRequests.filter(id => guilds[id] == null).length;
});
return (
<span id="vc-guildcount" style={{
@ -71,24 +88,6 @@ function ServersIndicator() {
);
}
function handlePresenceUpdate() {
onlineFriends = 0;
const relations = RelationshipStore.getRelationships();
for (const id of Object.keys(relations)) {
const type = relations[id];
// FRIEND relationship type
if (type === 1 && PresenceStore.getStatus(id) !== "offline") {
onlineFriends += 1;
}
}
forceUpdateFriendCount?.();
}
function handleGuildUpdate() {
guildCount = GuildStore.getGuildCount();
forceUpdateGuildCount?.();
}
export default definePlugin({
name: "ServerListIndicators",
description: "Add online friend count or server count in the server list",
@ -117,18 +116,8 @@ export default definePlugin({
</ErrorBoundary>;
},
flux: {
PRESENCE_UPDATES: handlePresenceUpdate,
GUILD_CREATE: handleGuildUpdate,
GUILD_DELETE: handleGuildUpdate,
},
start() {
addServerListElement(ServerListRenderPosition.Above, this.renderIndicator);
handlePresenceUpdate();
handleGuildUpdate();
},
stop() {

View file

@ -168,7 +168,7 @@ export default definePlugin({
},
// Add the hidden eye icon if the channel is hidden
{
match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/,
match: /\.name,{.{0,140}\.children.+?:null(?<=,channel:(\i).+?)/,
replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null`
},
// Make voice channels also appear as muted if they are muted

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginSettingDef } from "@utils/types";
@ -35,7 +35,6 @@ const settings = definePluginSettings({
disableDisallowedDiscoveryFilters: opt("Disable filters in Server Discovery search that hide NSFW & disallowed servers."),
});
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({
name: "ShowHiddenThings",
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
@ -75,6 +74,15 @@ export default definePlugin({
replace: "$1$2arguments[0].member.highestRoleId]",
}
},
// allows you to open mod view on yourself
{
find: ".MEMBER_SAFETY,{modViewPanel:",
predicate: () => settings.store.showModView,
replacement: {
match: /\i(?=\?null)/,
replace: "false"
}
},
{
find: "prod_discoverable_guilds",
predicate: () => settings.store.disableDiscoveryFilters,

View file

@ -54,9 +54,17 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
tooltip={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}
onClick={toggle}
>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ scale: "1.2" }}>
<path fill="currentColor" mask="url(#silent-typing-msg-mask)" d="M18.333 15.556H1.667a1.667 1.667 0 0 1 -1.667 -1.667v-10a1.667 1.667 0 0 1 1.667 -1.667h16.667a1.667 1.667 0 0 1 1.667 1.667v10a1.667 1.667 0 0 1 -1.667 1.667M4.444 6.25V4.861a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333V8.194a0.417 0.417 0 0 0 -0.417 -0.417H4.306a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417H7.639a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m10 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h8.056a0.417 0.417 0 0 0 0.417 -0.417m3.333 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417" transform="translate(2, 3)" />
{isEnabled && (
<>
<mask id="silent-typing-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z"></path>
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" transform="translate(-2, -3)"></path>
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>
)}
</svg>
</ChatBarButton>
);

View file

@ -163,7 +163,7 @@ export default definePlugin({
{
find: "UNREAD_IMPORTANT:",
replacement: {
match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/,
match: /\.name,{.{0,140}\.children.+?:null(?<=,channel:(\i).+?)/,
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
}
},

View file

@ -91,34 +91,31 @@ export default definePlugin({
name: "TypingTweaks",
description: "Show avatars and role colours in the typing indicator",
authors: [Devs.zt],
settings,
patches: [
// Style the indicator and add function call to modify the children before rendering
{
find: "getCooldownTextStyle",
replacement: {
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i/,
replace: "$self.mutateChildren(this.props, $1, $&), style: $self.TYPING_TEXT_STYLE"
}
},
// Changes the indicator to keep the user object when creating the list of typing users
{
find: "getCooldownTextStyle",
replacement: {
match: /(?<=map\(\i=>)\i\.\i\.getName\(\i,this\.props\.channel\.id,(\i)\)/,
replace: "$1"
}
},
// Adds the alternative formatting for several users typing
{
find: "getCooldownTextStyle",
replacement: {
match: /(,{a:(\i),b:(\i),c:\i}\):)\i\.\i\.string\(\i\.\i#{intl::SEVERAL_USERS_TYPING}\)(?<=(\i)\.length.+?)/,
replace: (_, rest, a, b, users) => `${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`
},
predicate: () => settings.store.alternativeFormatting
find: "#{intl::THREE_USERS_TYPING}",
replacement: [
{
// Style the indicator and add function call to modify the children before rendering
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i(?<=guildId:(\i).+?)/,
replace: "$self.mutateChildren($2,$1,$&),style:$self.TYPING_TEXT_STYLE"
},
{
// Changes the indicator to keep the user object when creating the list of typing users
match: /\.map\((\i)=>\i\.\i\.getName\(\i,\i\.id,\1\)\)/,
replace: ""
},
{
// Adds the alternative formatting for several users typing
match: /(,{a:(\i),b:(\i),c:\i}\):\i\.length>3&&\(\i=)\i\.\i\.string\(\i\.\i#{intl::SEVERAL_USERS_TYPING}\)(?<=(\i)\.length.+?)/,
replace: (_, rest, a, b, users) => `${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`,
predicate: () => settings.store.alternativeFormatting
}
]
}
],
settings,
TYPING_TEXT_STYLE: {
display: "grid",
@ -128,7 +125,7 @@ export default definePlugin({
buildSeveralUsers,
mutateChildren(props: any, users: User[], children: any) {
mutateChildren(guildId: any, users: User[], children: any) {
try {
if (!Array.isArray(children)) {
return children;
@ -138,7 +135,7 @@ export default definePlugin({
return children.map(c =>
c.type === "strong" || (typeof c !== "string" && !React.isValidElement(c))
? <TypingUser {...props} user={users[element++]} />
? <TypingUser guildId={guildId} user={users[element++]} />
: c
);
} catch (e) {

View file

@ -0,0 +1,46 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
name: "UseAlternativeSearch",
description: "Use alternative search engine in right click menu",
authors: [
{
id: 1038096782963507210n,
name: "skyevg",
},
],
patches: [
{
find: "https://www.google.com/search?q=",
replacement: {
match: /"https:\/\/www.google.com\/search\?q=".concat\(encodeURIComponent\(e\)\)/,
replace: "Vencord.Settings.plugins.UseAlternativeSearch.source.replace(\"!QUERY!\", encodeURIComponent(e))"
}
}
],
options: {
source: {
description: "Search engine's url (use !QUERY! as replacement for the search term)",
type: OptionType.STRING,
default: "https://duckduckgo.com/?q=!QUERY!",
}
}
});

143
src/plugins/uwuifier.ts Normal file
View file

@ -0,0 +1,143 @@
/*
* 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 { findOption, RequiredMessageOption } from "@api/Commands";
import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const endings = [
"rawr x3",
"OwO",
"UwU",
"o.O",
"-.-",
">w<",
"(⑅˘꒳˘)",
"(ꈍᴗꈍ)",
"(˘ω˘)",
"(U ᵕ U❁)",
"σωσ",
"òωó",
"(///ˬ///✿)",
"(U U)",
"( ͡o ω ͡o )",
"ʘwʘ",
":3",
":3", // important enough to have twice
"XD",
"nyaa~~",
"mya",
">_<",
"😳",
"🥺",
"😳😳😳",
"rawr",
"^^",
"^^;;",
"(ˆˆ)♡",
"^•ﻌ•^",
"/(^•ω•^)",
"(✿oωo)"
];
const replacements = [
["small", "smol"],
["cute", "kawaii~"],
["fluff", "floof"],
["love", "luv"],
["stupid", "baka"],
["what", "nani"],
["meow", "nya~"],
["hello", "hewwo"],
];
const settings = definePluginSettings({
uwuEveryMessage: {
description: "Make every single message uwuified",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: false
}
});
function selectRandomElement(arr) {
// generate a random index based on the length of the array
const randomIndex = Math.floor(Math.random() * arr.length);
// return the element at the randomly generated index
return arr[randomIndex];
}
function uwuify(message: string): string {
message = message.toLowerCase();
// words
for (const pair of replacements) {
message = message.replaceAll(pair[0], pair[1]);
}
message = message
.replaceAll(/([ \t\n])n/g, "$1ny") // nyaify
.replaceAll(/[lr]/g, "w") // [lr] > w
.replaceAll(/([ \t\n])([a-z])/g, (_, p1, p2) => Math.random() < .5 ? `${p1}${p2}-${p2}` : `${p1}${p2}`) // stutter
.replaceAll(/([^.,!][.,!])([ \t\n])/g, (_, p1, p2) => `${p1} ${selectRandomElement(endings)}${p2}`); // endings
return message;
}
// actual command declaration
export default definePlugin({
name: "UwUifier",
description: "Simply uwuify commands",
authors: [Devs.echo, Devs.skyevg, Devs.PandaNinjas],
dependencies: ["CommandsAPI", "MessageEventsAPI"],
settings,
commands: [
{
name: "uwuify",
description: "uwuifies your messages",
options: [RequiredMessageOption],
execute: opts => ({
content: uwuify(findOption(opts, "message", "")),
}),
},
],
onSend(msg: MessageObject) {
// Only run when it's enabled
if (settings.store.uwuEveryMessage) {
msg.content = uwuify(msg.content);
}
},
start() {
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
this.onSend(msg)
);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
},
});

View file

@ -164,8 +164,8 @@ export default definePlugin({
find: 'getElementById("slate-toolbar"',
predicate: () => settings.store.addBack,
replacement: {
match: /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\?/,
replace: "||true?"
match: /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\)/,
replace: "||true)"
}
},
{
@ -216,9 +216,12 @@ export default definePlugin({
replace: "true"
},
{
match: /\i\.\i\.copy/,
match: /\i\.\i\.copy(?=\(\i)/,
replace: "Vencord.Webpack.Common.Clipboard.copy"
}]
}
],
all: true,
noWarn: true
},
// Automod add filter words
{

View file

@ -4,12 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// The entire code of this plugin can be found in native.ts
migratePluginSettings("YoutubeAdblock", "WatchTogetherAdblock");
export default definePlugin({
name: "YoutubeAdblock",
description: "Block ads in YouTube embeds and the WatchTogether activity via AdGuard",

View file

@ -22,6 +22,7 @@ import { findByPropsLazy, waitFor } from "../webpack";
export let React: typeof import("react");
export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect;
export let useLayoutEffect: typeof React.useLayoutEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export let useReducer: typeof React.useReducer;
@ -31,5 +32,5 @@ export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/cli
waitFor("useState", m => {
React = m;
({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
({ useEffect, useState, useLayoutEffect, useMemo, useRef, useReducer, useCallback } = React);
});