Plugin
TypingIndicator
Adds an indicator if someone is typing on a channel.
1
import "./style.css";2
3
import { isPluginEnabled } from "@api/PluginManager";4
import { definePluginSettings } from "@api/Settings";5
import ErrorBoundary from "@components/ErrorBoundary";6
import TypingTweaksPlugin, { buildSeveralUsers } from "@plugins/typingTweaks";7
import { Devs } from "@utils/constants";8
import { getIntlMessage } from "@utils/discord";9
import definePlugin, { OptionType } from "@utils/types";10
import { findComponentByCodeLazy } from "@webpack";11
import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserGuildSettingsStore, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common";12
13
const ThreeDots = findComponentByCodeLazy("Math.min(1,Math.max(", "dotRadius:");14
15
const enum IndicatorMode {16
Dots = 1 << 0,17
Avatars = 1 << 118
}19
20
function getDisplayName(guildId: string, userId: string) {21
const user = UserStore.getUser(userId);22
return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username;23
}24
25
function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: string; }) {26
const typingUsers: Record<string, number> = useStateFromStores(27
[TypingStore],28
() => ({ ...TypingStore.getTypingUsers(channelId) }),29
null,30
(old, current) => {31
const oldKeys = Object.keys(old);32
const currentKeys = Object.keys(current);33
34
return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);35
}36
);37
const currentChannelId = useStateFromStores([SelectedChannelStore], () => SelectedChannelStore.getChannelId());38
39
if (!settings.store.includeMutedChannels) {40
const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);41
if (isChannelMuted) return null;42
}43
44
if (!settings.store.includeCurrentChannel) {45
if (currentChannelId === channelId) return null;46
}47
48
const myId = UserStore.getCurrentUser()?.id;49
50
const typingUsersArray = Object.keys(typingUsers).filter(id =>51
id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)52
);53
const [a, b, c] = typingUsersArray;54
let tooltipText: string;55
56
switch (typingUsersArray.length) {57
case 0: break;58
case 1: {59
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, a) });60
break;61
}62
case 2: {63
tooltipText = getIntlMessage("TWO_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b) });64
break;65
}66
case 3: {67
tooltipText = getIntlMessage("THREE_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b), c: getDisplayName(guildId, c) });68
break;69
}70
default: {71
tooltipText = isPluginEnabled(TypingTweaksPlugin.name)72
? buildSeveralUsers({ users: [a, b].map(UserStore.getUser), count: typingUsersArray.length - 2, guildId })73
: getIntlMessage("SEVERAL_USERS_TYPING");74
break;75
}76
}77
78
if (typingUsersArray.length > 0) {79
return (80
<Tooltip text={tooltipText!}>81
{props => (82
<div className="vc-typing-indicator" {...props}>83
{((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && (84
<div85
onClick={e => {86
e.stopPropagation();87
e.preventDefault();88
}}89
onKeyPress={e => e.stopPropagation()}90
>91
<UserSummaryItem92
users={typingUsersArray.map(id => UserStore.getUser(id))}93
guildId={guildId}94
renderIcon={false}95
max={3}96
showDefaultAvatarsForNullUsers97
showUserPopout98
size={16}99
className="vc-typing-indicator-avatars"100
/>101
</div>102
)}103
{((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (104
<div className="vc-typing-indicator-dots">105
<ThreeDots dotRadius={3} themed={true} />106
</div>107
)}108
</div>109
)}110
</Tooltip>111
);112
}113
114
return null;115
}116
117
const settings = definePluginSettings({118
includeCurrentChannel: {119
type: OptionType.BOOLEAN,120
description: "Whether to show the typing indicator for the currently selected channel",121
default: true122
},123
includeMutedChannels: {124
type: OptionType.BOOLEAN,125
description: "Whether to show the typing indicator for muted channels.",126
default: false127
},128
includeBlockedUsers: {129
type: OptionType.BOOLEAN,130
description: "Whether to show the typing indicator for blocked users.",131
default: false132
},133
indicatorMode: {134
type: OptionType.SELECT,135
description: "How should the indicator be displayed?",136
options: [137
{ label: "Avatars and animated dots", value: IndicatorMode.Dots | IndicatorMode.Avatars, default: true },138
{ label: "Animated dots", value: IndicatorMode.Dots },139
{ label: "Avatars", value: IndicatorMode.Avatars },140
],141
}142
});143
144
export default definePlugin({145
name: "TypingIndicator",146
description: "Adds an indicator if someone is typing on a channel.",147
tags: ["Notifications", "Appearance", "Servers"],148
authors: [Devs.Nuckyz, Devs.fawn, Devs.Sqaaakoi],149
settings,150
151
patches: [152
// Normal channel153
{154
find: "UNREAD_IMPORTANT:",155
replacement: {156
match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/,157
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"158
}159
},160
// Theads161
{162
// This is the thread "spine" that shows in the left163
find: "M0 15H2c0 1.6569",164
replacement: {165
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,166
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"167
}168
}169
],170
171
TypingIndicator: (channelId: string, guildId: string) => (172
<ErrorBoundary noop>173
<TypingIndicator channelId={channelId} guildId={guildId} />174
</ErrorBoundary>175
),176
});177