Plugin
TypingTweaks
Show avatars and role colours in the typing indicator
1
import { definePluginSettings } from "@api/Settings";2
import ErrorBoundary from "@components/ErrorBoundary";3
import { Devs } from "@utils/constants";4
import { classNameFactory } from "@utils/css";5
import { openUserProfile } from "@utils/discord";6
import { isNonNullish } from "@utils/guards";7
import { Logger } from "@utils/Logger";8
import definePlugin, { OptionType } from "@utils/types";9
import { Channel, User } from "@vencord/discord-types";10
import { AuthenticationStore, Avatar, GuildMemberStore, React, RelationshipStore, TypingStore, UserStore, useStateFromStores } from "@webpack/common";11
import { PropsWithChildren } from "react";12
13
import managedStyle from "./style.css?managed";14
15
const cl = classNameFactory("vc-typing-tweaks-");16
const 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
34
export 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
48
interface TypingUserProps {49
user: User;50
guildId: string;51
}52
53
const TypingUser = ErrorBoundary.wrap(function TypingUser({ user, guildId }: TypingUserProps) {54
return (55
<strong56
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
<Avatar67
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).globalName74
|| user.username75
}76
</strong>77
);78
}, { noop: true });79
80
export 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 rendering96
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 names105
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 ReferenceError107
replace: "$&typingUserObjects: $1 || typeof typingUserObjects === 039;undefined039; ? [] : typingUserObjects,"108
},109
{110
// Adds the alternative formatting for several users typing111
// 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.alternativeFormatting115
}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