From 1f3eb2e61a3b17090cadc0a5d26d3312e63acb6d Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:13:26 -0400 Subject: [PATCH] Add 2 Sqaaakoi Plugins (hehe) --- README.md | 2 + .../components/MoreUsersModal.tsx | 3 +- .../VoiceChannelLogEntryComponent.css | 41 +++++ .../VoiceChannelLogEntryComponent.tsx | 32 ++++ .../components/VoiceChannelLogEntryIcons.tsx | 29 ++++ .../components/VoiceChannelLogModal.tsx | 67 ++++++++ src/equicordplugins/voiceChannelLog/index.tsx | 157 ++++++++++++++++++ src/equicordplugins/voiceChannelLog/logs.ts | 40 +++++ .../voiceJoinMessages/index.ts | 153 +++++++++++++++++ 9 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.css create mode 100644 src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.tsx create mode 100644 src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryIcons.tsx create mode 100644 src/equicordplugins/voiceChannelLog/components/VoiceChannelLogModal.tsx create mode 100644 src/equicordplugins/voiceChannelLog/index.tsx create mode 100644 src/equicordplugins/voiceChannelLog/logs.ts create mode 100644 src/equicordplugins/voiceJoinMessages/index.ts diff --git a/README.md b/README.md index cbe1b9bb..de054ebd 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,9 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - VencordRPC by AutumnVN - VideoSpeed by Samwich - ViewRaw2 by Kyuuhachi +- VoiceChannelLog by Sqaaakoi (maintained by thororen) - VoiceChatUtilities by D3SOX +- VoiceJoinMessages by Sqaaakoi (maintained by thororen) - WebpackTarball by Kyuuhachi - WhosWatching by fres - WigglyText by nexpid diff --git a/src/equicordplugins/soundBoardLogger/components/MoreUsersModal.tsx b/src/equicordplugins/soundBoardLogger/components/MoreUsersModal.tsx index 01181892..38c8b15a 100644 --- a/src/equicordplugins/soundBoardLogger/components/MoreUsersModal.tsx +++ b/src/equicordplugins/soundBoardLogger/components/MoreUsersModal.tsx @@ -9,7 +9,7 @@ import { Flex } from "@components/Flex"; import { closeModal, ModalContent, ModalRoot, openModal } from "@utils/modal"; import { Clickable, Forms } from "@webpack/common"; -import { cl, getEmojiUrl,SoundLogEntry, User } from "../utils"; +import { cl, getEmojiUrl, SoundLogEntry, User } from "../utils"; export function openMoreUsersModal(item: SoundLogEntry, users: User[], onClickUser: Function) { const key = openModal(props => ( @@ -21,7 +21,6 @@ export function openMoreUsersModal(item: SoundLogEntry, users: User[], onClickUs )); } - export default function MoreUsersModal({ item, users, onClickUser, closeModal }: { item: SoundLogEntry, users: User[], onClickUser: Function, closeModal: Function; }) { return ( diff --git a/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.css b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.css new file mode 100644 index 00000000..2dce0e54 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.css @@ -0,0 +1,41 @@ +.vc-voice-channel-log { + list-style-type: none; + height: 50px; + display: grid; + grid-template-columns: 2.25rem 24px 40px max-content; + gap: 4px; + width: 4px; + background-color: var(--background-modifier-active); +} + +.vc-voice-channel-log-date-separator, +.vc-voice-channel-log-timestamp { + margin-left: 4px; + height: 48px; + line-height: 48px; + font-size: .75rem; + text-align: center; + display: inline-block; + vertical-align: middle; + color: var(--text-normal, white); +} + +.vc-voice-channel-log-avatar { + height: 40px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-weight: 600; + overflow: hidden; + text-align: center; + margin-top: 10px; + padding-left: 10px; + padding-right: 10px +} + +.vc-voice-channel-log-icon { + margin: auto 3px; + margin-left: 15px; +} diff --git a/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.tsx b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.tsx new file mode 100644 index 00000000..79b1990f --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryComponent.tsx @@ -0,0 +1,32 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./VoiceChannelLogEntryComponent.css"; + +import { classes } from "@utils/misc"; +import { React, Timestamp, UserStore } from "@webpack/common"; +import { Channel } from "discord-types/general"; +import { Util } from "Vencord"; + +import { cl } from ".."; +import { VoiceChannelLogEntry } from "../logs"; +import Icon from "./VoiceChannelLogEntryIcons"; + +export function VoiceChannelLogEntryComponent({ logEntry, channel }: { logEntry: VoiceChannelLogEntry; channel: Channel; }) { + const user = UserStore.getUser(logEntry.userId); + return
  • + + + Util.openUserProfile(logEntry.userId)} + src={user.getAvatarURL(channel.getGuildId())} + /> +
    + { } +
    +
  • ; +} diff --git a/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryIcons.tsx b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryIcons.tsx new file mode 100644 index 00000000..78cac6ec --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogEntryIcons.tsx @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classes } from "@utils/misc"; +import { React } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +import { cl } from ".."; +import { VoiceChannelLogEntry } from "../logs"; + +export default function Icon({ logEntry, channel, className }: { logEntry: VoiceChannelLogEntry; channel: Channel; className: string; }) { + // Taken from /assets/7378a83d74ce97d83380.svg + const Join = ; + // Taken from /assets/192510ade1abc3149b46.svg + const Leave = ; + // 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 = ; + const MovedFrom = ; + + 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 ; +} diff --git a/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogModal.tsx b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogModal.tsx new file mode 100644 index 00000000..3b523b55 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/components/VoiceChannelLogModal.tsx @@ -0,0 +1,67 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classes } from "@utils/misc"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { findStoreLazy } from "@webpack"; +import { React, ScrollerThin, Text } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +import { cl } from ".."; +import { getVcLogs, vcLogSubscribe } from "../logs"; +import { VoiceChannelLogEntryComponent } from "./VoiceChannelLogEntryComponent"; + +const AccessibilityStore = findStoreLazy("AccessibilityStore"); + +export function openVoiceChannelLog(channel: Channel) { + return openModal(props => ( + + )); +} + +export 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(
    + + {logEntry.timestamp.toDateString()} + +
    ); + } else { + logElements.push(); + } + } + } else { + logElements.push(
    No logs to display.
    ); + } + + return ( + + + {channel.name} logs + + + + + + {logElements} + + + + ); +} diff --git a/src/equicordplugins/voiceChannelLog/index.tsx b/src/equicordplugins/voiceChannelLog/index.tsx new file mode 100644 index 00000000..e702a2f3 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { FluxDispatcher, Menu, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { Message, User } from "discord-types/general"; + +import { openVoiceChannelLog } from "./components/VoiceChannelLogModal"; +import { addLogEntry } from "./logs"; + +export const cl = classNameFactory("vc-voice-channel-log-"); +const createBotMessage = findByCodeLazy('username:"Clyde"'); + +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 = 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 = MessageStore.hasPresent(channelId) ? new Promise(r => r()) : MessageActions.fetchMessages({ channelId }); + messagesLoaded.then(() => { + FluxDispatcher.dispatch({ + type: "MESSAGE_CREATE", + channelId, + message, + optimistic: true, + sendMessageOptions: {}, + isPushNotification: false + }); + }); + return message; +} + +const patchChannelContextMenu: NavContextMenuPatchCallback = (children, { channel }) => { + const group = findGroupChildrenByChildId("mark-channel-read", children) ?? children; + group.push( + { 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, EquicordDevs.thororen], + contextMenus: { + "channel-context": patchChannelContextMenu + }, + settings, + flux: { + VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { + 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); + } + + }); + }, + } +}); diff --git a/src/equicordplugins/voiceChannelLog/logs.ts b/src/equicordplugins/voiceChannelLog/logs.ts new file mode 100644 index 00000000..9e165180 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog/logs.ts @@ -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(); +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); + }; +} diff --git a/src/equicordplugins/voiceJoinMessages/index.ts b/src/equicordplugins/voiceJoinMessages/index.ts new file mode 100644 index 00000000..ea2048d4 --- /dev/null +++ b/src/equicordplugins/voiceJoinMessages/index.ts @@ -0,0 +1,153 @@ +/* + * 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, EquicordDevs } from "@utils/constants"; +import { humanFriendlyJoin } from "@utils/text"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { Message, User } from "discord-types/general"; + +const createBotMessage = findByCodeLazy('username:"Clyde"'); +const SortedVoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); + +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 = 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 = MessageStore.hasPresent(channelId) ? new Promise(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, EquicordDevs.thororen], + settings, + flux: { + VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { + const clientUserId = UserStore.getCurrentUser().id; + for (const state of voiceStates) { + // 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 voiceState = SortedVoiceStateStore.getVoiceStatesForChannel(channelId); + const sortedVoiceStates: User[] = Object.values(voiceState as { [key: string]: VoiceState; }) + .filter((voiceState: VoiceState) => { voiceState.user && voiceState.user.id !== userId; }) + .map((voiceState: VoiceState) => voiceState.user); + console.log(sortedVoiceStates); + const otherMembers = sortedVoiceStates.filter(s => s.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.id}>`)); + } + } + const dmChannelId = ChannelStore.getDMFromUserId(userId); + if (dmChannelId && (selfInChannel ? settings.store.friendDirectMessagesSelf : true)) sendVoiceStatusMessage(dmChannelId, `Joined voice channel <#${channelId}>${memberListContent}`, userId); + } + } + }, + }, +});