Plugin
MessageLatency
Displays an indicator for messages that took ≥n seconds to send
1
import { definePluginSettings } from "@api/Settings";2
import ErrorBoundary from "@components/ErrorBoundary";3
import { Devs } from "@utils/constants";4
import { isNonNullish } from "@utils/guards";5
import definePlugin, { OptionType } from "@utils/types";6
import { Message } from "@vencord/discord-types";7
import { AuthenticationStore, SnowflakeUtils, Tooltip } from "@webpack/common";8
9
type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");10
type Fill = [FillValue, FillValue, FillValue];11
type DiffKey = keyof Diff;12
13
interface Diff {14
days: number,15
hours: number,16
minutes: number,17
seconds: number;18
milliseconds: number;19
}20
21
const DISCORD_KT_DELAY = 1471228928;22
23
export 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: 234
},35
detectDiscordKotlin: {36
type: OptionType.BOOLEAN,37
description: "Detect old Discord Android clients",38
default: true39
},40
showMillis: {41
type: OptionType.BOOLEAN,42
description: "Show milliseconds",43
default: false44
},45
ignoreSelf: {46
type: OptionType.BOOLEAN,47
description: "Don039;t add indicator to your own messages",48
default: false49
}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
: "") + s84
: ""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 gateway96
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 snowflake99
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">// milliseconds105
if (!showMillis) {106
delta = Math.round(delta / 1000) * 1000;107
}108
109
// Old Discord Android clients have a delay of around 17 days110
// This is a workaround for that111
if (-delta >= DISCORD_KT_DELAY - 86400000) { class="ts-cmt">// One day of padding for good measure112
isDiscordKotlin = detectDiscordKotlin;113
delta += DISCORD_KT_DELAY;114
}115
116
// Thanks dziurwa (I hate you)117
// This is when the user's clock is ahead118
// Can't do anything if the clock is behind119
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 dziurwa126
// 2 minutes127
const TROLL_LIMIT = 2 * 60 * 1000;128
129
const fill: Fill = isDiscordKotlin130
? ["status-positive", "status-positive", "text-muted"]131
: delta >= TROLL_LIMIT || ahead132
? ["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 user039;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 <Tooltip154
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 <svg176
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
<path188
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
<path192
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
<path196
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