generated from nhcarrigan/template
feat: add opt-out toggle for prestige bot announcements
Adds enablePrestigeAnnouncements to ProfileSettings (defaults to true). The prestige route now checks this setting before posting the Discord webhook, and the edit profile modal exposes a toggle in the Sounds & Notifications section so players can opt out. Closes #169
This commit is contained in:
@@ -147,17 +147,30 @@ prestigeRouter.post("/", async(context) => {
|
||||
|
||||
const prestigeCount = prestigeData.count;
|
||||
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
||||
void postMilestoneWebhook(discordId, "prestige", {
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next -- @preserve */
|
||||
apotheosis: prestigeState.apotheosis?.count ?? 0,
|
||||
|
||||
prestige: prestigeData.count,
|
||||
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next 2 -- @preserve */
|
||||
transcendence: prestigeState.transcendence?.count ?? 0,
|
||||
const playerRecord = await prisma.player.findUnique({
|
||||
select: { profileSettings: true },
|
||||
where: { discordId },
|
||||
});
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Runtime shape check for JSON field */
|
||||
const playerSettings = playerRecord?.profileSettings as
|
||||
Record<string, unknown> | null | undefined;
|
||||
const announcementsEnabled
|
||||
= playerSettings?.enablePrestigeAnnouncements !== false;
|
||||
|
||||
if (announcementsEnabled) {
|
||||
void postMilestoneWebhook(discordId, "prestige", {
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next -- @preserve */
|
||||
apotheosis: prestigeState.apotheosis?.count ?? 0,
|
||||
|
||||
prestige: prestigeData.count,
|
||||
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next 2 -- @preserve */
|
||||
transcendence: prestigeState.transcendence?.count ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json({
|
||||
milestoneRunestones: milestoneRunestones,
|
||||
|
||||
@@ -47,6 +47,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
||||
: "suffix";
|
||||
return {
|
||||
enableNotifications: rawObject.enableNotifications === true,
|
||||
enablePrestigeAnnouncements: rawObject.enablePrestigeAnnouncements !== false,
|
||||
enableSounds: rawObject.enableSounds === true,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
||||
@@ -222,6 +223,7 @@ profileRouter.put("/", authMiddleware, async(context) => {
|
||||
: "suffix";
|
||||
const profileSettings: ProfileSettings = {
|
||||
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
||||
enablePrestigeAnnouncements: body.profileSettings.enablePrestigeAnnouncements ?? true,
|
||||
enableSounds: body.profileSettings.enableSounds ?? false,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { GameState } from "@elysium/types";
|
||||
|
||||
vi.mock("../../src/db/client.js", () => ({
|
||||
prisma: {
|
||||
player: { update: vi.fn() },
|
||||
player: { findUnique: vi.fn(), update: vi.fn() },
|
||||
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
|
||||
},
|
||||
}));
|
||||
@@ -47,7 +47,7 @@ const makeState = (overrides: Partial<GameState> = {}): GameState => ({
|
||||
describe("prestige route", () => {
|
||||
let app: Hono;
|
||||
let prisma: {
|
||||
player: { update: ReturnType<typeof vi.fn> };
|
||||
player: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
||||
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn>; updateMany: ReturnType<typeof vi.fn> };
|
||||
};
|
||||
|
||||
@@ -128,6 +128,18 @@ describe("prestige route", () => {
|
||||
const body = await res.json() as { runestones: number; newPrestigeCount: number };
|
||||
expect(body.newPrestigeCount).toBe(1);
|
||||
});
|
||||
|
||||
it("skips webhook when enablePrestigeAnnouncements is false", async () => {
|
||||
const { postMilestoneWebhook } = await import("../../src/services/webhook.js");
|
||||
const state = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce({ profileSettings: { enablePrestigeAnnouncements: false } } as never);
|
||||
const res = await post("");
|
||||
expect(res.status).toBe(200);
|
||||
expect(postMilestoneWebhook).not.toHaveBeenCalledWith(expect.anything(), "prestige", expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /buy-upgrade", () => {
|
||||
|
||||
@@ -225,6 +225,10 @@ const EditProfileModal = ({
|
||||
void handleNotificationsEnable();
|
||||
}
|
||||
|
||||
function handlePrestigeAnnouncementsToggle(): void {
|
||||
toggleSetting("enablePrestigeAnnouncements");
|
||||
}
|
||||
|
||||
const isSaveDisabled = saving || characterName.trim() === "";
|
||||
|
||||
let saveLabel = "Save Profile";
|
||||
@@ -417,6 +421,23 @@ const EditProfileModal = ({
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`stat-toggle-btn ${
|
||||
profileSettings.enablePrestigeAnnouncements
|
||||
? "stat-toggle-on"
|
||||
: "stat-toggle-off"
|
||||
}`}
|
||||
onClick={handlePrestigeAnnouncementsToggle}
|
||||
type="button"
|
||||
>
|
||||
<span>{"⭐ Prestige Bot Announcements"}</span>
|
||||
<span className="stat-toggle-indicator">
|
||||
{profileSettings.enablePrestigeAnnouncements
|
||||
? "✓ On"
|
||||
: "Off"
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="edit-profile-section">
|
||||
|
||||
@@ -48,11 +48,17 @@ interface ProfileSettings {
|
||||
* Whether browser system notifications are enabled.
|
||||
*/
|
||||
enableNotifications: boolean;
|
||||
|
||||
/**
|
||||
* Whether prestige milestones are announced in the Discord server.
|
||||
*/
|
||||
enablePrestigeAnnouncements: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||
enableNotifications: false,
|
||||
enablePrestigeAnnouncements: true,
|
||||
enableSounds: false,
|
||||
numberFormat: "suffix",
|
||||
showAchievementsUnlocked: true,
|
||||
|
||||
Reference in New Issue
Block a user