diff --git a/src/equicordplugins/reactionTimestamps/index.tsx b/src/equicordplugins/reactionTimestamps/index.tsx new file mode 100644 index 000000000..da8c4cb13 --- /dev/null +++ b/src/equicordplugins/reactionTimestamps/index.tsx @@ -0,0 +1,130 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import ErrorBoundary from "@components/ErrorBoundary"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import type { ReactionEmoji } from "@vencord/discord-types"; + +interface ReactionEvent { + optimistic?: boolean; + channelId: string; + messageId: string; + userId: string; + emoji: ReactionEmoji; +} + +interface ReactionUserRowProps { + user?: { + id: string; + username?: string; + globalName?: string; + global_name?: string; + displayName?: string; + }; + channelId?: string; + messageId?: string; + message?: { + id: string; + channel_id?: string; + channelId?: string; + }; + emoji?: ReactionEmoji; + reaction?: { + emoji?: ReactionEmoji; + }; +} + +const MAX_TIMESTAMPS = 2000; + +const timestamps = new Map(); +const formatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "medium" +}); + +function getEmojiKey(emoji: ReactionEmoji) { + return `${emoji.name}:${emoji.id ?? ""}`; +} + +function getKey(channelId: string, messageId: string, userId: string, emoji: ReactionEmoji) { + return `${channelId}:${messageId}:${userId}:${getEmojiKey(emoji)}`; +} + +function rememberTimestamp(channelId: string, messageId: string, userId: string, emoji: ReactionEmoji) { + timestamps.set(getKey(channelId, messageId, userId, emoji), Date.now()); + + if (timestamps.size <= MAX_TIMESTAMPS) return; + + const oldestKey = timestamps.keys().next().value; + if (oldestKey) timestamps.delete(oldestKey); +} + +function getTimestamp(props: ReactionUserRowProps) { + const userId = props.user?.id; + const channelId = props.channelId ?? props.message?.channel_id ?? props.message?.channelId; + const messageId = props.messageId ?? props.message?.id; + const emoji = props.emoji ?? props.reaction?.emoji; + if (!userId || !channelId || !messageId || !emoji) return; + + const timestamp = timestamps.get(getKey(channelId, messageId, userId, emoji)); + return timestamp ? formatter.format(timestamp) : undefined; +} + +function ReactionName(props: ReactionUserRowProps) { + const timestamp = getTimestamp(props); + const { user } = props; + if (!user) return null; + + const name = user.displayName ?? user.globalName ?? user.global_name ?? user.username; + if (!name) return null; + + return ( +
+ {name} + {timestamp ? + {user.username ?? user.id} at {timestamp} + : null} +
+ ); +} + +export default definePlugin({ + name: "ReactionTimestamps", + description: "Shows each reaction time in the reaction popout.", + authors: [EquicordDevs.Kurt], + tags: ["Reactions", "Chat"], + + patches: [ + { + find: ".MESSAGE,userId:", + replacement: { + match: /(?<=Child,{className:\i\.\i,children:)/, + replace: "$self.renderReactionName(arguments[0])??" + } + } + ], + + flux: { + MESSAGE_REACTION_ADD(event: ReactionEvent) { + if (event.optimistic) return; + + rememberTimestamp(event.channelId, event.messageId, event.userId, event.emoji); + }, + + MESSAGE_REACTION_REMOVE(event: ReactionEvent) { + timestamps.delete(getKey(event.channelId, event.messageId, event.userId, event.emoji)); + } + }, + + renderReactionName: ErrorBoundary.wrap(ReactionName, { noop: true }), + + stop() { + timestamps.clear(); + } +}); diff --git a/src/equicordplugins/reactionTimestamps/style.css b/src/equicordplugins/reactionTimestamps/style.css new file mode 100644 index 000000000..50f0a16b1 --- /dev/null +++ b/src/equicordplugins/reactionTimestamps/style.css @@ -0,0 +1,12 @@ +.vc-reaction-timestamps-name { + display: flex; + flex-direction: column; + gap: 4px; + margin-left: 4px; +} + +.vc-reaction-timestamps-time { + color: var(--text-muted); + font-size: 12px; + line-height: 16px; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5e7c3a7dd..0ef4c089f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1434,6 +1434,10 @@ export const EquicordDevs = Object.freeze({ name: "penguinwokrs", id: 385266832136863746n }, + Kurt: { + name: "Kurt", + id: 112222963276750848n + }, } satisfies Record); // iife so #__PURE__ works correctly