Compare commits

...

27 Commits

Author SHA1 Message Date
V
c696c186e8 WebKeybinds: Fix scope 2023-07-02 01:23:51 +02:00
V
30d5e2108f Bump to 1.3.2 2023-07-02 01:22:53 +02:00
V
1eabd1b701 WebKeybinds: Fix broken zoom on vesktop 2023-07-02 01:22:37 +02:00
V
1cbf2b43e1 Fix Badges 2023-07-02 01:17:02 +02:00
V
4c197d5d51 New Plugin: WebKeyBinds 2023-07-02 01:08:06 +02:00
V
f89027f46a Bump to 1.3.1 2023-06-30 17:08:57 +02:00
V
07a0ebb1d2 PronounDB: Fix crash when having pronouns in profile disabled 2023-06-30 17:08:43 +02:00
V
f09b44b0d5 VcNarrator: Ignore stage channels 2023-06-30 15:55:20 +02:00
V
b607eebcb7 ImageZoom: Add square lens option 2023-06-30 15:50:56 +02:00
V
0936ca2985 ValidUser: Fix mentions with ! (<@!...>) 2023-06-30 15:43:21 +02:00
V
13bde79ec8 PronounDB: Fix profile patch, add pronoun source to tooltip 2023-06-30 15:39:33 +02:00
V
b592defaaf OpenInApp: Add Epic Games; properly respect settings 2023-06-30 15:16:42 +02:00
V
73354973a3 OpenInApp: Support steam; integrate with SpotifyControls & ShowConnections 2023-06-29 16:15:07 +02:00
V
e12c0e546c PronounDB: Fix crash 2023-06-29 14:28:16 +02:00
V
088a8bd1b6 OpenInApp: Support profile activity on web & profile connection 2023-06-29 05:02:13 +02:00
V
51adb26d01 New plugin: OpenInApp - Open spotify urls in spotify app 2023-06-29 04:33:35 +02:00
Nuckyz
cb980a1cad Fix broken SHC patches (#1360) 2023-06-28 03:45:16 +00:00
V
69b10c1f07 Bump to 1.3.0 2023-06-27 23:12:56 +02:00
alexia
8e9ba7c7ee PronounDB: fix caching not respecting user preference (#1344) 2023-06-27 22:46:52 +02:00
Commandtechno
12e3c9234d ViewIcons: Fix animated server pfps & profile edit button (#1350)
fixes #1236
2023-06-27 22:41:52 +02:00
Sammy
1d8dcef394 InvisibleChat: fix indicator & crashes (#1356) 2023-06-27 22:41:33 +02:00
Syncx
4fe2845234 ImageZoom: add nearest neighbour (#1341) 2023-06-27 22:41:19 +02:00
V
5e71ed286e PronounDB: Strip newlines from Discord Pronouns (Discord is stupid) 2023-06-25 19:08:18 +02:00
V
5edbd2391d Fix build 2023-06-25 18:33:44 +02:00
ActuallyTheSun
8472c3823e feat(🗿): ignore blocked users (#1327)
Co-authored-by: V <vendicated@riseup.net>
2023-06-25 18:31:39 +02:00
V
2103e52115 WebContextMenus: Support all text areas 2023-06-25 18:22:36 +02:00
Nuckyz
afbfb641e8 Fix IgnoreActivities broken patch (#1337) 2023-06-25 09:57:18 +00:00
28 changed files with 450 additions and 89 deletions

View File

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

View File

@ -151,7 +151,6 @@ async function parseFile(fileName: string) {
case "required":
case "enabledByDefault":
data[key] = value.kind === SyntaxKind.TrueKeyword;
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
break;
}
}

View File

@ -58,4 +58,10 @@ export default {
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
},
pluginHelpers: {
OpenInApp: {
resolveRedirect: (url: string) => invoke<string>(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, url),
},
}
};

View File

@ -17,6 +17,7 @@
*/
import "./updater";
import "./ipcPlugins";
import { debounce } from "@utils/debounce";
import { IpcEvents } from "@utils/IpcEvents";

46
src/main/ipcPlugins.ts Normal file
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 { IpcEvents } from "@utils/IpcEvents";
import { ipcMain } from "electron";
import { request } from "https";
// #region OpenInApp
// These links don't support CORS, so this has to be native
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
function getRedirect(url: string) {
return new Promise<string>((resolve, reject) => {
const req = request(new URL(url), { method: "HEAD" }, res => {
resolve(
res.headers.location
? getRedirect(res.headers.location)
: url
);
});
req.on("error", reject);
req.end();
});
}
ipcMain.handle(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, async (_, url: string) => {
if (!validRedirectUrls.test(url)) return url;
return getRedirect(url);
});
// #endregion

View File

@ -31,7 +31,8 @@ export const ALLOWED_PROTOCOLS = [
"https:",
"http:",
"steam:",
"spotify:"
"spotify:",
"com.epicgames.launcher:",
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

View File

@ -80,8 +80,8 @@ export default definePlugin({
find: "Messages.PROFILE_USER_BADGES,role:",
replacement: [
{
match: /(?<=(\i)\.isTryItOutFlow,)(.{0,300})null==\i\?void 0:(\i)\.getBadges\(\)/,
replace: (_, props, restCode, badgesMod) => `vencordProps=${props},${restCode}Vencord.Api.Badges._getBadges(vencordProps).concat(${badgesMod}?.getBadges()??[])`,
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...Vencord.Api.Badges._getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc

View File

@ -45,7 +45,7 @@ function ToggleIconOff() {
className={RegisteredGamesClasses.overlayToggleIconOff}
height="24"
width="24"
viewBox="0 0 32 26"
viewBox="0 2.2 32 26"
aria-hidden={true}
role="img"
>
@ -77,7 +77,7 @@ function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
className={RegisteredGamesClasses.overlayToggleIconOn}
height="24"
width="24"
viewBox="0 0 32 26"
viewBox="0 2.2 32 26"
>
<path
className={forceWhite ? "" : RegisteredGamesClasses.fill}
@ -119,7 +119,7 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored
return (
<div
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
style={{ padding: "0px 2px" }}
style={{ padding: "0px 2px", height: 28 }}
>
<ToggleActivityComponent activity={activity} forceWhite={true} />
</div>
@ -157,10 +157,16 @@ export default definePlugin({
},
{
find: ".overlayBadge",
replacement: {
match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/,
replace: (_, props) => `,$self.renderToggleActivityButton(${props})`
}
replacement: [
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/,
replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]`
},
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/,
replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})`
}
]
},
{
find: '.displayName="LocalActivityStore"',

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
@ -33,6 +34,8 @@ export interface MagnifierProps {
instance: any;
}
const cl = classNameFactory("vc-imgzoom-");
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false);
@ -156,7 +159,7 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
return (
<div
className="vc-imgzoom-lens"
className={cl("lens", { "nearest-neighbor": settings.store.nearestNeighbour, square: settings.store.square })}
style={{
opacity,
width: size.current + "px",

View File

@ -23,7 +23,7 @@ import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce";
import definePlugin, { OptionType } from "@utils/types";
import { Menu, React, ReactDOM } from "@webpack/common";
import { ContextMenu, Menu, React, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -50,6 +50,18 @@ export const settings = definePluginSettings({
default: true,
},
nearestNeighbour: {
type: OptionType.BOOLEAN,
description: "Use Nearest Neighbour Interpolation when scaling images",
default: false,
},
square: {
type: OptionType.BOOLEAN,
description: "Make the lens square",
default: false,
},
zoom: {
description: "Zoom of the lens",
type: OptionType.SLIDER,
@ -78,9 +90,17 @@ export const settings = definePluginSettings({
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
children.push(
<Menu.MenuGroup id="image-zoom">
{/* thanks SpotifyControls */}
<Menu.MenuCheckboxItem
id="vc-square"
label="Square Lens"
checked={settings.store.square}
action={() => {
settings.store.square = !settings.store.square;
ContextMenu.close();
}}
/>
<Menu.MenuControlItem
id="zoom"
id="vc-zoom"
label="Zoom"
control={(props, ref) => (
<Menu.MenuSliderControl
@ -94,7 +114,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
)}
/>
<Menu.MenuControlItem
id="size"
id="vc-size"
label="Lens Size"
control={(props, ref) => (
<Menu.MenuSliderControl
@ -108,7 +128,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
)}
/>
<Menu.MenuControlItem
id="zoom-speed"
id="vc-zoom-speed"
label="Zoom Speed"
control={(props, ref) => (
<Menu.MenuSliderControl

View File

@ -11,6 +11,14 @@
pointer-events: none;
}
.vc-imgzoom-square {
border-radius: 0;
}
.vc-imgzoom-nearest-neighbor > img {
image-rendering: pixelated; /* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
}
/* make the carousel take up less space so we can click the backdrop and exit out of it */
[class|="carouselModal"] {
height: fit-content;

View File

@ -132,7 +132,7 @@ export default definePlugin({
find: ".Messages.MESSAGE_EDITED,",
replacement: {
match: /var .,.,.=(.)\.className,.=.\.message,.=.\.children,.=.\.content,.=.\.onUpdate/gm,
replace: "try {$1 && $self.INV_REGEX.test($1.content[0]) ? $1.content.push($self.indicator()) : null } catch {};$&"
replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&"
}
},
{
@ -200,8 +200,11 @@ export default definePlugin({
},
});
if (urlCheck?.length)
message.embeds.push(await this.getEmbed(new URL(urlCheck[0])));
if (urlCheck?.length) {
const embed = await this.getEmbed(new URL(urlCheck[0]));
if (embed)
message.embeds.push(embed);
}
this.updateMessage(message);
},

View File

@ -21,7 +21,7 @@ import { makeRange } from "@components/PluginSettings/components/SettingSliderCo
import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { SelectedChannelStore, UserStore } from "@webpack/common";
import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { Message, ReactionEmoji } from "discord-types/general";
interface IMessageCreate {
@ -37,6 +37,7 @@ interface IReactionAdd {
optimistic: boolean;
channelId: string;
messageId: string;
messageAuthorId: string;
userId: "195136840355807232";
emoji: ReactionEmoji;
}
@ -71,6 +72,11 @@ const settings = definePluginSettings({
description: "Ignore bots",
type: OptionType.BOOLEAN,
default: true
},
ignoreBlocked: {
description: "Ignore blocked users",
type: OptionType.BOOLEAN,
default: true
}
});
@ -85,6 +91,7 @@ export default definePlugin({
if (optimistic || type !== "MESSAGE_CREATE") return;
if (message.state === "SENDING") return;
if (settings.store.ignoreBots && message.author?.bot) return;
if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return;
if (!message.content) return;
if (channelId !== SelectedChannelStore.getChannelId()) return;
@ -96,9 +103,10 @@ export default definePlugin({
}
},
MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, emoji }: IReactionAdd) {
MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) {
if (optimistic || type !== "MESSAGE_REACTION_ADD") return;
if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return;
if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return;
if (channelId !== SelectedChannelStore.getChannelId()) return;
const name = emoji.name.toLowerCase();

147
src/plugins/openInApp.ts Normal file
View File

@ -0,0 +1,147 @@
/*
* 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 { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { MouseEvent } from "react";
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
const settings = definePluginSettings({
spotify: {
type: OptionType.BOOLEAN,
description: "Open Spotify links in the Spotify app",
default: true,
},
steam: {
type: OptionType.BOOLEAN,
description: "Open Steam links in the Steam app",
default: true,
},
epic: {
type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher",
default: true,
}
});
export default definePlugin({
name: "OpenInApp",
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven],
settings,
patches: [
{
find: '"MaskedLinkStore"',
replacement: {
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)"
}
},
// Make Spotify profile activity links open in app on web
{
find: "WEB_OPEN(",
predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify,
replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal"
}
},
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: {
match: /(?<=href:\i,onClick:function\(\)\{)(?=return \i=(\i)\.type,.{0,50}CONNECTED_ACCOUNT_VIEWED)/,
replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
}
}
],
async handleLink(data: { href: string; }, event: MouseEvent) {
if (!data) return false;
let url = data.href;
if (!IS_WEB && ShortUrlMatcher.test(url)) {
event.preventDefault();
// CORS jumpscare
url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url);
}
spotify: {
if (!settings.store.spotify) break spotify;
const match = SpotifyMatcher.exec(url);
if (!match) break spotify;
const [, type, id] = match;
VencordNative.native.openExternal(`spotify:${type}:${id}`);
event.preventDefault();
return true;
}
steam: {
if (!settings.store.steam) break steam;
if (!SteamMatcher.test(url)) break steam;
VencordNative.native.openExternal(`steam://openurl/${url}`);
event.preventDefault();
// Steam does not focus itself so show a toast so it's slightly less confusing
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
return true;
}
epic: {
if (!settings.store.epic) break epic;
const match = EpicMatcher.exec(url);
if (!match) break epic;
VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`);
event.preventDefault();
return true;
}
// in case short url didn't end up being something we can handle
if (event.defaultPrevented) {
window.open(url, "_blank");
return true;
}
return false;
},
handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) {
if (platformType === "spotify" && settings.store.spotify) {
VencordNative.native.openExternal(`spotify:user:${userId}`);
event.preventDefault();
} else if (platformType === "steam" && settings.store.steam) {
VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`);
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
event.preventDefault();
}
}
});

View File

@ -52,7 +52,7 @@ export function CompactPronounsChatComponentWrapper({ message }: { message: Mess
}
function PronounsChatComponent({ message }: { message: Message; }) {
const result = useFormattedPronouns(message.author.id);
const [result] = useFormattedPronouns(message.author.id);
return result
? (
@ -64,7 +64,7 @@ function PronounsChatComponent({ message }: { message: Message; }) {
}
export function CompactPronounsChatComponent({ message }: { message: Message; }) {
const result = useFormattedPronouns(message.author.id);
const [result] = useFormattedPronouns(message.author.id);
return result
? (

View File

@ -26,6 +26,11 @@ import { CompactPronounsChatComponentWrapper, PronounsChatComponentWrapper } fro
import { useProfilePronouns } from "./pronoundbUtils";
import { settings } from "./settings";
const PRONOUN_TOOLTIP_PATCH = {
match: /text:(.{0,10}.Messages\.USER_PROFILE_PRONOUNS)(?=,)/,
replace: '$& + (typeof vcPronounSource !== "undefined" ? ` (${vcPronounSource})` : "")'
};
export default definePlugin({
name: "PronounDB",
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
@ -50,18 +55,24 @@ export default definePlugin({
// Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns
{
find: ".userTagNoNickname",
replacement: {
match: /=(\i)\.pronouns/,
replace: "=$self.useProfilePronouns($1.user.id,$1.pronouns)"
}
replacement: [
{
match: /,(\i)=(\i)\.pronouns/,
replace: ",[$1,vcPronounSource]=$self.useProfilePronouns($2.user.id)"
},
PRONOUN_TOOLTIP_PATCH
]
},
// Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns
{
find: ".USER_PROFILE_ACTIVITY",
replacement: {
match: /\).showPronouns/,
replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id,arguments[0].displayProfile?.pronouns);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce"
}
replacement: [
{
match: /getGlobalName\(\i\);(?<=displayProfile.{0,200})/,
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
},
PRONOUN_TOOLTIP_PATCH
]
}
],

View File

@ -29,6 +29,9 @@ import { PronounCode, PronounMapping, PronounsResponse } from "./types";
const UserProfileStore = findStoreLazy("UserProfileStore");
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];
export const enum PronounsFormat {
Lowercase = "LOWERCASE",
Capitalized = "CAPITALIZED"
@ -62,45 +65,48 @@ function getDiscordPronouns(id: string) {
);
}
export function useFormattedPronouns(id: string, discordPronouns: string = getDiscordPronouns(id)): string | null {
const [result] = useAwaiter(() => fetchPronouns(id, discordPronouns), {
fallbackValue: getCachedPronouns(id, discordPronouns),
export function useFormattedPronouns(id: string): PronounsWithSource {
// Discord is so stupid you can put tons of newlines in pronouns
const discordPronouns = getDiscordPronouns(id)?.trim().replace(NewLineRe, " ");
const [result] = useAwaiter(() => fetchPronouns(id), {
fallbackValue: getCachedPronouns(id),
onError: e => console.error("Fetching pronouns failed: ", e)
});
if (result && result !== "unspecified")
return Object.hasOwn(PronounMapping, result)
? formatPronouns(result) // PronounDB
: result; // Discord
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"];
return null;
if (result && result !== "unspecified")
return [formatPronouns(result), "PronounDB"];
return [discordPronouns, "Discord"];
}
export function useProfilePronouns(id: string, discordPronouns: string) {
const pronouns = useFormattedPronouns(id, discordPronouns);
export function useProfilePronouns(id: string): PronounsWithSource {
const pronouns = useFormattedPronouns(id);
if (!settings.store.showInProfile) return null;
if (!settings.store.showSelf && id === UserStore.getCurrentUser().id) return null;
if (!settings.store.showInProfile) return EmptyPronouns;
if (!settings.store.showSelf && id === UserStore.getCurrentUser().id) return EmptyPronouns;
return pronouns;
}
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string, discordPronouns: string): string | null {
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return discordPronouns;
const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): string | null {
const cached = cache[id];
if (cached && cached !== "unspecified") return cached;
return discordPronouns || cached || null;
return cached || null;
}
// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
export function fetchPronouns(id: string, discordPronouns: string): Promise<string> {
export function fetchPronouns(id: string): Promise<string> {
return new Promise(res => {
const cached = getCachedPronouns(id, discordPronouns);
const cached = getCachedPronouns(id);
if (cached) return res(cached);
// If there is already a request added, then just add this callback to it

View File

@ -147,6 +147,13 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
className="vc-user-connection"
href={url}
target="_blank"
onClick={e => {
if (Vencord.Plugins.isPluginEnabled("OpenInApp")) {
const OpenInApp = Vencord.Plugins.plugins.OpenInApp as any as typeof import("../openInApp").default;
// handleLink will .preventDefault() if applicable
OpenInApp.handleLink(e.currentTarget, e);
}
}}
>
{img}
</a>

View File

@ -342,32 +342,27 @@ export default definePlugin({
]
},
{
find: "Guild voice channel without guild id.",
find: "useNotificationSettingsItem: channel cannot be undefined",
replacement: [
{
// Render our HiddenChannelLockScreen component instead of the main stage channel component
match: /Guild voice channel without guild id.+?children:(?<=(\i)\.getGuildId\(\).+?)(?=.{0,20}?}\)}function)/,
match: /"124px".+?children:(?<=var (\i)=\i\.channel.+?)(?=.{0,20}?}\)}function)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`
},
{
// Disable useless components for the HiddenChannelLockScreen of stage channels
match: /render(?!Header).{0,30}?:(?<=(\i)\.getGuildId\(\).+?Guild voice channel without guild id.+?)/g,
match: /render(?:BottomLeft|BottomCenter|BottomRight|ChatToasts):(?<=var (\i)=\i\.channel.+?)/g,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:`
},
// Prevent Discord from replacing our route if we aren't connected to the stage channel
{
match: /(?=!\i&&!\i&&!\i.{0,80}?(\i)\.getGuildId\(\).{0,50}?Guild voice channel without guild id)(?<=if\()/,
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
},
{
// Disable gradients for the HiddenChannelLockScreen of stage channels
match: /Guild voice channel without guild id.+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/,
match: /"124px".+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||`
},
{
// Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels
match: /Guild voice channel without guild id.+?style:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?undefined:`
match: /"124px".+?style:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?void 0:`
},
{
// Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen
@ -376,7 +371,7 @@ export default definePlugin({
},
{
// Remove the open chat button for the HiddenChannelLockScreen
match: /"recents".+?null,(?=.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
match: /"recents".+?&&(?=\(.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
}
],

View File

@ -23,6 +23,7 @@ import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
@ -231,7 +232,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
id="view-cover"
label="View Album Cover"
// trolley
action={() => (Vencord.Plugins.plugins.ViewIcons as any).openImage(track.album.image.url)}
action={() => openImageModal(track.album.image.url)}
icon={ImageIcon}
/>
<Menu.MenuControlItem

View File

@ -89,7 +89,7 @@ export const SpotifyStore = proxyLazy(() => {
public isSettingPosition = false;
public openExternal(path: string) {
const url = Settings.plugins.SpotifyControls.useSpotifyUris
const url = Settings.plugins.SpotifyControls.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp")
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path;

View File

@ -75,7 +75,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
const mention = children?.[0]?.props?.children;
if (typeof mention !== "string") return;
const id = mention.match(/<@(\d+)>/)?.[1];
const id = mention.match(/<@!?(\d+)>/)?.[1];
if (!id) return;
if (fetching.has(id))

View File

@ -156,6 +156,8 @@ export default definePlugin({
const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id;
if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return;
for (const state of voiceStates) {
const { userId, channelId, oldChannelId } = state;
const isMe = userId === myId;

View File

@ -35,7 +35,7 @@ interface UserContextProps {
}
interface GuildContextProps {
guild: Guild;
guild?: Guild;
}
const settings = definePluginSettings({
@ -100,7 +100,8 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
action={() => openImage(BannerStore.getGuildMemberAvatarURLSimple({
userId: user.id,
avatar: memberAvatar,
guildId
guildId,
canAnimate: true
}, true))}
icon={ImageIcon}
/>
@ -109,7 +110,10 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
));
};
const GuildContext: NavContextMenuPatchCallback = (children, { guild: { id, icon, banner } }: GuildContextProps) => () => {
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => {
if(!guild) return;
const { id, icon, banner } = guild;
if (!banner && !icon) return;
children.splice(-1, 0, (
@ -181,8 +185,8 @@ export default definePlugin({
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/,
replace:
// onClick: () => shouldShowBanner && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:()=>$1&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
}
},
{

View File

@ -47,9 +47,10 @@ const settings = definePluginSettings({
export default definePlugin({
name: "WebContextMenus",
description: "Re-adds context menus missing in the web version of Discord: Images, ChatInputBar, Links, 'Copy Link', 'Open Link', 'Copy Image', 'Save Image'",
description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)",
authors: [Devs.Ven],
enabledByDefault: true,
required: IS_VENCORD_DESKTOP,
settings,
@ -146,36 +147,30 @@ export default definePlugin({
}
},
{
find: ':"command-suggestions"',
find: 'navId:"textarea-context"',
all: true,
predicate: () => settings.store.addBack,
replacement: [
{
// desktopOnlyEntries = makeEntries(), spellcheckChildren = desktopOnlyEntries[0], languageChildren = desktopOnlyEntries[1]
match: /\i=.{0,30}text:\i,target:\i,onHeightUpdate:\i\}\),2\),(\i)=\i\[0\],(\i)=\i\[1\]/,
// set spellcheckChildren & languageChildren to empty arrays, so just in case patch 3 fails, we don't
// reference undefined variables
replace: "$1=[],$2=[]",
},
{
// if (!IS_DESKTOP) return null;
match: /if\(!\i\.\i\)return null;/,
replace: ""
},
{
// do not add menu items for entries removed in patch 1. Using a lookbehind for group 1 is slow,
// so just capture and add back
match: /("submit-button".+?)(\(0,\i\.jsx\)\(\i\.MenuGroup,\{children:\i\}\),){2}/,
replace: "$1"
},
{
// Change calls to DiscordNative.clipboard to us instead
match: /\b\i\.default\.(copy|cut|paste)/g,
replace: "$self.$1"
}
]
},
{
find: '"add-to-dictionary"',
predicate: () => settings.store.addBack,
replacement: {
match: /var \i=\i\.text,/,
replace: "return [null,null];$&"
}
}
// TODO: Maybe add spellcheck for VencordDesktop
],
async copyImage(url: string) {

View File

@ -0,0 +1,79 @@
/*
* 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";
import { findLazy, mapMangledModuleLazy } from "@webpack";
import { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from "@webpack/common";
const GuildNavBinds = mapMangledModuleLazy("mod+alt+down", {
CtrlTab: m => m.binds?.at(-1) === "ctrl+tab",
CtrlShiftTab: m => m.binds?.at(-1) === "ctrl+shift+tab",
});
const DigitBinds = findLazy(m => m.binds?.[0] === "mod+1");
export default definePlugin({
name: "WebKeybinds",
description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,",
authors: [Devs.Ven],
enabledByDefault: true,
onKey(e: KeyboardEvent) {
const hasCtrl = e.ctrlKey || (e.metaKey && navigator.platform.includes("Mac"));
if (hasCtrl) switch (e.key) {
case "t":
case "T":
e.preventDefault();
if (e.shiftKey) {
if (SelectedGuildStore.getGuildId()) NavigationRouter.transitionToGuild("@me");
ComponentDispatch.safeDispatch("TOGGLE_DM_CREATE");
} else {
FluxDispatcher.dispatch({
type: "QUICKSWITCHER_SHOW",
query: "",
queryMode: null
});
}
break;
case ",":
e.preventDefault();
SettingsRouter.open("My Account");
break;
case "Tab":
const handler = e.shiftKey ? GuildNavBinds.CtrlShiftTab : GuildNavBinds.CtrlTab;
handler.action(e);
break;
default:
if (e.key >= "1" && e.key <= "9") {
e.preventDefault();
DigitBinds.action(e, `mod+${e.key}`);
}
break;
}
},
start() {
document.addEventListener("keydown", this.onKey);
},
stop() {
document.removeEventListener("keydown", this.onKey);
}
});

View File

@ -30,4 +30,6 @@ export const enum IpcEvents {
UPDATE = "VencordUpdate",
BUILD = "VencordBuild",
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
}

View File

@ -77,6 +77,17 @@ export const Toasts = {
}
};
/**
* Show a simple toast. If you need more options, use Toasts.show manually
*/
export function showToast(message: string, type = ToastType.MESSAGE) {
Toasts.show({
id: Toasts.genId(),
message,
type
});
}
export const UserUtils = {
fetchUser: findByCodeLazy(".USER(", "getUser") as (id: string) => Promise<User>,
};