Merge branch 'dev' into immediate-finds

This commit is contained in:
Nuckyz 2024-05-11 18:42:11 -03:00
commit 258e1efdce
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
9 changed files with 203 additions and 53 deletions

View file

@ -17,13 +17,16 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByProps, findComponentByCode } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCode("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCode("onOpenGameProfile");
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN],
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
{!gameActivityEnabled && (
<ErrorCard
className={classes(Margins.top16, Margins.bottom16)}
style={{ padding: "1em" }}
>
<Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button
color={Button.Colors.TRANSPARENT}
className={Margins.top8}
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
>
Enable
</Button>
</ErrorCard>
)}
<Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID.
@ -407,7 +429,9 @@ export default definePlugin({
<Forms.FormText>
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
</Forms.FormText>
<Forms.FormDivider />
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}

View file

@ -189,7 +189,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"],
settings,

View file

@ -13,7 +13,7 @@ import { findExportedComponent } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
type FillValue = ("status-danger" | "status-warning" | "text-muted");
type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff;
@ -54,10 +54,24 @@ export default definePlugin({
seconds: Math.round(delta % 60),
};
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${k}` : null;
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
const keys = Object.keys(diff) as DiffKey[];
return keys.map(str).filter(isNonNullish).join(" ") || "0 seconds";
const ts = keys.reduce((prev, k) => {
const s = str(k);
return prev + (
isNonNullish(s)
? (prev !== ""
? k === "seconds"
? " and "
: " "
: "") + s
: ""
);
}, "");
return [ts || "0 seconds", diff.days === 17 && diff.hours === 1] as const;
},
latencyTooltipData(message: Message) {
const { id, nonce } = message;
@ -73,16 +87,23 @@ export default definePlugin({
const abs = Math.abs(delta);
const ahead = abs !== delta;
const stringDelta = this.stringDelta(abs);
const [stringDelta, isSuspectedKotlinDiscord] = this.stringDelta(abs);
const isKotlinDiscord = ahead && isSuspectedKotlinDiscord;
// Also thanks dziurwa
// 2 minutes
const TROLL_LIMIT = 2 * 60;
const { latency } = this.settings.store;
const fill: Fill = delta >= TROLL_LIMIT || ahead ? ["text-muted", "text-muted", "text-muted"] : delta >= (latency * 2) ? ["status-danger", "text-muted", "text-muted"] : ["status-warning", "status-warning", "text-muted"];
const fill: Fill = isKotlinDiscord
? ["status-positive", "status-positive", "text-muted"]
: delta >= TROLL_LIMIT || ahead
? ["text-muted", "text-muted", "text-muted"]
: delta >= (latency * 2)
? ["status-danger", "text-muted", "text-muted"]
: ["status-warning", "status-warning", "text-muted"];
return abs >= latency ? { delta: stringDelta, ahead: abs !== delta, fill } : null;
return abs >= latency ? { delta: stringDelta, ahead, fill, isKotlinDiscord } : null;
},
Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
@ -92,7 +113,7 @@ export default definePlugin({
if (!isNonNullish(d)) return null;
return <Tooltip
text={d.ahead ? `This user's clock is ${d.delta} ahead` : `This message was sent with a delay of ${d.delta}.`}
text={d.ahead ? `This user's clock is ${d.delta} ahead. ${d.isKotlinDiscord ? "User is suspected to be on an old mobile client" : ""}` : `This message was sent with a delay of ${d.delta}.`}
position="top"
>
{

View file

@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu
guild={guild}
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
onClose={modalProps.onClose}
/>
));
else if (permission.type === PermissionType.User) {
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
onClose={modalProps.onClose}
/>
));
}
}}
>
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options"
>
<Menu.MenuItem
id="vc-pw-view-as-role"
label="View As Role"
id="vc-copy-role-id"
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
const role = GuildStore.getRole(guild.id, roleId);
if (!role) return;
Clipboard.copy(roleId);
}}
/>
onClose();
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem
id="vc-pw-view-as-role"
label={i18n.Messages.VIEW_AS_ROLE}
action={() => {
const role = GuildStore.getRole(guild.id, roleId);
if (!role) return;
FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE",
guildId: guild.id,
data: {
type: "ROLES",
roles: {
[roleId]: role
onClose();
FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE",
guildId: guild.id,
data: {
type: "ROLES",
roles: {
[roleId]: role
}
}
}
});
});
}
}
/>
)}
</Menu.Menu>
);
}
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
return (
<Menu.Menu
navId={cl("user-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
label={i18n.Messages.COPY_ID_USER}
action={() => {
Clipboard.copy(userId);
}}
/>
</Menu.Menu>

View file

@ -18,10 +18,11 @@
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, React } from "@webpack/common";
import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({
showIcon: {
@ -30,6 +31,11 @@ const settings = definePluginSettings({
description: "Show an icon for toggling the plugin",
restartNeeded: true,
},
contextMenu: {
type: OptionType.BOOLEAN,
description: "Add option to toggle the functionality in the chat input context menu",
default: true
},
isEnabled: {
type: OptionType.BOOLEAN,
description: "Toggle functionality",
@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
);
};
const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
if (!contextMenu) return;
const group = findGroupChildrenByChildId("submit-button", children);
if (!group) return;
const idx = group.findIndex(c => c?.props?.id === "submit-button");
group.splice(idx + 1, 0,
<Menu.MenuCheckboxItem
id="vc-silent-typing"
label="Enable Silent Typing"
checked={isEnabled}
action={() => settings.store.isEnabled = !settings.store.isEnabled}
/>
);
};
export default definePlugin({
name: "SilentTyping",
authors: [Devs.Ven, Devs.Rini],
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
"textarea-context": ChatBarContextCheckbox
},
patches: [
{
find: '.dispatch({type:"TYPING_START_LOCAL"',

View file

@ -15,6 +15,7 @@ This allows themes to more easily theme those elements or even do things that ot
### Chat Messages
- `data-author-id` contains the id of the author
- `data-author-username` contains the username of the author
- `data-is-self` is a boolean indicating whether this is the current user's message
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)

View file

@ -36,10 +36,12 @@ export default definePlugin({
],
getMessageProps(props: { message: Message; }) {
const authorId = props.message?.author?.id;
const author = props.message?.author;
const authorId = author?.id;
return {
"data-author-id": authorId,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
"data-author-username": author?.username,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
};
}
});

View file

@ -1,6 +1,6 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@ -13,10 +13,7 @@ import { findByProps } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const enum ChannelTypes {
DM = 1,
GROUP_DM = 3
}
const { ChannelTypes } = findByProps("ChannelTypes");
interface Message {
guild_id: string,
@ -72,14 +69,35 @@ interface Call {
}
const MuteStore = findByProps("isSuppressEveryoneEnabled");
const Notifs = findByProps("makeTextChatNotification");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
ignoreBots: {
botNotifications: {
type: OptionType.BOOLEAN,
description: "Ignore messages from bots",
description: "Allow bot notifications",
default: false
},
serverNotifications: {
type: OptionType.BOOLEAN,
description: "Allow server notifications",
default: true
},
dmNotifications: {
type: OptionType.BOOLEAN,
description: "Allow Direct Message notifications",
default: true
},
groupDmNotifications: {
type: OptionType.BOOLEAN,
description: "Allow Group DM notifications",
default: true
},
callNotifications: {
type: OptionType.BOOLEAN,
description: "Allow call notifications",
default: true
},
pingColor: {
type: OptionType.STRING,
description: "User mention color",
@ -100,6 +118,11 @@ const settings = definePluginSettings({
description: "Notif duration (secs)",
default: 1.0,
},
timeoutPerCharacter: {
type: OptionType.NUMBER,
description: "Duration multiplier per character",
default: 0.5
},
opacity: {
type: OptionType.SLIDER,
description: "Notif opacity",
@ -124,7 +147,7 @@ export default definePlugin({
settings,
flux: {
CALL_UPDATE({ call }: { call: Call; }) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
const channel = ChannelStore.getChannel(call.channel_id);
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
}
@ -134,7 +157,7 @@ export default definePlugin({
try {
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id);
if (!shouldNotify(message, channel)) return;
if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
@ -194,6 +217,7 @@ export default definePlugin({
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
}
// color role mentions (unity styling btw lol)
if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) {
const role = GuildStore.getRole(channel.guild_id, roleId);
@ -213,6 +237,7 @@ export default definePlugin({
}
}
// color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
@ -221,6 +246,7 @@ export default definePlugin({
}
}
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
@ -229,13 +255,20 @@ export default definePlugin({
}
});
function shouldIgnoreForChannelType(channel: Channel) {
if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
else return !settings.store.serverNotifications;
}
function sendMsgNotif(titleString: string, content: string, message: Message) {
const timeout = Math.max(settings.store.timeout, content.length * settings.store.timeoutPerCharacter);
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = {
messageType: 1,
index: 0,
timeout: settings.store.timeout,
height: calculateHeight(cleanMessage(content)),
timeout,
height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@ -254,7 +287,7 @@ function sendOtherNotif(content: string, titleString: string) {
messageType: 1,
index: 0,
timeout: settings.store.timeout,
height: calculateHeight(cleanMessage(content)),
height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@ -267,13 +300,11 @@ function sendOtherNotif(content: string, titleString: string) {
Native.sendToOverlay(msgData);
}
function shouldNotify(message: Message, channel: Channel) {
function shouldNotify(message: Message, channel: string) {
const currentUser = UserStore.getCurrentUser();
if (message.author.id === currentUser.id) return false;
if (message.author.bot && settings.store.ignoreBots) return false;
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
return message.mentions.some(m => m.id === currentUser.id);
if (message.author.bot && !settings.store.botNotifications) return false;
return Notifs.shouldNotify(message, channel);
}
function calculateHeight(content: string) {
@ -282,7 +313,3 @@ function calculateHeight(content: string) {
if (content.length <= 300) return 200;
return 250;
}
function cleanMessage(content: string) {
return content.replace(new RegExp("<[^>]*>", "g"), "");
}

View file

@ -418,6 +418,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Kyuuhachi",
id: 236588665420251137n,
},
nin0dev: {
name: "nin0dev",
id: 886685857560539176n
},
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
@ -461,6 +465,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
},
ImBanana: {
name: "Im_Banana",
id: 635250116688871425n
}
} satisfies Record<string, Dev>);