/* * 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 ; } });