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>
This commit is contained in:
Moowi 2026-05-09 05:32:42 +03:00 • committed by GitHub
parent 91ceac2c3d
commit 747c7f23ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 165 additions and 7 deletions

View file

@ -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;
},
});

View file

@ -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,

View file

@ -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"() {

View file

@ -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<string, UserStreak> = {};
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 (
<Tooltip text={`${info.count} day streak`}>
{tooltipProps => (
<span {...tooltipProps} className={cl("badge")} style={{ color }}>
{FireIcon && <FireIcon size="xs" color={color} />}
<span className={cl("count")}>{info.count}</span>
</span>
)}
</Tooltip>
);
};
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 <StreakBadge userId={userId} />;
},
renderMemberListDecorator({ user }: DecoratorProps) {
if (!user || user.id === UserStore.getCurrentUser()?.id) return null;
return <StreakBadge userId={user.id} />;
},
});

View file

@ -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;
}