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,6 +147,18 @@ prestigeRouter.post("/", async(context) => {
|
|||||||
|
|
||||||
const prestigeCount = prestigeData.count;
|
const prestigeCount = prestigeData.count;
|
||||||
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
||||||
|
|
||||||
|
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", {
|
void postMilestoneWebhook(discordId, "prestige", {
|
||||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||||
/* v8 ignore next -- @preserve */
|
/* v8 ignore next -- @preserve */
|
||||||
@@ -158,6 +170,7 @@ prestigeRouter.post("/", async(context) => {
|
|||||||
/* v8 ignore next 2 -- @preserve */
|
/* v8 ignore next 2 -- @preserve */
|
||||||
transcendence: prestigeState.transcendence?.count ?? 0,
|
transcendence: prestigeState.transcendence?.count ?? 0,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return context.json({
|
return context.json({
|
||||||
milestoneRunestones: milestoneRunestones,
|
milestoneRunestones: milestoneRunestones,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
|||||||
: "suffix";
|
: "suffix";
|
||||||
return {
|
return {
|
||||||
enableNotifications: rawObject.enableNotifications === true,
|
enableNotifications: rawObject.enableNotifications === true,
|
||||||
|
enablePrestigeAnnouncements: rawObject.enablePrestigeAnnouncements !== false,
|
||||||
enableSounds: rawObject.enableSounds === true,
|
enableSounds: rawObject.enableSounds === true,
|
||||||
numberFormat: numberFormat,
|
numberFormat: numberFormat,
|
||||||
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
||||||
@@ -222,6 +223,7 @@ profileRouter.put("/", authMiddleware, async(context) => {
|
|||||||
: "suffix";
|
: "suffix";
|
||||||
const profileSettings: ProfileSettings = {
|
const profileSettings: ProfileSettings = {
|
||||||
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
||||||
|
enablePrestigeAnnouncements: body.profileSettings.enablePrestigeAnnouncements ?? true,
|
||||||
enableSounds: body.profileSettings.enableSounds ?? false,
|
enableSounds: body.profileSettings.enableSounds ?? false,
|
||||||
numberFormat: numberFormat,
|
numberFormat: numberFormat,
|
||||||
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { GameState } from "@elysium/types";
|
|||||||
|
|
||||||
vi.mock("../../src/db/client.js", () => ({
|
vi.mock("../../src/db/client.js", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
player: { update: vi.fn() },
|
player: { findUnique: vi.fn(), update: vi.fn() },
|
||||||
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: 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", () => {
|
describe("prestige route", () => {
|
||||||
let app: Hono;
|
let app: Hono;
|
||||||
let prisma: {
|
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> };
|
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 };
|
const body = await res.json() as { runestones: number; newPrestigeCount: number };
|
||||||
expect(body.newPrestigeCount).toBe(1);
|
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", () => {
|
describe("POST /buy-upgrade", () => {
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ const EditProfileModal = ({
|
|||||||
void handleNotificationsEnable();
|
void handleNotificationsEnable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePrestigeAnnouncementsToggle(): void {
|
||||||
|
toggleSetting("enablePrestigeAnnouncements");
|
||||||
|
}
|
||||||
|
|
||||||
const isSaveDisabled = saving || characterName.trim() === "";
|
const isSaveDisabled = saving || characterName.trim() === "";
|
||||||
|
|
||||||
let saveLabel = "Save Profile";
|
let saveLabel = "Save Profile";
|
||||||
@@ -417,6 +421,23 @@ const EditProfileModal = ({
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div className="edit-profile-section">
|
<div className="edit-profile-section">
|
||||||
|
|||||||
@@ -48,11 +48,17 @@ interface ProfileSettings {
|
|||||||
* Whether browser system notifications are enabled.
|
* Whether browser system notifications are enabled.
|
||||||
*/
|
*/
|
||||||
enableNotifications: boolean;
|
enableNotifications: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether prestige milestones are announced in the Discord server.
|
||||||
|
*/
|
||||||
|
enablePrestigeAnnouncements: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||||
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||||
enableNotifications: false,
|
enableNotifications: false,
|
||||||
|
enablePrestigeAnnouncements: true,
|
||||||
enableSounds: false,
|
enableSounds: false,
|
||||||
numberFormat: "suffix",
|
numberFormat: "suffix",
|
||||||
showAchievementsUnlocked: true,
|
showAchievementsUnlocked: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user