diff --git a/package.json b/package.json index 076b2999..5ffac7ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.7", + "version": "1.6.9", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs index 8b2510b2..145ea5a5 100644 --- a/scripts/runInstaller.mjs +++ b/scripts/runInstaller.mjs @@ -118,11 +118,15 @@ const installerBin = await ensureBinary(); console.log("Now running Installer..."); -execFileSync(installerBin, { - stdio: "inherit", - env: { - ...process.env, - VENCORD_USER_DATA_DIR: BASE_DIR, - VENCORD_DEV_INSTALL: "1" - } -}); +try { + execFileSync(installerBin, { + stdio: "inherit", + env: { + ...process.env, + VENCORD_USER_DATA_DIR: BASE_DIR, + VENCORD_DEV_INSTALL: "1" + } + }); +} catch { + console.error("Something went wrong. Please check the logs above."); +} diff --git a/src/api/ChatButton.css b/src/api/ChatButton.css new file mode 100644 index 00000000..30869a84 --- /dev/null +++ b/src/api/ChatButton.css @@ -0,0 +1,4 @@ +.vc-chatbar-button { + display: flex; + align-items: center; +} diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx new file mode 100644 index 00000000..fcb76fff --- /dev/null +++ b/src/api/ChatButtons.tsx @@ -0,0 +1,128 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./ChatButton.css"; + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Logger } from "@utils/Logger"; +import { waitFor } from "@webpack"; +import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; +import { HTMLProps, MouseEventHandler, ReactNode } from "react"; + +let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>; +waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m); + +export interface ChatBarProps { + channel: Channel; + disabled: boolean; + isEmpty: boolean; + type: { + analyticsName: string; + attachments: boolean; + autocomplete: { + addReactionShortcut: boolean, + forceChatLayer: boolean, + reactions: boolean; + }, + commands: { + enabled: boolean; + }, + drafts: { + type: number, + commandType: number, + autoSave: boolean; + }, + emojis: { + button: boolean; + }, + gifs: { + button: boolean, + allowSending: boolean; + }, + gifts: { + button: boolean; + }, + permissions: { + requireSendMessages: boolean; + }, + showThreadPromptOnReply: boolean, + stickers: { + button: boolean, + allowSending: boolean, + autoSuggest: boolean; + }, + users: { + allowMentioning: boolean; + }, + submit: { + button: boolean, + ignorePreference: boolean, + disableEnterToSubmit: boolean, + clearOnSubmit: boolean, + useDisabledStylesOnSubmit: boolean; + }, + uploadLongMessages: boolean, + upsellLongMessages: { + iconOnly: boolean; + }, + showCharacterCount: boolean, + sedReplace: boolean; + }; +} + +export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null; + +const buttonFactories = new Map(); +const logger = new Logger("ChatButtons"); + +export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { + if (props.disabled) return; + + for (const [key, Button] of buttonFactories) { + buttons.push( + logger.error(`Failed to render ${key}`, e.error)}> + + + )} + + ); +}, { noop: true }); diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 9535fb62..6f79ef70 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -21,7 +21,7 @@ import { Settings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; -import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; +import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { nanoid } from "nanoid"; import type { DispatchWithoutAction } from "react"; @@ -129,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) { richBody={
{data.body} - +
} /> diff --git a/src/api/index.ts b/src/api/index.ts index 08f23810..5dca6310 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ */ import * as $Badges from "./Badges"; +import * as $ChatButtons from "./ChatButtons"; import * as $Commands from "./Commands"; import * as $ContextMenu from "./ContextMenu"; import * as $DataStore from "./DataStore"; @@ -104,3 +105,8 @@ export const Notifications = $Notifications; * An api allowing you to patch and add/remove items to/from context menus */ export const ContextMenu = $ContextMenu; + +/** + * An API allowing you to add buttons to the chat input + */ +export const ChatButtons = $ChatButtons; diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 76d1ccaf..3ee44d92 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -129,6 +129,15 @@ if (!IS_VANILLA) { }); process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); + + // Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790 + const originalAppend = app.commandLine.appendSwitch; + app.commandLine.appendSwitch = function (...args) { + if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) { + args[1] += ",WidgetLayering"; + } + return originalAppend.apply(this, args); + }; } else { console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); } diff --git a/src/plugins/_api/chatButtons.ts b/src/plugins/_api/chatButtons.ts new file mode 100644 index 00000000..ca85964c --- /dev/null +++ b/src/plugins/_api/chatButtons.ts @@ -0,0 +1,22 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ChatInputButtonAPI", + description: "API to add buttons to the chat input", + authors: [Devs.Ven], + + patches: [{ + find: 'location:"ChannelTextAreaButtons"', + replacement: { + match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/, + replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);" + } + }] +}); diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts index 95a59c03..b5aae5ff 100644 --- a/src/plugins/clearURLs/defaultRules.ts +++ b/src/plugins/clearURLs/defaultRules.ts @@ -140,11 +140,11 @@ export const defaultRules = [ "tt_content", "lr@yandex.*", "redircnt@yandex.*", - "feature@youtube.com", - "kw@youtube.com", - "si@youtube.com", - "pp@youtube.com", - "si@youtu.be", + "feature@*.youtube.com", + "kw@*.youtube.com", + "si@*.youtube.com", + "pp@*.youtube.com", + "si@*.youtu.be", "wt_zmc", "utm_source", "utm_content", diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts index 9d38b7d1..f8c76d7f 100644 --- a/src/plugins/crashHandler/index.ts +++ b/src/plugins/crashHandler/index.ts @@ -25,7 +25,6 @@ import definePlugin, { OptionType } from "@utils/types"; import { maybePromptToUpdate } from "@utils/updater"; import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common"; -import type { ReactElement } from "react"; const CrashHandlerLogger = new Logger("CrashHandler"); const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => { @@ -57,13 +56,13 @@ const settings = definePluginSettings({ } }); -let crashCount: number = 0; -let lastCrashTimestamp: number = 0; -let shouldAttemptNextHandle = false; +let hasCrashedOnce = false; +let isRecovering = false; +let shouldAttemptRecover = true; export default definePlugin({ name: "CrashHandler", - description: "Utility plugin for handling and possibly recovering from Crashes without a restart", + description: "Utility plugin for handling and possibly recovering from crashes without a restart", authors: [Devs.Nuckyz], enabledByDefault: true, @@ -73,61 +72,67 @@ export default definePlugin({ { find: ".Messages.ERRORS_UNEXPECTED_CRASH", replacement: { - match: /(?=this\.setState\()/, - replace: "$self.handleCrash(this)||" + match: /this\.setState\((.+?)\)/, + replace: "$self.handleCrash(this,$1);" } } ], - handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true; + handleCrash(_this: any, errorState: any) { + _this.setState(errorState); - shouldAttemptNextHandle = false; + // Already recovering, prevent error which happens more than once too fast to trigger another recover + if (isRecovering) return; + isRecovering = true; - if (++crashCount > 5) { + // 1 ms timeout to avoid react breaking when re-rendering + setTimeout(() => { try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Awn :( Discord has crashed more than five times, not attempting to recover.", - noPersist: true, - }); + // Prevent a crash loop with an error that could not be handled + if (!shouldAttemptRecover) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.", + noPersist: true + }); + } catch { } + + return; + } + + shouldAttemptRecover = false; + // This is enough to avoid a crash loop + setTimeout(() => shouldAttemptRecover = true, 500); } catch { } - lastCrashTimestamp = Date.now(); - return false; - } + try { + if (!hasCrashedOnce) { + hasCrashedOnce = true; + maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); + } + } catch { } - setTimeout(() => crashCount--, 60_000); - - try { - if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); - - if (settings.store.attemptToPreventCrashes) { - this.handlePreventCrash(_this); - return true; + try { + if (settings.store.attemptToPreventCrashes) { + this.handlePreventCrash(_this); + } + } catch (err) { + CrashHandlerLogger.error("Failed to handle crash", err); } - - return false; - } catch (err) { - CrashHandlerLogger.error("Failed to handle crash", err); - return false; - } finally { - lastCrashTimestamp = Date.now(); - } + }, 1); }, - handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp >= 1_000) { - try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Attempting to recover...", - noPersist: true, - }); - } catch { } - } + handlePreventCrash(_this: any) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Attempting to recover...", + noPersist: true + }); + } catch { } try { const channelId = SelectedChannelStore.getChannelId(); @@ -176,9 +181,12 @@ export default definePlugin({ } } + + // Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens + setImmediate(() => isRecovering = false); + try { - shouldAttemptNextHandle = true; - _this.forceUpdate(); + _this.setState({ error: null, info: null }); } catch (err) { CrashHandlerLogger.debug("Failed to update crash handler component.", err); } diff --git a/src/plugins/disableDMCallIdle/index.ts b/src/plugins/disableCallIdle/index.ts similarity index 70% rename from src/plugins/disableDMCallIdle/index.ts rename to src/plugins/disableCallIdle/index.ts index bce119d0..904d53b0 100644 --- a/src/plugins/disableDMCallIdle/index.ts +++ b/src/plugins/disableCallIdle/index.ts @@ -16,18 +16,27 @@ * along with this program. If not, see . */ +import { migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; +migratePluginSettings("DisableCallIdle", "DisableDMCallIdle"); export default definePlugin({ - name: "DisableDMCallIdle", - description: "Disables automatically getting kicked from a DM voice call after 3 minutes.", + name: "DisableCallIdle", + description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.", authors: [Devs.Nuckyz], patches: [ { find: ".Messages.BOT_CALL_IDLE_DISCONNECT", replacement: { - match: /(?<=function \i\(\){)(?=.{1,120}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/, + match: /,?(?=this\.idleTimeout=new \i\.Timeout)/, + replace: ";return;" + } + }, + { + find: "handleIdleUpdate(){", + replacement: { + match: /(?<=_initialize\(\){)/, replace: "return;" } } diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts index 6107df00..560cae38 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.ts @@ -108,6 +108,7 @@ const enum FakeNoticeType { const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/; const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./; const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/; +const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/; const settings = definePluginSettings({ enableEmojiBypass: { @@ -156,6 +157,11 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true, restartNeeded: true + }, + useHyperLinks: { + description: "Whether to use hyperlinks when sending fake emojis and stickers", + type: OptionType.BOOLEAN, + default: true } }); @@ -345,7 +351,7 @@ export default definePlugin({ predicate: () => settings.store.transformEmojis, replacement: { // Add the fake nitro emoji notice - match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.+?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/, + match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.*?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/, replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)` } }, @@ -447,13 +453,23 @@ export default definePlugin({ trimContent(content: Array) { const firstContent = content[0]; - if (typeof firstContent === "string") content[0] = firstContent.trimStart(); - if (content[0] === "") content.shift(); + if (typeof firstContent === "string") { + content[0] = firstContent.trimStart(); + content[0] || content.shift(); + } else if (firstContent?.type === "span") { + firstContent.props.children = firstContent.props.children.trimStart(); + firstContent.props.children || content.shift(); + } const lastIndex = content.length - 1; const lastContent = content[lastIndex]; - if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd(); - if (content[lastIndex] === "") content.pop(); + if (typeof lastContent === "string") { + content[lastIndex] = lastContent.trimEnd(); + content[lastIndex] || content.pop(); + } else if (lastContent?.type === "span") { + lastContent.props.children = lastContent.props.children.trimEnd(); + lastContent.props.children || content.pop(); + } }, clearEmptyArrayItems(array: Array) { @@ -465,7 +481,7 @@ export default definePlugin({ }, patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { - // If content has more than one child or it's a single ReactElement like a header or list + // If content has more than one child or it's a single ReactElement like a header, list or span if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content; let nextIndex = content.length; @@ -574,7 +590,7 @@ export default definePlugin({ itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url)); for (const item of itemsToMaybePush) { - if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue; + if (!settings.store.transformCompoundSentence && !item.startsWith("http") && !hyperLinkRegex.test(item)) continue; const imgMatch = item.match(fakeNitroStickerRegex); if (imgMatch) { @@ -619,8 +635,7 @@ export default definePlugin({ case "image": { if ( !settings.store.transformCompoundSentence - && !contentItems.includes(embed.url!) - && !contentItems.includes(embed.image?.proxyURL!) + && !contentItems.some(item => item === embed.url! || item.match(hyperLinkRegex)?.[1] === embed.url!) ) return false; if (settings.store.transformEmojis) { @@ -698,7 +713,7 @@ export default definePlugin({ }, getStickerLink(stickerId: string) { - return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`; + return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`; }, async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) { @@ -795,12 +810,16 @@ export default definePlugin({ if (sticker.format_type === StickerType.GIF && link.includes(".png")) { link = link.replace(".png", ".gif"); } + if (sticker.format_type === StickerType.APNG) { this.sendAnimatedSticker(link, sticker.id, channelId); return { cancel: true }; } else { + const url = new URL(link); + url.searchParams.set("name", sticker.name); + + messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`; extra.stickers!.length = 0; - messageObj.content += ` ${link}&name=${encodeURIComponent(sticker.name)}`; } } @@ -813,12 +832,13 @@ export default definePlugin({ if (emoji.guildId === guildId && !emoji.animated) continue; const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; - const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ - size: Settings.plugins.FakeNitro.emojiSize, - name: encodeURIComponent(emoji.name) - })); + + const url = new URL(emoji.url); + url.searchParams.set("size", s.emojiSize.toString()); + url.searchParams.set("name", emoji.name); + messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; }); } } @@ -840,11 +860,11 @@ export default definePlugin({ if (emoji.available !== false && canUseEmotes) return emojiStr; if (emoji.guildId === guildId && !emoji.animated) return emojiStr; - const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ - size: Settings.plugins.FakeNitro.emojiSize, - name: encodeURIComponent(emoji.name) - })); - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + emojiStr.length)}`; + const url = new URL(emoji.url); + url.searchParams.set("size", s.emojiSize.toString()); + url.searchParams.set("name", emoji.name); + + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); }); }, diff --git a/src/plugins/fixYoutubeEmbeds.desktop/native.ts b/src/plugins/fixYoutubeEmbeds.desktop/native.ts index 5a3ef2c6..d5c2df36 100644 --- a/src/plugins/fixYoutubeEmbeds.desktop/native.ts +++ b/src/plugins/fixYoutubeEmbeds.desktop/native.ts @@ -16,8 +16,9 @@ app.on("browser-window-created", (_, win) => { frame.executeJavaScript(` new MutationObserver(() => { - let err = document.querySelector(".ytp-error-content-wrap-subreason span")?.textContent; - if (err && err.includes("blocked it from display")) window.location.reload() + if( + document.querySelector('div.ytp-error-content-wrap-subreason a[href*="www.youtube.com/watch?v="]') + ) location.reload() }).observe(document.body, { childList: true, subtree:true }); `); } diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx index 6a3fc05a..81671735 100644 --- a/src/plugins/imageZoom/components/Magnifier.tsx +++ b/src/plugins/imageZoom/components/Magnifier.tsx @@ -123,14 +123,13 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz waitFor(() => instance.state.readyState === "READY", () => { const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement; element.current = elem; - elem.firstElementChild!.setAttribute("draggable", "false"); + elem.querySelector("img,video")?.setAttribute("draggable", "false"); if (instance.props.animated) { originalVideoElementRef.current = elem!.querySelector("video")!; originalVideoElementRef.current.addEventListener("timeupdate", syncVideos); - setReady(true); - } else { - setReady(true); } + + setReady(true); }); document.addEventListener("keydown", onKeyDown); document.addEventListener("keyup", onKeyUp); @@ -155,7 +154,9 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz if (!ready) return null; - const box = element.current!.getBoundingClientRect(); + const box = element.current?.getBoundingClientRect(); + + if (!box) return null; return (
. */ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; @@ -64,54 +65,31 @@ function Indicator() { } -function ChatBarIcon(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - if (chatBoxProps.type.analyticsName !== "normal") return null; +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + if (!isMainChat) return null; return ( - - {({ onMouseEnter, onMouseLeave }) => ( - // size="" = Button.Sizes.NONE - /* - many themes set "> button" to display: none, as the gift button is - the only directly descending button (all the other elements are divs.) - Thus, wrap in a div here to avoid getting hidden by that. - flex is for some reason necessary as otherwise the button goes flying off - */ -
- -
- ) - } -
+ buildEncModal()} + + buttonProps={{ + "aria-haspopup": "dialog", + }} + > + + + + ); -} +}; const settings = definePluginSettings({ savedPasswords: { @@ -125,7 +103,7 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI"], + dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], patches: [ { // Indicator @@ -135,13 +113,6 @@ export default definePlugin({ replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" } }, - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], EMBED_API_URL: "https://embed.sammcheese.net", @@ -151,10 +122,7 @@ export default definePlugin({ ), settings, async start() { - const { default: StegCloak } = await getStegCloak(); - steggo = new StegCloak(true, false); - - addButton("invDecrypt", message => { + addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) ? { label: "Decrypt Message", @@ -170,10 +138,16 @@ export default definePlugin({ } : null; }); + + addChatBarButton("InvisibleChat", ChatBarIcon); + + const { default: StegCloak } = await getStegCloak(); + steggo = new StegCloak(true, false); }, stop() { - removeButton("invDecrypt"); + removeButton("InvisibleChat"); + removeButton("InvisibleChat"); }, // Gets the Embed of a Link @@ -216,7 +190,6 @@ export default definePlugin({ }); }, - chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }), popOverIcon: () => , indicator: ErrorBoundary.wrap(Indicator, { noop: true }) }); diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 76282999..50c09ec9 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -29,15 +29,17 @@ import { ChannelStore, FluxDispatcher, GuildStore, + IconUtils, MessageStore, Parser, + PermissionsBits, PermissionStore, RestAPI, Text, TextAndImagesSettingsStores, UserStore } from "@webpack/common"; -import { Channel, Guild, Message } from "discord-types/general"; +import { Channel, Message } from "discord-types/general"; const messageCache = new Map id && idList.includes(id)); + const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id)); if (listMode === "blacklist" && isListed) continue; if (listMode === "whitelist" && !isListed) continue; @@ -265,8 +267,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { const messageProps: MessageEmbedProps = { message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), - channel: linkedChannel, - guildID + channel: linkedChannel }; const type = settings.store.automodEmbeds; @@ -280,59 +281,64 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { return accessories.length ? <>{accessories} : null; } -function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null { - const isDM = guildID === "@me"; +function getChannelLabelAndIconUrl(channel: Channel) { + if (channel.isDM()) return ["Direct Message", IconUtils.getUserAvatarURL(UserStore.getUser(channel.recipients[0]))]; + if (channel.isGroupDM()) return ["Group DM", IconUtils.getChannelIconURL(channel)]; + return ["Server", IconUtils.getGuildIconURL(GuildStore.getGuild(channel.guild_id))]; +} - const guild = !isDM && GuildStore.getGuild(channel.guild_id); +function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null { const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); + const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel); - return - {isDM ? "Direct Message - " : (guild as Guild).name + " - "} - {isDM - ? Parser.parse(`<@${dmReceiver.id}>`) - : Parser.parse(`<#${channel.id}>`) - } - , - iconProxyURL: guild - ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` - : `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}` - } - }} - renderDescription={() => ( -
- -
- )} - />; + return ( + + {channelLabel} - + {Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)} + , + iconProxyURL: iconUrl + } + }} + renderDescription={() => ( +
+ +
+ )} + /> + ); } function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { - const { message, channel, guildID } = props; + const { message, channel } = props; const compact = TextAndImagesSettingsStores.MessageDisplayCompact.useSetting(); - const isDM = guildID === "@me"; const images = getImages(message); const { parse } = Parser; + const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel); + return - {isDM - ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) - : parse(`<#${channel.id}>`) - } - {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} + + {iconUrl && } + + {channelLabel} - + {channel.isDM() + ? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) + : Parser.parse(`<#${channel.id}>`) + } + } compact={compact} diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index ef0aa03b..ef986bf8 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -26,7 +26,7 @@ import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, i18n, Menu, moment, Parser, Timestamp, UserStore } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, i18n, Menu, Parser, Timestamp, UserStore } from "@webpack/common"; import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; @@ -122,7 +122,7 @@ export default definePlugin({ makeEdit(newMessage: any, oldMessage: any): any { return { - timestamp: moment?.call(newMessage.edited_timestamp), + timestamp: new Date(newMessage.edited_timestamp), content: oldMessage.content }; }, diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 9921bca8..d1ad941b 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,7 +198,7 @@ export default definePlugin({ replacement: [ // make the tag show the right text { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.BOT_TAG_BOT/, replace: (_, origSwitch, variant, tags, displayedText, strings) => `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` }, diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 226d000f..40d5201c 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -20,11 +20,10 @@ import { Devs } from "@utils/constants"; import { isNonNullish } from "@utils/guards"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Avatar, ChannelStore, Clickable, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common"; +import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common"; import { Channel, User } from "discord-types/general"; const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); -const AvatarUtils = findByPropsLazy("getChannelIconURL"); const UserUtils = findByPropsLazy("getGlobalName"); const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); @@ -71,7 +70,7 @@ export default definePlugin({ }} > diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index 3c676771..bcd6bdf0 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -104,6 +104,7 @@ function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: G guildMember.nick || UserStore.getUser(guildMember.userId).username ) } + onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state} defaultState={settings.store.defaultPermissionsDropdownState} buttons={[ ( diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index ba4aa838..ca766aff 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -24,7 +24,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "PictureInPicture", description: "Adds picture in picture to videos (next to the Download button)", - authors: [Devs.Lumap], + authors: [Devs.Nobody], settings, patches: [ { diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index f2634ae6..fe6b227a 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -16,22 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { StartAt } from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common"; import { MessageAttachment } from "discord-types/general"; -interface Props { - type: { - analyticsName: string; - isEmpty: boolean; - attachments: boolean; - }; -} - const UploadStore = findByPropsLazy("getUploads"); const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); @@ -81,13 +73,11 @@ const getAttachments = async (channelId: string) => ); -export function PreviewButton(chatBoxProps: Props) { - const { isEmpty, attachments } = chatBoxProps.type; - +const PreviewButton: ChatBarButton = ({ isMainChat, isEmpty, type: { attachments } }) => { const channelId = SelectedChannelStore.getChannelId(); const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0; const hasContent = !isEmpty && draft?.length > 0; @@ -95,47 +85,47 @@ export function PreviewButton(chatBoxProps: Props) { if (!hasContent && !hasAttachments) return null; return ( - - {tooltipProps => ( - - )} - + + sendBotMessage( + channelId, + { + content: getDraft(channelId), + author: UserStore.getCurrentUser(), + attachments: hasAttachments ? await getAttachments(channelId) : undefined, + } + )} + buttonProps={{ + style: { + translate: "0 2px" + } + }} + > + + + + ); -} +}; export default definePlugin({ name: "PreviewMessage", description: "Lets you preview your message before sending it.", authors: [Devs.Aria], - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + dependencies: ["ChatInputButtonAPI"], + // start early to ensure we're the first plugin to add our button + // This makes the popping in less awkward + startAt: StartAt.Init, - chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), + start: () => addChatBarButton("previewMessage", PreviewButton), + stop: () => removeChatBarButton("previewMessage"), }); diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx index b5b0b5f8..0e08edb6 100644 --- a/src/plugins/readAllNotificationsButton/index.tsx +++ b/src/plugins/readAllNotificationsButton/index.tsx @@ -25,16 +25,17 @@ function onClick() { const channels: Array = []; Object.values(GuildStore.getGuilds()).forEach(guild => { - GuildChannelStore.getChannels(guild.id).SELECTABLE.forEach((c: { channel: { id: string; }; }) => { - if (!ReadStateStore.hasUnread(c.channel.id)) return; + GuildChannelStore.getChannels(guild.id).SELECTABLE + .concat(GuildChannelStore.getChannels(guild.id).VOCAL) + .forEach((c: { channel: { id: string; }; }) => { + if (!ReadStateStore.hasUnread(c.channel.id)) return; - channels.push({ - channelId: c.channel.id, - // messageId: c.channel?.lastMessageId, - messageId: ReadStateStore.lastMessageId(c.channel.id), - readStateType: 0 + channels.push({ + channelId: c.channel.id, + messageId: ReadStateStore.lastMessageId(c.channel.id), + readStateType: 0 + }); }); - }); }); FluxDispatcher.dispatch({ diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx index 977745a2..20b298cc 100644 --- a/src/plugins/reviewDB/components/ReviewComponent.tsx +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord"; import { classes } from "@utils/misc"; import { LazyComponent } from "@utils/react"; import { filters, findBulk } from "@webpack"; -import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common"; +import { Alerts, Parser, Timestamp, useState } from "@webpack/common"; import { Auth, getToken } from "../auth"; import { Review, ReviewType } from "../entities"; @@ -163,7 +163,7 @@ export default LazyComponent(() => { { !settings.store.hideTimestamps && review.type !== ReviewType.System && ( - + {dateFormat.format(review.timestamp * 1000)} ) } diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 6d488add..ac7c5edb 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -18,6 +18,7 @@ import "./styles.css"; +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; @@ -26,7 +27,7 @@ import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common"; const settings = definePluginSettings({ replaceMessageContents: { @@ -122,25 +123,49 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi ); } +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + if (!isMainChat) return null; + + return ( + { + const key = openModal(props => ( + closeModal(key)} + /> + )); + }} + buttonProps={{ "aria-haspopup": "dialog" }} + > + + + ); +}; + export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], - dependencies: ["MessageEventsAPI"], + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - settings: settings, - - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + settings, start() { + addChatBarButton("SendTimestamps", ChatBarIcon); this.listener = addPreSendListener((_, msg) => { if (settings.store.replaceMessageContents) { msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); @@ -149,56 +174,10 @@ export default definePlugin({ }, stop() { + removeChatBarButton("SendTimestamps"); removePreSendListener(this.listener); }, - chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) { - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- ) - } -
- ); - }, - settingsAboutComponent() { const samples = [ "12:00", diff --git a/src/plugins/sendTimestamps/styles.css b/src/plugins/sendTimestamps/styles.css index 75c3f16b..34a856a6 100644 --- a/src/plugins/sendTimestamps/styles.css +++ b/src/plugins/sendTimestamps/styles.css @@ -42,10 +42,6 @@ margin-bottom: 1em; } -.vc-st-button { - padding: 0 6px; -} - .vc-st-button svg { transform: scale(1.1) translateY(1px); } diff --git a/src/plugins/serverProfile/GuildProfileModal.tsx b/src/plugins/serverProfile/GuildProfileModal.tsx index 97b40b76..834367e0 100644 --- a/src/plugins/serverProfile/GuildProfileModal.tsx +++ b/src/plugins/serverProfile/GuildProfileModal.tsx @@ -12,10 +12,9 @@ import { classes } from "@utils/misc"; import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findExportedComponentLazy } from "@webpack"; -import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; +import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; import { Guild, User } from "discord-types/general"; -const IconUtils = findByPropsLazy("getGuildBannerURL"); const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper"); const FriendRow = findExportedComponentLazy("FriendRow"); @@ -50,7 +49,7 @@ const fetched = { function renderTimestamp(timestamp: number) { return ( - + ); } @@ -65,10 +64,7 @@ function GuildProfileModal({ guild }: GuildProps) { const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo); - const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({ - id: guild.id, - banner: guild.banner - }, true).replace(/\?size=\d+$/, "?size=1024"); + const bannerUrl = guild.banner && IconUtils.getGuildBannerURL(guild, true)!.replace(/\?size=\d+$/, "?size=1024"); const iconUrl = guild.icon && IconUtils.getGuildIconURL({ id: guild.id, @@ -89,7 +85,7 @@ function GuildProfileModal({ guild }: GuildProps) { )}
- {guild.icon + {iconUrl ? ([]); const { @@ -216,12 +216,12 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { {lastMessageId && Last {channel.isForumChannel() ? "post" : "message"} created: - + } {lastPinTimestamp && - Last message pin: + Last message pin: } {(rateLimitPerUser ?? 0) > 0 && Slowmode: {formatDuration(rateLimitPerUser!, "seconds")} @@ -301,19 +301,19 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { )} Allowed users and roles: - + {({ onMouseLeave, onMouseEnter }) => (
- {viewAllowedUsersAndRoles && } + {defaultAllowedUsersAndRolesDropdownState && }
diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 906bed50..919f3f3c 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -29,7 +29,7 @@ import type { Channel, Role } from "discord-types/general"; import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen"; -const ChannelListClasses = findByPropsLazy("channelEmoji", "unread", "icon"); +const ChannelListClasses = findByPropsLazy("modeMuted", "modeSelected", "unread", "icon"); const enum ShowMode { LockIcon, @@ -162,7 +162,7 @@ export default definePlugin({ }, // Add the hidden eye icon if the channel is hidden { - match: /\i\.children.+?:null(?<=,channel:(\i).+?)/, + match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/, replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null` }, // Make voice channels also appear as muted if they are muted diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx index b7b33826..d4207a68 100644 --- a/src/plugins/silentMessageToggle/index.tsx +++ b/src/plugins/silentMessageToggle/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; +import { React, useEffect, useState } from "@webpack/common"; let lastState = false; @@ -41,19 +41,15 @@ const settings = definePluginSettings({ } }); -function SilentMessageToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const [enabled, setEnabled] = React.useState(lastState); +const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => { + const [enabled, setEnabled] = useState(lastState); function setEnabledValue(value: boolean) { if (settings.store.persistState) lastState = value; setEnabled(value); } - React.useEffect(() => { + useEffect(() => { const listener: SendListener = (_, message) => { if (enabled) { if (settings.store.autoDisable) setEnabledValue(false); @@ -65,55 +61,39 @@ function SilentMessageToggle(chatBoxProps: { return () => void removePreSendListener(listener); }, [enabled]); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; return ( - - {tooltipProps => ( -
- -
- )} -
+ setEnabledValue(!enabled)} + > + + + {!enabled && <> + + + + + + } + + ); -} +}; export default definePlugin({ name: "SilentMessageToggle", authors: [Devs.Nuckyz, Devs.CatNoir], description: "Adds a button to the chat bar to toggle sending a silent message.", - dependencies: ["MessageEventsAPI"], - + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], settings, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - chatBarIcon: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), + start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), + stop: () => removeChatBarButton("SilentMessageToggle") }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index dae7ad4c..8b59c6ac 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; +import { FluxDispatcher, React } from "@webpack/common"; const settings = definePluginSettings({ showIcon: { @@ -37,45 +37,32 @@ const settings = definePluginSettings({ } }); -function SilentTypingToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const { isEnabled } = settings.use(["isEnabled"]); +const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => { + const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat || !showIcon) return null; return ( - - {(tooltipProps: any) => ( -
- -
- )} -
+ + + + {isEnabled && } + + ); -} +}; export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini], description: "Hide that you are typing", + dependencies: ["CommandsAPI", "ChatInputButtonAPI"], + settings, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -84,17 +71,8 @@ export default definePlugin({ replace: "startTyping:$self.startTyping,stop" } }, - { - find: "ChannelTextAreaButtons", - predicate: () => settings.store.showIcon, - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], - dependencies: ["CommandsAPI"], - settings, + commands: [{ name: "silenttype", description: "Toggle whether you're hiding that you're typing or not.", @@ -120,5 +98,6 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), + start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + stop: () => removeChatBarButton("SilentTyping"), }); diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index 64958943..cc0ed5e9 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import { ChatBarButton } from "@api/ChatButtons"; +import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; -import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Alerts, Forms } from "@webpack/common"; import { settings } from "./settings"; import { TranslateModal } from "./TranslateModal"; @@ -37,42 +39,49 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: ); } -export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { analyticsName: string; }; }; }) { +export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { const { autoTranslate } = settings.use(["autoTranslate"]); - if (slateProps.type.analyticsName !== "normal") - return null; + if (!isMainChat) return null; - const toggle = () => settings.store.autoTranslate = !autoTranslate; + const toggle = () => { + const newState = !autoTranslate; + settings.store.autoTranslate = newState; + if (newState && settings.store.showAutoTranslateAlert !== false) + Alerts.show({ + title: "Vencord Auto-Translate Enabled", + body: <> + + You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent. + + + If this was an accident, disable it again, or it will change your message content before sending. + + , + cancelText: "Disable Auto-Translate", + confirmText: "Got it", + secondaryConfirmText: "Don't show again", + onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false, + onCancel: () => settings.store.autoTranslate = false + }); + }; return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- )} -
+ openModal(props => ( + + )); + }} + onContextMenu={() => toggle()} + buttonProps={{ + "aria-haspopup": "dialog" + }} + > + + ); -} +}; diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 3b067c63..702e60cf 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -18,11 +18,11 @@ import "./styles.css"; +import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore, Menu } from "@webpack/common"; @@ -55,25 +55,16 @@ export default definePlugin({ name: "Translate", description: "Translate messages with Google Translate", authors: [Devs.Ven], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"], + dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, // not used, just here in case some other plugin wants it or w/e translate, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - start() { addAccessory("vc-translation", props => ); addContextMenuPatch("message", messageCtxPatch); + addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { if (!message.content) return null; @@ -101,13 +92,8 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); removeContextMenuPatch("message", messageCtxPatch); + removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); }, - - chatBarIcon: (slateProps: any) => ( - - - - ) }); diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts index aa4f633a..cef003a8 100644 --- a/src/plugins/translate/settings.ts +++ b/src/plugins/translate/settings.ts @@ -49,4 +49,6 @@ export const settings = definePluginSettings({ description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this", default: false } -}); +}).withPrivateSettings<{ + showAutoTranslateAlert: boolean; +}>(); diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css index b6d2223a..e9085b4e 100644 --- a/src/plugins/translate/styles.css +++ b/src/plugins/translate/styles.css @@ -35,3 +35,7 @@ .vc-trans-auto-translate { color: var(--green-360); } + +.vc-trans-chat-button { + scale: 1.085; +} diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 171c560d..28030148 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -133,7 +133,7 @@ export default definePlugin({ { find: "UNREAD_IMPORTANT:", replacement: { - match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/, + match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/, replace: "$&,$self.TypingIndicator($1.id)" } }, diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index c95307cc..935ff1c5 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -96,7 +96,7 @@ export default definePlugin({ patches: [ // above message box { - find: ".lastEditedByContainer", + find: ".popularApplicationCommandIds,", replacement: { match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/, replace: "$self.patchPopout(arguments[0]),$&", diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 2da3e21c..0920c418 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -22,11 +22,9 @@ import { ImageIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { GuildMemberStore, Menu } from "@webpack/common"; +import { GuildMemberStore, IconUtils, Menu } from "@webpack/common"; import type { Channel, Guild, User } from "discord-types/general"; -const BannerStore = findByPropsLazy("getGuildBannerURL"); interface UserContextProps { channel: Channel; @@ -91,19 +89,19 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U openImage(BannerStore.getUserAvatarURL(user, true))} + action={() => openImage(IconUtils.getUserAvatarURL(user, true))} icon={ImageIcon} /> {memberAvatar && ( openImage(BannerStore.getGuildMemberAvatarURLSimple({ + action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({ userId: user.id, avatar: memberAvatar, - guildId, + guildId: guildId!, canAnimate: true - }, true))} + }))} icon={ImageIcon} /> )} @@ -124,11 +122,11 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-icon" label="View Icon" action={() => - openImage(BannerStore.getGuildIconURL({ + openImage(IconUtils.getGuildIconURL({ id, icon, canAnimate: true - })) + })!) } icon={ImageIcon} /> @@ -138,10 +136,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-banner" label="View Banner" action={() => - openImage(BannerStore.getGuildBannerURL({ - id, - banner, - }, true)) + openImage(IconUtils.getGuildBannerURL(guild, true)!) } icon={ImageIcon} /> diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index 5f6beca2..faa24078 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -47,18 +47,23 @@ const settings = definePluginSettings({ }); const MEDIA_PROXY_URL = "https://media.discordapp.net"; -const CDN_URL = "https://cdn.discordapp.com"; +const CDN_URL = "cdn.discordapp.com"; -function fixImageUrl(urlString: string, explodeWebp: boolean) { +function fixImageUrl(urlString: string) { const url = new URL(urlString); - if (url.origin === CDN_URL) return urlString; - if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + if (url.host === CDN_URL) return urlString; url.searchParams.delete("width"); url.searchParams.delete("height"); - url.searchParams.set("quality", "lossless"); - if (explodeWebp && url.searchParams.get("format") === "webp") - url.searchParams.set("format", "png"); + + if (url.origin === MEDIA_PROXY_URL) { + url.host = CDN_URL; + url.searchParams.delete("size"); + url.searchParams.delete("quality"); + url.searchParams.delete("format"); + } else { + url.searchParams.set("quality", "lossless"); + } return url.toString(); } @@ -199,7 +204,7 @@ export default definePlugin({ ], async copyImage(url: string) { - url = fixImageUrl(url, true); + url = fixImageUrl(url); let imageData = await fetch(url).then(r => r.blob()); if (imageData.type !== "image/png") { @@ -231,7 +236,7 @@ export default definePlugin({ }, async saveImage(url: string) { - url = fixImageUrl(url, false); + url = fixImageUrl(url); const data = await fetchImage(url); if (!data) return; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 89993612..55af9360 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,10 @@ export interface Dev { * If you are fine with attribution but don't want the badge, add badge: false */ export const Devs = /* #__PURE__*/ Object.freeze({ + Nobody: { + name: "Nobody", + id: 0n, + }, Ven: { name: "Vendicated", id: 343383572805058560n @@ -359,10 +363,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "bb010g", id: 72791153467990016n, }, - Lumap: { - name: "lumap", - id: 635383782576357407n - }, Dolfies: { name: "Dolfies", id: 852892297661906993n, diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index b9bc434c..72a8a69b 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import type { Moment } from "moment"; import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; @@ -154,7 +153,7 @@ export type Switch = ComponentType>; export type Timestamp = ComponentType. */ +import { Guild, GuildMember } from "discord-types/general"; import type { ReactNode } from "react"; import type { FluxEvents } from "./fluxEvents"; @@ -182,3 +183,47 @@ export interface NavigationRouter { getLastRouteChangeSource(): any; getLastRouteChangeSourceLocationStack(): any; } + +export interface IconUtils { + getUserAvatarURL(user: User, canAnimate?: boolean, size?: number, format?: string): string; + getDefaultAvatarURL(id: string, discriminator?: string): string; + getUserBannerURL(data: { id: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + getAvatarDecorationURL(dara: { avatarDecoration: string, size: number; canCanimate?: boolean; }): string | undefined; + + getGuildMemberAvatarURL(member: GuildMember, canAnimate?: string): string | null; + getGuildMemberAvatarURLSimple(data: { guildId: string, userId: string, avatar: string, canAnimate?: boolean; size?: number; }): string; + getGuildMemberBannerURL(data: { id: string, guildId: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + + getGuildIconURL(data: { id: string, icon?: string, size?: number, canAnimate?: boolean; }): string | undefined; + getGuildBannerURL(guild: Guild, canAnimate?: boolean): string | null; + + getChannelIconURL(data: { id: string; icon?: string; applicationId?: string; size?: number; }): string | undefined; + getEmojiURL(data: { id: string, animated: boolean, size: number, forcePNG?: boolean; }): string; + + hasAnimatedGuildIcon(guild: Guild): boolean; + isAnimatedIconHash(hash: string): boolean; + + getGuildSplashURL: any; + getGuildDiscoverySplashURL: any; + getGuildHomeHeaderURL: any; + getResourceChannelIconURL: any; + getNewMemberActionIconURL: any; + getGuildTemplateIconURL: any; + getApplicationIconURL: any; + getGameAssetURL: any; + getVideoFilterAssetURL: any; + + getGuildMemberAvatarSource: any; + getUserAvatarSource: any; + getGuildSplashSource: any; + getGuildDiscoverySplashSource: any; + makeSource: any; + getGameAssetSource: any; + getGuildIconSource: any; + getGuildTemplateIconSource: any; + getGuildBannerSource: any; + getGuildHomeHeaderSource: any; + getChannelIconSource: any; + getApplicationIconSource: any; + getAnimatableSourceWithFallback: any; +} diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index c62f745a..7060573d 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -137,3 +137,5 @@ export const { persist: zustandPersist }: typeof import("zustand/middleware") = export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); export const InviteActions = findByPropsLazy("resolveInvite"); + +export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL");