Plugin

TypingTweaks

Show avatars and role colours in the typing indicator

Appearance Customisation
index.tsx
Download

Source

src/plugins/typingTweaks/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import ErrorBoundary from "@components/ErrorBoundary";
3import { Devs } from "@utils/constants";
4import { classNameFactory } from "@utils/css";
5import { openUserProfile } from "@utils/discord";
6import { isNonNullish } from "@utils/guards";
7import { Logger } from "@utils/Logger";
8import definePlugin, { OptionType } from "@utils/types";
9import { Channel, User } from "@vencord/discord-types";
10import { AuthenticationStore, Avatar, GuildMemberStore, React, RelationshipStore, TypingStore, UserStore, useStateFromStores } from "@webpack/common";
11import { PropsWithChildren } from "react";
12
13import managedStyle from "./style.css?managed";
14
15const cl = classNameFactory("vc-typing-tweaks-");
16const settings = definePluginSettings({
17 showAvatars: {
18 type: OptionType.BOOLEAN,
19 default: true,
20 description: "Show avatars in the typing indicator"
21 },
22 showRoleColors: {
23 type: OptionType.BOOLEAN,
24 default: true,
25 description: "Show role colors in the typing indicator"
26 },
27 alternativeFormatting: {
28 type: OptionType.BOOLEAN,
29 default: true,
30 description: "Show a more useful message when several users are typing"
31 }
32});
33
34export const buildSeveralUsers = ErrorBoundary.wrap(function buildSeveralUsers({ users, count, guildId }: { users: User[], count: number; guildId: string; }) {
35 return (
36 <>
37 {users.slice(0, count).map(user => (
38 <React.Fragment key={user.id}>
39 <TypingUser user={user} guildId={guildId} />
40 {", "}
41 </React.Fragment>
42 ))}
43 and {count} others are typing...
44 </>
45 );
46}, { noop: true });
47
48interface TypingUserProps {
49 user: User;
50 guildId: string;
51}
52
53const TypingUser = ErrorBoundary.wrap(function TypingUser({ user, guildId }: TypingUserProps) {
54 return (
55 <strong
56 className={cl("user")}
57 role="button"
58 onClick={() => {
59 openUserProfile(user.id);
60 }}
61 style={{
62 color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined,
63 }}
64 >
65 {settings.store.showAvatars && (
66 <Avatar
67 className={cl("avatar")}
68 size="SIZE_16"
69 src={user.getAvatarURL(guildId, 128)} />
70 )}
71 {GuildMemberStore.getNick(guildId!, user.id)
72 || (!guildId && RelationshipStore.getNickname(user.id))
73 || (user as any).globalName
74 || user.username
75 }
76 </strong>
77 );
78}, { noop: true });
79
80export default definePlugin({
81 name: "TypingTweaks",
82 description: "Show avatars and role colours in the typing indicator",
83 tags: ["Appearance", "Customisation"],
84 authors: [Devs.zt, Devs.sadan],
85 settings,
86
87 managedStyle,
88
89 patches: [
90 {
91 find: "#{intl::SEVERAL_USERS_TYPING_STRONG}",
92 group: true,
93 replacement: [
94 {
95 // Style the indicator and add function call to modify the children before rendering
96 match: /(?<="aria-hidden":!0,children:)\i/,
97 replace: "$self.renderTypingUsers({ users: arguments[0]?.typingUserObjects, guildId: arguments[0]?.channel?.guild_id, children: $& })"
98 },
99 {
100 match: /(?<=function \i\(\i\)\{)(?=[^}]+?\{channel:\i,isThreadCreation:\i=!1,\.\.\.\i\})/,
101 replace: "let typingUserObjects = $self.useTypingUsers(arguments[0]?.channel);"
102 },
103 {
104 // Get the typing users as user objects instead of names
105 match: /typingUsers:(\i)\?\[\]:\i,/,
106 // check by typeof so if the variable is not defined due to other patch failing, it won't throw a ReferenceError
107 replace: "$&typingUserObjects: $1 || typeof typingUserObjects === &#039;undefined&#039; ? [] : typingUserObjects,"
108 },
109 {
110 // Adds the alternative formatting for several users typing
111 // users.length > 3 && (component = intl(key))
112 match: /(&&\(\i=)\i\.\i\.format\(\i\.\i#{intl::SEVERAL_USERS_TYPING_STRONG},\{\}\)/,
113 replace: "$1$self.buildSeveralUsers({ users: arguments[0]?.typingUserObjects, count: arguments[0]?.typingUserObjects?.length - 2, guildId: arguments[0]?.channel?.guild_id })",
114 predicate: () => settings.store.alternativeFormatting
115 }
116 ]
117 }
118 ],
119
120 useTypingUsers(channel: Channel | undefined): User[] {
121 try {
122 if (!channel) {
123 throw new Error("No channel");
124 }
125
126 const typingUsers = useStateFromStores([TypingStore], () => TypingStore.getTypingUsers(channel.id));
127 const myId = useStateFromStores([AuthenticationStore], () => AuthenticationStore.getId());
128
129 return Object.keys(typingUsers)
130 .filter(id => id && id !== myId && !RelationshipStore.isBlockedOrIgnored(id))
131 .map(id => UserStore.getUser(id))
132 .filter(isNonNullish);
133 } catch (e) {
134 new Logger("TypingTweaks").error("Failed to get typing users:", e);
135 return [];
136 }
137 },
138
139 buildSeveralUsers,
140
141 renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => {
142 try {
143 if (!Array.isArray(children)) {
144 return children;
145 }
146
147 let element = 0;
148
149 return children.map(c => {
150 if (c.type !== "strong" && !(typeof c !== "string" && !React.isValidElement(c))) return c;
151
152 const user = users[element++];
153 return <TypingUser key={user.id} guildId={guildId} user={user} />;
154 });
155 } catch (e) {
156 new Logger("TypingTweaks").error("Failed to render typing users:", e);
157 }
158
159 return children;
160 }, { noop: true })
161});
162