mirror of
https://github.com/RobinRMC/VencordPlus.git
synced 2025-05-11 01:45:38 +02:00
Update Vencord+
This commit is contained in:
parent
496d62f40a
commit
63f4fc7169
19 changed files with 909 additions and 1010 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -19,6 +19,3 @@ lerna-debug.log*
|
|||
*.tsbuildinfo
|
||||
|
||||
ExtensionCache/
|
||||
settings/
|
||||
|
||||
src/userplugins
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR, SWATCHES } from "../constants";
|
||||
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
|
||||
import { forceUpdate } from "../index";
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
}
|
||||
|
||||
interface ColorPickerWithSwatchesProps {
|
||||
defaultColor: number;
|
||||
colors: number[];
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
onChange(value: number | null): void;
|
||||
renderDefaultButton?: () => React.ReactNode;
|
||||
renderCustomButton?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('"color-picker"', ".customContainer");
|
||||
|
||||
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}el\("(.+?)"\).{0,50}"UserSettings"/);
|
||||
|
||||
const cl = classNameFactory("vc-pindms-modal-");
|
||||
|
||||
interface Props {
|
||||
categoryId: string | null;
|
||||
initalChannelId: string | null;
|
||||
modalProps: ModalProps;
|
||||
}
|
||||
|
||||
const useCategory = (categoryId: string | null, initalChannelId: string | null) => {
|
||||
const [category, setCategory] = useState<Category | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (categoryId)
|
||||
setCategory(getCategory(categoryId)!);
|
||||
else if (initalChannelId)
|
||||
setCategory({
|
||||
id: Toasts.genId(),
|
||||
name: `Pin Category ${categories.length + 1}`,
|
||||
color: 10070709,
|
||||
collapsed: false,
|
||||
channels: [initalChannelId]
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
category,
|
||||
setCategory
|
||||
};
|
||||
};
|
||||
|
||||
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
|
||||
const { category, setCategory } = useCategory(categoryId, initalChannelId);
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (!categoryId)
|
||||
await createCategory(category);
|
||||
else
|
||||
await updateCategory(category);
|
||||
|
||||
forceUpdate();
|
||||
modalProps.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
|
||||
</ModalHeader>
|
||||
|
||||
{/* form is here so when you press enter while in the text input it submits */}
|
||||
<form onSubmit={onSave}>
|
||||
<ModalContent className={cl("content")}>
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>Name</Forms.FormTitle>
|
||||
<TextInput
|
||||
value={category.name}
|
||||
onChange={e => setCategory({ ...category, name: e })}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
<Forms.FormDivider />
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>Color</Forms.FormTitle>
|
||||
<ColorPickerWithSwatches
|
||||
key={category.name}
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
colors={SWATCHES}
|
||||
onChange={c => setCategory({ ...category, color: c! })}
|
||||
value={category.color}
|
||||
renderDefaultButton={() => null}
|
||||
renderCustomButton={() => (
|
||||
<ColorPicker
|
||||
color={category.color}
|
||||
onChange={c => setCategory({ ...category, color: c! })}
|
||||
key={category.name}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
|
||||
openModal(modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />);
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { Menu } from "@webpack/common";
|
||||
|
||||
import { moveChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
|
||||
import { forceUpdate, settings } from "../index";
|
||||
import { openCategoryModal } from "./CreateCategoryModal";
|
||||
|
||||
function PinMenuItem(channelId: string) {
|
||||
const pinned = isPinned(channelId);
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id="better-pin-dm"
|
||||
label="Move DM to category"
|
||||
>
|
||||
|
||||
{(
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
id="add-category"
|
||||
label="New category"
|
||||
color="brand"
|
||||
action={() => openCategoryModal(null, channelId)}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
{
|
||||
categories.map(category => (
|
||||
<Menu.MenuItem
|
||||
id={`pin-category-${category.name}`}
|
||||
label={category.name}
|
||||
disabled={category.channels.includes(channelId)}
|
||||
action={() => moveChannelToCategory(channelId, category.id).then(() => forceUpdate())}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
id="unpin-dm"
|
||||
label="Unpin DM"
|
||||
color="danger"
|
||||
disabled={!pinned}
|
||||
action={() => removeChannelFromCategory(channelId).then(() => forceUpdate())}
|
||||
/>
|
||||
|
||||
{
|
||||
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, -1) && (
|
||||
<Menu.MenuItem
|
||||
id="move-up"
|
||||
label="Move Up"
|
||||
action={() => moveChannel(channelId, -1).then(() => forceUpdate())}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, 1) && (
|
||||
<Menu.MenuItem
|
||||
id="move-down"
|
||||
label="Move Down"
|
||||
action={() => moveChannel(channelId, 1).then(() => forceUpdate())}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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 container = findGroupChildrenByChildId("close-dm", children);
|
||||
if (container) {
|
||||
const idx = container.findIndex(c => c?.props?.id === "close-dm");
|
||||
container.splice(idx, 0, PinMenuItem(props.channel.id));
|
||||
}
|
||||
};
|
||||
|
||||
export const contextMenus = {
|
||||
"gdm-context": GroupDMContext,
|
||||
"user-context": UserContext
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const DEFAULT_COLOR = 10070709;
|
||||
|
||||
export const SWATCHES = [
|
||||
1752220,
|
||||
3066993,
|
||||
3447003,
|
||||
10181046,
|
||||
15277667,
|
||||
15844367,
|
||||
15105570,
|
||||
15158332,
|
||||
9807270,
|
||||
6323595,
|
||||
|
||||
1146986,
|
||||
2067276,
|
||||
2123412,
|
||||
7419530,
|
||||
11342935,
|
||||
12745742,
|
||||
11027200,
|
||||
10038562,
|
||||
9936031,
|
||||
5533306
|
||||
];
|
|
@ -1,209 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR } from "./constants";
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
color: number;
|
||||
channels: string[];
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export const KEYS = {
|
||||
CATEGORY_BASE_KEY: "BetterPinDMsCategories-",
|
||||
CATEGORY_MIGRATED_PINDMS_KEY: "BetterPinDMsMigratedPinDMs",
|
||||
CATEGORY_MIGRATED_KEY: "BetterPinDMsMigratedOldCategories",
|
||||
OLD_CATEGORY_KEY: "betterPinDmsCategories"
|
||||
};
|
||||
|
||||
|
||||
export let categories: Category[] = [];
|
||||
|
||||
export async function saveCats(cats: Category[]) {
|
||||
const { id } = UserStore.getCurrentUser();
|
||||
await DataStore.set(KEYS.CATEGORY_BASE_KEY + id, cats);
|
||||
}
|
||||
|
||||
export async function initCategories(userId: string) {
|
||||
categories = await DataStore.get<Category[]>(KEYS.CATEGORY_BASE_KEY + userId) ?? [];
|
||||
}
|
||||
|
||||
export function getCategory(id: string) {
|
||||
return categories.find(c => c.id === id);
|
||||
}
|
||||
|
||||
export async function createCategory(category: Category) {
|
||||
categories.push(category);
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export async function updateCategory(category: Category) {
|
||||
const index = categories.findIndex(c => c.id === category.id);
|
||||
if (index === -1) return;
|
||||
|
||||
categories[index] = category;
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export async function moveChannelToCategory(channelId: string, categoryId: string) {
|
||||
|
||||
const targetcategory = categories.find(c => c.id === categoryId);
|
||||
if (!targetcategory) return;
|
||||
|
||||
const oldcategory = categories.find(c => c.channels.includes(channelId));
|
||||
if (oldcategory) oldcategory.channels = oldcategory.channels.filter(c => c !== channelId);
|
||||
// remove channel from old category if it was in one
|
||||
|
||||
if (categoryId !== '0') targetcategory.channels.push(channelId);
|
||||
// if categoryId is not 0, add channel to category
|
||||
|
||||
saveCats(categories);
|
||||
|
||||
}
|
||||
|
||||
/* substituted with an implementation of "moveChannelToCategory";
|
||||
export async function addChannelToCategory(channelId: string, categoryId: string) {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
if (!category) return;
|
||||
|
||||
if (category.channels.includes(channelId)) return;
|
||||
|
||||
category.channels.push(channelId);
|
||||
saveCats(categories);
|
||||
}
|
||||
*/
|
||||
|
||||
export async function removeChannelFromCategory(channelId: string) {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return;
|
||||
|
||||
category.channels = category.channels.filter(c => c !== channelId);
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export async function removeCategory(categoryId: string) {
|
||||
const catagory = categories.find(c => c.id === categoryId);
|
||||
if (!catagory) return;
|
||||
|
||||
catagory?.channels.forEach(c => removeChannelFromCategory(c));
|
||||
categories = categories.filter(c => c.id !== categoryId);
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export function isPinned(id: string) {
|
||||
return categories.some(c => c.channels.includes(id));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
|
||||
const a = array[index];
|
||||
const b = array[index + direction];
|
||||
|
||||
return a && b;
|
||||
};
|
||||
|
||||
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
|
||||
const index = categories.findIndex(m => m.id === id);
|
||||
return canMoveArrayInDirection(categories, index, direction);
|
||||
};
|
||||
|
||||
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
|
||||
|
||||
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return false;
|
||||
|
||||
const index = category.channels.indexOf(channelId);
|
||||
return canMoveArrayInDirection(category.channels, index, direction);
|
||||
};
|
||||
|
||||
|
||||
function swapElementsInArray(array: any[], index1: number, index2: number) {
|
||||
if (!array[index1] || !array[index2]) return;
|
||||
[array[index1], array[index2]] = [array[index2], array[index1]];
|
||||
}
|
||||
|
||||
|
||||
export async function moveCategory(id: string, direction: -1 | 1) {
|
||||
const a = categories.findIndex(m => m.id === id);
|
||||
const b = a + direction;
|
||||
|
||||
swapElementsInArray(categories, a, b);
|
||||
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export async function moveChannel(channelId: string, direction: -1 | 1) {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return;
|
||||
|
||||
const a = category.channels.indexOf(channelId);
|
||||
const b = a + direction;
|
||||
|
||||
swapElementsInArray(category.channels, a, b);
|
||||
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
export async function collapseCategory(id: string, value = true) {
|
||||
const category = categories.find(c => c.id === id);
|
||||
if (!category) return;
|
||||
|
||||
category.collapsed = value;
|
||||
saveCats(categories);
|
||||
}
|
||||
|
||||
|
||||
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
|
||||
|
||||
async function migratePinDMs() {
|
||||
if (categories.some(m => m.id === "oldPins")) {
|
||||
return await DataStore.set(KEYS.CATEGORY_MIGRATED_PINDMS_KEY, true);
|
||||
}
|
||||
|
||||
const pindmspins = getPinDMsPins();
|
||||
|
||||
// we dont want duplicate pins
|
||||
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
|
||||
if (difference?.length) {
|
||||
categories.push({
|
||||
id: "oldPins",
|
||||
name: "Pins",
|
||||
color: DEFAULT_COLOR,
|
||||
channels: difference
|
||||
});
|
||||
}
|
||||
|
||||
await DataStore.set(KEYS.CATEGORY_MIGRATED_PINDMS_KEY, true);
|
||||
}
|
||||
|
||||
async function migrateOldCategories() {
|
||||
const oldCats = await DataStore.get<Category[]>(KEYS.OLD_CATEGORY_KEY);
|
||||
// dont want to migrate if the user has already has categories.
|
||||
if (categories.length === 0 && oldCats?.length) {
|
||||
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
|
||||
}
|
||||
await DataStore.set(KEYS.CATEGORY_MIGRATED_KEY, true);
|
||||
}
|
||||
|
||||
export async function migrateData() {
|
||||
const m1 = await DataStore.get(KEYS.CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(KEYS.CATEGORY_MIGRATED_PINDMS_KEY);
|
||||
if (m1 && m2) return;
|
||||
|
||||
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
|
||||
if (!m1) await migrateOldCategories();
|
||||
if (!m2) await migratePinDMs();
|
||||
|
||||
await saveCats(categories);
|
||||
}
|
|
@ -1,379 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy, waitFor } from "@webpack";
|
||||
import { Alerts, Button, ContextMenuApi, FluxDispatcher, Menu, React, UserStore } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
import { contextMenus } from "./components/contextMenu";
|
||||
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
|
||||
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, collapseCategory, initCategories, isPinned, migrateData, moveCategory, removeCategory } from "./data";
|
||||
|
||||
interface ChannelComponentProps {
|
||||
children: React.ReactNode,
|
||||
channel: Channel,
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
||||
|
||||
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||
|
||||
export let instance: any;
|
||||
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
||||
|
||||
// the flux property in definePlugin doenst fire, probably because startAt isnt Init
|
||||
waitFor(["dispatch", "subscribe"], m => {
|
||||
m.subscribe("CONNECTION_OPEN", async () => {
|
||||
if (!Settings.plugins.BetterPinDMs?.enabled) return;
|
||||
|
||||
const id = UserStore.getCurrentUser()?.id;
|
||||
await initCategories(id);
|
||||
await migrateData();
|
||||
forceUpdate();
|
||||
// dont want to unsubscribe because if they switch accounts we want to reinit
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
sortDmsByNewestMessage: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Sort DMs by newest message",
|
||||
default: false,
|
||||
onChange: () => forceUpdate()
|
||||
},
|
||||
|
||||
dmSectioncollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Collapse DM sections",
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterPinDMs",
|
||||
description: "Pin DMs but with categories",
|
||||
authors: [Devs.Aria, Devs.Ven, Devs.Strencher],
|
||||
settings,
|
||||
contextMenus,
|
||||
patches: [
|
||||
{
|
||||
find: ".privateChannelsHeaderContainer,",
|
||||
predicate: () => !Settings.plugins.PinDMs?.enabled,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
|
||||
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
|
||||
},
|
||||
{
|
||||
match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
|
||||
replace: "...$self.makeProps(this,{$&})"
|
||||
},
|
||||
{
|
||||
match: /this\.renderDM=\(.+?(\i\.default),{channel.+?this.renderRow=(\i)=>{/,
|
||||
replace: "$&if($self.isChannelIndex($2.section, $2.row))return $self.renderChannel($2.section,$2.row,$1);"
|
||||
},
|
||||
{
|
||||
match: /this\.renderSection=(\i)=>{/,
|
||||
replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
|
||||
},
|
||||
{
|
||||
match: /(?<=span",{)className:\i\.headerText,/,
|
||||
replace: "onClick: (e) => $self.collapseDMList(e),$&"
|
||||
},
|
||||
{
|
||||
match: /(this\.getRowHeight=.{1,100}return 1===)(\i)/,
|
||||
replace: "$1($2-$self.categoryLen())"
|
||||
},
|
||||
{
|
||||
match: /componentDidMount\(\){/,
|
||||
replace: "$&$self._instance = this;"
|
||||
},
|
||||
{
|
||||
match: /this.getRowHeight=\((\i),(\i)\)=>{/,
|
||||
replace: "$&if($self.isChannelHidden($1,$2))return 0;"
|
||||
},
|
||||
{
|
||||
// Copied from PinDMs
|
||||
// Override scrollToChannel to properly account for pinned channels
|
||||
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
|
||||
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
|
||||
},
|
||||
{
|
||||
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
|
||||
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// forceUpdate moment
|
||||
// https://regex101.com/r/kDN9fO/1
|
||||
{
|
||||
find: ".FRIENDS},\"friends\"",
|
||||
predicate: () => !Settings.plugins.PinDMs?.enabled,
|
||||
replacement: {
|
||||
match: /(\i=\i=>{)(.{1,850})showDMHeader:/,
|
||||
replace: "$1let forceUpdate = Vencord.Util.useForceUpdater();$2_forceUpdate:forceUpdate,showDMHeader:"
|
||||
}
|
||||
},
|
||||
|
||||
// copied from PinDMs
|
||||
// Fix Alt Up/Down navigation
|
||||
{
|
||||
find: ".Routes.APPLICATION_STORE&&",
|
||||
predicate: () => !Settings.plugins.PinDMs?.enabled,
|
||||
replacement: {
|
||||
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
|
||||
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
|
||||
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
||||
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
}
|
||||
},
|
||||
|
||||
// copied from PinDMs
|
||||
// fix alt+shift+up/down
|
||||
{
|
||||
find: ".getFlattenedGuildIds()],",
|
||||
predicate: () => !Settings.plugins.PinDMs?.enabled,
|
||||
replacement: {
|
||||
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
||||
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
}
|
||||
},
|
||||
],
|
||||
sections: null as number[] | null,
|
||||
|
||||
set _instance(i: any) {
|
||||
this.instance = i;
|
||||
instance = i;
|
||||
},
|
||||
|
||||
isPinned,
|
||||
|
||||
start() {
|
||||
if (Settings.plugins.PinDMs?.enabled) {
|
||||
console.log("disable PinDMs to use this plugin");
|
||||
setTimeout(() => {
|
||||
Alerts.show({
|
||||
title: "PinDMs Enabled",
|
||||
body: "BetterPinDMs requires PinDMs to be disabled. Please disable it to use this plugin.",
|
||||
confirmText: "Disable",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Cancel",
|
||||
|
||||
onConfirm: () => {
|
||||
Settings.plugins.PinDMs.enabled = false;
|
||||
location.reload();
|
||||
},
|
||||
});
|
||||
}, 5_000);
|
||||
return;
|
||||
}
|
||||
|
||||
requireSettingsMenu();
|
||||
},
|
||||
|
||||
makeProps(instance, { sections }: { sections: number[]; }) {
|
||||
this.sections = sections;
|
||||
|
||||
this.sections.splice(1, 0, ...this.usePinCount(instance.props.privateChannelIds || []));
|
||||
|
||||
if (this.instance?.props?.privateChannelIds?.length === 0) {
|
||||
this.sections[this.sections.length - 1] = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
sections: this.sections,
|
||||
chunkSize: this.getChunkSize(),
|
||||
};
|
||||
},
|
||||
|
||||
categoryLen() {
|
||||
return categories.length;
|
||||
},
|
||||
|
||||
getChunkSize() {
|
||||
return 256 + this.getSections().reduce((acc, v) => acc += v, 0) * 20;
|
||||
},
|
||||
|
||||
getAllChannels() {
|
||||
return categories.map(c => c.channels).flat();
|
||||
},
|
||||
|
||||
getAllUncollapsedChannels() {
|
||||
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
|
||||
},
|
||||
|
||||
usePinCount(channelIds: string[]) {
|
||||
return channelIds.length ? this.getSections() : [];
|
||||
},
|
||||
|
||||
collapseDMList() {
|
||||
// console.log("HI");
|
||||
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
|
||||
forceUpdate();
|
||||
},
|
||||
|
||||
getSections() {
|
||||
return categories.reduce((acc, category) => {
|
||||
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
},
|
||||
|
||||
isCategoryIndex(sectionIndex: number) {
|
||||
return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;
|
||||
},
|
||||
|
||||
isChannelIndex(sectionIndex: number, channelIndex: number) {
|
||||
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
|
||||
return true;
|
||||
const cat = categories[sectionIndex - 1];
|
||||
return this.isCategoryIndex(sectionIndex) && (cat.channels.length === 0 || cat?.channels[channelIndex]);
|
||||
},
|
||||
|
||||
isChannelHidden(categoryIndex: number, channelIndex: number) {
|
||||
if (categoryIndex === 0) return false;
|
||||
|
||||
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
|
||||
return true;
|
||||
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
|
||||
|
||||
const category = categories[categoryIndex - 1];
|
||||
if (!category) return false;
|
||||
|
||||
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
|
||||
},
|
||||
|
||||
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
||||
if (!isPinned(channelId))
|
||||
return (
|
||||
(rowHeight + padding) * 2 // header
|
||||
+ rowHeight * this.getAllUncollapsedChannels().length // pins
|
||||
+ originalOffset // original pin offset minus pins
|
||||
);
|
||||
|
||||
return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;
|
||||
},
|
||||
|
||||
renderCategory({ section }: { section: number; }) {
|
||||
const category = categories[section - 1];
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
return (
|
||||
<h2
|
||||
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
|
||||
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
|
||||
onClick={async () => {
|
||||
await collapseCategory(category.id, !category.collapsed);
|
||||
forceUpdate();
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<Menu.Menu
|
||||
navId="vc-pindms-header-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
color="danger"
|
||||
aria-label="Pin DMs Category Menu"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="vc-pindms-edit-category"
|
||||
label="Edit Category"
|
||||
action={() => openCategoryModal(category.id, null)}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
id="vc-pindms-delete-category"
|
||||
color="danger"
|
||||
label="Delete Category"
|
||||
action={() => removeCategory(category.id).then(() => forceUpdate())}
|
||||
/>
|
||||
|
||||
{
|
||||
canMoveCategory(category.id) && (
|
||||
<>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="vc-pindms-move-category"
|
||||
label="Move Category"
|
||||
>
|
||||
{
|
||||
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
|
||||
id="vc-pindms-move-category-up"
|
||||
label="Move Up"
|
||||
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
|
||||
/>
|
||||
}
|
||||
{
|
||||
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
|
||||
id="vc-pindms-move-category-down"
|
||||
label="Move Down"
|
||||
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
|
||||
/>
|
||||
}
|
||||
</Menu.MenuItem>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
</Menu.Menu>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<span className={headerClasses.headerText}>
|
||||
{category?.name ?? "uh oh"}
|
||||
</span>
|
||||
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
|
||||
</svg>
|
||||
</h2>
|
||||
);
|
||||
},
|
||||
|
||||
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||
|
||||
if (!channel || !category) return null;
|
||||
const selected = this.instance.props.selectedChannelId === channel.id;
|
||||
|
||||
if (!selected && category.collapsed) return null;
|
||||
|
||||
return (
|
||||
<ChannelComponent
|
||||
channel={channel}
|
||||
selected={selected}
|
||||
>
|
||||
{channel.id}
|
||||
</ChannelComponent>
|
||||
);
|
||||
},
|
||||
|
||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||
const category = categories[sectionIndex - 1];
|
||||
if (!category) return { channel: null, category: null };
|
||||
|
||||
const channelId = this.getCategoryChannels(category)[index];
|
||||
|
||||
return { channel: channels[channelId], category };
|
||||
},
|
||||
|
||||
getCategoryChannels(category: Category) {
|
||||
if (settings.store.sortDmsByNewestMessage) {
|
||||
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category?.channels?.includes(c));
|
||||
}
|
||||
|
||||
return category?.channels ?? [];
|
||||
}
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
.vc-pindms-section-container {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: .02em;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
flex: 1 1 auto;
|
||||
color: var(--channels-default);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-pindms-modal-content {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.vc-pindms-modal-content [class^="defaultContainer"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-pindms-collapse-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--interactive-normal);
|
||||
transform: rotate(90deg)
|
||||
}
|
||||
|
||||
.vc-pindms-collapsed .vc-pindms-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[class^="privateChannelsHeaderContainer"] > [class^="headerText"] {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 { classNameFactory } from "@api/Styles";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { Button, Forms, Toasts } from "@webpack/common";
|
||||
|
||||
import { Native, settings } from "../..";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "../../utils/constants";
|
||||
|
||||
const cl = classNameFactory("folder-upload");
|
||||
|
||||
function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessage: string) {
|
||||
return function DirSelector({ option }) {
|
||||
if (IS_WEB) return null;
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<SelectFolderInput
|
||||
settingsKey={settingKey}
|
||||
successMessage={successMessage}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const ImageCacheDir = createDirSelector("imageCacheDir", "Successfully updated Image Cache Dir");
|
||||
export const LogsDir = createDirSelector("logsDir", "Successfully updated Logs Dir");
|
||||
|
||||
interface Props {
|
||||
settingsKey: "imageCacheDir" | "logsDir",
|
||||
successMessage: string,
|
||||
}
|
||||
|
||||
export function SelectFolderInput({ settingsKey, successMessage }: Props) {
|
||||
const path = settings.store[settingsKey];
|
||||
|
||||
function getDirName(path: string) {
|
||||
const parts = path.split("\\").length > 1 ? path.split("\\") : path.split("/");
|
||||
|
||||
return parts.slice(parts.length - 2, parts.length).join("\\");
|
||||
}
|
||||
|
||||
async function onFolderSelect() {
|
||||
try {
|
||||
const res = await Native.chooseDir(settingsKey);
|
||||
settings.store[settingsKey] = res;
|
||||
|
||||
return Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.SUCCESS,
|
||||
message: successMessage
|
||||
});
|
||||
} catch (err) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to update directory"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cl("-container")}>
|
||||
<div onClick={() => copyWithToast(path)} className={cl("-input")}>
|
||||
{path == null || path === DEFAULT_IMAGE_CACHE_DIR ? "Choose Folder" : getDirName(path)}
|
||||
</div>
|
||||
<Button
|
||||
className={cl("-button")}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={onFolderSelect}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { getApplicationAsset } from "plugins/customRPC";
|
||||
import { Message } from "discord-types/general";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
async function setRpc(disable?: boolean, details?: string) {
|
||||
|
||||
const activity = {
|
||||
"application_id": "0",
|
||||
"name": "Today's Stats",
|
||||
"details": details ? details : "No info right now :(",
|
||||
"type": 0,
|
||||
"flags": 1,
|
||||
"assets": {
|
||||
"large_image": await getApplicationAsset(UserStore.getCurrentUser().getAvatarURL())
|
||||
}
|
||||
}
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: !disable ? activity : null,
|
||||
socketId: "CustomRPC",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getCurrentDate(): string {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function getCurrentTime(): string {
|
||||
const today = new Date();
|
||||
let hour = today.getHours();
|
||||
const minute = today.getMinutes();
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
hour = hour % 12 || 12
|
||||
|
||||
const formattedHour = String(hour).padStart(2, '0');
|
||||
const formattedMinute = String(minute).padStart(2, '0');
|
||||
|
||||
return `${formattedHour}:${formattedMinute} ${ampm}`;
|
||||
}
|
||||
|
||||
|
||||
interface IMessageCreate {
|
||||
type: "MESSAGE_CREATE";
|
||||
optimistic: boolean;
|
||||
isPushNotification: boolean;
|
||||
channelId: string;
|
||||
message: Message;
|
||||
}
|
||||
|
||||
async function updateData()
|
||||
{
|
||||
let messagesSent;
|
||||
if(await DataStore.get("RPCStatsDate") == getCurrentDate())
|
||||
{
|
||||
messagesSent = await DataStore.get("RPCStatsMessages");
|
||||
}
|
||||
else
|
||||
{
|
||||
await DataStore.set("RPCStatsDate", getCurrentDate());
|
||||
await DataStore.set("RPCStatsMessages", 0);
|
||||
messagesSent = 0;
|
||||
}
|
||||
setRpc(false, `Messages sent: ${messagesSent}`);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "RPCStats",
|
||||
description: "Displays stats about your current session in your rpc",
|
||||
authors: [Devs.Samwich],
|
||||
async start()
|
||||
{
|
||||
updateData();
|
||||
},
|
||||
stop()
|
||||
{
|
||||
setRpc(true);
|
||||
},
|
||||
flux:
|
||||
{
|
||||
async MESSAGE_CREATE({ optimistic, type, message }: IMessageCreate) {
|
||||
if (optimistic || type !== "MESSAGE_CREATE") return;
|
||||
if (message.state === "SENDING") return;
|
||||
if (message.author.id != UserStore.getCurrentUser().id) return;
|
||||
await DataStore.set("RPCStatsMessages", await DataStore.get("RPCStatsMessages") + 1);
|
||||
updateData();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let lastCheckedDate: string = getCurrentDate();
|
||||
|
||||
function checkForNewDay(): void {
|
||||
const currentDate = getCurrentDate();
|
||||
if (currentDate !== lastCheckedDate) {
|
||||
updateData();
|
||||
lastCheckedDate = currentDate;
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(checkForNewDay, 1000 * 60);
|
206
src/plusplugins/autoMute/index.tsx
Normal file
206
src/plusplugins/autoMute/index.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { Notices } from "@api/index";
|
||||
import { popNotice } from "@api/Notices";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { clearableDebounce, debounce } from "@shared/debounce";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { Menu, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
const { toggleSelfMute } = findByPropsLazy("toggleSelfMute");
|
||||
|
||||
// We cannot destructure isSelfMute as it depends on isEnabled
|
||||
const MediaEngineStore = findStoreLazy("MediaEngineStore");
|
||||
|
||||
|
||||
interface SpeakingState {
|
||||
speakingFlags: number,
|
||||
type: string,
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const enum SpeakingFlagsMask {
|
||||
VOICE = 1 << 0,
|
||||
SOUNDSHARE = 1 << 1,
|
||||
PRIORITY = 1 << 2
|
||||
}
|
||||
|
||||
interface VoiceState {
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
oldChannelId?: string;
|
||||
user: User;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
let [setAutoMute, cancelAutoMute] = [() => { }, () => { }];
|
||||
|
||||
function updateTimeout(seconds: number) {
|
||||
cancelAutoMute();
|
||||
[setAutoMute, cancelAutoMute] = clearableDebounce(autoMute, seconds * 1000);
|
||||
updateAutoMute();
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
isEnabled: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether the plugin will automatically mute you or not",
|
||||
default: true,
|
||||
onChange() {
|
||||
updateAutoMute();
|
||||
}
|
||||
},
|
||||
timeout: {
|
||||
description: "Inactivity timeout (seconds)",
|
||||
type: OptionType.SLIDER,
|
||||
markers: [15, ...makeRange(60, 900, 60)],
|
||||
default: 300,
|
||||
stickToMarkers: false,
|
||||
onChange(value) {
|
||||
updateTimeout(value);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const AudioDeviceContextMenuPatch: NavContextMenuPatchCallback = (children, props: { renderInputVolume?: boolean; }) => {
|
||||
const { isEnabled, timeout } = settings.use(["isEnabled", "timeout"]);
|
||||
|
||||
if ("renderInputVolume" in props) {
|
||||
children.splice(children.length - 1, 0,
|
||||
<Menu.MenuGroup
|
||||
label="Auto Mute"
|
||||
>
|
||||
<Menu.MenuCheckboxItem
|
||||
checked={isEnabled}
|
||||
id="vc-auto-mute-toggle"
|
||||
label="Enable Auto Mute"
|
||||
action={() => {
|
||||
settings.store.isEnabled = !isEnabled;
|
||||
updateAutoMute();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuControlItem
|
||||
id="vc-auto-mute-timeout"
|
||||
label="Inactivity Timeout"
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSliderControl
|
||||
{...props}
|
||||
ref={ref}
|
||||
minValue={15}
|
||||
maxValue={900}
|
||||
value={timeout}
|
||||
onChange={debounce((rawValue: number) => {
|
||||
const value = Math.round(rawValue);
|
||||
settings.store.timeout = value;
|
||||
updateTimeout(value);
|
||||
}, 10)}
|
||||
renderValue={(value: number) => {
|
||||
const minutes = Math.floor(value / 60);
|
||||
const seconds = Math.round(value % 60);
|
||||
return [
|
||||
minutes,
|
||||
`${seconds < 10 ? "0" + seconds : seconds}${minutes ? "" : "s"}`
|
||||
].filter(Boolean).join(":");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Menu.MenuGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let isSpeaking = false;
|
||||
|
||||
function updateAutoMute() {
|
||||
if (!settings.store.isEnabled) return cancelAutoMute();
|
||||
if (!SelectedChannelStore.getVoiceChannelId()) return cancelAutoMute();
|
||||
isSpeaking ? cancelAutoMute() : setAutoMute();
|
||||
}
|
||||
|
||||
function autoMute() {
|
||||
if (!MediaEngineStore.isSelfMute()) {
|
||||
toggleSelfMute();
|
||||
Notices.showNotice("You have been silent for a while, so your mic has been automatically muted.", "Unmute", () => {
|
||||
popNotice();
|
||||
if (MediaEngineStore.isSelfMute()) toggleSelfMute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Blatantly stolen from VcNarrator plugin
|
||||
|
||||
// For every user, channelId and oldChannelId will differ when moving channel.
|
||||
// Only for the local user, channelId and oldChannelId will be the same when moving channel,
|
||||
// for some ungodly reason
|
||||
let clientOldChannelId: string | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "AutoMute",
|
||||
description: "Automatically mute yourself in voice channels if you're not speaking for too long.",
|
||||
authors: [Devs.Sqaaakoi],
|
||||
settings,
|
||||
flux: {
|
||||
SPEAKING(s: SpeakingState) {
|
||||
if (s.userId !== UserStore.getCurrentUser().id) return;
|
||||
isSpeaking = (s.speakingFlags & SpeakingFlagsMask.VOICE) === 1;
|
||||
isSpeaking ? cancelAutoMute() : setAutoMute();
|
||||
},
|
||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||
// Blatantly stolen from my other unfinished plugin
|
||||
if (!voiceStates) return;
|
||||
voiceStates.forEach(state => {
|
||||
if (state.userId !== UserStore.getCurrentUser().id) return;
|
||||
const { channelId } = state;
|
||||
let { oldChannelId } = state;
|
||||
if (channelId !== clientOldChannelId) {
|
||||
oldChannelId = clientOldChannelId;
|
||||
clientOldChannelId = channelId;
|
||||
}
|
||||
|
||||
if (!oldChannelId && channelId) {
|
||||
console.log("join");
|
||||
updateAutoMute();
|
||||
}
|
||||
if (oldChannelId && !channelId) {
|
||||
console.log("dc");
|
||||
cancelAutoMute();
|
||||
isSpeaking = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
AUDIO_TOGGLE_SELF_MUTE() {
|
||||
updateAutoMute();
|
||||
},
|
||||
AUDIO_TOGGLE_SELF_DEAF() {
|
||||
updateAutoMute();
|
||||
},
|
||||
AUDIO_TOGGLE_SET_MUTE() {
|
||||
updateAutoMute();
|
||||
},
|
||||
AUDIO_TOGGLE_SET_DEAF() {
|
||||
updateAutoMute();
|
||||
},
|
||||
},
|
||||
contextMenus: {
|
||||
"audio-device-context": AudioDeviceContextMenuPatch
|
||||
},
|
||||
start() {
|
||||
updateTimeout(settings.store.timeout);
|
||||
},
|
||||
stop() {
|
||||
cancelAutoMute();
|
||||
}
|
||||
});
|
||||
|
39
src/plusplugins/contextMenuSelectFix/index.tsx
Normal file
39
src/plusplugins/contextMenuSelectFix/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
||||
const classes = findByPropsLazy("menu", "submenu");
|
||||
|
||||
export default definePlugin({
|
||||
name: "ContextMenuSelectFix",
|
||||
description: "Releasing right click when hovering over a context menu entry selects it, bringing the behaviour in line with other apps",
|
||||
authors: [Devs.Sqaaakoi],
|
||||
pointerUpEventHandler(e: PointerEvent) {
|
||||
let target = e.target as HTMLElement | null;
|
||||
if (!target || e.button !== 2) return;
|
||||
let parent = target.parentElement;
|
||||
try {
|
||||
while (parent && !parent?.classList.contains(classes.menu)) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
} catch (err) { return console.error(err); }
|
||||
if (parent) {
|
||||
while (target && !target?.click) {
|
||||
target = target?.parentElement;
|
||||
}
|
||||
target?.click();
|
||||
}
|
||||
},
|
||||
start() {
|
||||
document.body.addEventListener("pointerup", this.pointerUpEventHandler);
|
||||
},
|
||||
stop() {
|
||||
document.body.addEventListener("pointerup", this.pointerUpEventHandler);
|
||||
}
|
||||
});
|
23
src/plusplugins/noDraftLengthLimit/index.ts
Normal file
23
src/plusplugins/noDraftLengthLimit/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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: "NoDraftLengthLimit",
|
||||
description: "Removes the 4500 character saved draft message truncation",
|
||||
authors: [Devs.Sqaaakoi],
|
||||
patches: [
|
||||
{
|
||||
find: "MAX_MESSAGE_LENGTH_PREMIUM+500",
|
||||
replacement: {
|
||||
match: /=[^=]{0,20}MAX_MESSAGE_LENGTH_PREMIUM\+500/,
|
||||
replace: "=Infinity"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
.vc-voice-channel-log-entry {
|
||||
list-style-type: none;
|
||||
height: 48px;
|
||||
|
||||
/* margin: 4px; */
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem 24px 40px max-content;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-voice-channel-log-entry-timestamp {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
font-size: .75rem;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: var(--text-muted-on-default);
|
||||
}
|
||||
|
||||
.vc-voice-channel-log-entry-avatar {
|
||||
left: calc(2.25rem + 32px);
|
||||
padding: 4px 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vc-voice-channel-log-entry-icon {
|
||||
margin: auto 3px;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./VoiceChannelLogEntryComponent.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { classes } from "@utils/misc";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { filters, find, findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
||||
import { GuildStore, React, Timestamp, UserStore } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { Util } from "Vencord";
|
||||
|
||||
import { VoiceChannelLogEntry } from "./logs";
|
||||
import Icon from "./VoiceChannelLogEntryIcons";
|
||||
|
||||
// this is terrible, blame mantika and vee for this, as I stole the code from them and adapted it (see ../reviewDB/components/ReviewComponent.tsx line 40 and 46 )
|
||||
|
||||
export const VoiceChannelLogEntryComponent = LazyComponent(() => {
|
||||
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
const FriendRow = findExportedComponentLazy("FriendRow");
|
||||
|
||||
// const NameWithRole = findByCode("name", "color", "roleName", "dotAlignment");
|
||||
|
||||
const { avatar, clickable } = find(filters.byProps("avatar", "zalgo", "clickable"));
|
||||
|
||||
const cl = classNameFactory("vc-voice-channel-log-entry-");
|
||||
|
||||
|
||||
return function VoiceChannelLogEntryComponent({ logEntry, channel }: { logEntry: VoiceChannelLogEntry; channel: Channel; }) {
|
||||
const guild = channel.getGuildId() ? GuildStore.getGuild(channel.getGuildId()) : null;
|
||||
const user = UserStore.getUser(logEntry.userId);
|
||||
return <li className="vc-voice-channel-log-entry">
|
||||
<Timestamp className={cl("timestamp")} timestamp={logEntry.timestamp} compact isVisibleOnlyOnHover isInline={false} cozyAlt></Timestamp>
|
||||
<Icon logEntry={logEntry} channel={channel} className={cl("icon")} />
|
||||
<img
|
||||
className={classes(avatar, clickable, cl("avatar"))}
|
||||
onClick={() => Util.openUserProfile(logEntry.userId)}
|
||||
src={user.getAvatarURL(channel.getGuildId())}
|
||||
// style={{ left: "0px", zIndex: 0 }}
|
||||
/>
|
||||
<div className={cl("content")}>
|
||||
{/* <NameWithRole ></NameWithRole> */}
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { classes } from "@utils/misc";
|
||||
import { React } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
import { VoiceChannelLogEntry } from "./logs";
|
||||
|
||||
const cl = classNameFactory("vc-voice-channel-log-entry-icon-");
|
||||
|
||||
export default function Icon({ logEntry, channel, className }: { logEntry: VoiceChannelLogEntry; channel: Channel; className: string; }) {
|
||||
// Taken from /assets/7378a83d74ce97d83380.svg
|
||||
const Join = <svg xmlns="http://www.w3.org/2000/svg" height="18" width="18"><g fill="none" fill-rule="evenodd"><path d="m18 0h-18v18h18z" /><path d="m0 8h14.2l-3.6-3.6 1.4-1.4 6 6-6 6-1.4-1.4 3.6-3.6h-14.2" fill="#3ba55c" /></g></svg>;
|
||||
// Taken from /assets/192510ade1abc3149b46.svg
|
||||
const Leave = <svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" ><g fill="none" fill-rule="evenodd"><path d="m18 0h-18v18h18z" /><path d="m3.8 8 3.6-3.6-1.4-1.4-6 6 6 6 1.4-1.4-3.6-3.6h14.2v-2" fill="#ed4245" /></g></svg>;
|
||||
// For other contributors, please DO make specific designs for these instead of how I just copied the join/leave icons and making them orange
|
||||
const MovedTo = <svg xmlns="http://www.w3.org/2000/svg" height="18" width="18"><g fill="none" fill-rule="evenodd"><path d="m18 0h-18v18h18z" /><path d="m3.8 8 3.6-3.6-1.4-1.4-6 6 6 6 1.4-1.4-3.6-3.6h14.2v-2" fill="#faa61a" /></g></svg>;
|
||||
const MovedFrom = <svg xmlns="http://www.w3.org/2000/svg" height="18" width="18"><g fill="none" fill-rule="evenodd"><path d="m18 0h-18v18h18z" /><path d="m0 8h14.2l-3.6-3.6 1.4-1.4 6 6-6 6-1.4-1.4 3.6-3.6h-14.2" fill="#faa61a" /></g></svg>;
|
||||
|
||||
if (logEntry.newChannel && !logEntry.oldChannel) return React.cloneElement(Join, { className: classes(className, cl("join")) });
|
||||
if (!logEntry.newChannel && logEntry.oldChannel) return React.cloneElement(Leave, { className: classes(className, cl("leave")) });
|
||||
if (logEntry.newChannel === channel.id && logEntry.oldChannel) return React.cloneElement(MovedFrom, { className: classes(className, cl("moved-from")) });
|
||||
if (logEntry.newChannel && logEntry.oldChannel === channel.id) return React.cloneElement(MovedTo, { className: classes(className, cl("moved-to")) });
|
||||
// we should never get here, this is just here to shut up the type checker
|
||||
return <svg></svg>;
|
||||
}
|
75
src/plusplugins/voiceChannelLog/VoiceChannelLogModal.tsx
Normal file
75
src/plusplugins/voiceChannelLog/VoiceChannelLogModal.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { filters, find, findByProps, findStoreLazy } from "@webpack";
|
||||
import { React, ScrollerThin, Text } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
import { getVcLogs, vcLogSubscribe } from "./logs";
|
||||
import { VoiceChannelLogEntryComponent } from "./VoiceChannelLogEntryComponent";
|
||||
|
||||
const AccessibilityStore = findStoreLazy("AccessibilityStore");
|
||||
const cl = classNameFactory("vc-voice-channel-log-");
|
||||
|
||||
export function openVoiceChannelLog(channel: Channel) {
|
||||
return openModal(props => (
|
||||
<VoiceChannelLogModal
|
||||
props={props}
|
||||
channel={channel}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export const VoiceChannelLogModal = LazyComponent(() => {
|
||||
const { avatar, clickable } = find(filters.byProps("avatar", "zalgo", "clickable"));
|
||||
const { divider, hasContent } = findByProps("divider", "hasContent", "ephemeral");
|
||||
const { divider: divider_, hasContent: hasContent_, content } = findByProps("divider", "hasContent", "isUnread", "content");
|
||||
|
||||
return function VoiceChannelLogModal({ channel, props }: { channel: Channel; props: ModalProps; }) {
|
||||
React.useSyncExternalStore(vcLogSubscribe, () => getVcLogs(channel.id));
|
||||
const vcLogs = getVcLogs(channel.id);
|
||||
const logElements: (React.ReactNode)[] = [];
|
||||
|
||||
if (vcLogs.length > 0) {
|
||||
for (let i = 0; i < vcLogs.length; i++) {
|
||||
const logEntry = vcLogs[i];
|
||||
if (i === 0 || logEntry.timestamp.toDateString() !== vcLogs[i - 1].timestamp.toDateString()) {
|
||||
logElements.push(<div className={classes(divider, hasContent, divider_, hasContent_, cl("date-separator"))} role="separator" aria-label={logEntry.timestamp.toDateString()}>
|
||||
<span className={content}>
|
||||
{logEntry.timestamp.toDateString()}
|
||||
</span>
|
||||
</div>);
|
||||
} else {
|
||||
logElements.push(<VoiceChannelLogEntryComponent logEntry={logEntry} channel={channel} />);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logElements.push(<div className={cl("empty")}>No logs to display.</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot
|
||||
{...props}
|
||||
size={ModalSize.LARGE}
|
||||
>
|
||||
<ModalHeader>
|
||||
<Text className={cl("header")} variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{channel.name} logs</Text>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<ScrollerThin fade className={classes(cl("scroller"), `group-spacing-${AccessibilityStore.messageGroupSpacing}`)}>
|
||||
{logElements}
|
||||
</ScrollerThin>
|
||||
</ModalContent>
|
||||
</ModalRoot >
|
||||
);
|
||||
};
|
||||
});
|
170
src/plusplugins/voiceChannelLog/index.tsx
Normal file
170
src/plusplugins/voiceChannelLog/index.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, Menu, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { Channel, Message, User } from "discord-types/general";
|
||||
|
||||
import { addLogEntry } from "./logs";
|
||||
import { openVoiceChannelLog } from "./VoiceChannelLogModal";
|
||||
|
||||
const MessageCreator = findByPropsLazy("createBotMessage");
|
||||
const SortedVoiceStateStore = findByPropsLazy("getVoiceStatesForChannel");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
mode: {
|
||||
type: OptionType.SELECT,
|
||||
description: "How to show the voice channel log",
|
||||
options: [
|
||||
{ label: "Log menu", value: 1, default: true },
|
||||
{ label: "Log to associated chat directly", value: 2 },
|
||||
{ label: "Log to chat and log menu", value: 3 },
|
||||
]
|
||||
},
|
||||
voiceChannelChatSelf: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Log your own voice channel events in the voice channels",
|
||||
default: true
|
||||
},
|
||||
voiceChannelChatSilent: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Join/leave/move messages in voice channel chats will be silent",
|
||||
default: true
|
||||
},
|
||||
voiceChannelChatSilentSelf: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Join/leave/move messages in voice channel chats will be silent if you are in the voice channel",
|
||||
default: false
|
||||
},
|
||||
ignoreBlockedUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Do not log blocked users",
|
||||
default: false
|
||||
},
|
||||
});
|
||||
|
||||
interface VoiceState {
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
oldChannelId?: string;
|
||||
user: User;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
function getMessageFlags(selfInChannel: boolean) {
|
||||
let flags = 1 << 6;
|
||||
if (selfInChannel ? settings.store.voiceChannelChatSilentSelf : settings.store.voiceChannelChatSilent) flags += 1 << 12;
|
||||
return flags;
|
||||
}
|
||||
|
||||
function sendVoiceStatusMessage(channelId: string, content: string, userId: string, selfInChannel: boolean): Message | null {
|
||||
if (!channelId) return null;
|
||||
const message: Message = MessageCreator.createBotMessage({ channelId, content, embeds: [] });
|
||||
message.flags = getMessageFlags(selfInChannel);
|
||||
message.author = UserStore.getUser(userId);
|
||||
// If we try to send a message into an unloaded channel, the client-sided messages get overwritten when the channel gets loaded
|
||||
// This might be messy but It Works:tm:
|
||||
const messagesLoaded: Promise<any> = MessageStore.hasPresent(channelId) ? new Promise<void>(r => r()) : MessageActions.fetchMessages({ channelId });
|
||||
messagesLoaded.then(() => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_CREATE",
|
||||
channelId,
|
||||
message,
|
||||
optimistic: true,
|
||||
sendMessageOptions: {},
|
||||
isPushNotification: false
|
||||
});
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
interface ChannelContextProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: ChannelContextProps) => () => {
|
||||
if (!channel) return;
|
||||
|
||||
const group = findGroupChildrenByChildId("hide-voice-names", children);
|
||||
const injectIndex = group?.findIndex(i => i?.props?.id === "hide-voice-names");
|
||||
if (!injectIndex || !group) return;
|
||||
|
||||
group.splice(injectIndex, 0, (
|
||||
<Menu.MenuItem
|
||||
id="vc-view-voice-channel-logs"
|
||||
label="View Channel Logs"
|
||||
action={() => { openVoiceChannelLog(channel); }}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
// Blatantly stolen from VcNarrator plugin
|
||||
|
||||
// For every user, channelId and oldChannelId will differ when moving channel.
|
||||
// Only for the local user, channelId and oldChannelId will be the same when moving channel,
|
||||
// for some ungodly reason
|
||||
let clientOldChannelId: string | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "VoiceChannelLog",
|
||||
description: "Logs who joins and leaves voice channels",
|
||||
authors: [Devs.Sqaaakoi],
|
||||
settings,
|
||||
flux: {
|
||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||
if (!voiceStates) return;
|
||||
const clientUserId = UserStore.getCurrentUser().id;
|
||||
voiceStates.forEach(state => {
|
||||
// mmmm hacky workaround
|
||||
const { userId, channelId } = state;
|
||||
let { oldChannelId } = state;
|
||||
if (userId === clientUserId && channelId !== clientOldChannelId) {
|
||||
oldChannelId = clientOldChannelId;
|
||||
clientOldChannelId = channelId;
|
||||
}
|
||||
if (settings.store.ignoreBlockedUsers && RelationshipStore.isBlocked(userId)) return;
|
||||
// Ignore events from same channel
|
||||
if (oldChannelId === channelId) return;
|
||||
|
||||
const logEntry = {
|
||||
userId,
|
||||
oldChannel: oldChannelId || null,
|
||||
newChannel: channelId || null,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
addLogEntry(logEntry, oldChannelId);
|
||||
addLogEntry(logEntry, channelId);
|
||||
|
||||
if (!settings.store.voiceChannelChatSelf && userId === clientUserId) return;
|
||||
// Join / Leave
|
||||
if ((!oldChannelId && channelId) || (oldChannelId && !channelId)) {
|
||||
// empty string is to make type checker shut up
|
||||
const targetChannelId = oldChannelId || channelId || "";
|
||||
const selfInChannel = SelectedChannelStore.getVoiceChannelId() === targetChannelId;
|
||||
sendVoiceStatusMessage(targetChannelId, `${(channelId ? "Joined" : "Left")} <#${targetChannelId}>`, userId, selfInChannel);
|
||||
}
|
||||
// Move between channels
|
||||
if (oldChannelId && channelId) {
|
||||
sendVoiceStatusMessage(oldChannelId, `Moved to <#${channelId}>`, userId, SelectedChannelStore.getVoiceChannelId() === oldChannelId);
|
||||
sendVoiceStatusMessage(channelId, `Moved from <#${oldChannelId}>`, userId, SelectedChannelStore.getVoiceChannelId() === channelId);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
},
|
||||
start() {
|
||||
addContextMenuPatch("channel-context", UserContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("channel-context", UserContextMenuPatch);
|
||||
},
|
||||
});
|
40
src/plusplugins/voiceChannelLog/logs.ts
Normal file
40
src/plusplugins/voiceChannelLog/logs.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export interface VoiceChannelLogEntry {
|
||||
userId: string;
|
||||
oldChannel: string | null;
|
||||
newChannel: string | null;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const vcLogs = new Map<string, VoiceChannelLogEntry[]>();
|
||||
let vcLogSubscriptions: (() => void)[] = [];
|
||||
|
||||
export function getVcLogs(channel?: string): VoiceChannelLogEntry[] {
|
||||
if (!channel) return [];
|
||||
if (!vcLogs.has(channel)) vcLogs.set(channel, []);
|
||||
return vcLogs.get(channel) || [];
|
||||
}
|
||||
|
||||
export function addLogEntry(logEntry: VoiceChannelLogEntry, channel?: string) {
|
||||
if (!channel) return;
|
||||
vcLogs.set(channel, [...getVcLogs(channel), logEntry]);
|
||||
vcLogSubscriptions.forEach(u => u());
|
||||
}
|
||||
|
||||
export function clearLogs(channel?: string) {
|
||||
if (!channel) return;
|
||||
vcLogs.set(channel, []);
|
||||
vcLogSubscriptions.forEach(u => u());
|
||||
}
|
||||
|
||||
export function vcLogSubscribe(listener: () => void) {
|
||||
vcLogSubscriptions = [...vcLogSubscriptions, listener];
|
||||
return () => {
|
||||
vcLogSubscriptions = vcLogSubscriptions.filter(l => l !== listener);
|
||||
};
|
||||
}
|
150
src/plusplugins/voiceJoinMessages/index.ts
Normal file
150
src/plusplugins/voiceJoinMessages/index.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { humanFriendlyJoin } from "@utils/text";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
||||
const MessageCreator = findByPropsLazy("createBotMessage");
|
||||
const SortedVoiceStateStore = findByPropsLazy("getVoiceStatesForChannel");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
friendDirectMessages: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Recieve notifications in your friends' DMs when they join a voice channel",
|
||||
default: true
|
||||
},
|
||||
friendDirectMessagesShowMembers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show a list of other members in the voice channel when recieving a DM notification of your friend joining a voice channel",
|
||||
default: true
|
||||
},
|
||||
friendDirectMessagesShowMemberCount: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show the count of other members in the voice channel when recieving a DM notification of your friend joining a voice channel",
|
||||
default: false
|
||||
},
|
||||
friendDirectMessagesSelf: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Recieve notifications in your friends' DMs even if you are in the same voice channel as them",
|
||||
default: false
|
||||
},
|
||||
friendDirectMessagesSilent: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Join messages in your friends DMs will be silent",
|
||||
default: false
|
||||
},
|
||||
allowedFriends: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma or space separated list of friends' user IDs you want to recieve join messages from",
|
||||
default: ""
|
||||
},
|
||||
ignoreBlockedUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Do not send messages about blocked users joining/leaving/moving voice channels",
|
||||
default: true
|
||||
},
|
||||
});
|
||||
|
||||
interface VoiceState {
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
oldChannelId?: string;
|
||||
user: User;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
function getMessageFlags() {
|
||||
let flags = 1 << 6;
|
||||
if (settings.store.friendDirectMessagesSilent) flags += 1 << 12;
|
||||
return flags;
|
||||
}
|
||||
|
||||
function sendVoiceStatusMessage(channelId: string, content: string, userId: string): Message | null {
|
||||
if (!channelId) return null;
|
||||
const message: Message = MessageCreator.createBotMessage({ channelId, content, embeds: [] });
|
||||
message.flags = getMessageFlags();
|
||||
message.author = UserStore.getUser(userId);
|
||||
// If we try to send a message into an unloaded channel, the client-sided messages get overwritten when the channel gets loaded
|
||||
// This might be messy but It Works:tm:
|
||||
const messagesLoaded: Promise<any> = MessageStore.hasPresent(channelId) ? new Promise<void>(r => r()) : MessageActions.fetchMessages({ channelId });
|
||||
messagesLoaded.then(() => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_CREATE",
|
||||
channelId,
|
||||
message,
|
||||
optimistic: true,
|
||||
sendMessageOptions: {},
|
||||
isPushNotification: false
|
||||
});
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
function isFriendAllowlisted(friendId: string) {
|
||||
if (!RelationshipStore.isFriend(friendId)) return false;
|
||||
const list = settings.store.allowedFriends.split(",").join(" ").split(" ").filter(i => i.length > 0);
|
||||
if (list.join(" ").length < 1) return true;
|
||||
return list.includes(friendId);
|
||||
}
|
||||
|
||||
// Blatantly stolen from VcNarrator plugin
|
||||
|
||||
// For every user, channelId and oldChannelId will differ when moving channel.
|
||||
// Only for the local user, channelId and oldChannelId will be the same when moving channel,
|
||||
// for some ungodly reason
|
||||
let clientOldChannelId: string | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "VoiceJoinMessages",
|
||||
description: "Recieve client-side ephemeral messages when your friends join voice channels",
|
||||
authors: [Devs.Sqaaakoi],
|
||||
settings,
|
||||
flux: {
|
||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||
if (!voiceStates) return;
|
||||
const clientUserId = UserStore.getCurrentUser().id;
|
||||
voiceStates.forEach(state => {
|
||||
// mmmm hacky workaround
|
||||
const { userId, channelId } = state;
|
||||
let { oldChannelId } = state;
|
||||
if (userId === clientUserId && channelId !== clientOldChannelId) {
|
||||
oldChannelId = clientOldChannelId;
|
||||
clientOldChannelId = channelId;
|
||||
}
|
||||
if (settings.store.ignoreBlockedUsers && RelationshipStore.isBlocked(userId)) return;
|
||||
// Ignore events from same channel
|
||||
if (oldChannelId === channelId) return;
|
||||
|
||||
// Friend joined a voice channel
|
||||
if (settings.store.friendDirectMessages && (!oldChannelId && channelId) && userId !== clientUserId && isFriendAllowlisted(userId)) {
|
||||
const selfInChannel = SelectedChannelStore.getVoiceChannelId() === channelId;
|
||||
let memberListContent = "";
|
||||
if (settings.store.friendDirectMessagesShowMembers || settings.store.friendDirectMessagesShowMemberCount) {
|
||||
const sortedVoiceStates: [{ user: { id: string; }; }] = SortedVoiceStateStore.getVoiceStatesForChannel(ChannelStore.getChannel(channelId));
|
||||
const otherMembers = sortedVoiceStates.filter(s => s.user.id !== userId);
|
||||
const otherMembersCount = otherMembers.length;
|
||||
if (otherMembersCount <= 0) {
|
||||
memberListContent += ", nobody else is in the voice channel";
|
||||
} else if (settings.store.friendDirectMessagesShowMemberCount) {
|
||||
memberListContent += ` with ${otherMembersCount} other member${otherMembersCount === 1 ? "s" : ""}`;
|
||||
}
|
||||
if (settings.store.friendDirectMessagesShowMembers && otherMembersCount > 0) {
|
||||
memberListContent += settings.store.friendDirectMessagesShowMemberCount ? ", " : " with ";
|
||||
memberListContent += humanFriendlyJoin(otherMembers.map(s => `<@${s.user.id}>`));
|
||||
}
|
||||
}
|
||||
const dmChannelId = ChannelStore.getDMFromUserId(userId);
|
||||
if (dmChannelId && (selfInChannel ? settings.store.friendDirectMessagesSelf : true)) sendVoiceStatusMessage(dmChannelId, `Joined voice channel <#${channelId}>${memberListContent}`, userId);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue