diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js index f8801551e..387389ce6 100644 --- a/browser/GMPolyfill.js +++ b/browser/GMPolyfill.js @@ -62,7 +62,7 @@ function GM_fetch(url, opt) { resp.arrayBuffer = () => blobTo("arrayBuffer", blob); resp.text = () => blobTo("text", blob); resp.json = async () => JSON.parse(await blobTo("text", blob)); - resp.headers = new Headers(parseHeaders(resp.responseHeaders)); + resp.headers = parseHeaders(resp.responseHeaders); resp.ok = resp.status >= 200 && resp.status < 300; resolve(resp); }; diff --git a/package.json b/package.json index 5ffac7ff5..dde55d311 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.9", + "version": "1.7.0", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0a17e8d7e..33b099ef8 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -428,10 +428,11 @@ function runTime(token: string) { if (searchType === "findComponent") method = "find"; if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (searchType === "waitFor" || searchType === "waitForComponent") { if (typeof args[0] === "string") method = "findByProps"; else method = "find"; } + if (searchType === "waitForStore") method = "findStore"; try { let result: any; diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index 341b4e678..d6eba748f 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -74,7 +74,7 @@ export interface MessageExtra { } export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; -export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; +export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; const sendListeners = new Set(); const editListeners = new Set(); @@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec for (const listener of sendListeners) { try { const result = await listener(channelId, messageObj, extra); - if (result && result.cancel === true) { + if (result?.cancel) { return true; } } catch (e) { @@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { for (const listener of editListeners) { try { - await listener(channelId, messageId, messageObj); + const result = await listener(channelId, messageId, messageObj); + if (result?.cancel) { + return true; + } } catch (e) { MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); } } + return false; } /** diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index 0392a451c..080dd8dd9 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -39,9 +39,7 @@ function validateUrl(url: string) { async function eraseAllData() { const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }) + headers: { Authorization: await getCloudAuth() } }); if (!res.ok) { diff --git a/src/plugins/_api/messageEvents.ts b/src/plugins/_api/messageEvents.ts index bc5f5abf2..1b4a2d15a 100644 --- a/src/plugins/_api/messageEvents.ts +++ b/src/plugins/_api/messageEvents.ts @@ -25,10 +25,13 @@ export default definePlugin({ authors: [Devs.Arjix, Devs.hunt, Devs.Ven], patches: [ { - find: '"MessageActionCreators"', + find: ".Messages.EDIT_TEXTAREA_HELP", replacement: { - match: /async editMessage\(.+?\)\{/, - replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);" + match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/, + replace: (match, args) => "" + + `async ${match}` + + `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` + + "return Promise.resolve({shoudClear:true,shouldRefocus:true});" } }, { diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index ce546d309..8cfd8c036 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -72,7 +72,7 @@ export default definePlugin({ replacement: [ // Add Decor avatar decoration hook to avatar decoration hook { - match: /(?<=TryItOut:\i}\),)(?<=user:(\i).+?)/, + match: /(?<=TryItOut:\i,guildId:\i}\),)(?<=user:(\i).+?)/, replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1)," }, // Use added hook diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.tsx similarity index 86% rename from src/plugins/fakeNitro/index.ts rename to src/plugins/fakeNitro/index.tsx index 560cae381..1cf1e536f 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.tsx @@ -17,14 +17,14 @@ */ import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { getCurrentGuild } from "@utils/discord"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; -import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; +import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import type { Message } from "discord-types/general"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; import type { ReactElement, ReactNode } from "react"; @@ -51,8 +51,6 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsA const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); -const USE_EXTERNAL_EMOJIS = 1n << 18n; -const USE_EXTERNAL_STICKERS = 1n << 37n; const enum EmojiIntentions { REACTION = 0, @@ -162,8 +160,28 @@ const settings = definePluginSettings({ description: "Whether to use hyperlinks when sending fake emojis and stickers", type: OptionType.BOOLEAN, default: true + }, + hyperLinkText: { + description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.", + type: OptionType.STRING, + default: "{{NAME}}" } -}); +}).withPrivateSettings<{ + disableEmbedPermissionCheck: boolean; +}>(); + +function hasPermission(channelId: string, permission: bigint) { + const channel = ChannelStore.getChannel(channelId); + + if (!channel || channel.isPrivate()) return true; + + return PermissionStore.can(permission, channel); +} + +const hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS); +const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS); +const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS); +const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES); export default definePlugin({ name: "FakeNitro", @@ -351,8 +369,8 @@ 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.+?)(?=},)/, - replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)` + match: /(?<=emojiDescription:)(\i)(?<=\1=\i\((\i)\).+?)/, + replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)` } }, // Allow using custom app icons @@ -456,7 +474,7 @@ export default definePlugin({ if (typeof firstContent === "string") { content[0] = firstContent.trimStart(); content[0] || content.shift(); - } else if (firstContent?.type === "span") { + } else if (typeof firstContent?.props?.children === "string") { firstContent.props.children = firstContent.props.children.trimStart(); firstContent.props.children || content.shift(); } @@ -466,7 +484,7 @@ export default definePlugin({ if (typeof lastContent === "string") { content[lastIndex] = lastContent.trimEnd(); content[lastIndex] || content.pop(); - } else if (lastContent?.type === "span") { + } else if (typeof lastContent?.props?.children === "string") { lastContent.props.children = lastContent.props.children.trimEnd(); lastContent.props.children || content.pop(); } @@ -696,22 +714,6 @@ export default definePlugin({ } }, - hasPermissionToUseExternalEmojis(channelId: string): boolean { - 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.store.stickerSize}`; }, @@ -722,7 +724,7 @@ export default definePlugin({ const { frames, width, height } = await parseURL(stickerLink); const gif = GIFEncoder(); - const resolution = Settings.plugins.FakeNitro.stickerSize; + const resolution = settings.store.stickerSize; const canvas = document.createElement("canvas"); canvas.width = resolution; @@ -783,9 +785,38 @@ export default definePlugin({ return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; } - this.preSend = addPreSendListener((channelId, messageObj, extra) => { + function cannotEmbedNotice() { + return new Promise(resolve => { + Alerts.show({ + title: "Hold on!", + body:
+ + You are trying to send/edit a message that contains a FakeNitro emoji or sticker + , however you do not have permissions to embed links in the current channel. + Are you sure you want to send this message? Your FakeNitro items will appear as a link only. + + + You can disable this notice in the plugin settings. + +
, + confirmText: "Send Anyway", + cancelText: "Cancel", + secondaryConfirmText: "Do not show again", + onConfirm: () => resolve(true), + onCloseCallback: () => setImmediate(() => resolve(false)), + onConfirmSecondary() { + settings.store.disableEmbedPermissionCheck = true; + resolve(true); + } + }); + }); + } + + this.preSend = addPreSendListener(async (channelId, messageObj, extra) => { const { guildId } = this; + let hasBypass = false; + stickerBypass: { if (!s.enableStickerBypass) break stickerBypass; @@ -798,7 +829,7 @@ export default definePlugin({ if ("pack_id" in sticker) break stickerBypass; - const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId); + const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId); if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId)) break stickerBypass; @@ -812,9 +843,24 @@ export default definePlugin({ } if (sticker.format_type === StickerType.APNG) { - this.sendAnimatedSticker(link, sticker.id, channelId); + if (!hasAttachmentPerms(channelId)) { + Alerts.show({ + title: "Hold on!", + body:
+ + You cannot send this message because it contains an animated FakeNitro sticker, + and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed. + +
+ }); + } else { + this.sendAnimatedSticker(link, sticker.id, channelId); + } + return { cancel: true }; } else { + hasBypass = true; + const url = new URL(link); url.searchParams.set("name", sticker.name); @@ -824,35 +870,47 @@ export default definePlugin({ } if (s.enableEmojiBypass) { - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); + const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId); for (const emoji of messageObj.validNonShortcutEmojis) { if (!emoji.require_colons) continue; if (emoji.available !== false && canUseEmotes) continue; if (emoji.guildId === guildId && !emoji.animated) continue; + hasBypass = true; + const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const url = new URL(emoji.url); url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name); + messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; }); } } + if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) { + if (!await cannotEmbedNotice()) { + return { cancel: true }; + } + } + return { cancel: false }; }); - this.preEdit = addPreEditListener((channelId, __, messageObj) => { + this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { if (!s.enableEmojiBypass) return; - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); - const { guildId } = this; + let hasBypass = false; + + const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId); + messageObj.content = messageObj.content.replace(/(?/ig, (emojiStr, emojiId, offset, origStr) => { const emoji = EmojiStore.getCustomEmojiById(emojiId); if (emoji == null) return emojiStr; @@ -860,12 +918,24 @@ export default definePlugin({ if (emoji.available !== false && canUseEmotes) return emojiStr; if (emoji.guildId === guildId && !emoji.animated) return emojiStr; + hasBypass = true; + 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)}`; + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name); + + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); + + if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) { + if (!await cannotEmbedNotice()) { + return { cancel: true }; + } + } + + return { cancel: false }; }); }, diff --git a/src/plugins/imageZoom/styles.css b/src/plugins/imageZoom/styles.css index 51e225c05..c3776d90e 100644 --- a/src/plugins/imageZoom/styles.css +++ b/src/plugins/imageZoom/styles.css @@ -9,6 +9,9 @@ box-shadow: inset 0 0 10px 2px grey; filter: drop-shadow(0 0 2px grey); pointer-events: none; + + /* negate the border offsetting the lens */ + margin: -2px; } .vc-imgzoom-square { diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 179b8260c..5dfec8a32 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -170,6 +170,11 @@ const settings = definePluginSettings({ } ], }, + showLastFmLogo: { + description: "show the Last.fm logo by the album cover", + type: OptionType.BOOLEAN, + default: true, + } }); export default definePlugin({ @@ -276,8 +281,10 @@ export default definePlugin({ { large_image: await getApplicationAsset(largeImage), large_text: trackData.album || undefined, - small_image: await getApplicationAsset("lastfm-small"), - small_text: "Last.fm", + ...(settings.store.showLastFmLogo && { + small_image: await getApplicationAsset("lastfm-small"), + small_text: "Last.fm" + }), } : { large_image: await getApplicationAsset("lastfm-large"), large_text: trackData.album || undefined, diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx new file mode 100644 index 000000000..50665353e --- /dev/null +++ b/src/plugins/memberCount/MemberCount.tsx @@ -0,0 +1,66 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { getCurrentChannel } from "@utils/discord"; +import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common"; + +import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } from "."; +import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; + +export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) { + const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); + + const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id; + + const totalCount = useStateFromStores( + [GuildMemberCountStore], + () => GuildMemberCountStore.getMemberCount(guildId) + ); + + let onlineCount = useStateFromStores( + [OnlineMemberCountStore], + () => OnlineMemberCountStore.getCount(guildId) + ); + + const { groups } = useStateFromStores( + [ChannelMemberStore], + () => ChannelMemberStore.getProps(guildId, currentChannel.id) + ); + + if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) { + onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0); + } + + useEffect(() => { + OnlineMemberCountStore.ensureCount(guildId); + }, [guildId]); + + if (totalCount == null) + return null; + + const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?"; + + return ( +
+ + {props => ( +
+ + {formattedOnlineCount} +
+ )} +
+ + {props => ( +
+ + {numberFormat(totalCount)} +
+ )} +
+
+ ); +} diff --git a/src/plugins/memberCount/OnlineMemberCountStore.ts b/src/plugins/memberCount/OnlineMemberCountStore.ts new file mode 100644 index 000000000..8790f5e29 --- /dev/null +++ b/src/plugins/memberCount/OnlineMemberCountStore.ts @@ -0,0 +1,52 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { sleep } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import { Flux, FluxDispatcher, GuildChannelStore, PrivateChannelsStore } from "@webpack/common"; + +export const OnlineMemberCountStore = proxyLazy(() => { + const preloadQueue = new Queue(); + + const onlineMemberMap = new Map(); + + class OnlineMemberCountStore extends Flux.Store { + getCount(guildId: string) { + return onlineMemberMap.get(guildId); + } + + async _ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id); + } + + ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + preloadQueue.push(() => + this._ensureCount(guildId) + .then( + () => sleep(200), + () => sleep(200) + ) + ); + } + } + + return new OnlineMemberCountStore(FluxDispatcher, { + GUILD_MEMBER_LIST_UPDATE({ guildId, groups }: { guildId: string, groups: { count: number; id: string; }[]; }) { + onlineMemberMap.set( + guildId, + groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0) + ); + }, + ONLINE_GUILD_MEMBER_COUNT_UPDATE({ guildId, count }) { + onlineMemberMap.set(guildId, count); + } + }); +}); diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index d9cd548e9..eb4ce372c 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -16,101 +16,48 @@ * along with this program. If not, see . */ +import "./style.css"; + +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import { getCurrentChannel } from "@utils/discord"; import definePlugin from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { SelectedChannelStore, Tooltip, useStateFromStores } from "@webpack/common"; import { FluxStore } from "@webpack/types"; -const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; -const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { +import { MemberCount } from "./MemberCount"; + +export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; +export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; }; const sharedIntlNumberFormat = new Intl.NumberFormat(); -const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); - -function MemberCount() { - const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); - const { groups } = useStateFromStores( - [ChannelMemberStore], - () => ChannelMemberStore.getProps(guildId, channelId) - ); - const total = useStateFromStores( - [GuildMemberCountStore], - () => GuildMemberCountStore.getMemberCount(guildId) - ); - - if (total == null) - return null; - - const online = - (groups.length === 1 && groups[0].id === "unknown") - ? 0 - : groups.reduce((count, curr) => count + (curr.id === "offline" ? 0 : curr.count), 0); - - return ( - - - {props => ( -
- - {numberFormat(online)} -
- )} -
- - {props => ( -
- - {numberFormat(total)} -
- )} -
-
- ); -} +export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); +export const cl = classNameFactory("vc-membercount-"); export default definePlugin({ name: "MemberCount", - description: "Shows the amount of online & total members in the server member list", + description: "Shows the amount of online & total members in the server member list and tooltip", authors: [Devs.Ven, Devs.Commandtechno], - patches: [{ - find: "{isSidebarVisible:", - replacement: { - match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, - replace: ":[$1?.startsWith('members')?$self.render():null,$2" + patches: [ + { + find: "{isSidebarVisible:", + replacement: { + match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, + replace: ":[$1?.startsWith('members')?$self.render():null,$2" + } + }, + { + find: ".invitesDisabledTooltip", + replacement: { + match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, + replace: ",$self.renderTooltip(arguments[0].guild)]" + } } - }], + ], - render: ErrorBoundary.wrap(MemberCount, { noop: true }) + render: ErrorBoundary.wrap(MemberCount, { noop: true }), + renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); diff --git a/src/plugins/memberCount/style.css b/src/plugins/memberCount/style.css new file mode 100644 index 000000000..f43bff830 --- /dev/null +++ b/src/plugins/memberCount/style.css @@ -0,0 +1,44 @@ +.vc-membercount-widget { + display: flex; + align-content: center; + + --color-online: var(--green-360); + --color-total: var(--primary-400); +} + +.vc-membercount-tooltip { + margin-top: 0.25em; + margin-left: 2px; +} + +.vc-membercount-member-list { + justify-content: center; + margin-top: 1em; + padding-inline: 1em; +} + +.vc-membercount-online { + color: var(--color-online); +} + +.vc-membercount-total { + color: var(--color-total); +} + +.vc-membercount-online-dot { + background-color: var(--color-online); + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.5em; +} + +.vc-membercount-total-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + border: 3px solid var(--color-total); + margin: 0 0.5em 0 1em; +} diff --git a/src/plugins/muteNewGuild/index.tsx b/src/plugins/newGuildSettings/index.tsx similarity index 72% rename from src/plugins/muteNewGuild/index.tsx rename to src/plugins/newGuildSettings/index.tsx index 08c558a95..ff6f1c261 100644 --- a/src/plugins/muteNewGuild/index.tsx +++ b/src/plugins/newGuildSettings/index.tsx @@ -16,16 +16,18 @@ * along with this program. If not, see . */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings,migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings"); +const { toggleShowAllChannels } = findByPropsLazy("toggleShowAllChannels"); +const { isOptInEnabledForGuild } = findByPropsLazy("isOptInEnabledForGuild"); const settings = definePluginSettings({ guild: { - description: "Mute Guild", + description: "Mute Guild automatically", type: OptionType.BOOLEAN, default: true }, @@ -38,13 +40,20 @@ const settings = definePluginSettings({ description: "Suppress All Role @mentions", type: OptionType.BOOLEAN, default: true + }, + showAllChannels: { + description: "Show all channels automatically", + type: OptionType.BOOLEAN, + default: true } }); +migratePluginSettings("NewGuildSettings", "MuteNewGuild"); export default definePlugin({ - name: "MuteNewGuild", - description: "Mutes newly joined guilds", - authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince], + name: "NewGuildSettings", + description: "Automatically mute new servers and change various other settings upon joining", + tags: ["MuteNewGuild", "mute", "server"], + authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi], patches: [ { find: ",acceptInvite(", @@ -70,7 +79,9 @@ export default definePlugin({ muted: settings.store.guild, suppress_everyone: settings.store.everyone, suppress_roles: settings.store.role - } - ); + }); + if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) { + toggleShowAllChannels(guildId); + } } }); diff --git a/src/plugins/notificationVolume/index.ts b/src/plugins/notificationVolume/index.ts index 50eabee73..bc3c7539d 100644 --- a/src/plugins/notificationVolume/index.ts +++ b/src/plugins/notificationVolume/index.ts @@ -27,8 +27,8 @@ export default definePlugin({ { find: "_ensureAudio(){", replacement: { - match: /onloadeddata=\(\)=>\{.\.volume=/, - replace: "$&$self.settings.store.notificationVolume/100*" + match: /(?=Math\.min\(\i\.\i\.getOutputVolume\(\)\/100)/, + replace: "$self.settings.store.notificationVolume/100*" }, }, ], diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index 136001b2d..4cd81f2ea 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -59,7 +59,7 @@ export function authorize(callback?: any) { const url = new URL(response.location); url.searchParams.append("clientMod", "vencord"); const res = await fetch(url, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); if (!res.ok) { diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 64cea1815..eea92bb81 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -32,6 +32,7 @@ const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); +const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer"); interface UserProps { discordId: string; @@ -125,19 +126,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { const inputType = ChatInputTypes.FORM; inputType.disableAutoFocus = true; - const channel = { - flags_: 256, - guild_id_: null, - id: "0", - getGuildId: () => null, - isPrivate: () => true, - isActiveThread: () => false, - isArchivedLockedThread: () => false, - isDM: () => true, - roles: { "0": { permissions: 0n } }, - getRecipientId: () => "0", - hasFlag: () => false, - }; + const channel = createChannelRecordFromServer({ id: "0", type: 1 }); return ( <> diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index ec6d9ff3b..3fe6dd061 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -118,10 +118,10 @@ export async function addReview(review: any): Promise { export async function deleteReview(id: number): Promise { return await rdbRequest(`/users/${id}/reviews`, { method: "DELETE", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ reviewid: id }) @@ -135,10 +135,10 @@ export async function deleteReview(id: number): Promise { export async function reportReview(id: number) { const res = await rdbRequest("/reports", { method: "PUT", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ reviewid: id, }) @@ -150,10 +150,10 @@ export async function reportReview(id: number) { async function patchBlock(action: "block" | "unblock", userId: string) { const res = await rdbRequest("/blocks", { method: "PATCH", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ action: action, discordId: userId @@ -180,9 +180,9 @@ export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { const res = await rdbRequest("/blocks", { method: "GET", - headers: new Headers({ + headers: { Accept: "application/json", - }) + } }); if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index f2370906b..8b3f04bf2 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -371,6 +371,10 @@ export function Player() { if (!track || !device?.is_active || shouldHide) return null; + const exportTrackImageStyle = { + "--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`, + } as React.CSSProperties; + return ( (
@@ -378,7 +382,7 @@ export function Player() {

Check the console for errors

)}> -
+
diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index cfb352efe..d7e4f6454 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -31,7 +31,7 @@ function toggleHoverControls(value: boolean) { export default definePlugin({ name: "SpotifyControls", description: "Adds a Spotify player above the account panel", - authors: [Devs.Ven, Devs.afn, Devs.KraXen72], + authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000], options: { hoverControls: { description: "Show controls on hover", diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css index 9e585ebec..72383c3e8 100644 --- a/src/plugins/spotifyControls/spotifyStyles.css +++ b/src/plugins/spotifyControls/spotifyStyles.css @@ -170,9 +170,16 @@ /* these importants are necessary, it applies a width and height through inline styles */ height: 10px !important; width: 10px !important; + margin-top: 4px; background-color: var(--interactive-normal); border-color: var(--interactive-normal); color: var(--interactive-normal); + opacity: 0; + transition: opacity 0.1s; +} + +#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] { + opacity: 1; } #vc-spotify-progress-text { diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 0e58eb0a8..89197b4c3 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +import { UserStore } from "@webpack/common"; export const settings = definePluginSettings({ superReactByDefault: { @@ -49,7 +50,7 @@ export default definePlugin({ find: ".trackEmojiSearchEmpty,200", replacement: { match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/, - replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})` + replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` } } ], @@ -59,5 +60,9 @@ export default definePlugin({ if (settings.store.unlimitedSuperReactionPlaying) return true; if (playingCount <= settings.store.superReactionPlayingLimit) return true; return false; + }, + + get shouldSuperReactByDefault() { + return settings.store.superReactByDefault && UserStore.getCurrentUser().premiumType != null; } }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index f4898de68..2393ef2b6 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -20,13 +20,15 @@ import "./styles.css"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; +import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; import { chooseFile } from "@utils/web"; import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Button, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; +import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { ComponentType } from "react"; import { VoiceRecorderDesktop } from "./DesktopRecorder"; @@ -164,6 +166,11 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { fallbackValue: EMPTY_META, }); + const isUnsupportedFormat = blob && ( + !blob.type.startsWith("audio/ogg") + || blob.type.includes("codecs") && !blob.type.includes("opus") + ); + return ( @@ -200,6 +207,16 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { recording={isRecording} /> + {isUnsupportedFormat && ( + + Voice Messages have to be OggOpus to be playable on iOS. This file is {blob.type} so it will not be playable on iOS. + + + To fix it, first convert it to OggOpus, for example using the convertio web converter + + + )} + diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 4a2bdeeda..6d994be16 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -69,14 +69,14 @@ function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { function makeRenderMoreUsers(users: User[]) { return function renderMoreUsers(_label: string, _count: number) { return ( - u.username).join(", ")} > + u.username).join(", ")} > {({ onMouseEnter, onMouseLeave }) => (
- +{users.length - 5} + +{users.length - 4}
)}
diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index f56c78dc5..508b1c7ef 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -106,7 +106,7 @@ export async function authorizeCloud() { try { const res = await fetch(location, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); const { secret } = await res.json(); if (secret) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 55af93605..d66bdc826 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -399,6 +399,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "maisy", id: 257109471589957632n, }, + Mopi: { + name: "Mopi", + id: 1022189106614243350n + }, Grzesiek11: { name: "Grzesiek11", id: 368475654662127616n, @@ -410,6 +414,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ coolelectronics: { name: "coolelectronics", id: 696392247205298207n, + }, + Av32000: { + name: "Av32000", + id: 593436735380127770n, } } satisfies Record); diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index ec32e2b1e..9a0f260af 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -118,10 +118,10 @@ export async function putCloudSettings(manual?: boolean) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), "Content-Type": "application/octet-stream" - }), + }, body: deflateSync(new TextEncoder().encode(settings)) }); @@ -162,11 +162,11 @@ export async function getCloudSettings(shouldNotify = true, force = false) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), Accept: "application/octet-stream", "If-None-Match": Settings.cloud.settingsSyncVersion.toString() - }), + }, }); if (res.status === 404) { @@ -251,9 +251,7 @@ export async function deleteCloudSettings() { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }), + headers: { Authorization: await getCloudAuth() }, }); if (!res.ok) { diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 2005581a1..3d1c0eea6 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -59,6 +59,7 @@ export interface Alerts { onCancel?(): void; onConfirm?(): void; onConfirmSecondary?(): void; + onCloseCallback?(): void; }): void; /** This is a noop, it does nothing. */ close(): void; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index d65f57fcb..a68890a83 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -83,8 +83,8 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { return true; } +let devToolsOpen = false; if (IS_DEV && IS_DISCORD_DESKTOP) { - var devToolsOpen = false; // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed setTimeout(() => { DiscordNative/* just to make sure */?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false);