From 747c7f23ed1b67a316fbb95db7c38a5cd5c536e5 Mon Sep 17 00:00:00 2001 From: Moowi Date: Sat, 9 May 2026 05:32:42 +0300 Subject: [PATCH] feat(Streaks): shows a streak next to a user when you exchange dms with them on consecutive days (#1062) * feat(Streaks): show DM streak badge for consecutive day messages * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * bleh * ConcatenatedModules stuff * Update concatenatedModules.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../_core/concatenatedModules.tsx | 11 +- src/equicordplugins/channelTabs/index.tsx | 2 +- src/equicordplugins/iconViewer/index.tsx | 2 +- src/equicordplugins/streaks/index.tsx | 144 ++++++++++++++++++ src/equicordplugins/streaks/style.css | 13 ++ 5 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 src/equicordplugins/streaks/index.tsx create mode 100644 src/equicordplugins/streaks/style.css diff --git a/src/equicordplugins/_core/concatenatedModules.tsx b/src/equicordplugins/_core/concatenatedModules.tsx index 0cde5f7be..c6b4a6662 100644 --- a/src/equicordplugins/_core/concatenatedModules.tsx +++ b/src/equicordplugins/_core/concatenatedModules.tsx @@ -14,19 +14,20 @@ export default definePlugin({ name: "ConcatenatedModules", description: "Extract modules that have been concatenated by the bundler", authors: [Devs.thororen], - - required: true, - patches: [ { find: "AngleBracketsIcon", replacement: { match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/, - replace: "$self.iconsModule($1),$&" + replace: "$self.setIconsModule($1),$&" } } ], - iconsModule(value: IconsDef) { + setIconsModule(value: IconsDef) { iconsModule = value; + + // incase you dont want to use iconviewer you can do + // Vencord.Plugins.plugins.ConcatenatedModules.iconsModule instead for icons and viewing paths + this.iconsModule = value; }, }); diff --git a/src/equicordplugins/channelTabs/index.tsx b/src/equicordplugins/channelTabs/index.tsx index b536a216c..0a7d3b75f 100644 --- a/src/equicordplugins/channelTabs/index.tsx +++ b/src/equicordplugins/channelTabs/index.tsx @@ -49,7 +49,7 @@ export default definePlugin({ description: "Group your commonly visited channels in tabs, like a browser", tags: ["Appearance", "Customisation", "Organisation", "Servers"], authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn], - dependencies: ["ContextMenuAPI"], + dependencies: ["ContextMenuAPI", "ConcatenatedModules"], contextMenus: { "channel-mention-context": contextMenuPatch, "channel-context": contextMenuPatch, diff --git a/src/equicordplugins/iconViewer/index.tsx b/src/equicordplugins/iconViewer/index.tsx index 6df0d72f1..320979e3f 100644 --- a/src/equicordplugins/iconViewer/index.tsx +++ b/src/equicordplugins/iconViewer/index.tsx @@ -21,7 +21,7 @@ export default definePlugin({ description: "Adds a new tab to settings to preview all icons.", tags: ["Developers"], authors: [EquicordDevs.iamme], - dependencies: ["Settings"], + dependencies: ["Settings", "ConcatenatedModules"], startAt: StartAt.WebpackReady, toolboxActions: { "Open Icons Tab"() { diff --git a/src/equicordplugins/streaks/index.tsx b/src/equicordplugins/streaks/index.tsx new file mode 100644 index 000000000..3a067e18b --- /dev/null +++ b/src/equicordplugins/streaks/index.tsx @@ -0,0 +1,144 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import { DataStore } from "@api/index"; +import { DecoratorProps } from "@api/MemberListDecorators"; +import { iconsModule } from "@equicordplugins/_core/concatenatedModules"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import definePlugin from "@utils/types"; +import { Message } from "@vencord/discord-types"; +import { ChannelStore, moment, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common"; + +const cl = classNameFactory("vc-streaks-"); +const dataKey = "vc-streaks-data"; + +const Flags = { + SENT: 1, + RECEIVED: 2, + BOTH: 3 +}; + +const STREAK_THRESHOLDS = { + ELITE: 100, + DIAMOND: 60, + GOLD: 30, + SILVER: 14 +}; + +interface UserStreak { + count: number; + lastDay: string; + todayFlags: number; + todayDate: string; +} + +let streaks: Record = {}; + +const todayKey = () => moment().format("YYYY-MM-DD"); +const yesterdayKey = () => moment().subtract(1, "day").format("YYYY-MM-DD"); + +const updateStreak = (userId: string, outgoing: boolean) => { + const today = todayKey(); + const entry = streaks[userId] ??= { count: 0, lastDay: "", todayFlags: 0, todayDate: "" }; + + if (entry.todayDate !== today) { + entry.todayDate = today; + entry.todayFlags = 0; + } + + const next = entry.todayFlags | (outgoing ? Flags.SENT : Flags.RECEIVED); + if (next === entry.todayFlags) return; + entry.todayFlags = next; + + if (next === Flags.BOTH && entry.lastDay !== today) { + entry.count = entry.lastDay === yesterdayKey() ? entry.count + 1 : 1; + entry.lastDay = today; + } + + DataStore.set(dataKey, streaks); +}; + +const getStreakInfo = (userId: string) => { + const entry = streaks[userId]; + if (!entry) return null; + const today = todayKey(); + if (entry.lastDay !== today && entry.lastDay !== yesterdayKey()) return null; + return { count: entry.count, active: entry.lastDay === today }; +}; + +const colorFor = (streak: number) => { + if (streak >= STREAK_THRESHOLDS.ELITE) return "#9b39fe"; + if (streak >= STREAK_THRESHOLDS.DIAMOND) return "#f7409c"; + if (streak >= STREAK_THRESHOLDS.GOLD) return "#f75340"; + if (streak >= STREAK_THRESHOLDS.SILVER) return "#f57b0b"; + return "#f59e0b"; +}; + +const StreakBadge = ({ userId }: { userId: string; }) => { + const info = getStreakInfo(userId); + if (!info || info.count < 1) return null; + + const FireIcon = iconsModule?.FireIcon; + const color = info.active ? colorFor(info.count) : "#9ca3af"; + + return ( + + {tooltipProps => ( + + {FireIcon && } + {info.count} + + )} + + ); +}; + +export default definePlugin({ + name: "Streaks", + description: "Shows a streak next to a user when you exchange DMs with them on consecutive days.", + authors: [EquicordDevs.Moowi], + tags: ["Friends", "Fun"], + dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI", "ConcatenatedModules"], + + async start() { + streaks = await DataStore.get(dataKey) ?? {}; + }, + + stop() { + streaks = {}; + }, + + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }: { optimistic: boolean; type: string; message: Message; channelId: string; }) { + if (optimistic || type !== "MESSAGE_CREATE" || message.state === "SENDING") return; + if (message.author?.bot || channelId !== SelectedChannelStore.getChannelId()) return; + + const channel = ChannelStore.getChannel(channelId); + if (!channel.isDM()) return; + + const recipientId = channel.recipients[0]; + if (!recipientId) return; + + const me = UserStore.getCurrentUser()?.id; + if (message.author.id === me) updateStreak(recipientId, true); + else if (message.author.id === recipientId) updateStreak(recipientId, false); + }, + }, + + renderMessageDecoration({ message }) { + const userId = message?.author?.id; + if (!userId || userId === UserStore.getCurrentUser()?.id) return null; + return ; + }, + + renderMemberListDecorator({ user }: DecoratorProps) { + if (!user || user.id === UserStore.getCurrentUser()?.id) return null; + return ; + }, +}); diff --git a/src/equicordplugins/streaks/style.css b/src/equicordplugins/streaks/style.css new file mode 100644 index 000000000..fc153cd8e --- /dev/null +++ b/src/equicordplugins/streaks/style.css @@ -0,0 +1,13 @@ +.vc-streaks-badge { + display: inline-flex; + align-items: center; + gap: 2px; + margin-left: 4px; + vertical-align: middle; +} + +.vc-streaks-count { + font-size: 12px; + font-weight: 700; + line-height: 1; +}