Compare commits

..

29 Commits

Author SHA1 Message Date
V
25a1d934c6 bump to v1.4.7 2023-09-09 00:22:24 +02:00
Luna
620319a4bc blurnsfw: fix crash (#31)
Reviewed-on: https://codeberg.org/Ven/cord/pulls/31
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-08 21:55:27 +00:00
Hugo C
4c9996d620 feat(plugin): PictureInPicture (#1697)
Co-authored-by: V <vendicated@riseup.net>
2023-09-08 03:57:44 +02:00
V
885c75fdaa add custom plugin author popouts (#1712) 2023-09-08 03:42:20 +02:00
Lewis Crichton
f2a22c5e57 feat: crash info in /vencord-debug (#1714)
Co-authored-by: V <vendicated@riseup.net>
2023-09-08 03:41:30 +02:00
V
5e3a485edc ci: generate plugin readme map 2023-09-08 02:26:47 +02:00
Luna
452bf72e56 rnnoise: fix error on webcam (#29)
errored because there was no audio stream, luckily the patched function had a boolean indicating if it was audio or not so just ignore it if it isnt

Reviewed-on: https://codeberg.org/Ven/cord/pulls/29
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-06 17:10:33 +00:00
Luna
afa47addd7 usrbg: export the data to allow other plugins to access (#30)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Ven <ven@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/Ven/cord/pulls/30
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-06 17:10:04 +00:00
V
8ecee3d09f fix ViewRaw & VoiceMessages context menu style 2023-09-06 19:08:48 +02:00
luk1337
d2aac5edc5 ClearURLs: Add share_id parameter on reddit.com (#1706) 2023-09-06 18:53:28 +02:00
lovenginx
fc45510053 LoadingQuotes: add a new quote (#1710) 2023-09-06 18:52:14 +02:00
AutumnVN
77bfaf38a5 HideAttachments: fix broken css (#1707) 2023-09-06 18:51:48 +02:00
AutumnVN
a4cadc03d8 viewRaw: dont add context menu to guild folders (#1708) 2023-09-06 18:51:18 +02:00
AutumnVN
315fcf1972 messageLogger: make ignoreChannels also ignore threads (#1709) 2023-09-06 18:50:20 +02:00
Nuckyz
f1b3b3c0a3 Uncanonicalize reported patches 2023-09-06 18:47:54 +02:00
V
4c805d08be [skip ci] PatchHelper: Fix copy button style 2023-09-05 21:37:39 +02:00
V
9b987d1e56 Bump to v1.4.6 2023-09-05 21:25:16 +02:00
V
774318d583 ViewRaw: Update description 2023-09-05 21:24:43 +02:00
V
7d954f9ade ViewRaw: Fix ugly copy icon & context menu position 2023-09-05 21:20:13 +02:00
Luna
860d6edc7b HideAttachments: allow hiding stickers (#13)
Self explanatory

Reviewed-on: https://codeberg.org/Ven/cord/pulls/13
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:56:28 +00:00
Rini
223b0366c6 pinDMs: fix alt+shift+up (#25)
applies the same fix as alt+up/down to the jump to unread one

confirmed non-explosion of shc

Reviewed-on: https://codeberg.org/Ven/cord/pulls/25
Co-authored-by: Rini <rini@rinici.de>
Co-committed-by: Rini <rini@rinici.de>
2023-09-05 18:51:22 +00:00
Luna
69cb7593eb View Raw on more objects (#27)
Shows view raw on guilds, channels, and users

Reviewed-on: https://codeberg.org/Ven/cord/pulls/27
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:49:57 +00:00
V
8614e17633 Fix some plugins wrongly displaying the settings cog 2023-09-05 20:10:42 +02:00
Ryan Cao
aecd9d8fda feat(memberCount): format count according to user locale (#1679) 2023-09-05 19:46:33 +02:00
Hugo C
faeb4fb585 previewMessage: Hide the button once a message is sent (#1692)
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:45:44 +02:00
dolfies
0d18b44ba7 feat(plugin): ShowTimeouts (#1687)
Co-authored-by: TheKodeToad <TheKodeToad@proton.me>
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:42:35 +02:00
omahs
d671bd65ac fix typos (#1703) 2023-09-05 19:40:32 +02:00
AutumnVN
8e952c630b MessageLogger: fix ignore by id doesn't ignore edited (#1705) 2023-09-05 19:39:22 +02:00
V
2c758ccdf8 new plugin: ServerProfile (#1704)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-09-05 18:36:14 +02:00
36 changed files with 543 additions and 112 deletions

View File

@ -38,7 +38,7 @@ jobs:
run: pnpm build --standalone
- name: Generate plugin list
run: pnpm generatePluginJson dist/plugins.json
run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json
- name: Clean up obsolete files
run: |

View File

@ -65,7 +65,7 @@ Also pay attention to the following:
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
#### "replace"

View File

@ -63,7 +63,7 @@ Then fully close Discord from your taskbar or task manager, and restart it. Venc
If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be neccessary to manually update if the GUI updater fails.
Sometimes it may be necessary to manually update if the GUI updater fails.
To pull latest changes:

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.4.5",
"version": "1.4.7",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View File

@ -165,7 +165,11 @@ async function parseFile(fileName: string) {
data.target = target as any;
}
return data;
let readme = "";
try {
readme = readFileSync(join(fileName, "..", "README.md"), "utf-8");
} catch { }
return [data, readme] as const;
}
throw fail("no default export called 'definePlugin' found");
@ -194,18 +198,24 @@ function isPluginFile({ name }: { name: string; }) {
(async () => {
parseDevs();
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
const plugins = [] as PluginData[];
const readmes = {} as Record<string, string>;
await Promise.all(["src/plugins", "src/plugins/_core"].flatMap(dir =>
readdirSync(dir, { withFileTypes: true })
.filter(isPluginFile)
.map(async dirent =>
parseFile(await getEntryPoint(dir, dirent))
)
);
.map(async dirent => {
const [data, readme] = await parseFile(await getEntryPoint(dir, dirent));
plugins.push(data);
if (readme) readmes[data.name] = readme;
})
));
const data = JSON.stringify(await Promise.all(plugins));
const data = JSON.stringify(plugins);
if (process.argv.length > 2) {
if (process.argv.length > 3) {
writeFileSync(process.argv[2], data);
writeFileSync(process.argv[3], JSON.stringify(readmes));
} else {
console.log(data);
}

View File

@ -171,7 +171,7 @@ page.on("console", async e => {
plugin,
type,
id,
match: regex,
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause
});
break;

View File

@ -0,0 +1,21 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByPropsLazy } from "@webpack";
import { Parser } from "@webpack/common";
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
/**
* Renders code in a Discord codeblock
*/
export function CodeBlock(props: { content?: string, lang: string; }) {
return (
<div className={CodeContainerClasses.markup}>
{Parser.defaultRules.codeBlock.react(props, null, {})}
</div>
);
}

View File

@ -0,0 +1,113 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./contributorModal.css";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { DevsById } from "@utils/constants";
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general";
import Plugins from "~plugins";
import { PluginCard } from ".";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
const cl = classNameFactory("vc-author-modal-");
export function openContributorModal(user: User) {
openModal(modalProps =>
<ModalRoot {...modalProps}>
<ErrorBoundary>
<ModalContent className={cl("root")}>
<ContributorModal user={user} />
</ModalContent>
</ErrorBoundary>
</ModalRoot>
);
}
function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} alt="GitHub" />;
}
function WebsiteIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
return <img src={src} alt="Website" />;
}
function ContributorModal({ user }: { user: User; }) {
useSettings();
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
useEffect(() => {
if (!profile && !user.bot && user.id)
fetchUserProfile(user.id);
}, [user.id]);
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
const plugins = useMemo(() => {
const allPlugins = Object.values(Plugins);
const pluginsByAuthor = DevsById[user.id]
? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))
: allPlugins.filter(p => p.authors.some(a => a.name === user.username));
return pluginsByAuthor
.filter(p => !p.name.endsWith("API"))
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
}, [user.id, user.username]);
return (
<>
<div className={cl("header")}>
<img
className={cl("avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
/>
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
<div className={cl("links")}>
{website && (
<MaskedLink
href={"https://" + website}
>
<WebsiteIcon />
</MaskedLink>
)}
{githubName && (
<MaskedLink href={`https://github.com/${githubName}`}>
<GithubIcon />
</MaskedLink>
)}
</div>
</div>
<div className={cl("plugins")}>
{plugins.map(p =>
<PluginCard
key={p.name}
plugin={p}
disabled={p.required ?? false}
onRestartNeeded={() => showToast("Restart to apply changes!")}
/>
)}
</div>
</>
);
}

View File

@ -18,17 +18,16 @@
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
@ -41,7 +40,7 @@ import {
SettingSliderComponent,
SettingTextComponent
} from "./components";
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
import { openContributorModal } from "./ContributorModal";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
@ -89,30 +88,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const canSubmit = () => Object.values(errors).every(e => !e);
const hasSettings = Boolean(pluginSettings && plugin.options);
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
React.useEffect(() => {
enableStyle(hideBotTagStyle);
let originalUser: User;
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.fetchUser(`${user.id}`)
// only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
.then(u => (originalUser = u, makeDummyUser(u)))
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
setAuthors(a => [...a, author]);
}
})();
return () => {
disableStyle(hideBotTagStyle);
if (originalUser)
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
};
}, []);
async function saveAndClose() {
@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => openContributorModal(user)}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
</div>
</Forms.FormSection>

View File

@ -0,0 +1,57 @@
.vc-author-modal-root {
padding: 1em;
}
.vc-author-modal-header {
display: flex;
align-items: center;
margin-bottom: 1em;
}
.vc-author-modal-name {
text-transform: none;
flex-grow: 0;
background: var(--background-tertiary);
border-radius: 0 9999px 9999px 0;
padding: 6px 0.8em 6px 0.5em;
font-size: 20px;
height: 20px;
position: relative;
}
.vc-author-modal-name::before {
content: "";
display: block;
position: absolute;
height: 100%;
width: 16px;
background: var(--background-tertiary);
z-index: -1;
left: -16px;
top: 0;
}
.vc-author-modal-avatar {
height: 32px;
width: 32px;
border-radius: 50%;
}
.vc-author-modal-links {
margin-left: auto;
display: flex;
gap: 0.2em;
}
.vc-author-modal-links img {
height: 32px;
width: 32px;
border-radius: 50%;
border: 4px solid var(--background-tertiary);
box-sizing: border-box
}
.vc-author-modal-plugins {
display: grid;
gap: 0.5em;
}

View File

@ -28,7 +28,7 @@ import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types";
@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
isNew?: boolean;
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
@ -161,7 +161,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
onMouseLeave={onMouseLeave}
infoButton={
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options
{plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel />
: <InfoIcon width="24" height="24" />}
</button>

View File

@ -1,3 +0,0 @@
[class|="userPopoutOuter"] [class*="botTag"] {
display: none;
}

View File

@ -17,6 +17,7 @@
*/
import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
@ -299,7 +300,7 @@ function PatchHelper() {
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</>
)}

View File

@ -8,6 +8,7 @@
width: 100%;
transition: 0.1s ease-out;
transition-property: box-shadow, transform, background, opacity;
box-sizing: border-box;
}
.vc-addon-card-disabled {

View File

@ -49,7 +49,7 @@ export default definePlugin({
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}, {
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')"
}]
}
],

View File

@ -136,4 +136,5 @@ export const defaultRules = [
"utm_term",
"si@open.spotify.com",
"igshid",
"share_id@reddit.com",
];

View File

@ -49,7 +49,7 @@ export default definePlugin({
await this.buildCss();
addButton("HideAttachments", msg => {
if (!msg.attachments.length && !msg.embeds.length) return null;
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id);
@ -72,7 +72,7 @@ export default definePlugin({
async buildCss() {
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
style.textContent = `
:is(${elements}) [class*="embedWrapper"] {
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
/* important is not necessary, but add it to make sure bad themes won't break it */
display: none !important;
}

View File

@ -56,7 +56,8 @@ const quotes = [
"hd{b${",
"<;vqkijbq33271:56<3799?24944:",
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno"
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno",
"Wdn`khc'|f*eghl{%"
];
export default definePlugin({

View File

@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
};
const sharedIntlNumberFormat = new Intl.NumberFormat();
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
function MemberCount() {
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const { groups } = useStateFromStores(
@ -57,7 +60,7 @@ function MemberCount() {
alignContent: "center",
gap: 0
}}>
<Tooltip text={`${online} Online in this Channel`} position="bottom">
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span
@ -70,11 +73,11 @@ function MemberCount() {
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--green-360)" }}>{online}</span>
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${total} Total Server Members`} position="bottom">
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
{props => (
<div {...props}>
<span
@ -88,7 +91,7 @@ function MemberCount() {
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--primary-400)" }}>{total}</span>
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
</div>
)}
</Tooltip>

View File

@ -169,21 +169,14 @@ export default definePlugin({
try {
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
function mutate(id: string) {
const mutate = (id: string) => {
const msg = cache.get(id);
if (!msg) return;
const EPHEMERAL = 64;
const shouldIgnore = data.mlDeleted ||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
ignoreBots && msg.author?.bot ||
ignoreSelf && msg.author?.id === myId ||
ignoreUsers.includes(msg.author?.id) ||
ignoreChannels.includes(msg.channel_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
this.shouldIgnore(msg);
if (shouldIgnore) {
cache = cache.remove(id);
@ -192,7 +185,7 @@ export default definePlugin({
.set("deleted", true)
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
}
}
};
if (isBulk) {
data.ids.forEach(mutate);
@ -205,6 +198,18 @@ export default definePlugin({
return cache;
},
shouldIgnore(message: any) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
ignoreSelf && message.author?.id === myId ||
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
patches: [
{
@ -237,7 +242,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" +

View File

@ -0,0 +1,83 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React, Tooltip } from "@webpack/common";
const settings = definePluginSettings({
loop: {
description: "Whether to make the PiP video loop or not",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});
export default definePlugin({
name: "PictureInPicture",
description: "Adds picture in picture to videos (next to the Download button)",
authors: [Devs.Lumap],
settings,
patches: [
{
find: ".onRemoveAttachment,",
replacement: {
match: /\.nonMediaAttachment.{0,10}children:\[(\i),/,
replace: "$&$1&&$self.renderPiPButton(),"
},
},
],
renderPiPButton: ErrorBoundary.wrap(() => {
return (
<Tooltip text="Toggle Picture in Picture">
{tooltipProps => (
<div
{...tooltipProps}
role="button"
style={{
cursor: "pointer",
paddingTop: "4px",
paddingLeft: "4px",
paddingRight: "4px",
}}
onClick={e => {
const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!;
const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;
videoClone.loop = settings.store.loop;
videoClone.style.display = "none";
videoClone.onleavepictureinpicture = () => videoClone.remove();
function launchPiP() {
videoClone.currentTime = video.currentTime;
videoClone.requestPictureInPicture();
video.pause();
videoClone.play();
}
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
launchPiP();
else
videoClone.onloadedmetadata = launchPiP;
}}
>
<svg width="24px" height="24px" viewBox="0 0 24 24">
<path
fill="var(--interactive-normal)"
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
/>
</svg>
</div>
)}
</Tooltip>
);
}, { noop: true })
});

View File

@ -122,6 +122,14 @@ export default definePlugin({
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
}
}
},
// fix alt+shift+up/down
{
find: '"alt+shift+down"',
replacement: {
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
}
},
]
});

View File

@ -20,7 +20,7 @@ import { sendBotMessage } from "@api/Commands";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
interface Props {
type: {
@ -31,10 +31,9 @@ interface Props {
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
export function PreviewButton(chatBoxProps: Props) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
const channelId = SelectedChannelStore.getChannelId();
const draft = getDraft(channelId);
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
if (chatBoxProps.type.analyticsName !== "normal") return null;
if (!draft) return null;
return (

View File

@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
import { findStoreLazy } from "@webpack";
import { UserStore } from "@webpack/common";
import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
const UserProfileStore = findStoreLazy("UserProfileStore");
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];

View File

@ -145,7 +145,7 @@ export default definePlugin({
find: "window.webkitAudioContext",
replacement: {
match: /(?<=\i\.acquire=function\((\i)\)\{return )navigator\.mediaDevices\.getUserMedia\(\1\)(?=\})/,
replace: m => `${m}.then(stream => $self.connectRnnoise(stream))`
replace: "$&.then(stream => $self.connectRnnoise(stream, $1.audio))"
},
},
{
@ -182,7 +182,8 @@ export default definePlugin({
setEnabled,
isEnabled: () => settings.store.isEnabled,
async connectRnnoise(stream: MediaStream): Promise<MediaStream> {
async connectRnnoise(stream: MediaStream, isAudio: boolean): Promise<MediaStream> {
if (!isAudio) return stream;
if (!settings.store.isEnabled) return stream;
const audioCtx = new AudioContext();

View File

@ -27,13 +27,12 @@ import { copyWithToast } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
import { Text, Tooltip } from "@webpack/common";
import { Text, Tooltip, UserProfileStore } from "@webpack/common";
import { User } from "discord-types/general";
import { VerifiedIcon } from "./VerifiedIcon";
const Section = LazyComponent(() => findByCode("().lastSection"));
const UserProfileStore = findStoreLazy("UserProfileStore");
const ThemeStore = findStoreLazy("ThemeStore");
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');

View File

@ -0,0 +1,35 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ShowTimeouts",
description: "Display member timeout icons in chat regardless of permissions.",
authors: [Devs.Dolfies],
patches: [
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
replace: "",
},
},
],
});

View File

@ -48,7 +48,7 @@ export default definePlugin({
name: "vencord-debug",
description: "Send Vencord Debug info",
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
execute() {
async execute() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
const client = (() => {
@ -75,6 +75,10 @@ export default definePlugin({
OpenAsar: "openasar" in window,
};
if (IS_DISCORD_DESKTOP) {
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
}
const debugInfo = `
**Vencord Debug Info**
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}

View File

@ -80,6 +80,9 @@ export default definePlugin({
}
],
data,
settingsAboutComponent: () => {
return (
<Link href="https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner">CLICK HERE TO GET YOUR OWN BANNER</Link>
@ -116,7 +119,9 @@ export default definePlugin({
enableStyle(style);
const res = await fetch(BASE_URL);
if (res.ok)
if (res.ok) {
data = await res.json();
this.data = data;
}
}
});

View File

@ -16,8 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
@ -25,12 +27,12 @@ import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general";
const CopyIcon = () => {
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="22" height="22">
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
</svg>;
@ -58,19 +60,7 @@ function cleanMessage(msg: Message) {
return clone;
}
function CodeBlock(props: { content: string, lang: string; }) {
return (
// make text selectable
<div style={{ userSelect: "text" }}>
{Parser.defaultRules.codeBlock.react(props, null, {})}
</div>
);
}
function openViewRawModal(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
function openViewRawModal(json: string, type: string, msgContent?: string) {
const key = openModal(props => (
<ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}>
@ -80,26 +70,28 @@ function openViewRawModal(msg: Message) {
</ModalHeader>
<ModalContent>
<div style={{ padding: "16px 0" }}>
{!!msg.content && (
{!!msgContent && (
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msg.content} lang="" />
<CodeBlock content={msgContent} lang="" />
<Forms.FormDivider className={Margins.bottom20} />
</>
)}
<Forms.FormTitle tag="h5">Message Data</Forms.FormTitle>
<CodeBlock content={msgJson} lang="json" />
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
<CodeBlock content={json} lang="json" />
</div>
</ModalContent >
<ModalFooter>
<Flex cellSpacing={10}>
<Button onClick={() => copyWithToast(msgJson, "Message data copied to clipboard!")}>
Copy Message JSON
</Button>
<Button onClick={() => copyWithToast(msg.content, "Content copied to clipboard!")}>
Copy Raw Content
<Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
Copy {type} JSON
</Button>
{!!msgContent && (
<Button onClick={() => copyWithToast(msgContent, "Content copied to clipboard!")}>
Copy Raw Content
</Button>
)}
</Flex>
</ModalFooter>
</ModalRoot >
@ -107,6 +99,13 @@ function openViewRawModal(msg: Message) {
));
}
function openViewRawModalMessage(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
return openViewRawModal(msgJson, "Message", msg.content);
}
const settings = definePluginSettings({
clickMethod: {
description: "Change the button to view the raw content/data of any message.",
@ -118,10 +117,34 @@ const settings = definePluginSettings({
}
});
function MakeContextCallback(name: string) {
const callback: NavContextMenuPatchCallback = (children, props) => () => {
if (name === "Guild" && !props.guild) return;
const lastChild = children.at(-1);
if (lastChild?.key === "developer-actions") {
const p = lastChild.props;
if (!Array.isArray(p.children))
p.children = [p.children];
({ children } = p);
}
children.splice(-1, 0,
<Menu.MenuItem
id={`vc-view-${name.toLowerCase()}-raw`}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)}
icon={CopyIcon}
/>
);
};
return callback;
}
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message.",
authors: [Devs.KingFish, Devs.Ven, Devs.rad],
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
@ -131,7 +154,7 @@ export default definePlugin({
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
} else {
openViewRawModal(msg);
openViewRawModalMessage(msg);
}
};
@ -143,7 +166,7 @@ export default definePlugin({
} else {
e.preventDefault();
e.stopPropagation();
openViewRawModal(msg);
openViewRawModalMessage(msg);
}
};
@ -160,9 +183,16 @@ export default definePlugin({
onContextMenu: handleContextMenu
};
});
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
addContextMenuPatch("user-context", MakeContextCallback("User"));
},
stop() {
removeButton("CopyRawMessage");
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
removeContextMenuPatch("user-context", MakeContextCallback("User"));
}
});

View File

@ -19,7 +19,6 @@
import "./styles.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Flex } from "@components/Flex";
import { Microphone } from "@components/Icons";
import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
@ -39,6 +38,7 @@ import { VoiceRecorderWeb } from "./WebRecorder";
const CloudUpload = findLazy(m => m.prototype?.uploadFileToCloud);
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
const PendingReplyStore = findStoreLazy("PendingReplyStore");
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
export type VoiceRecorder = ComponentType<{
setAudioBlob(blob: Blob): void;
@ -226,12 +226,10 @@ const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
<Menu.MenuItem
id="vc-send-vmsg"
label={
<>
<Flex flexDirection="row" style={{ alignItems: "center", gap: 8 }}>
<Microphone height={24} width={24} />
Send voice message
</Flex>
</>
<div className={OptionClasses.optionLabel}>
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
<div className={OptionClasses.optionName}>Send voice message</div>
</div>
}
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
/>

View File

@ -355,10 +355,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "bb010g",
id: 72791153467990016n,
},
Lumap: {
name: "lumap",
id: 635383782576357407n
},
Dolfies: {
name: "Dolfies",
id: 852892297661906993n,
},
RuukuLada: {
name: "RuukuLada",
id: 119705748346241027n,
},
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View File

@ -18,7 +18,7 @@
import { MessageObject } from "@api/MessageEvents";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common";
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileStore, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@ -118,6 +118,41 @@ export async function openUserProfile(id: string) {
});
}
interface FetchUserProfileOptions {
friend_token?: string;
connections_role_id?: string;
guild_id?: string;
with_mutual_guilds?: boolean;
with_mutual_friends_count?: boolean;
}
/**
* Fetch a user's profile
*/
export async function fetchUserProfile(id: string, options?: FetchUserProfileOptions) {
const cached = UserProfileStore.getUserProfile(id);
if (cached) return cached;
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
const { body } = await RestAPI.get({
url: `/users/${id}/profile`,
query: {
with_mutual_guilds: false,
with_mutual_friends_count: false,
...options
},
oldFormErrors: true,
});
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user });
await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body });
if (options?.guild_id && body.guild_member)
FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member });
return UserProfileStore.getUserProfile(id);
}
/**
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
*/

View File

@ -74,6 +74,16 @@ export function isObject(obj: unknown): obj is object {
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
}
/**
* Check if an object is empty or in other words has no own properties
*/
export function isObjectEmpty(obj: object) {
for (const k in obj)
if (Object.hasOwn(obj, k)) return false;
return true;
}
/**
* Returns null if value is not a URL, otherwise return URL object.
* Avoids having to wrap url checks in a try/catch

View File

@ -48,6 +48,7 @@ export let PoggerModeSettingsStore: GenericStore;
export let GuildStore: Stores.GuildStore & t.FluxStore;
export let UserStore: Stores.UserStore & t.FluxStore;
export let UserProfileStore: GenericStore;
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
export let SelectedGuildStore: t.FluxStore & Record<string, any>;
export let ChannelStore: Stores.ChannelStore & t.FluxStore;
@ -86,6 +87,7 @@ export const useStateFromStores: <T>(
waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s);
waitForStore("UserProfileStore", m => UserProfileStore = m);
waitForStore("ChannelStore", m => ChannelStore = m);
waitForStore("SelectedChannelStore", m => SelectedChannelStore = m);
waitForStore("SelectedGuildStore", m => SelectedGuildStore = m);

View File

@ -398,12 +398,18 @@ export type Paginator = ComponentType<{
hideMaxPage?: boolean;
}>;
export type MaskedLink = ComponentType<{
onClick(): void;
trusted: boolean;
title: string,
export type MaskedLink = ComponentType<PropsWithChildren<{
href: string;
}>;
rel?: string;
target?: string;
title?: string,
className?: string;
tabIndex?: number;
onClick?(): void;
trusted?: boolean;
messageId?: string;
channelId?: string;
}>>;
export type ScrollerThin = ComponentType<PropsWithChildren<{
className?: string;