Plugin

MessageLatency

Displays an indicator for messages that took ≥n seconds to send

Chat Utility
index.tsx
Download

Source

src/plugins/messageLatency/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import ErrorBoundary from "@components/ErrorBoundary";
3import { Devs } from "@utils/constants";
4import { isNonNullish } from "@utils/guards";
5import definePlugin, { OptionType } from "@utils/types";
6import { Message } from "@vencord/discord-types";
7import { AuthenticationStore, SnowflakeUtils, Tooltip } from "@webpack/common";
8
9type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
10type Fill = [FillValue, FillValue, FillValue];
11type DiffKey = keyof Diff;
12
13interface Diff {
14 days: number,
15 hours: number,
16 minutes: number,
17 seconds: number;
18 milliseconds: number;
19}
20
21const DISCORD_KT_DELAY = 1471228928;
22
23export default definePlugin({
24 name: "MessageLatency",
25 description: "Displays an indicator for messages that took ≥n seconds to send",
26 tags: ["Chat", "Utility"],
27 authors: [Devs.arHSM],
28
29 settings: definePluginSettings({
30 latency: {
31 type: OptionType.NUMBER,
32 description: "Threshold in seconds for latency indicator",
33 default: 2
34 },
35 detectDiscordKotlin: {
36 type: OptionType.BOOLEAN,
37 description: "Detect old Discord Android clients",
38 default: true
39 },
40 showMillis: {
41 type: OptionType.BOOLEAN,
42 description: "Show milliseconds",
43 default: false
44 },
45 ignoreSelf: {
46 type: OptionType.BOOLEAN,
47 description: "Don't add indicator to your own messages",
48 default: false
49 }
50 }),
51
52 patches: [
53 {
54 find: "showCommunicationDisabledStyles",
55 replacement: {
56 match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
57 replace: "$1$self.Tooltip()({ message: $2 }),$3"
58 }
59 }
60 ],
61
62 stringDelta(delta: number, showMillis: boolean) {
63 const diff: Diff = {
64 days: Math.floor(delta / (60 * 60 * 24 * 1000)),
65 hours: Math.floor((delta / (60 * 60 * 1000)) % 24),
66 minutes: Math.floor((delta / (60 * 1000)) % 60),
67 seconds: Math.floor(delta / 1000 % 60),
68 milliseconds: Math.floor(delta % 1000)
69 };
70
71 const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
72 const keys = Object.keys(diff) as DiffKey[];
73
74 const ts = keys.reduce((prev, k) => {
75 const s = str(k);
76
77 return prev + (
78 isNonNullish(s)
79 ? (prev !== ""
80 ? (showMillis ? k === "milliseconds" : k === "seconds")
81 ? " and "
82 : " "
83 : "") + s
84 : ""
85 );
86 }, "");
87
88 return ts || "0 seconds";
89 },
90
91 latencyTooltipData(message: Message) {
92 const { latency, detectDiscordKotlin, showMillis, ignoreSelf } = this.settings.store;
93 const { id, nonce } = message;
94
95 // Message wasn't received through gateway
96 if (!isNonNullish(nonce)) return null;
97
98 // Bots basically never send a nonce, and if someone does do it then it's usually not a snowflake
99 if (message.author.bot) return null;
100
101 if (ignoreSelf && message.author.id === AuthenticationStore.getId()) return null;
102
103 let isDiscordKotlin = false;
104 let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); class="ts-cmt">// milliseconds
105 if (!showMillis) {
106 delta = Math.round(delta / 1000) * 1000;
107 }
108
109 // Old Discord Android clients have a delay of around 17 days
110 // This is a workaround for that
111 if (-delta >= DISCORD_KT_DELAY - 86400000) { class="ts-cmt">// One day of padding for good measure
112 isDiscordKotlin = detectDiscordKotlin;
113 delta += DISCORD_KT_DELAY;
114 }
115
116 // Thanks dziurwa (I hate you)
117 // This is when the user's clock is ahead
118 // Can't do anything if the clock is behind
119 const abs = Math.abs(delta);
120 const ahead = abs !== delta;
121 const latencyMillis = latency * 1000;
122
123 const stringDelta = abs >= latencyMillis ? this.stringDelta(abs, showMillis) : null;
124
125 // Also thanks dziurwa
126 // 2 minutes
127 const TROLL_LIMIT = 2 * 60 * 1000;
128
129 const fill: Fill = isDiscordKotlin
130 ? ["status-positive", "status-positive", "text-muted"]
131 : delta >= TROLL_LIMIT || ahead
132 ? ["text-muted", "text-muted", "text-muted"]
133 : delta >= (latencyMillis * 2)
134 ? ["status-danger", "text-muted", "text-muted"]
135 : ["status-warning", "status-warning", "text-muted"];
136
137 return (abs >= latencyMillis || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
138 },
139
140 Tooltip() {
141 return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
142 const d = this.latencyTooltipData(message);
143
144 if (!isNonNullish(d)) return null;
145
146 let text: string;
147 if (!d.delta) {
148 text = "User is suspected to be on an old Discord Android client";
149 } else {
150 text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
151 }
152
153 return <Tooltip
154 text={text}
155 position="top"
156 >
157 {props => <this.Icon delta={d.delta} fill={d.fill} props={props} />}
158 </Tooltip>;
159 }, { noop: true });
160 },
161
162 Icon({ delta, fill, props }: {
163 delta: string | null;
164 fill: Fill,
165 props: {
166 onClick(): void;
167 onMouseEnter(): void;
168 onMouseLeave(): void;
169 onContextMenu(): void;
170 onFocus(): void;
171 onBlur(): void;
172 "aria-label"?: string;
173 };
174 }) {
175 return <svg
176 xmlns="http:class="ts-cmt">//www.w3.org/2000/svg"
177 viewBox="0 0 16 16"
178 width="12"
179 height="12"
180 role="img"
181 fill="none"
182 style={{ marginRight: "8px", verticalAlign: -1 }}
183 aria-label={delta ?? "Old Discord Android client"}
184 aria-hidden="false"
185 {...props}
186 >
187 <path
188 fill={`var(--${fill[0]})`}
189 d="M4.8001 12C4.8001 11.5576 4.51344 11.2 4.16023 11.2H2.23997C1.88676 11.2 1.6001 11.5576 1.6001 12V13.6C1.6001 14.0424 1.88676 14.4 2.23997 14.4H4.15959C4.5128 14.4 4.79946 14.0424 4.79946 13.6L4.8001 12Z"
190 />
191 <path
192 fill={`var(--${fill[1]})`}
193 d="M9.6001 7.12724C9.6001 6.72504 9.31337 6.39998 8.9601 6.39998H7.0401C6.68684 6.39998 6.40011 6.72504 6.40011 7.12724V13.6727C6.40011 14.0749 6.68684 14.4 7.0401 14.4H8.9601C9.31337 14.4 9.6001 14.0749 9.6001 13.6727V7.12724Z"
194 />
195 <path
196 fill={`var(--${fill[2]})`}
197 d="M14.4001 2.31109C14.4001 1.91784 14.1134 1.59998 13.7601 1.59998H11.8401C11.4868 1.59998 11.2001 1.91784 11.2001 2.31109V13.6888C11.2001 14.0821 11.4868 14.4 11.8401 14.4H13.7601C14.1134 14.4 14.4001 14.0821 14.4001 13.6888V2.31109Z"
198 />
199 </svg>;
200 }
201});
202