Compare commits

..

22 Commits

Author SHA1 Message Date
Vendicated
eb190b660e Bump to v1.0.8 2023-02-28 01:50:17 +01:00
Lewis Crichton
d6f9068695 feat: SearchableSelect (#518)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:48:58 +01:00
Nico
cb507babaa fix: vcDoubleClick and revealAllSpoilers patch (#517)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:41:14 +01:00
Vendicated
235d114193 Improve ConsoleShortcuts plugin 2023-02-28 00:38:28 +01:00
Vendicated
9aba70dcb1 Fix MenuItemDeobfuscator 2023-02-28 00:17:39 +01:00
Vendicated
0b61d29c31 Fix TypingTweaks 2023-02-28 00:17:28 +01:00
megumin
335a13a38a fix tooltip component check (#541) 2023-02-27 21:19:01 +00:00
Vendicated
128ee41252 ErrorBoundary: Do not use any Discord components to be more robust 2023-02-25 19:10:01 +01:00
Vendicated
ccca41a168 Bump to v1.0.7 2023-02-24 06:08:45 +01:00
Vendicated
af4c7d8a90 Fix Cards (they look ugly now, wtf Discord) 2023-02-24 05:48:37 +01:00
nick
77c691651e ReviewDB: Show edit instead of create review where applicable (#466)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:35:51 +01:00
Nuckyz
e14ec96e21 feat(FakeNitro): Bypass client themes and fixes (#504)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:32:02 +01:00
Vendicated
ff1f337699 Fix QuickCSS on electron 20+ 2023-02-17 15:37:38 +01:00
Nuckyz
3ca87848e5 TypingIndicator: Fix a dumb (#503)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-17 01:31:55 +01:00
Vendicated
9420735bc7 Version 1.0.6 2023-02-16 23:40:38 +01:00
Vendicated
6807820f6c Badges should use ErrorBoundaries 2023-02-16 22:46:51 +01:00
Vendicated
3cad0d60b4 Silly Discord changed a bunch of css vars 2023-02-16 22:40:19 +01:00
Vendicated
fbbc198b1b Fix PlatformIndicator 2023-02-16 22:31:13 +01:00
Nuckyz
224ae979f2 feat(plugins): Typing Indicator (#502) 2023-02-16 03:57:57 +01:00
Lewis Crichton
27fc20118b feat(plugin): RoleColorEverywhere (#482)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:50:42 +01:00
Nuckyz
60ccd8cc25 Various plugin fixes (#492)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:00:09 +01:00
Lewis Crichton
5c1519156b feat(plugin): ColorSighted (#501) 2023-02-16 01:46:14 +01:00
27 changed files with 534 additions and 102 deletions

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.0.5",
"version": "1.0.8",
"description": "The cutest Discord client mod",
"keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme",

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { User } from "discord-types/general";
import { ComponentType, HTMLProps } from "react";
@ -52,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
* @param badge The badge to register
*/
export function addBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge);
}

View File

@ -17,8 +17,9 @@
*/
import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common";
import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
@ -84,15 +85,13 @@ const ErrorBoundary = LazyComponent(() => {
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return (
<ErrorCard style={{
overflow: "hidden",
}}>
<ErrorCard style={{ overflow: "hidden" }}>
<h1>Oh no!</h1>
<p>{msg}</p>
<code>
{this.state.message}
{!!this.state.stack && (
<pre className={Margins.marginTop8}>
<pre className={Margins.top8}>
{this.state.stack}
</pre>
)}

View File

@ -0,0 +1,7 @@
.vc-error-card {
padding: 2em;
background-color: #e7828430;
border: 1px solid #e78284;
border-radius: 5px;
color: var(--text-normal, white);
}

View File

@ -16,24 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Card } from "@webpack/common";
import "./ErrorCard.css";
interface Props {
style?: React.CSSProperties;
className?: string;
}
export function ErrorCard(props: React.PropsWithChildren<Props>) {
import { classes } from "@utils/misc";
import type { HTMLProps } from "react";
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
return (
<Card className={props.className} style={
{
padding: "2em",
backgroundColor: "#e7828430",
borderColor: "#e78284",
color: "var(--text-normal)",
...props.style
}
}>
<div {...props} className={classes(props.className, "vc-error-card")}>
{props.children}
</Card>
</div>
);
}

View File

@ -26,8 +26,8 @@ interface SwitchProps {
disabled?: boolean;
}
const SWITCH_ON = "var(--status-green-600)";
const SWITCH_OFF = "var(--primary-dark-400)";
const SWITCH_ON = "var(--green-360)";
const SWITCH_OFF = "var(--primary-400)";
const SwitchClasses = findByPropsLazy("slider", "input", "container");
export function Switch({ checked, onChange, disabled }: SwitchProps) {

View File

@ -91,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
nodeIntegration: false,
sandbox: false
}
});
await win.loadURL(`data:text/html;base64,${monacoHtml}`);

View File

@ -43,7 +43,7 @@ export default definePlugin({
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
let nicenNames = "";
const redefines = [] as string[];

View File

@ -0,0 +1,37 @@
/*
* 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: "ColorSighted",
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
authors: [Devs.lewisakura],
patches: [
{
find: "Masks.STATUS_ONLINE",
replacement: {
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
// so it keeps the patch and plugin small and simple
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
replace: "Masks.STATUS_ONLINE"
}
}
]
});

View File

@ -18,6 +18,9 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack";
import { React } from "@webpack/common";
const WEB_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`);
@ -29,19 +32,48 @@ export default definePlugin({
authors: [Devs.Ven],
getShortcuts() {
function newFindWrapper(filterFactory: (props: any) => Webpack.FilterFn) {
const cache = new Map<string, any>();
return function (filterProps: any) {
const cacheKey = String(filterProps);
if (cache.has(cacheKey)) return cache.get(cacheKey);
const matches = findAll(filterFactory(filterProps));
const result = (() => {
switch (matches.length) {
case 0: return null;
case 1: return matches[0];
default:
const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
return matches[0];
}
})();
if (result && cacheKey) cache.set(cacheKey, result);
return result;
};
}
return {
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
wp: Vencord.Webpack,
wpc: Vencord.Webpack.wreq.c,
wreq: Vencord.Webpack.wreq,
wpsearch: Vencord.Webpack.search,
wpex: Vencord.Webpack.extract,
wp: Webpack,
wpc: Webpack.wreq.c,
wreq: Webpack.wreq,
wpsearch: search,
wpex: extract,
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
findByProps: Vencord.Webpack.findByProps,
find: Vencord.Webpack.find,
Plugins: Vencord.Plugins,
React: Vencord.Webpack.Common.React,
find: newFindWrapper(f => f),
findAll,
findByProps: newFindWrapper(filters.byProps),
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
React,
Settings: Vencord.Settings,
Api: Vencord.Api,
reload: () => location.reload(),

View File

@ -22,11 +22,25 @@ import { Devs } from "@utils/constants";
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, UserStore } from "@webpack/common";
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n;
enum EmojiIntentions {
REACTION = 0,
STATUS = 1,
COMMUNITY_CONTENT = 2,
CHAT = 3,
GUILD_STICKER_RELATED_EMOJI = 4,
GUILD_ROLE_BENEFIT_EMOJI = 5,
COMMUNITY_CONTENT_ONLY = 6,
SOUNDBOARD = 7
}
interface BaseSticker {
available: boolean;
description: string;
@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass");
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity],
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"],
patches: [
{
find: "canUseAnimatedEmojis:function",
find: ".PREMIUM_LOCKED;",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: [
"canUseAnimatedEmojis",
"canUseEmojisEverywhere"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?\\{`),
replace: "$&return true;"
};
})
{
match: /(?<=(?<intention>\i)=\i\.intention)/,
replace: ",fakeNitroIntention=$<intention>"
},
{
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
replace: ",fakeNitroIntention"
},
{
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
}
]
},
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: {
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
}
},
{
find: "canUseStickersEverywhere:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: {
match: /canUseStickersEverywhere:function\(.+?\{/,
@ -93,7 +120,7 @@ export default definePlugin({
}
},
{
find: "canUseAnimatedEmojis:function",
find: "canStreamHighQuality:function",
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
replacement: [
"canUseHighVideoUploadQuality",
@ -114,6 +141,13 @@ export default definePlugin({
replace: ""
}
},
{
find: "canUseClientThemes:function",
replacement: {
match: /(?<=canUseClientThemes:function\(\i\){)/,
replace: "return true;"
}
}
],
options: {
@ -161,6 +195,22 @@ export default definePlugin({
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
},
hasPermissionToUseExternalEmojis(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
},
hasPermissionToUseExternalStickers(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
},
getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
},
@ -245,7 +295,7 @@ export default definePlugin({
if (!sticker)
break stickerBypass;
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
break stickerBypass;
let link = this.getStickerLink(sticker.id);
@ -268,7 +318,7 @@ export default definePlugin({
}
}
if (!this.canUseEmotes && settings.enableEmojiBypass) {
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
@ -284,8 +334,9 @@ export default definePlugin({
return { cancel: false };
});
if (!this.canUseEmotes && settings.enableEmojiBypass) {
this.preEdit = addPreEditListener((_, __, messageObj) => {
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
const { guildId } = this;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
@ -299,7 +350,6 @@ export default definePlugin({
});
}
});
}
},
stop() {

View File

@ -56,7 +56,7 @@ function MemberCount() {
<div {...props}>
<span
style={{
backgroundColor: "var(--status-green-600)",
backgroundColor: "var(--green-360)",
width: "12px",
height: "12px",
borderRadius: "50%",
@ -64,7 +64,7 @@ function MemberCount() {
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--status-green-600)" }}>{online}</span>
<span style={{ color: "var(--green-360)" }}>{online}</span>
</div>
)}
</Tooltip>
@ -76,13 +76,13 @@ function MemberCount() {
width: "6px",
height: "6px",
borderRadius: "50%",
border: "3px solid var(--status-grey-500)",
border: "3px solid var(--primary-400)",
display: "inline-block",
marginRight: "0.5em",
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--status-grey-500)" }}>{total}</span>
<span style={{ color: "var(--primary-400)" }}>{total}</span>
</div>
)}
</Tooltip>

View File

@ -55,7 +55,7 @@ const Icons = {
};
type Platform = keyof typeof Icons;
const getStatusColor = findByCodeLazy("STATUS_YELLOW", "TWITCH", "STATUS_GREY");
const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE");
const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);

View File

@ -30,10 +30,10 @@ export default definePlugin({
patches: [
{
find: ".revealSpoiler=function",
find: ".removeObscurity=function",
replacement: {
match: /\.revealSpoiler=function\((.{1,2})\){/,
replace: ".revealSpoiler=function($1){$self.reveal($1);"
match: /\.removeObscurity=function\((\i)\){/,
replace: ".removeObscurity=function($1){$self.reveal($1);"
}
}
],

View File

@ -32,6 +32,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
fallbackValue: [],
deps: [refetchCount],
});
const username = UserStore.getUser(userId)?.username ?? "";
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
@ -79,7 +80,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
<textarea
className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")}
// this produces something like '-_59yqs ...' but since no class exists with that name its fine
placeholder={"Review @" + UserStore.getUser(userId)?.username ?? ""}
placeholder={reviews?.some(r => r.senderdiscordid === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`}
onKeyDown={onKeyPress}
style={{
marginTop: "6px",

View File

@ -0,0 +1,123 @@
/*
* 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 { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
const settings = definePluginSettings({
chatMentions: {
type: OptionType.BOOLEAN,
default: true,
description: "Show role colors in chat mentions (including in the message box)",
restartNeeded: true
},
memberList: {
type: OptionType.BOOLEAN,
default: true,
description: "Show role colors in member list role headers",
restartNeeded: true
},
voiceUsers: {
type: OptionType.BOOLEAN,
default: true,
description: "Show role colors in the voice chat user list",
restartNeeded: true
}
});
export default definePlugin({
name: "RoleColorEverywhere",
authors: [Devs.KingFish, Devs.lewisakura],
description: "Adds the top role color anywhere possible",
patches: [
// Chat Mentions
{
find: 'className:"mention"',
replacement: [
{
match: /user:(\i),channelId:(\i).{0,300}?"@"\.concat\(.+?\)/,
replace: "$&,color:$self.getUserColor($1.id,{channelId:$2})"
}
],
predicate: () => settings.store.chatMentions,
},
// Slate
{
// taken from CommandsAPI
find: ".source,children",
replacement: [
{
match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/,
replace: "$&color:$self.getUserColor($1.id,{guildId:$1.guildId}),"
}
],
predicate: () => settings.store.chatMentions,
},
// Member List Role Names
{
find: ".memberGroupsPlaceholder",
replacement: [
{
match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/,
replace: "$1$self.roleGroupColor($2)]"
},
],
predicate: () => settings.store.memberList,
},
// Voice chat users
{
find: "renderPrioritySpeaker",
replacement: [
{
match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/,
replace: "$&...$self.getVoiceProps(this.props),"
}
],
predicate: () => settings.store.voiceUsers,
}
],
settings,
getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) {
if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null;
return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null;
},
getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) {
const colorString = this.getColor(userId, ids);
return colorString && parseInt(colorString.slice(1), 16);
},
roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) {
const guild = GuildStore.getGuild(guildId);
const role = guild?.roles[id];
return <span style={{
color: role?.colorString,
fontWeight: "unset",
letterSpacing: ".05em"
}}>{title} &mdash; {count}</span>;
},
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
return {
style: {
color: this.getColor(userId, { guildId })
}
};
}
});

View File

@ -263,7 +263,7 @@ export default definePlugin({
replacement: [
{
// Remove the divider and the open chat button for the HiddenChannelLockScreen
match: /(?<=function \i\((?<props>\i)\).{1,1800}"more-options-popout"\)\);if\()/,
match: /(?<=function \i\((?<props>\i)\).{1,2000}"more-options-popout"\)\);if\()/,
replace: "(!$self.isHiddenChannel($<props>.channel)||$<props>.inCall)&&"
},
{

View File

@ -56,7 +56,7 @@ function SilentTypingToggle() {
<div className={ButtonWrapperClasses.buttonWrapper}>
<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(--status-red-500)" stroke-width="72" stroke-linecap="round" />}
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
</svg>
</div>
</Button>

View File

@ -0,0 +1,135 @@
/*
* 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 { definePluginSettings, Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { find, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "./typingTweaks";
const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots")));
const TypingStore = findStoreLazy("TypingStore");
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING);
function getDisplayName(guildId: string, userId: string) {
return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username;
}
function TypingIndicator({ channelId }: { channelId: string; }) {
const typingUsers: Record<string, number> = useStateFromStores(
[TypingStore],
() => ({ ...TypingStore.getTypingUsers(channelId) as Record<string, number> }),
null,
(old, current) => {
const oldKeys = Object.keys(old);
const currentKeys = Object.keys(current);
return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys);
}
);
const guildId = ChannelStore.getChannel(channelId).guild_id;
if (!settings.store.includeMutedChannels) {
const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);
if (isChannelMuted) return null;
}
delete typingUsers[UserStore.getCurrentUser().id];
const typingUsersArray = Object.keys(typingUsers);
let tooltipText: string;
switch (typingUsersArray.length) {
case 0: break;
case 1: {
tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) });
break;
}
case 2: {
tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
break;
}
case 3: {
tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
break;
}
default: {
tooltipText = Settings.plugins.TypingTweaks.enabled
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 })
: Formatters.Messages.SEVERAL_USERS_TYPING;
break;
}
}
if (typingUsersArray.length > 0) {
return (
<Tooltip text={tooltipText!}>
{({ onMouseLeave, onMouseEnter }) => (
<div
style={{ marginLeft: 6, zIndex: 0, cursor: "pointer" }}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<ThreeDots dotRadius={3} themed={true} />
</div>
)}
</Tooltip>
);
}
return null;
}
const settings = definePluginSettings({
includeMutedChannels: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for muted channels.",
default: false
}
});
export default definePlugin({
name: "TypingIndicator",
description: "Adds an indicator if someone is typing on a channel.",
authors: [Devs.Nuckyz],
settings,
patches: [
{
find: ".UNREAD_HIGHLIGHT",
replacement: {
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
replace: ",$self.TypingIndicator($<channel>.id)"
}
}
],
TypingIndicator: (channelId: string) => (
<ErrorBoundary noop>
<TypingIndicator channelId={channelId} />
</ErrorBoundary>
),
});

View File

@ -24,7 +24,7 @@ import { findByCodeLazy } from "@webpack";
import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general";
const Avatar = findByCodeLazy(".Positions.TOP,spacing:");
const Avatar = findByCodeLazy('"top",spacing:');
const settings = definePluginSettings({
showAvatars: {
@ -44,6 +44,15 @@ const settings = definePluginSettings({
}
});
export function buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) {
return [
<strong key="0">{a}</strong>,
", ",
<strong key="2">{b}</strong>,
`, and ${c} others are typing...`
];
}
export default definePlugin({
name: "TypingTweaks",
description: "Show avatars and role colours in the typing indicator",
@ -77,14 +86,7 @@ export default definePlugin({
],
settings,
buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) {
return [
<strong key="0">{a}</strong>,
", ",
<strong key="2">{b}</strong>,
`, and ${c} others are typing...`
];
},
buildSeveralUsers,
mutateChildren(props: any, users: User[], children: any) {
if (!Array.isArray(children)) return children;

View File

@ -48,10 +48,10 @@ export default definePlugin({
},
{
// channel mentions
find: 'className:"channelMention",iconType:(',
find: ".shouldCloseDefaultModals",
replacement: {
match: /onClick:(.{1,3}),/,
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)(),",
match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/,
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()",
}
}
],

View File

@ -121,6 +121,13 @@ export default definePlugin({
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Object.values(reactions).filter(Boolean) as User[];
for (const user of users) {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user
});
}
return (
<div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}

View File

@ -196,5 +196,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
whqwert: {
name: "whqwert",
id: 586239091520176128n
},
lewisakura: {
name: "lewisakura",
id: 96269247411400704n
}
});

View File

@ -141,8 +141,8 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
* Calls .join(" ") on the arguments
* classes("one", "two") => "one two"
*/
export function classes(...classes: string[]) {
return classes.filter(c => typeof c === "string").join(" ");
export function classes(...classes: Array<string | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
/**

View File

@ -32,10 +32,10 @@ export const Forms = {
FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER),
};
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY === "cardPrimary");
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", ["Positions", "Colors"]);
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
@ -45,6 +45,10 @@ export const Text = waitForComponent<t.Text>("Text", m => {
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
});
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
m.render && searchableSelectFilter(m.render)
);
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);

View File

@ -90,16 +90,17 @@ export type Tooltip = ComponentType<{
/** Tooltip.Colors.BLACK */
color?: string;
/** Tooltip.Positions.TOP */
/** TooltipPositions.TOP */
position?: string;
tooltipClassName?: string;
tooltipContentClassName?: string;
}> & {
Positions: Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>;
};
export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
editable?: boolean;
outline?: boolean;
@ -234,6 +235,49 @@ export type Select = ComponentType<PropsWithChildren<{
"aria-labelledby"?: boolean;
}>>;
export type SearchableSelect = ComponentType<PropsWithChildren<{
placeholder?: string;
options: ReadonlyArray<SelectOption>; // TODO
value?: SelectOption;
/**
* - 0 ~ Filled
* - 1 ~ Custom
*/
look?: 0 | 1;
className?: string;
popoutClassName?: string;
wrapperClassName?: string;
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center";
optionClassName?: string;
autoFocus?: boolean;
isDisabled?: boolean;
clearable?: boolean;
closeOnSelect?: boolean;
clearOnSelect?: boolean;
multi?: boolean;
onChange(value: any): void;
onSearchChange?(value: string): void;
onClose?(): void;
onOpen?(): void;
onBlur?(): void;
renderOptionPrefix?(option: SelectOption): ReactNode;
renderOptionSuffix?(option: SelectOption): ReactNode;
filter?(option: SelectOption[], query: string): SelectOption[];
centerCaret?: boolean;
debounceTime?: number;
maxVisibleItems?: number;
popoutWidth?: number;
"aria-labelledby"?: boolean;
}>>;
export type Slider = ComponentType<PropsWithChildren<{
initialValue: number;
defaultValue?: number;

View File

@ -307,13 +307,6 @@ export function findByPropsLazy(...props: string[]) {
return findLazy(filters.byProps(...props));
}
/**
* Find all modules that have the specified properties
*/
export function findAllByProps(...props: string[]) {
return findAll(filters.byProps(...props));
}
/**
* Find a function by its code
*/