Merge branch 'Vendicated:main' into main

This commit is contained in:
camila 2024-03-08 15:28:57 -06:00 committed by GitHub
commit 57d22f9b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 1728 additions and 963 deletions

View file

@ -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);
};

View file

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

View file

@ -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;

View file

@ -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.");
}

4
src/api/ChatButton.css Normal file
View file

@ -0,0 +1,4 @@
.vc-chatbar-button {
display: flex;
align-items: center;
}

128
src/api/ChatButtons.tsx Normal file
View file

@ -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<string, ChatBarButton>();
const logger = new Logger("ChatButtons");
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
if (props.disabled) return;
for (const [key, Button] of buttonFactories) {
buttons.push(
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
</ErrorBoundary>
);
}
}
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button);
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
export interface ChatBarButtonProps {
children: ReactNode;
tooltip: string;
onClick: MouseEventHandler<HTMLButtonElement>;
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">;
}
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
<Tooltip text={props.tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
<Button
aria-label={props.tooltip}
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
onClick={props.onClick}
onContextMenu={props.onContextMenu}
{...props.buttonProps}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
{props.children}
</div>
</Button>
</div>
)}
</Tooltip>
);
}, { noop: true });

View file

@ -17,22 +17,20 @@
*/
import { Logger } from "@utils/Logger";
import { Menu, React } from "@webpack/common";
import type { ReactElement } from "react";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/**
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
/**
* @param navId The navId of the context menu being patched
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
const ContextMenuLogger = new Logger("ContextMenu");
@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
*/
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
for (const child of children) {
if (child == null) continue;
if (Array.isArray(child)) {
const found = findGroupChildrenByChildId(id, child);
if (found !== null) return found;
}
if (
(Array.isArray(id) && id.some(id => child.props?.id === id))
|| child.props?.id === id
) return _itemsArray ?? null;
) return children;
let nextChildren = child.props?.children;
if (nextChildren) {
@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
const found = findGroupChildrenByChildId(id, nextChildren);
if (found !== null) return found;
}
}
@ -126,9 +129,12 @@ interface ContextMenuProps {
onClose: (callback: (...args: Array<any>) => any) => void;
}
const patchedMenus = new WeakSet();
export function _usePatchContextMenu(props: ContextMenuProps) {
props = {
...props,
children: cloneMenuChildren(props.children),
};
export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId);
@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
const callback = patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
patch(props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) {
try {
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
patch(props.navId, props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
patchedMenus.add(props);
return props;
}
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
if (Array.isArray(obj)) {
return obj.map(cloneMenuChildren);
}
if (React.isValidElement(obj)) {
obj = React.cloneElement(obj);
if (
obj?.props?.children &&
(obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)
) {
obj.props.children = cloneMenuChildren(obj.props.children);
}
}
return obj;
}

View file

@ -74,7 +74,7 @@ export interface MessageExtra {
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>();
@ -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;
}
/**

View file

@ -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={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>

View file

@ -223,13 +223,13 @@ export const Settings = makeProxy(settings);
export function useSettings(paths?: UseSettings<Settings>[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
: forceUpdate;
if (paths) {
(forceUpdate as SubscriptionCallback)._paths = paths;
}
React.useEffect(() => {
subscriptions.add(onUpdate);
return () => void subscriptions.delete(onUpdate);
subscriptions.add(forceUpdate);
return () => void subscriptions.delete(forceUpdate);
}, []);
return Settings;
@ -253,8 +253,10 @@ type ResolvePropDeep<T, P> = P extends "" ? T :
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
if (path)
if (path) {
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
}
subscriptions.add(onUpdate);
}

View file

@ -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;

View file

@ -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) {

View file

@ -23,7 +23,7 @@ import { debounce } from "@utils/debounce";
import { IpcEvents } from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { FSWatcher, mkdirSync, readFileSync, watch } from "fs";
import { open, readdir, readFile, writeFile } from "fs/promises";
import { join, normalize } from "path";
@ -126,16 +126,23 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
export function initIpc(mainWindow: BrowserWindow) {
let quickCssWatcher: FSWatcher | undefined;
open(QUICKCSS_PATH, "a+").then(fd => {
fd.close();
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
}, 50));
});
}).catch(() => { });
watch(THEMES_DIR, { persistent: false }, debounce(() => {
const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => {
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
}));
mainWindow.once("closed", () => {
quickCssWatcher?.close();
themesWatcher.close();
});
}
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {

View file

@ -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");
}

View file

@ -49,9 +49,12 @@ async function getRepo() {
async function calculateGitChanges() {
await git("fetch");
const branch = await git("branch", "--show-current");
const branch = (await git("branch", "--show-current")).stdout.trim();
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0;
if (!existsOnOrigin) return [];
const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {

View file

@ -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]);"
}
}]
});

View file

@ -22,15 +22,15 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "ContextMenuAPI",
description: "API for adding/removing items to/from context menus.",
authors: [Devs.Nuckyz, Devs.Ven],
authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi],
required: true,
patches: [
{
find: "♫ (つ。◕‿‿◕。)つ ♪",
replacement: {
match: /let{navId:/,
replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&"
match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/,
replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);"
}
},
{

View file

@ -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});"
}
},
{

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -30,21 +30,21 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu],
required: true,
start() {
contextMenus: {
// The settings shortcuts in the user settings cog context menu
// read the elements from a hardcoded map which for obvious reason
// doesn't contain our sections. This patches the actions of our
// sections to manually use SettingsRouter (which only works on desktop
// but the context menu is usually not available on mobile anyway)
addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
"user-settings-cog"(children) {
const section = findGroupChildrenByChildId("VencordSettings", children);
section?.forEach(c => {
const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c.props.action = () => SettingsRouter.open(id);
c!.props.action = () => SettingsRouter.open(id);
}
});
});
}
},
patches: [{

View file

@ -0,0 +1,6 @@
# BetterRoleContext
Adds options to copy role color and edit role when right clicking roles in the user profile
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)

View file

@ -0,0 +1,79 @@
/*
* 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 { getCurrentGuild } from "@utils/discord";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
function PencilIcon() {
return (
<svg
role="img"
width="18"
height="18"
fill="none"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
</svg>
);
}
function AppearanceIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z" />
</svg>
);
}
export default definePlugin({
name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
authors: [Devs.Ven],
start() {
// DeveloperMode needs to be enabled for the context menu to be shown
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
},
contextMenus: {
"dev-context"(children, { id }: { id: string; }) {
const guild = getCurrentGuild();
const role = guild?.roles[id];
if (!role) return;
if (role.colorString) {
children.push(
<Menu.MenuItem
id="vc-copy-role-color"
label="Copy Role Color"
action={() => Clipboard.copy(role.colorString!)}
icon={AppearanceIcon}
/>
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
}
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ScreenshareIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { openImageModal } from "@utils/discord";
@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
openImageModal(previewUrl);
};
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => {
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
if (!stream) return;
@ -89,12 +89,8 @@ export default definePlugin({
name: "BiggerStreamPreview",
description: "This plugin allows you to enlarge stream previews",
authors: [Devs.phil],
start: () => {
addContextMenuPatch("user-context", userContextPatch);
addContextMenuPatch("stream-context", streamContextPatch);
},
stop: () => {
removeContextMenuPatch("user-context", userContextPatch);
removeContextMenuPatch("stream-context", streamContextPatch);
contextMenus: {
"user-context": userContextPatch,
"stream-context": streamContextPatch
}
});

View file

@ -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",

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common";
import { Button, Forms, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -200,8 +200,8 @@ function captureOne(str, regex) {
return (result === null) ? null : result[1];
}
function mapReject(arr, mapFunc, rejectFunc = _.isNull) {
return _.reject(arr.map(mapFunc), rejectFunc);
function mapReject(arr, mapFunc) {
return arr.map(mapFunc).filter(Boolean);
}
function updateColorVars(color: string) {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -29,7 +29,7 @@ interface UserContextProps {
user: User;
}
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
if (!user) return;
children.push(
@ -46,12 +46,7 @@ export default definePlugin({
name: "CopyUserURLs",
authors: [Devs.castdrian],
description: "Adds a 'Copy User URL' option to the user context menu.",
start() {
addContextMenuPatch("user-context", UserContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", UserContextMenuPatch);
},
contextMenus: {
"user-context": UserContextMenuPatch
}
});

View file

@ -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);
}

View file

@ -175,7 +175,7 @@ const settings = definePluginSettings({
},
startTime: {
type: OptionType.NUMBER,
description: "Start timestamp (only for custom timestamp mode)",
description: "Start timestamp in milisecond (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -185,7 +185,7 @@ const settings = definePluginSettings({
},
endTime: {
type: OptionType.NUMBER,
description: "End timestamp (only for custom timestamp mode)",
description: "End timestamp in milisecond (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -313,12 +313,12 @@ async function createActivity(): Promise<Activity | undefined> {
switch (settings.store.timestampMode) {
case TimestampMode.NOW:
activity.timestamps = {
start: Math.floor(Date.now() / 1000)
start: Date.now()
};
break;
case TimestampMode.TIME:
activity.timestamps = {
start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds()
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
};
break;
case TimestampMode.CUSTOM:

View file

@ -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

View file

@ -16,18 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;"
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
@ -312,7 +312,7 @@ function isGifUrl(url: string) {
return new URL(url).pathname.endsWith(".gif");
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
if (!favoriteableId) return;
@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
findGroupChildrenByChildId("copy-link", children)?.push(menuItem);
};
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
const { id, name, type } = props?.target?.dataset ?? {};
if (!id) return;
@ -363,14 +363,8 @@ export default definePlugin({
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"],
authors: [Devs.Ven, Devs.Nuckyz],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
addContextMenuPatch("expression-picker", expressionPickerPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
removeContextMenuPatch("expression-picker", expressionPickerPatch);
contextMenus: {
"message": messageContextMenuPatch,
"expression-picker": expressionPickerPatch
}
});

View file

@ -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,
@ -108,6 +106,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,8 +155,33 @@ 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
},
hyperLinkText: {
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker 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",
@ -345,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
@ -447,13 +471,23 @@ export default definePlugin({
trimContent(content: Array<any>) {
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 (typeof firstContent?.props?.children === "string") {
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 (typeof lastContent?.props?.children === "string") {
lastContent.props.children = lastContent.props.children.trimEnd();
lastContent.props.children || content.pop();
}
},
clearEmptyArrayItems(array: Array<any>) {
@ -465,7 +499,7 @@ export default definePlugin({
},
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, 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;
@ -551,13 +585,15 @@ export default definePlugin({
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children);
this.trimContent(children);
return children;
};
try {
return modifyChildren(lodash.cloneDeep(content));
const newContent = modifyChildren(lodash.cloneDeep(content));
this.trimContent(newContent);
return newContent;
} catch (err) {
new Logger("FakeNitro").error(err);
return content;
@ -574,7 +610,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 +655,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) {
@ -681,24 +716,8 @@ 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.plugins.FakeNitro.stickerSize}`;
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`;
},
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
@ -707,7 +726,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;
@ -768,9 +787,38 @@ export default definePlugin({
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
function cannotEmbedNotice() {
return new Promise<boolean>(resolve => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
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.
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
You can disable this notice in the plugin settings.
</Forms.FormText>
</div>,
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;
@ -783,7 +831,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;
@ -795,44 +843,78 @@ 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);
if (!hasAttachmentPerms(channelId)) {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
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.
</Forms.FormText>
</div>
});
} else {
this.sendAnimatedSticker(link, sticker.id, channelId);
}
return { cancel: true };
} else {
hasBypass = true;
const url = new URL(link);
url.searchParams.set("name", sticker.name);
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name);
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;
extra.stickers!.length = 0;
messageObj.content += ` ${link}&name=${encodeURIComponent(sticker.name)}`;
}
}
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 = 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);
const linkText = s.hyperLinkText.replaceAll("{{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 ? `[${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(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {
const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null) return emojiStr;
@ -840,12 +922,24 @@ 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)}`;
hasBypass = true;
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);
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 };
});
},

View file

@ -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 });
`);
}

View file

@ -5,12 +5,14 @@
*/
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { StatusSettingsStores, Tooltip } from "webpack/common";
import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common";
const enum ActivitiesTypes {
Game,
@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
}
const settings = definePluginSettings({}).withPrivateSettings<{
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
<div>
<Button
onClick={() => {
const id = Settings.plugins.CustomRPC?.appID as string | undefined;
if (!id) {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}
const isAlreadyAdded = allowedIdsPushID?.(id);
if (isAlreadyAdded) {
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
}
}}
>
Import CustomRPC ID
</Button>
</div>
</Flex>
);
}
let allowedIdsPushID: ((id: string) => boolean) | null = null;
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
allowedIdsPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", ");
setAllowedIds(ids);
props.setValue(ids);
return isAlreadyAdded;
};
useEffect(() => () => {
allowedIdsPushID = null;
}, []);
function handleChange(newValue: string) {
setAllowedIds(newValue);
props.setValue(newValue);
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
<TextInput
type="text"
value={allowedIds}
onChange={handleChange}
placeholder="235834946571337729, 343383572805058560"
/>
</Forms.FormSection>
);
}
const settings = definePluginSettings({
importCustomRPC: {
type: OptionType.COMPONENT,
description: "",
component: () => <ImportCustomRPCComponent />
},
allowedIds: {
type: OptionType.COMPONENT,
description: "",
default: "",
onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.allowedIds = Array.from(ids).join(", ");
},
component: props => <AllowedIdsComponent setValue={props.setValue} />
},
ignorePlaying: {
type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false
},
ignoreStreaming: {
type: OptionType.BOOLEAN,
description: "Ignore all streaming activities",
default: false
},
ignoreListening: {
type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)",
default: false
},
ignoreWatching: {
type: OptionType.BOOLEAN,
description: "Ignore all watching activities",
default: false
},
ignoreCompeting: {
type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)",
default: false
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
}>();
@ -77,10 +185,26 @@ function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
}
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.allowedIds.includes(id)) {
return false;
}
switch (type) {
case 0: return settings.store.ignorePlaying;
case 1: return settings.store.ignoreStreaming;
case 2: return settings.store.ignoreListening;
case 3: return settings.store.ignoreWatching;
case 5: return settings.store.ignoreCompeting;
}
return false;
}
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.",
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
settings,
@ -141,13 +265,17 @@ export default definePlugin({
},
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0 || props.type === 3) {
if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath);
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id);
} else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {
return !getIgnoredActivities().some(activity => activity.id === exePath);
}
}
return true;
},

View file

@ -123,14 +123,13 @@ export const Magnifier: React.FC<MagnifierProps> = ({ 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<MagnifierProps> = ({ instance, size: initialSiz
if (!ready) return null;
const box = element.current!.getBoundingClientRect();
const box = element.current?.getBoundingClientRect();
if (!box) return null;
return (
<div

View file

@ -16,14 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce";
import definePlugin, { OptionType } from "@utils/types";
import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common";
import { Menu, React, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -80,25 +80,25 @@ export const settings = definePluginSettings({
});
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
const imageContextMenuPatch: NavContextMenuPatchCallback = children => {
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
children.push(
<Menu.MenuGroup id="image-zoom">
<Menu.MenuCheckboxItem
id="vc-square"
label="Square Lens"
checked={settings.store.square}
checked={square}
action={() => {
settings.store.square = !settings.store.square;
ContextMenuApi.closeContextMenu();
settings.store.square = !square;
}}
/>
<Menu.MenuCheckboxItem
id="vc-nearest-neighbour"
label="Nearest Neighbour"
checked={settings.store.nearestNeighbour}
checked={nearestNeighbour}
action={() => {
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
ContextMenuApi.closeContextMenu();
settings.store.nearestNeighbour = !nearestNeighbour;
}}
/>
<Menu.MenuControlItem
@ -171,7 +171,7 @@ export default definePlugin({
find: "handleImageLoad=",
replacement: [
{
match: /showThumbhashPlaceholder:\i,/,
match: /placeholderVersion:\i,/,
replace: "...$self.makeProps(this),$&"
},
@ -196,6 +196,9 @@ export default definePlugin({
],
settings,
contextMenus: {
"image-context": imageContextMenuPatch
},
// to stop from rendering twice /shrug
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
@ -245,7 +248,6 @@ export default definePlugin({
start() {
enableStyle(styles);
addContextMenuPatch("image-context", imageContextMenuPatch);
this.element = document.createElement("div");
this.element.classList.add("MagnifierContainer");
document.body.appendChild(this.element);
@ -256,6 +258,5 @@ export default definePlugin({
// so componenetWillUnMount gets called if Magnifier component is still alive
this.root && this.root.unmount();
this.element?.remove();
removeContextMenuPatch("image-context", imageContextMenuPatch);
}
});

View file

@ -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 {

View file

@ -17,6 +17,7 @@
*/
import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { Patch, Plugin, StartAt } from "@utils/types";
@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) {
}
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const { name, commands, flux } = p;
const { name, commands, flux, contextMenus } = p;
if (p.start) {
logger.info("Starting plugin", name);
@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
}
}
if (contextMenus) {
for (const navId in contextMenus) {
addContextMenuPatch(navId, contextMenus[navId]);
}
}
return true;
}, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux } = p;
const { name, commands, flux, contextMenus } = p;
if (p.stop) {
logger.info("Stopping plugin", name);
if (!p.started) {
@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
}
}
if (contextMenus) {
for (const navId in contextMenus) {
removeContextMenuPatch(navId, contextMenus[navId]);
}
}
return true;
}, p => `stopPlugin ${p.name}`);

View file

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 (
<Tooltip text="Encrypt Message">
{({ 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
*/
<div style={{ display: "flex" }}>
<Button
aria-haspopup="dialog"
aria-label="Encrypt Message"
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={() => buildEncModal()}
style={{ padding: "0 2px", scale: "0.9" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg
aria-hidden
role="img"
width="32"
height="32"
viewBox={"0 0 64 64"}
style={{ scale: "1.1" }}
>
<path fill="currentColor" d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" />
</svg>
</div>
</Button>
</div>
)
}
</Tooltip >
<ChatBarButton
tooltip="Encrypt Message"
onClick={() => buildEncModal()}
buttonProps={{
"aria-haspopup": "dialog",
}}
>
<svg
aria-hidden
role="img"
width="24"
height="24"
viewBox={"0 0 64 64"}
style={{ scale: "1.39", translate: "0 -1px" }}
>
<path fill="currentColor" d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" />
</svg>
</ChatBarButton>
);
}
};
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: () => <PopOverIcon />,
indicator: ErrorBoundary.wrap(Indicator, { noop: true })
});

View file

@ -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,

View file

@ -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 (
<div className={cl("widget", { tooltip: isTooltip, "member-list": !isTooltip })}>
<Tooltip text={`${formattedOnlineCount} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span className={cl("online-dot")} />
<span className={cl("online")}>{formattedOnlineCount}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
{props => (
<div {...props}>
<span className={cl("total-dot")} />
<span className={cl("total")}>{numberFormat(totalCount)}</span>
</div>
)}
</Tooltip>
</div>
);
}

View file

@ -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<string, number>();
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);
}
});
});

View file

@ -16,101 +16,66 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { definePluginSettings } from "@api/Settings";
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 definePlugin, { OptionType } 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 settings = definePluginSettings({
toolTip: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the server tooltip",
default: true,
restartNeeded: true
},
memberList: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the member list",
default: true,
restartNeeded: true
}
});
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 (
<Flex id="vc-membercount" style={{
marginTop: "1em",
paddingInline: "1em",
justifyContent: "center",
alignContent: "center",
gap: 0
}}>
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span
style={{
backgroundColor: "var(--green-360)",
width: "12px",
height: "12px",
borderRadius: "50%",
display: "inline-block",
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
{props => (
<div {...props}>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
border: "3px solid var(--primary-400)",
display: "inline-block",
marginRight: "0.5em",
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
</div>
)}
</Tooltip>
</Flex>
);
}
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],
settings,
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"
},
predicate: () => settings.store.memberList
},
{
find: ".invitesDisabledTooltip",
replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
replace: ",$self.renderTooltip(arguments[0].guild)]"
},
predicate: () => settings.store.toolTip
}
}],
render: ErrorBoundary.wrap(MemberCount, { noop: true })
],
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
});

View file

@ -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;
}

View file

@ -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<string, {
message?: Message;
@ -49,8 +51,9 @@ const AutoModEmbed = findComponentByCodeLazy(".withFooter]:", "childrenMessageCo
const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)");
const SearchResultClasses = findByPropsLazy("message", "searchResult");
const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor");
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
interface Attachment {
@ -63,7 +66,6 @@ interface Attachment {
interface MessageEmbedProps {
message: Message;
channel: Channel;
guildID: string;
}
const messageFetchQueue = new Queue();
@ -226,19 +228,19 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
let match = null as RegExpMatchArray | null;
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
const [_, guildID, channelID, messageID] = match;
const [_, channelID, messageID] = match;
if (embeddedBy.includes(messageID)) {
continue;
}
const linkedChannel = ChannelStore.getChannel(channelID);
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
if (!linkedChannel || (!linkedChannel.isPrivate() && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, linkedChannel))) {
continue;
}
const { listMode, idList } = settings.store;
const isListed = [guildID, channelID, message.author.id].some(id => 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 <Embed
embed={{
rawDescription: "",
color: "var(--background-secondary)",
author: {
name: <Text variant="text-xs/medium" tag="span">
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>
{isDM
? Parser.parse(`<@${dmReceiver.id}>`)
: Parser.parse(`<#${channel.id}>`)
}
</Text>,
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={() => (
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
<ChannelMessage
id={`message-link-embeds-${message.id}`}
message={message}
channel={channel}
subscribeToComponentDispatch={false}
/>
</div>
)}
/>;
return (
<Embed
embed={{
rawDescription: "",
color: "var(--background-secondary)",
author: {
name: <Text variant="text-xs/medium" tag="span">
<span>{channelLabel} - </span>
{Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)}
</Text>,
iconProxyURL: iconUrl
}
}}
renderDescription={() => (
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
<ChannelMessage
id={`message-link-embeds-${message.id}`}
message={message}
channel={channel}
subscribeToComponentDispatch={false}
/>
</div>
)}
/>
);
}
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 <AutoModEmbed
channel={channel}
childrenAccessories={
<Text color="text-muted" variant="text-xs/medium" tag="span">
{isDM
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
: parse(`<#${channel.id}>`)
}
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
<Text color="text-muted" variant="text-xs/medium" tag="span" className={`${EmbedClasses.embedAuthor} ${EmbedClasses.embedMargin}`}>
{iconUrl && <img src={iconUrl} className={EmbedClasses.embedAuthorIcon} alt="" />}
<span>
<span>{channelLabel} - </span>
{channel.isDM()
? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
: Parser.parse(`<#${channel.id}>`)
}
</span>
</Text>
}
compact={compact}

View file

@ -18,7 +18,7 @@
import "./messageLogger.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
@ -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";
@ -45,7 +45,7 @@ function addDeleteStyle() {
const REMOVE_HISTORY_ID = "ml-remove-history";
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => {
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {
const { message } = props;
const { deleted, editHistory, id, channel_id } = message;
@ -94,13 +94,12 @@ export default definePlugin({
description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN],
start() {
addDeleteStyle();
addContextMenuPatch("message", patchMessageContextMenu);
contextMenus: {
"message": patchMessageContextMenu
},
stop() {
removeContextMenuPatch("message", patchMessageContextMenu);
start() {
addDeleteStyle();
},
renderEdit(edit: { timestamp: any, content: string; }) {
@ -122,7 +121,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
};
},

View file

@ -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})}`
},

View file

@ -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");
@ -48,8 +47,8 @@ export default definePlugin({
{
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/,
replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
}
},
{
@ -71,7 +70,7 @@ export default definePlugin({
}}
>
<Avatar
src={AvatarUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>

View file

@ -16,16 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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);
}
}
});

View file

@ -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*"
},
},
],

View file

@ -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={[
(<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>

View file

@ -18,7 +18,7 @@
import "./styles.css";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -125,10 +125,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
}
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
return (children, props) => () => {
return (children, props) => {
if (!props) return;
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
return children;
return;
const group = findGroupChildrenByChildId(childId, children);
@ -173,19 +173,10 @@ export default definePlugin({
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User),
channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild),
start() {
addContextMenuPatch("user-context", this.userContextMenuPatch);
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", this.userContextMenuPatch);
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
contextMenus: {
"user-context": makeContextMenuPatch("roles", MenuItemParentType.User),
"channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
"guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild),
"guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild)
}
});

View file

@ -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: [
{

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
@ -50,13 +50,13 @@ function PinMenuItem(channelId: string) {
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => {
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("leave-channel", children);
if (container)
container.unshift(PinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
const UserContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
@ -64,12 +64,7 @@ const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
}
};
export function addContextMenus() {
addContextMenuPatch("gdm-context", GroupDMContext);
addContextMenuPatch("user-context", UserContext);
}
export function removeContextMenus() {
removeContextMenuPatch("gdm-context", GroupDMContext);
removeContextMenuPatch("user-context", UserContext);
}
export const contextMenus = {
"gdm-context": GroupDMContext,
"user-context": UserContext
};

View file

@ -20,7 +20,7 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "discord-types/general";
import { addContextMenus, removeContextMenus } from "./contextMenus";
import { contextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
export default definePlugin({
@ -29,9 +29,7 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Strencher],
settings,
start: addContextMenus,
stop: removeContextMenus,
contextMenus,
usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms();

View file

@ -16,22 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 (
<Tooltip text="Preview Message">
{tooltipProps => (
<Button
{...tooltipProps}
onClick={async () =>
sendBotMessage(
channelId,
{
content: getDraft(channelId),
author: UserStore.getCurrentUser(),
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
}
)}
size=""
look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button}
style={{ padding: "0 2px", height: "100%" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<img width={24} height={24} src="https://discord.com/assets/4c5a77a89716352686f590a6f014770c.svg" />
</div>
</Button>
)}
</Tooltip>
<ChatBarButton
tooltip="Preview Message"
onClick={async () =>
sendBotMessage(
channelId,
{
content: getDraft(channelId),
author: UserStore.getCurrentUser(),
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
}
)}
buttonProps={{
style: {
translate: "0 2px"
}
}}
>
<svg
fill="currentColor"
fillRule="evenodd"
width="24"
height="24"
viewBox="0 0 24 24"
style={{ scale: "1.096", translate: "0 -1px" }}
>
<path d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
</svg>
</ChatBarButton>
);
}
};
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"),
});

View file

@ -25,16 +25,17 @@ function onClick() {
const channels: Array<any> = [];
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({

View file

@ -0,0 +1,5 @@
# ResurrectHome
Brings back the phased out [Server Home](https://support.discord.com/hc/en-us/articles/6156116949911-Server-Home-Beta) feature!
![](https://private-user-images.githubusercontent.com/47677887/309572891-a9ee7354-9e5e-4b81-8faf-304d9c44f512.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDk0OTE5MTIsIm5iZiI6MTcwOTQ5MTYxMiwicGF0aCI6Ii80NzY3Nzg4Ny8zMDk1NzI4OTEtYTllZTczNTQtOWU1ZS00YjgxLThmYWYtMzA0ZDljNDRmNTEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAzMDMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMzAzVDE4NDY1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUxMWY1MzQxNTA4NDE1MWU0YjAxNzM1NzI1YWJkMTNiZmNkNjRmYTRkZDg1ZDE5NzdkMjM0MGVjMDA0OWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.TPYWPRWHTJstfviT9HOaBWFkbBhokyxiDC-gOVL2dqs)

View file

@ -0,0 +1,119 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findGroupChildrenByChildId } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Menu } from "@webpack/common";
const settings = definePluginSettings({
forceServerHome: {
type: OptionType.BOOLEAN,
description: "Force the Server Guide to be the Server Home tab when it is enabled.",
default: false
}
});
function useForceServerHome() {
const { forceServerHome } = settings.use(["forceServerHome"]);
return forceServerHome;
}
export default definePlugin({
name: "ResurrectHome",
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
authors: [Devs.Dolfies, Devs.Nuckyz],
settings,
patches: [
// Force home deprecation override
{
find: "GuildFeatures.GUILD_HOME_DEPRECATION_OVERRIDE",
all: true,
replacement: [
{
match: /\i\.hasFeature\(\i\.GuildFeatures\.GUILD_HOME_DEPRECATION_OVERRIDE\)/g,
replace: "true"
}
],
},
// Disable feedback prompts
{
find: "GuildHomeFeedbackExperiment.definition.id",
replacement: [
{
match: /return{showFeedback:\i,setOnDismissedFeedback:(\i)}/,
replace: "return{showFeedback:false,setOnDismissedFeedback:$1}"
}
]
},
// This feature was never finished, so the patch is disabled
// Enable guild feed render mode selector
// {
// find: "2022-01_home_feed_toggle",
// replacement: [
// {
// match: /showSelector:!1/,
// replace: "showSelector:true"
// }
// ]
// },
// Fix focusMessage clearing previously cached messages and causing a loop when fetching messages around home messages
{
find: '"MessageActionCreators"',
replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
replace: "before:$1,"
}
},
// Force Server Home instead of Server Guide
{
find: "61eef9_2",
replacement: {
match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/,
replace: "if($self.useForceServerHome())return false;"
}
}
],
useForceServerHome,
contextMenus: {
"guild-context"(children, props) {
const forceServerHome = useForceServerHome();
if (!props?.guild) return;
const group = findGroupChildrenByChildId("hide-muted-channels", children);
group?.unshift(
<Menu.MenuCheckboxItem
key="force-server-home"
id="force-server-home"
label="Force Server Home"
checked={forceServerHome}
action={() => settings.store.forceServerHome = !forceServerHome}
/>
);
}
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Flex } from "@components/Flex";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
@ -84,7 +84,7 @@ function makeSearchItem(src: string) {
);
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (props?.reverseImageSearchType !== "img") return;
const src = props.itemHref ?? props.itemSrc;
@ -93,7 +93,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
group?.push(makeSearchItem(src));
};
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!props?.src) return;
const group = findGroupChildrenByChildId("copy-native-link", children) ?? children;
@ -115,14 +115,8 @@ export default definePlugin({
}
}
],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
addContextMenuPatch("image-context", imageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
removeContextMenuPatch("image-context", imageContextMenuPatch);
contextMenus: {
"message": messageContextMenuPatch,
"image-context": imageContextMenuPatch
}
});

View file

@ -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) {

View file

@ -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 && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
<Timestamp timestamp={new Date(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
import { find, findByPropsLazy } from "@webpack";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth";
@ -31,7 +31,8 @@ import ReviewComponent from "./ReviewComponent";
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 InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA");
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 (
<>

View file

@ -18,7 +18,7 @@
import "./style.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
@ -36,7 +36,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => {
children.push(
<Menu.MenuItem
label="View Reviews"
@ -53,6 +53,9 @@ export default definePlugin({
authors: [Devs.mantikafasi, Devs.Ven],
settings,
contextMenus: {
"guild-header-popout": guildPopoutPatch
},
patches: [
{
@ -69,8 +72,6 @@ export default definePlugin({
},
async start() {
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
const s = settings.store;
const { lastReviewId, notifyReviews } = s;
@ -127,10 +128,6 @@ export default definePlugin({
}, 4000);
},
stop() {
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
},
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
const [reviewCount, setReviewCount] = useState<number>();

View file

@ -118,10 +118,10 @@ export async function addReview(review: any): Promise<Response | null> {
export async function deleteReview(id: number): Promise<Response | null> {
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<Response | null> {
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<ReviewDBUser[]> {
const res = await rdbRequest("/blocks", {
method: "GET",
headers: new Headers({
headers: {
Accept: "application/json",
})
}
});
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ReplyIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -27,7 +27,7 @@ import { Message } from "discord-types/general";
const messageUtils = findByPropsLazy("replyToMessage");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
// make sure the message is in the selected channel
if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
const channel = ChannelStore.getChannel(message?.channel_id);
@ -38,7 +38,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
const dmGroup = findGroupChildrenByChildId("pin", children);
if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) {
const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin");
return dmGroup.splice(pinIndex + 1, 0, (
dmGroup.splice(pinIndex + 1, 0, (
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
@ -46,12 +46,13 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
/>
));
return;
}
// servers
const serverGroup = findGroupChildrenByChildId("mark-unread", children);
if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) {
return serverGroup.unshift((
serverGroup.unshift((
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
@ -59,6 +60,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
/>
));
return;
}
};
@ -67,12 +69,7 @@ export default definePlugin({
name: "SearchReply",
description: "Adds a reply button to search results",
authors: [Devs.Aria],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -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 (
<ChatBarButton
tooltip="Insert Timestamp"
onClick={() => {
const key = openModal(props => (
<PickerModal
rootProps={props}
close={() => closeModal(key)}
/>
));
}}
buttonProps={{ "aria-haspopup": "dialog" }}
>
<svg
aria-hidden="true"
role="img"
width="24"
height="24"
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
</svg>
</ChatBarButton>
);
};
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 (
<Tooltip text="Insert Timestamp">
{({ onMouseEnter, onMouseLeave }) => (
<div style={{ display: "flex" }}>
<Button
aria-haspopup="dialog"
aria-label="Insert Timestamp"
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={() => {
const key = openModal(props => (
<PickerModal
rootProps={props}
close={() => closeModal(key)}
/>
));
}}
className={cl("button")}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg
aria-hidden="true"
role="img"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
</svg>
</div>
</Button>
</div>
)
}
</Tooltip >
);
},
settingsAboutComponent() {
const samples = [
"12:00",

View file

@ -42,10 +42,6 @@
margin-bottom: 1em;
}
.vc-st-button {
padding: 0 6px;
}
.vc-st-button svg {
transform: scale(1.1) translateY(1px);
}

View file

@ -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 (
<Timestamp timestamp={moment(timestamp)} />
<Timestamp timestamp={new Date(timestamp)} />
);
}
@ -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) {
)}
<div className={cl("header")}>
{guild.icon
{iconUrl
? <img
src={iconUrl}
alt=""
@ -150,7 +146,7 @@ function Owner(guildId: string, owner: User) {
avatar: guildAvatar,
guildId,
canAnimate: true
}, true)
})
: IconUtils.getUserAvatarURL(owner, true);
return (

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
@ -12,7 +12,7 @@ import { Guild } from "discord-types/general";
import { openGuildProfileModal } from "./GuildProfileModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
@ -29,12 +29,8 @@ export default definePlugin({
description: "Allows you to view info about a server by right clicking it in the server list",
authors: [Devs.Ven, Devs.Nuckyz],
tags: ["guild", "info"],
start() {
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
},
stop() {
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
}
});

View file

@ -20,7 +20,7 @@ import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { formatDuration } from "@utils/text";
import { findByPropsLazy, findComponentByCodeLazy, findComponentLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import type { Channel } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "../../permissionsViewer/components/RolesAndUsersPermissions";
@ -120,7 +120,7 @@ const VideoQualityModesToNames = {
const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg";
function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
const [viewAllowedUsersAndRoles, setViewAllowedUsersAndRoles] = useState(settings.store.defaultAllowedUsersAndRolesDropdownState);
const { defaultAllowedUsersAndRolesDropdownState } = settings.use(["defaultAllowedUsersAndRolesDropdownState"]);
const [permissions, setPermissions] = useState<RoleOrUserPermission[]>([]);
const {
@ -216,12 +216,12 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
{lastMessageId &&
<Text variant="text-md/normal">
Last {channel.isForumChannel() ? "post" : "message"} created:
<Timestamp timestamp={moment(SnowflakeUtils.extractTimestamp(lastMessageId))} />
<Timestamp timestamp={new Date(SnowflakeUtils.extractTimestamp(lastMessageId))} />
</Text>
}
{lastPinTimestamp &&
<Text variant="text-md/normal">Last message pin: <Timestamp timestamp={moment(lastPinTimestamp)} /></Text>
<Text variant="text-md/normal">Last message pin: <Timestamp timestamp={new Date(lastPinTimestamp)} /></Text>
}
{(rateLimitPerUser ?? 0) > 0 &&
<Text variant="text-md/normal">Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}</Text>
@ -301,19 +301,19 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
</Tooltip>
)}
<Text variant="text-lg/bold">Allowed users and roles:</Text>
<Tooltip text={viewAllowedUsersAndRoles ? "Hide Allowed Users and Roles" : "View Allowed Users and Roles"}>
<Tooltip text={defaultAllowedUsersAndRolesDropdownState ? "Hide Allowed Users and Roles" : "View Allowed Users and Roles"}>
{({ onMouseLeave, onMouseEnter }) => (
<button
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className="shc-lock-screen-allowed-users-and-roles-container-toggle-btn"
onClick={() => setViewAllowedUsersAndRoles(v => !v)}
onClick={() => settings.store.defaultAllowedUsersAndRolesDropdownState = !defaultAllowedUsersAndRolesDropdownState}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={viewAllowedUsersAndRoles ? "scale(1 -1)" : "scale(1 1)"}
transform={defaultAllowedUsersAndRolesDropdownState ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="currentColor" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
@ -321,7 +321,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
)}
</Tooltip>
</div>
{viewAllowedUsersAndRoles && <ChannelBeginHeader channel={channel} />}
{defaultAllowedUsersAndRolesDropdownState && <ChannelBeginHeader channel={channel} />}
</div>
</div>
</div>

View file

@ -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
@ -305,27 +305,27 @@ export default definePlugin({
]
},
{
find: ".avatars),children",
find: '+1]})},"overflow"))',
replacement: [
{
// Create a variable for the channel prop
match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
match: /maxUsers:\i,users:\i.+?}=(\i).*?;/,
replace: (m, props) => `${m}let{shcChannel}=${props};`
},
{
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
match: /\i>0(?=&&.{0,60}renderPopout)/,
replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})`
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})`
},
{
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)`
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)`
},
{
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<="\+",)(\i)\+1/,
replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}`
replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}`
}
]
},

View file

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 (
<Tooltip text={enabled ? "Disable Silent Message" : "Enable Silent Message"}>
{tooltipProps => (
<div style={{ display: "flex" }}>
<Button
{...tooltipProps}
onClick={() => setEnabledValue(!enabled)}
size=""
look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button}
style={{ padding: "0 6px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" mask="url(#_)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{!enabled && <>
<mask id="_">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>}
</svg>
</div>
</Button>
</div>
)}
</Tooltip>
<ChatBarButton
tooltip={enabled ? "Disable Silent Message" : "Enable Silent Message"}
onClick={() => setEnabledValue(!enabled)}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<path fill="currentColor" mask="url(#_)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{!enabled && <>
<mask id="_">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>}
</svg>
</ChatBarButton>
);
}
};
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")
});

View file

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 (
<Tooltip text={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}>
{(tooltipProps: any) => (
<div style={{ display: "flex" }}>
<Button
{...tooltipProps}
onClick={toggle}
size=""
look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button}
style={{ padding: "0 6px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
</svg>
</div>
</Button>
</div>
)}
</Tooltip>
<ChatBarButton
tooltip={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}
onClick={toggle}
>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
</svg>
</ChatBarButton>
);
}
};
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"),
});

View file

@ -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 (
<ErrorBoundary fallback={() => (
<div className="vc-spotify-fallback">
@ -378,7 +382,7 @@ export function Player() {
<p >Check the console for errors</p>
</div>
)}>
<div id={cl("player")}>
<div id={cl("player")} style={exportTrackImageStyle}>
<Info track={track} />
<SeekBar />
<Controls />

View file

@ -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",

View file

@ -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 {

View file

@ -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;
}
});

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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: <>
<Forms.FormText>
You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent.
</Forms.FormText>
<Forms.FormText className={Margins.top16}>
If this was an accident, disable it again, or it will change your message content before sending.
</Forms.FormText>
</>,
cancelText: "Disable Auto-Translate",
confirmText: "Got it",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
onCancel: () => settings.store.autoTranslate = false
});
};
return (
<Tooltip text="Open Translate Modal">
{({ onMouseEnter, onMouseLeave }) => (
<div style={{ display: "flex" }}>
<Button
aria-haspopup="dialog"
aria-label="Open Translate Modal"
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={e => {
if (e.shiftKey) return toggle();
<ChatBarButton
tooltip="Open Translate Modal"
onClick={e => {
if (e.shiftKey) return toggle();
openModal(props => (
<TranslateModal rootProps={props} />
));
}}
onContextMenu={() => toggle()}
style={{ padding: "0 4px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<TranslateIcon className={cl({ "auto-translate": autoTranslate })} />
</div>
</Button>
</div>
)}
</Tooltip>
openModal(props => (
<TranslateModal rootProps={props} />
));
}}
onContextMenu={() => toggle()}
buttonProps={{
"aria-haspopup": "dialog"
}}
>
<TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} />
</ChatBarButton>
);
}
};

View file

@ -18,11 +18,11 @@
import "./styles.css";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } 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";
@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
import { translate } from "./utils";
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => {
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
if (!message.content) return;
const group = findGroupChildrenByChildId("copy-text", children);
@ -55,25 +55,18 @@ export default definePlugin({
name: "Translate",
description: "Translate messages with Google Translate",
authors: [Devs.Ven],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
"message": messageCtxPatch
},
// 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 => <TranslationAccessory message={props.message} />);
addContextMenuPatch("message", messageCtxPatch);
addChatBarButton("vc-translate", TranslateChatBarIcon);
addButton("vc-translate", message => {
if (!message.content) return null;
@ -100,14 +93,8 @@ export default definePlugin({
stop() {
removePreSendListener(this.preSend);
removeContextMenuPatch("message", messageCtxPatch);
removeChatBarButton("vc-translate");
removeButton("vc-translate");
removeAccessory("vc-translation");
},
chatBarIcon: (slateProps: any) => (
<ErrorBoundary noop>
<TranslateChatBarIcon slateProps={slateProps} />
</ErrorBoundary>
)
});

View file

@ -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;
}>();

View file

@ -35,3 +35,7 @@
.vc-trans-auto-translate {
color: var(--green-360);
}
.vc-trans-chat-button {
scale: 1.085;
}

View file

@ -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)"
}
},

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web
const EMBED_SUPPRESSED = 1 << 2;
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => {
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
if (!isEmbedSuppressed && !embeds.length) return;
@ -56,12 +56,7 @@ export default definePlugin({
name: "UnsuppressEmbeds",
authors: [Devs.rad, Devs.HypedDomi],
description: "Allows you to unsuppress embeds in messages",
start() {
addContextMenuPatch("message", messageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
},
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -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]),$&",

View file

@ -19,7 +19,7 @@
import "./index.css";
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings } from "@api/Settings";
import { Settings, useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -30,6 +30,8 @@ import type { ReactNode } from "react";
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
function VencordPopout(onClose: () => void) {
const { useQuickCss } = useSettings(["useQuickCss"]);
const pluginEntries = [] as ReactNode[];
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) {
/>
<Menu.MenuCheckboxItem
id="vc-toolbox-quickcss-toggle"
checked={Settings.useQuickCss}
checked={useQuickCss}
label={"Enable QuickCSS"}
action={() => {
Settings.useQuickCss = !Settings.useQuickCss;
onClose();
Settings.useQuickCss = !useQuickCss;
}}
/>
<Menu.MenuItem

View file

@ -16,17 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
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;
@ -82,7 +80,7 @@ function openImage(url: string) {
});
}
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => {
if (!user) return;
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
@ -91,19 +89,19 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
<Menu.MenuItem
id="view-avatar"
label="View Avatar"
action={() => openImage(BannerStore.getUserAvatarURL(user, true))}
action={() => openImage(IconUtils.getUserAvatarURL(user, true))}
icon={ImageIcon}
/>
{memberAvatar && (
<Menu.MenuItem
id="view-server-avatar"
label="View Server Avatar"
action={() => openImage(BannerStore.getGuildMemberAvatarURLSimple({
action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({
userId: user.id,
avatar: memberAvatar,
guildId,
guildId: guildId!,
canAnimate: true
}, true))}
}))}
icon={ImageIcon}
/>
)}
@ -111,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
));
};
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => {
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => {
if (!guild) return;
const { id, icon, banner } = guild;
@ -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}
/>
@ -160,14 +155,9 @@ export default definePlugin({
openImage,
start() {
addContextMenuPatch("user-context", UserContext);
addContextMenuPatch("guild-context", GuildContext);
},
stop() {
removeContextMenuPatch("user-context", UserContext);
removeContextMenuPatch("guild-context", GuildContext);
contextMenus: {
"user-context": UserContext,
"guild-context": GuildContext
},
patches: [

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
@ -117,8 +117,8 @@ const settings = definePluginSettings({
}
});
function MakeContextCallback(name: "Guild" | "User" | "Channel") {
const callback: NavContextMenuPatchCallback = (children, props) => () => {
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => {
const value = props[name.toLowerCase()];
if (!value) return;
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") {
/>
);
};
return callback;
}
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User")
},
start() {
addButton("ViewRaw", msg => {
@ -187,16 +190,9 @@ export default definePlugin({
onContextMenu: handleContextMenu
};
});
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
addContextMenuPatch("user-context", MakeContextCallback("User"));
},
stop() {
removeButton("CopyRawMessage");
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
removeContextMenuPatch("user-context", MakeContextCallback("User"));
removeButton("ViewRaw");
}
});

View file

@ -18,15 +18,17 @@
import "./styles.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } 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";
@ -46,18 +48,30 @@ export type VoiceRecorder = ComponentType<{
const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb;
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
children.push(
<Menu.MenuItem
id="vc-send-vmsg"
label={
<div className={OptionClasses.optionLabel}>
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
<div className={OptionClasses.optionName}>Send voice message</div>
</div>
}
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
/>
);
};
export default definePlugin({
name: "VoiceMessages",
description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message",
authors: [Devs.Ven, Devs.Vap, Devs.Nickyux],
settings,
start() {
addContextMenuPatch("channel-attach", ctxMenuPatch);
},
stop() {
removeContextMenuPatch("channel-attach", ctxMenuPatch);
contextMenus: {
"channel-attach": ctxMenuPatch
}
});
@ -164,6 +178,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 (
<ModalRoot {...modalProps}>
<ModalHeader>
@ -200,6 +219,16 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
recording={isRecording}
/>
{isUnsupportedFormat && (
<Card className={`vc-plugins-restart-card ${Margins.top16}`}>
<Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
To fix it, first convert it to OggOpus, for example using the <Link href="https://convertio.co/mp3-opus/">convertio web converter</Link>
</Forms.FormText>
</Card>
)}
</ModalContent>
<ModalFooter>
@ -217,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
</ModalRoot>
);
}
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
children.push(
<Menu.MenuItem
id="vc-send-vmsg"
label={
<div className={OptionClasses.optionLabel}>
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
<div className={OptionClasses.optionName}>Send voice message</div>
</div>
}
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
/>
);
};

View file

@ -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;

View file

@ -69,14 +69,14 @@ function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
function makeRenderMoreUsers(users: User[]) {
return function renderMoreUsers(_label: string, _count: number) {
return (
<Tooltip text={users.slice(5).map(u => u.username).join(", ")} >
<Tooltip text={users.slice(4).map(u => u.username).join(", ")} >
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{users.length - 5}
+{users.length - 4}
</div>
)}
</Tooltip >

View file

@ -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) {

View file

@ -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,
@ -403,6 +403,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "maisy",
id: 257109471589957632n,
},
Mopi: {
name: "Mopi",
id: 1022189106614243350n
},
Grzesiek11: {
name: "Grzesiek11",
id: 368475654662127616n,
@ -414,6 +418,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
coolelectronics: {
name: "coolelectronics",
id: 696392247205298207n,
},
Av32000: {
name: "Av32000",
id: 593436735380127770n,
},
Kyuuhachi: {
name: "Kyuuhachi",
id: 236588665420251137n,
}
} satisfies Record<string, Dev>);

View file

@ -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) {

View file

@ -17,6 +17,7 @@
*/
import { Command } from "@api/Commands";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { FluxEvents } from "@webpack/types";
import { Promisable } from "type-fest";
@ -115,6 +116,10 @@ export interface PluginDef {
flux?: {
[E in FluxEvents]?: (event: any) => void;
};
/**
* Allows you to manipulate context menus
*/
contextMenus?: Record<string, NavContextMenuPatchCallback>;
/**
* Allows you to add custom actions to the Vencord Toolbox.
* The key will be used as text for the button

View file

@ -51,7 +51,7 @@ export let Avatar: t.Avatar;
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
export let useToken: t.useToken;
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)"));
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);

View file

@ -6,7 +6,10 @@
import { findByPropsLazy } from "@webpack";
export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact");
export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame");
import * as t from "./types/settingsStores";
export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record<string, t.SettingsStore>;
export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record<string, t.SettingsStore>;
export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators");

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<PropsWithChildren<{
}>>;
export type Timestamp = ComponentType<PropsWithChildren<{
timestamp: Moment;
timestamp: Date;
isEdited?: boolean;
className?: string;

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./classes";
export * from "./components";
export * from "./fluxEvents";
export * from "./i18nMessages";
export * from "./menu";
export * from "./settingsStores";
export * from "./stores";
export * from "./utils";

View file

@ -0,0 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SettingsStore<T = any> {
getSetting(): T;
updateSetting(value: T): void;
useSetting(): T;
}

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Guild, GuildMember } from "discord-types/general";
import type { ReactNode } from "react";
import type { FluxEvents } from "./fluxEvents";
@ -58,6 +59,7 @@ export interface Alerts {
onCancel?(): void;
onConfirm?(): void;
onConfirmSecondary?(): void;
onCloseCallback?(): void;
}): void;
/** This is a noop, it does nothing. */
close(): void;
@ -182,3 +184,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;
}

Some files were not shown because too many files have changed in this diff Show more