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 000000000..3ad2354f6 --- /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/TypingStore.d.ts b/packages/discord-types/src/stores/TypingStore.d.ts new file mode 100644 index 000000000..0ebe994f5 --- /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 e00df8bf5..a0dc8c576 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/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 6cb9cb2ae..f35879f69 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -257,8 +257,10 @@ export default definePlugin({ { find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}", replacement: { - match: /#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}.+?}\(\),(?<={overlay:\i,.+?=(\i),.+?)(?=!(\i))/, - replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` + // let { ... nowPlaying: a = !1 ... + // let { overlay: b ... } = Props + match: /#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}.+?}\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)/, + replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` } }, // Activities from the apps launcher in the bottom right of the chat bar diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index ee4428f9c..326e929a1 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -23,7 +23,6 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; import { classes } from "@utils/misc"; -import { canonicalizeMatch } from "@utils/patches"; import definePlugin, { OptionType } from "@utils/types"; import type { Channel, Role } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; @@ -327,15 +326,18 @@ export default definePlugin({ }, { // Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen) - match: /MANAGE_ROLES.{0,90}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?\]}\)))/, - replace: (m, component, channel) => { - // Export the channel for the users allowed component patch - component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,shcChannel:${channel}`); - // Always render the component for multiple allowed users - component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true"); - - return `${m} $self.isHiddenChannel(${channel},true)?${component}:`; - } + match: /\.members.+?\]}\),(?<=channel:(\i).+?)/, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel},true)?null:` + }, + { + // Export the channel for the users allowed component patch + match: /maxUsers:\i,users:\i(?<=channel:(\i).+?)/, + replace: (m, channel) => `${m},shcChannel:${channel}` + }, + { + // Always render the component for multiple allowed users + match: /1!==\i\.length(?=\|\|)/, + replace: "true" } ] }, @@ -344,8 +346,8 @@ export default definePlugin({ replacement: [ { // Create a variable for the channel prop - match: /(function \i\(\i\)\{)([^}]+?hideOverflowCount)/, - replace: "$1let {shcChannel}=arguments[0];$2" + match: /let{users:\i,maxUsers:\i,/, + replace: "let{shcChannel}=arguments[0];$&" }, { // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index a54df151d..445144c42 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -24,12 +24,11 @@ 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 fe5e39343..1c9bae563 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -21,9 +21,11 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { getCustomColorString } from "@equicordplugins/customUserColors"; 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"; @@ -46,19 +48,21 @@ 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; } @@ -74,7 +78,7 @@ function typingUserColor(guildId: string, userId: string): string | undefined { return GuildMemberStore.getMember(guildId, userId)?.colorString; } -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[]; }>) => { @@ -152,7 +181,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 8d05d56fc..dc7085661 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -41,12 +41,13 @@ 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 TypingStore: GenericStore; export let EmojiStore: t.EmojiStore; export let StickerStore: t.StickerStore; @@ -54,11 +55,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);