diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 31cbd60ec..7de5df488 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -23,9 +23,18 @@ import { plugins } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { openPluginModal } from "@components/settings"; import { Devs, EquicordDevs } from "@utils/constants"; +import { useForceUpdater } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; import { Channel } from "@vencord/discord-types"; -import { ChannelStore, FluxDispatcher, Menu, React } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, Menu, MessageStore, React, SelectedChannelStore, useEffect, UserStore } from "@webpack/common"; + +const rerenderListeners = new Set<() => void>(); + +function triggerChatToggleRerender() { + for (const listener of rerenderListeners) { + listener(); + } +} const settings = definePluginSettings({ enabledGlobally: { @@ -88,6 +97,21 @@ const settings = definePluginSettings({ description: "If enabled, the plugin will hide your typing from others in any DMs/channels/guilds not listed in \"Disabled Locations\" below. If disabled, the plugin will show your typing to others for any DMs/channels/guilds not listed in \"Enabled Locations\" below.", default: true, }, + alwaysEnableInActiveVoiceChat: { + type: OptionType.BOOLEAN, + description: "Always allow your typing indicator to show when typing in a voice channel you are connected to.", + default: false + }, + temporaryEnableThresholdServers: { + type: OptionType.NUMBER, + description: "Temporarily allow your typing indicator to show for this many seconds after sending a message in a server channel. If the typing indicator is already visible in the channel, this setting will have no effect.", + default: 0, + }, + temporaryEnableThresholdDirectMessages: { + type: OptionType.NUMBER, + description: "Temporarily allow your typing indicator to show for this many seconds after sending a message in a DM or Group DM. If the typing indicator is already visible in the channel, this setting will have no effect.", + default: 0, + }, enabledLocations: { type: OptionType.STRING, description: "Enable functionality for these IDs. Accepts a comma separated list of DM IDs, channel IDs, and guild IDs. Only used if \"Default Hidden\" is disabled.", @@ -125,6 +149,9 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { defaultHidden, enabledLocations, disabledLocations, + alwaysEnableInActiveVoiceChat, + temporaryEnableThresholdServers, + temporaryEnableThresholdDirectMessages, chatIconLeftClickAction, chatIconMiddleClickAction, chatIconRightClickAction, @@ -134,21 +161,40 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { "defaultHidden", "enabledLocations", "disabledLocations", + "alwaysEnableInActiveVoiceChat", + "temporaryEnableThresholdServers", + "temporaryEnableThresholdDirectMessages", "chatIconLeftClickAction", "chatIconMiddleClickAction", "chatIconRightClickAction", ]); + const forceUpdate = useForceUpdater(); + + useEffect(() => { + rerenderListeners.add(forceUpdate); + + return () => { + rerenderListeners.delete(forceUpdate); + }; + }, []); + const validChat = ["normal", "sidebar"].some(x => type.analyticsName === x); if (!validChat || !chatIcon) return null; const effectiveList = getEffectiveList(); - const enabledLocally = enabledGlobally && checkEnabled(channel); + const isLocallyDisallowed = isExplicitlyDisallowed(channel); + const allowedLocallyImplicitly = isImplicitlyAllowed(channel); const location = channel.guild_id && effectiveList.includes(channel.guild_id) ? "Guild" : effectiveList.includes(channel.id) ? "Channel" : "Global"; const tooltip = enabledGlobally ? ( - enabledLocally ? `Typing Hidden (${location})` : `Typing Visible (${location})` + allowedLocallyImplicitly === 1 + ? "Typing Always Visible In The Connected Voice Channel Chat" + : allowedLocallyImplicitly === 2 + ? "Typing Temporarily Visible In This Channel Due To A Recent Message" + : isLocallyDisallowed + ? `Typing Hidden (${location})` : `Typing Visible (${location})` ) : "Typing Visible (Global)"; function performAction(action: string): void { @@ -188,7 +234,7 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { }}> - {(enabledLocally) && ( + {(isLocallyDisallowed && !allowedLocallyImplicitly) && ( <> @@ -228,17 +274,47 @@ function getEffectiveList(): string[] { } } -function checkEnabled(channel: string | Channel): boolean { +function isImplicitlyAllowed(channel: string | Channel): number { + const resolvedChannel = typeof channel === "string" ? ChannelStore.getChannel(channel) : channel; + + if (!resolvedChannel) { + return 0; + } + + const connectedVoiceChannelID = SelectedChannelStore.getVoiceChannelId(); + const guildID = resolvedChannel.guild_id; + + if (settings.store.alwaysEnableInActiveVoiceChat && connectedVoiceChannelID === resolvedChannel.id) { + return 1; + } + + const lastMessage = MessageStore.getLastEditableMessage(resolvedChannel.id); + const messageTimestamp = lastMessage?.timestamp?.getTime?.(); + + if (messageTimestamp) { + const addedThreshold = !guildID + ? settings.store.temporaryEnableThresholdDirectMessages + : settings.store.temporaryEnableThresholdServers; + + if (Date.now() < messageTimestamp + (addedThreshold * 1000)) { + return 2; + } + } + + return 0; +} + +function isExplicitlyDisallowed(channel: string | Channel): boolean { if (!settings.store.enabledGlobally) return false; - const channelId = typeof channel === "string" ? channel : channel.id; - const guildId = typeof channel === "string" ? ChannelStore.getChannel(channelId)?.guild_id : channel.guild_id; + const channelID = typeof channel === "string" ? channel : channel.id; + const guildID = typeof channel === "string" ? ChannelStore.getChannel(channelID)?.guild_id : channel.guild_id; const effectiveChannels = getEffectiveList(); if (settings.store.defaultHidden) { - return !effectiveChannels.includes(guildId) && !effectiveChannels.includes(channelId); + return !effectiveChannels.includes(guildID) && !effectiveChannels.includes(channelID); } else { - return effectiveChannels.includes(guildId) || effectiveChannels.includes(channelId); + return effectiveChannels.includes(guildID) || effectiveChannels.includes(channelID); } } @@ -308,11 +384,50 @@ export default definePlugin({ contextMenus: { "textarea-context": ChatBarContextCheckbox }, + chatBarButton: { icon: SilentTypingChatIcon, render: SilentTypingChatToggle }, + flux: { + VOICE_STATE_UPDATES({ voiceStates }) { + const state = voiceStates?.[0]; + + if (!state) { + return; + } + + if (state.userId !== UserStore.getCurrentUser().id) { + return; + } + + if (state.channelId === state.oldChannelId) { + return; + } + + if (settings.store.alwaysEnableInActiveVoiceChat) { + triggerChatToggleRerender(); + } + }, + + MESSAGE_CREATE({ message }) { + if (message.author.id === UserStore.getCurrentUser().id) { + const threshold = message.guild_id + ? settings.store.temporaryEnableThresholdServers + : settings.store.temporaryEnableThresholdDirectMessages; + + if (threshold > 0) { + triggerChatToggleRerender(); + + setTimeout(() => { + triggerChatToggleRerender(); + }, (threshold * 1000) + 25); + } + } + } + }, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -438,7 +553,10 @@ export default definePlugin({ ], async startTyping(channelId: string) { - if (checkEnabled(channelId)) return; + if (isExplicitlyDisallowed(channelId) && !isImplicitlyAllowed(channelId)) { + return; + } + FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, });