/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin, { OptionType } from "@utils/types";
import { findExportedComponentLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff;
interface Diff {
days: number,
hours: number,
minutes: number,
seconds: number;
milliseconds: number;
}
const DISCORD_KT_DELAY = 14712289280;
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
export default definePlugin({
name: "MessageLatency",
description: "Displays an indicator for messages that took ≥n seconds to send",
authors: [Devs.arHSM],
settings: definePluginSettings({
latency: {
type: OptionType.NUMBER,
description: "Threshold in seconds for latency indicator",
default: 2
},
detectDiscordKotlin: {
type: OptionType.BOOLEAN,
description: "Detect old Discord Android clients",
default: true
},
showMillis: {
type: OptionType.BOOLEAN,
description: "Show milliseconds",
default: false
}
}),
patches: [
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
replace: "$1$self.Tooltip()({ message: $2 }),$3"
}
}
],
stringDelta(delta: number, showMillis: boolean) {
const diff: Diff = {
days: Math.round(delta / (60 * 60 * 24 * 1000)),
hours: Math.round((delta / (60 * 60 * 1000)) % 24),
minutes: Math.round((delta / (60 * 1000)) % 60),
seconds: Math.round(delta / 1000 % 60),
milliseconds: Math.round(delta % 1000)
};
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
const keys = Object.keys(diff) as DiffKey[];
const ts = keys.reduce((prev, k) => {
const s = str(k);
return prev + (
isNonNullish(s)
? (prev !== ""
? (showMillis ? k === "milliseconds" : k === "seconds")
? " and "
: " "
: "") + s
: ""
);
}, "");
return ts || "0 seconds";
},
latencyTooltipData(message: Message) {
const { latency, detectDiscordKotlin, showMillis } = this.settings.store;
const { id, nonce } = message;
// Message wasn't received through gateway
if (!isNonNullish(nonce)) return null;
let isDiscordKotlin = false;
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
if (!showMillis) {
delta = Math.round(delta / 1000) * 1000;
}
// Old Discord Android clients have a delay of around 17 days
// This is a workaround for that
if (-delta >= DISCORD_KT_DELAY - 86400000) { // One day of padding for good measure
isDiscordKotlin = detectDiscordKotlin;
delta += DISCORD_KT_DELAY;
}
// Thanks dziurwa (I hate you)
// This is when the user's clock is ahead
// Can't do anything if the clock is behind
const abs = Math.abs(delta);
const ahead = abs !== delta;
const stringDelta = abs >= latency * 1000 ? this.stringDelta(abs, showMillis) : null;
// Also thanks dziurwa
// 2 minutes
const TROLL_LIMIT = 2 * 60 * 1000;
const fill: Fill = isDiscordKotlin
? ["status-positive", "status-positive", "text-muted"]
: delta >= TROLL_LIMIT || ahead
? ["text-muted", "text-muted", "text-muted"]
: delta >= (latency * 2000)
? ["status-danger", "text-muted", "text-muted"]
: ["status-warning", "status-warning", "text-muted"];
return (abs >= latency || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
},
Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
const d = this.latencyTooltipData(message);
if (!isNonNullish(d)) return null;
let text: string;
if (!d.delta) {
text = "User is suspected to be on an old Discord Android client";
} else {
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." : "");
}
return
{
props => <>
{}
{/* Time Out indicator uses this, I think this is for a11y */}
Delayed Message
>
}
;
});
},
Icon({ delta, fill, props }: {
delta: string | null;
fill: Fill,
props: {
onClick(): void;
onMouseEnter(): void;
onMouseLeave(): void;
onContextMenu(): void;
onFocus(): void;
onBlur(): void;
"aria-label"?: string;
};
}) {
return ;
}
});