Plugin

TypingIndicator

Adds an indicator if someone is typing on a channel.

Notifications Appearance Servers
index.tsx
Download

Source

src/plugins/typingIndicator/index.tsx
1import "./style.css";
2
3import { isPluginEnabled } from "@api/PluginManager";
4import { definePluginSettings } from "@api/Settings";
5import ErrorBoundary from "@components/ErrorBoundary";
6import TypingTweaksPlugin, { buildSeveralUsers } from "@plugins/typingTweaks";
7import { Devs } from "@utils/constants";
8import { getIntlMessage } from "@utils/discord";
9import definePlugin, { OptionType } from "@utils/types";
10import { findComponentByCodeLazy } from "@webpack";
11import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserGuildSettingsStore, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common";
12
13const ThreeDots = findComponentByCodeLazy("Math.min(1,Math.max(", "dotRadius:");
14
15const enum IndicatorMode {
16 Dots = 1 << 0,
17 Avatars = 1 << 1
18}
19
20function 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
25function 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 <div
85 onClick={e => {
86 e.stopPropagation();
87 e.preventDefault();
88 }}
89 onKeyPress={e => e.stopPropagation()}
90 >
91 <UserSummaryItem
92 users={typingUsersArray.map(id => UserStore.getUser(id))}
93 guildId={guildId}
94 renderIcon={false}
95 max={3}
96 showDefaultAvatarsForNullUsers
97 showUserPopout
98 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
117const settings = definePluginSettings({
118 includeCurrentChannel: {
119 type: OptionType.BOOLEAN,
120 description: "Whether to show the typing indicator for the currently selected channel",
121 default: true
122 },
123 includeMutedChannels: {
124 type: OptionType.BOOLEAN,
125 description: "Whether to show the typing indicator for muted channels.",
126 default: false
127 },
128 includeBlockedUsers: {
129 type: OptionType.BOOLEAN,
130 description: "Whether to show the typing indicator for blocked users.",
131 default: false
132 },
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
144export 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 channel
153 {
154 find: "UNREAD_IMPORTANT:",
155 replacement: {
156 match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/,
157 replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
158 }
159 },
160 // Theads
161 {
162 // This is the thread "spine" that shows in the left
163 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