From 74d78d89ed087cf25ad2c1e565f0736b92d89539 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:11:38 -0400 Subject: [PATCH] fix TypingTweaks (#3586) Co-authored-by: V --- .../src/stores/AuthenticationStore.d.ts | 11 +++ .../src/stores/RelationshipStore.d.ts | 5 ++ .../discord-types/src/stores/TypingStore.d.ts | 9 +++ packages/discord-types/src/stores/index.d.ts | 2 + src/plugins/typingIndicator/index.tsx | 7 +- src/plugins/typingTweaks/index.tsx | 67 +++++++++++++------ src/webpack/common/stores.ts | 5 ++ 7 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 packages/discord-types/src/stores/AuthenticationStore.d.ts create mode 100644 packages/discord-types/src/stores/TypingStore.d.ts diff --git a/packages/discord-types/src/stores/AuthenticationStore.d.ts b/packages/discord-types/src/stores/AuthenticationStore.d.ts new file mode 100644 index 00000000..3ad2354f --- /dev/null +++ b/packages/discord-types/src/stores/AuthenticationStore.d.ts @@ -0,0 +1,11 @@ +import { FluxStore } from ".."; + +export class AuthenticationStore extends FluxStore { + /** + * Gets the id of the current user + */ + getId(): string; + + // This Store has a lot more methods related to everything Auth, but they really should + // not be needed, so they are not typed +} diff --git a/packages/discord-types/src/stores/RelationshipStore.d.ts b/packages/discord-types/src/stores/RelationshipStore.d.ts index 5d1a08af..9aceac47 100644 --- a/packages/discord-types/src/stores/RelationshipStore.d.ts +++ b/packages/discord-types/src/stores/RelationshipStore.d.ts @@ -15,6 +15,11 @@ export class RelationshipStore extends FluxStore { isFriend(userId: string): boolean; isBlocked(userId: string): boolean; isIgnored(userId: string): boolean; + /** + * @see {@link isBlocked} + * @see {@link isIgnored} + */ + isBlockedOrIgnored(userId: string): boolean; getSince(userId: string): string; getMutableRelationships(): Map; diff --git a/packages/discord-types/src/stores/TypingStore.d.ts b/packages/discord-types/src/stores/TypingStore.d.ts new file mode 100644 index 00000000..0ebe994f --- /dev/null +++ b/packages/discord-types/src/stores/TypingStore.d.ts @@ -0,0 +1,9 @@ +import { FluxStore } from ".."; + +export class TypingStore extends FluxStore { + /** + * returns a map of user ids to timeout ids + */ + getTypingUsers(channelId: string): Record; + isTyping(channelId: string, userId: string): boolean; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index 23045832..16a329c5 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -1,4 +1,5 @@ // please keep in alphabetical order +export * from "./AuthenticationStore"; export * from "./ChannelStore"; export * from "./DraftStore"; export * from "./EmojiStore"; @@ -11,6 +12,7 @@ export * from "./RelationshipStore"; export * from "./SelectedChannelStore"; export * from "./SelectedGuildStore"; export * from "./ThemeStore"; +export * from "./TypingStore"; export * from "./UserProfileStore"; export * from "./UserStore"; export * from "./WindowStore"; diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 0a0cd53a..47a0fbeb 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -24,13 +24,12 @@ import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common"; +import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common"; import { buildSeveralUsers } from "../typingTweaks"; const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:"); -const TypingStore = findStoreLazy("TypingStore"); const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); const enum IndicatorMode { @@ -46,7 +45,7 @@ function getDisplayName(guildId: string, userId: string) { function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: string; }) { const typingUsers: Record = useStateFromStores( [TypingStore], - () => ({ ...TypingStore.getTypingUsers(channelId) as Record }), + () => ({ ...TypingStore.getTypingUsers(channelId) }), null, (old, current) => { const oldKeys = Object.keys(old); @@ -90,7 +89,7 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s } default: { tooltipText = Settings.plugins.TypingTweaks.enabled - ? buildSeveralUsers({ a: UserStore.getUser(a), b: UserStore.getUser(b), count: typingUsersArray.length - 2, guildId }) + ? buildSeveralUsers({ users: [a, b].map(UserStore.getUser), count: typingUsersArray.length - 2, guildId }) : getIntlMessage("SEVERAL_USERS_TYPING"); break; } diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx index bcfea898..8369497c 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -20,9 +20,11 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { openUserProfile } from "@utils/discord"; +import { isNonNullish } from "@utils/guards"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { User } from "@vencord/discord-types"; -import { Avatar, GuildMemberStore, React, RelationshipStore } from "@webpack/common"; +import { Channel, User } from "@vencord/discord-types"; +import { AuthenticationStore, Avatar, GuildMemberStore, React, RelationshipStore, TypingStore, UserStore, useStateFromStores } from "@webpack/common"; import { PropsWithChildren } from "react"; import managedStyle from "./style.css?managed"; @@ -45,24 +47,26 @@ const settings = definePluginSettings({ } }); -export const buildSeveralUsers = ErrorBoundary.wrap(({ a, b, count, guildId }: { a: User, b: User, count: number; guildId: string; }) => { +export const buildSeveralUsers = ErrorBoundary.wrap(function buildSeveralUsers({ users, count, guildId }: { users: User[], count: number; guildId: string; }) { return ( <> - - {", "} - - {", "} + {users.slice(0, count).map(user => ( + + + {", "} + + ))} and {count} others are typing... ); }, { noop: true }); -interface Props { +interface TypingUserProps { user: User; guildId: string; } -const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) { +const TypingUser = ErrorBoundary.wrap(function TypingUser({ user, guildId }: TypingUserProps) { return ( 0.{0,300}?"aria-atomic":!0,children:)\i/, - replace: "$self.renderTypingUsers({ users: $1, guildId: arguments[0]?.channel?.guild_id, children: $& })" + match: /(?<="aria-atomic":!0,children:)\i/, + replace: "$self.renderTypingUsers({ users: arguments[0]?.typingUserObjects, guildId: arguments[0]?.channel?.guild_id, children: $& })" }, { - // Changes the indicator to keep the user object when creating the list of typing users - match: /\.map\((\i)=>\i\.\i\.getName\(\i(?:\.guild_id)?,\i\.id,\1\)\)/, - replace: "" + match: /(?<=function \i\(\i\)\{)(?=[^}]+?\{channel:\i,isThreadCreation:\i=!1\})/, + replace: "let typingUserObjects = $self.useTypingUsers(arguments[0]?.channel);" + }, + { + // Get the typing users as user objects instead of names + match: /typingUsers:(\i)\?\[\]:\i,/, + // check by typeof so if the variable is not defined due to other patch failing, it won't throw a ReferenceError + replace: "$&typingUserObjects: $1 || typeof typingUserObjects === 'undefined' ? [] : typingUserObjects," }, { // Adds the alternative formatting for several users typing - match: /(,{a:(\i),b:(\i),c:\i}\):\i\.length>3&&\(\i=)\i\.\i\.string\(\i\.\i#{intl::SEVERAL_USERS_TYPING}\)(?<=(\i)\.length.+?)/, - replace: (_, rest, a, b, users) => - `${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2, guildId: arguments[0]?.channel?.guild_id })`, + // users.length > 3 && (component = intl(key)) + match: /(&&\(\i=)\i\.\i\.format\(\i\.\i#{intl::SEVERAL_USERS_TYPING_STRONG},\{\}\)/, + replace: "$1$self.buildSeveralUsers({ users: arguments[0]?.typingUserObjects, count: arguments[0]?.typingUserObjects?.length - 2, guildId: arguments[0]?.channel?.guild_id })", predicate: () => settings.store.alternativeFormatting } ] } ], + useTypingUsers(channel: Channel | undefined): User[] { + try { + if (!channel) { + throw new Error("No channel"); + } + + const typingUsers = useStateFromStores([TypingStore], () => TypingStore.getTypingUsers(channel.id)); + const myId = useStateFromStores([AuthenticationStore], () => AuthenticationStore.getId()); + + return Object.keys(typingUsers) + .filter(id => id && id !== myId && !RelationshipStore.isBlockedOrIgnored(id)) + .map(id => UserStore.getUser(id)) + .filter(isNonNullish); + } catch (e) { + new Logger("TypingTweaks").error("Failed to get typing users:", e); + return []; + } + }, + + buildSeveralUsers, renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => { @@ -140,7 +169,7 @@ export default definePlugin({ return ; }); } catch (e) { - console.error(e); + new Logger("TypingTweaks").error("Failed to render typing users:", e); } return children; diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index ca867c70..221218c6 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -40,10 +40,12 @@ export let GuildStore: t.GuildStore; export let GuildRoleStore: t.GuildRoleStore; export let GuildMemberStore: t.GuildMemberStore; export let UserStore: t.UserStore; +export let AuthenticationStore: t.AuthenticationStore; export let UserProfileStore: t.UserProfileStore; export let SelectedChannelStore: t.SelectedChannelStore; export let SelectedGuildStore: t.SelectedGuildStore; export let ChannelStore: t.ChannelStore; +export let TypingStore: t.TypingStore; export let RelationshipStore: t.RelationshipStore; export let EmojiStore: t.EmojiStore; @@ -51,11 +53,13 @@ export let ThemeStore: t.ThemeStore; export let WindowStore: t.WindowStore; export let DraftStore: t.DraftStore; + /** * @see jsdoc of {@link t.useStateFromStores} */ export const useStateFromStores: t.useStateFromStores = findByCodeLazy("useStateFromStores"); +waitForStore("AuthenticationStore", s => AuthenticationStore = s); waitForStore("DraftStore", s => DraftStore = s); waitForStore("UserStore", s => UserStore = s); waitForStore("UserProfileStore", m => UserProfileStore = m); @@ -73,6 +77,7 @@ waitForStore("GuildRoleStore", m => GuildRoleStore = m); waitForStore("MessageStore", m => MessageStore = m); waitForStore("WindowStore", m => WindowStore = m); waitForStore("EmojiStore", m => EmojiStore = m); +waitForStore("TypingStore", m => TypingStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly can easily cause circular imports. For this reason, use a non import access here.