From 11a8bacf2f5d84c46593b75625b6707eab1554d6 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:24:23 -0400 Subject: [PATCH 01/13] Link the privacy policy and terms from the About panel Renames Terms&Conditions.md to TermsAndConditions.md so the link needs no URL escaping. Co-Authored-By: Claude Fable 5 --- .dockerignore | 2 +- Terms&Conditions.md => TermsAndConditions.md | 0 src/routes/about/get.ts | 11 +++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) rename Terms&Conditions.md => TermsAndConditions.md (100%) diff --git a/.dockerignore b/.dockerignore index 7801848..5f37fd6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,4 @@ icon.svg LICENSE Privacy.md README.md -Terms&Conditions.md \ No newline at end of file +TermsAndConditions.md \ No newline at end of file diff --git a/Terms&Conditions.md b/TermsAndConditions.md similarity index 100% rename from Terms&Conditions.md rename to TermsAndConditions.md diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index 5e3e3a1..d37841e 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -8,6 +8,9 @@ const COLOR = "#5865f2"; const GITHUB_URL = "https://github.com/altrup/RingVC"; const SUPPORT_URL = "https://discord.gg/bxBePEnndq"; +const PRIVACY_URL = "https://github.com/altrup/RingVC/blob/main/Privacy.md"; +const TERMS_URL = + "https://github.com/altrup/RingVC/blob/main/TermsAndConditions.md"; export const aboutGet: Handler<"GET"> = (router, interaction, state) => { return { @@ -32,6 +35,14 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setLabel("Support Server") .setStyle(ButtonStyle.Link) .setURL(SUPPORT_URL), + new ButtonBuilder() + .setLabel("Privacy Policy") + .setStyle(ButtonStyle.Link) + .setURL(PRIVACY_URL), + new ButtonBuilder() + .setLabel("Terms & Conditions") + .setStyle(ButtonStyle.Link) + .setURL(TERMS_URL), ), navBar(router, interaction), ], From d11a968fc7b87fce3801b84939630f00f0b33483 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:25:19 -0400 Subject: [PATCH 02/13] Add an anonymous feedback form to the About panel A Give feedback button opens a modal whose text goes to a new feedback table, storing only the text and a timestamp. Co-Authored-By: Claude Fable 5 --- Privacy.md | 1 + src/main/db/database.types.ts | 18 ++++++ src/main/db/feedback.ts | 5 ++ src/routes/about/_shared.ts | 2 + src/routes/about/about.test.ts | 57 +++++++++++++++++++ src/routes/about/feedback/modal.ts | 25 ++++++++ src/routes/about/feedback/post.ts | 28 +++++++++ src/routes/about/get.ts | 11 +++- src/routes/index.ts | 6 ++ .../migrations/20260725000000_feedback.sql | 11 ++++ 10 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/main/db/feedback.ts create mode 100644 src/routes/about/_shared.ts create mode 100644 src/routes/about/about.test.ts create mode 100644 src/routes/about/feedback/modal.ts create mode 100644 src/routes/about/feedback/post.ts create mode 100644 supabase/migrations/20260725000000_feedback.sql diff --git a/Privacy.md b/Privacy.md index ea9555b..0819988 100644 --- a/Privacy.md +++ b/Privacy.md @@ -8,6 +8,7 @@ - User IDs of users who use block or filter commands to set up a filter of who can ring them, and the User IDs of the people that they block - Voice Channel IDs and User IDs when a user is signed up for a Voice Channel +- Feedback submitted through the feedback form (the text and a timestamp only, never who submitted it) ## Temporary Data diff --git a/src/main/db/database.types.ts b/src/main/db/database.types.ts index 050ab38..62d4578 100644 --- a/src/main/db/database.types.ts +++ b/src/main/db/database.types.ts @@ -107,6 +107,24 @@ export type Database = { }; Relationships: []; }; + feedback: { + Row: { + content: string; + created_at: string; + id: number; + }; + Insert: { + content: string; + created_at?: string; + id?: never; + }; + Update: { + content?: string; + created_at?: string; + id?: never; + }; + Relationships: []; + }; filter_entries: { Row: { channel_id: string | null; diff --git a/src/main/db/feedback.ts b/src/main/db/feedback.ts new file mode 100644 index 0000000..4325b84 --- /dev/null +++ b/src/main/db/feedback.ts @@ -0,0 +1,5 @@ +import { db, throwOnError } from "./client"; + +export const submitFeedback = async (content: string): Promise => { + throwOnError(await db.from("feedback").insert({ content })); +}; diff --git a/src/routes/about/_shared.ts b/src/routes/about/_shared.ts new file mode 100644 index 0000000..5d5ba10 --- /dev/null +++ b/src/routes/about/_shared.ts @@ -0,0 +1,2 @@ +export const PANEL = "/about"; +export const FEEDBACK = "/about/feedback"; diff --git a/src/routes/about/about.test.ts b/src/routes/about/about.test.ts new file mode 100644 index 0000000..06c2068 --- /dev/null +++ b/src/routes/about/about.test.ts @@ -0,0 +1,57 @@ +import { Interaction } from "discord.js"; +import { beforeEach, expect, test, vi } from "vitest"; + +import { submitFeedback } from "@db/feedback"; + +import { aboutFeedbackPost } from "./feedback/post"; + +vi.mock("@db/feedback", () => ({ + submitFeedback: vi.fn(), +})); + +const interaction = { + user: { id: "caller" }, + isChatInputCommand: () => false, +} as unknown as Interaction; + +const state = (content: string) => + ({ + params: {}, + path: "/about/feedback", + queryParams: new URLSearchParams(), + timestamp: 0, + fields: { getTextInputValue: () => content }, + }) as unknown as Parameters[2]; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +test("submitting feedback stores the trimmed text", async () => { + const result = await aboutFeedbackPost( + undefined as never, + interaction, + state(" great bot! "), + ); + + expect(submitFeedback).toHaveBeenCalledExactlyOnceWith("great bot!"); + expect(result.redirect).toBe("/about"); + const flashParams = new URLSearchParams( + result.queryParams as Record, + ); + expect(flashParams.get("level")).toBe("success"); +}); + +test("whitespace-only feedback stores nothing", async () => { + const result = await aboutFeedbackPost( + undefined as never, + interaction, + state(" "), + ); + + expect(submitFeedback).not.toHaveBeenCalled(); + const flashParams = new URLSearchParams( + result.queryParams as Record, + ); + expect(flashParams.get("level")).toBe("warn"); +}); diff --git a/src/routes/about/feedback/modal.ts b/src/routes/about/feedback/modal.ts new file mode 100644 index 0000000..840d543 --- /dev/null +++ b/src/routes/about/feedback/modal.ts @@ -0,0 +1,25 @@ +import { RouteModalBuilder } from "discord-embed-router"; +import { LabelBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; + +import { Handler } from "@routes/types"; + +import { FEEDBACK } from "../_shared"; + +export const aboutFeedbackModal: Handler<"MODAL"> = (router) => + new RouteModalBuilder(router) + .setTo(FEEDBACK, { method: "POST" }) + .setTitle("Give feedback") + .addLabelComponents( + new LabelBuilder() + .setLabel("Your feedback") + .setDescription( + "Submissions are anonymous. Nothing about who you are is stored.", + ) + .setTextInputComponent( + new TextInputBuilder() + .setCustomId("content") + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setPlaceholder("Bug reports, feature ideas, anything"), + ), + ); diff --git a/src/routes/about/feedback/post.ts b/src/routes/about/feedback/post.ts new file mode 100644 index 0000000..8d8d540 --- /dev/null +++ b/src/routes/about/feedback/post.ts @@ -0,0 +1,28 @@ +import { submitFeedback } from "@db/feedback"; +import { flashRedirect } from "@routes/lib/flash"; +import { Handler } from "@routes/types"; + +import { PANEL } from "../_shared"; + +export const aboutFeedbackPost: Handler<"POST"> = async ( + router, + interaction, + state, +) => { + const content = (state.fields?.getTextInputValue("content") ?? "").trim(); + if (!content) + return flashRedirect( + interaction, + PANEL, + "Feedback was empty, nothing was sent", + "warn", + ); + + await submitFeedback(content); + return flashRedirect( + interaction, + PANEL, + "Feedback sent. Thank you!", + "success", + ); +}; diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index d37841e..c6f2f44 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -1,9 +1,12 @@ +import { RouteButtonBuilder } from "discord-embed-router"; import { ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { navBar, row } from "@routes/lib/components"; import { withFlash } from "@routes/lib/flash"; import { Handler } from "@routes/types"; +import { FEEDBACK } from "./_shared"; + const COLOR = "#5865f2"; const GITHUB_URL = "https://github.com/altrup/RingVC"; @@ -21,7 +24,7 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setDescription( withFlash( state.queryParams, - "RingVC is free and open source. Star it on GitHub, or join the support server for help and updates.", + "RingVC is free and open source. Star it on GitHub, or join the support server for help and updates.\n\nHave a bug report or feature idea? Give anonymous feedback below.", ), ), ], @@ -44,6 +47,12 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setStyle(ButtonStyle.Link) .setURL(TERMS_URL), ), + row( + new RouteButtonBuilder(router) + .setLabel("Give feedback") + .setStyle(ButtonStyle.Secondary) + .setTo(FEEDBACK, { method: "MODAL" }), + ), navBar(router, interaction), ], }; diff --git a/src/routes/index.ts b/src/routes/index.ts index ddb22f9..1bc87c1 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,7 @@ import { RingRouter } from "@routes/types"; +import { aboutFeedbackModal } from "./about/feedback/modal"; +import { aboutFeedbackPost } from "./about/feedback/post"; import { aboutGet } from "./about/get"; import { deleteDataGet } from "./delete-data/get"; import { deleteDataModal } from "./delete-data/modal"; @@ -47,6 +49,10 @@ export const registerRoutes = (router: RingRouter) => { router.get("/help", helpGet); router.get("/help/catalog", catalogGet); router.get("/about", aboutGet); + router.route("/about/feedback", { + modal: aboutFeedbackModal, + post: aboutFeedbackPost, + }); // scoped panels answer their bare path as the global scope, so the scope-switch // select can target "{/:channelId}" and fall back to global when cleared diff --git a/supabase/migrations/20260725000000_feedback.sql b/supabase/migrations/20260725000000_feedback.sql new file mode 100644 index 0000000..4875543 --- /dev/null +++ b/supabase/migrations/20260725000000_feedback.sql @@ -0,0 +1,11 @@ +-- Feedback submitted through the About panel's feedback form. Anonymous by +-- design: only the text and a timestamp, never the submitter's identity. +create table feedback ( + id bigint generated always as identity primary key, + content text not null, + created_at timestamptz not null default now() +); + +grant select, insert, update, delete on feedback to service_role; + +alter table feedback enable row level security; From 74c2887fd7afc671a4aeea290b5e32e16df92876 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:42:00 -0400 Subject: [PATCH 03/13] Add an /about command opening the About panel Co-Authored-By: Claude Fable 5 --- src/commands/about.ts | 18 ++++++++++++++++++ src/commands/commandNames.ts | 1 + src/commands/commands.ts | 2 ++ src/routes/help/catalog/get.ts | 1 + 4 files changed, 22 insertions(+) create mode 100644 src/commands/about.ts diff --git a/src/commands/about.ts b/src/commands/about.ts new file mode 100644 index 0000000..da444d2 --- /dev/null +++ b/src/commands/about.ts @@ -0,0 +1,18 @@ +import { + ChatInputCommandInteraction, + MessageFlags, + SlashCommandBuilder, +} from "discord.js"; + +import { RingRouter } from "@routes/types"; + +export const about = { + data: new SlashCommandBuilder() + .setName("about") + .setDescription("Project links, policies, and feedback"), + async execute(router: RingRouter, interaction: ChatInputCommandInteraction) { + await router.dispatch(interaction, "/about", { + flags: [MessageFlags.Ephemeral], + }); + }, +}; diff --git a/src/commands/commandNames.ts b/src/commands/commandNames.ts index 856a275..e17af9b 100644 --- a/src/commands/commandNames.ts +++ b/src/commands/commandNames.ts @@ -2,6 +2,7 @@ export const commandNamesList = [ "ringvc", "help", "catalog", + "about", "delete_data", "ring", "ring_defaults", diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 36c3af6..74a7ffe 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -1,5 +1,6 @@ import { ChatInputCommandInteraction, SharedSlashCommand } from "discord.js"; +import { about } from "@commands/about"; import { catalog } from "@commands/catalog"; import { defaultRingRecipients } from "@commands/defaultRingRecipient"; import { deleteData } from "@commands/deleteData"; @@ -34,6 +35,7 @@ export const commands: CommandImplementation[] = [ ringvc, help, catalog, + about, deleteData, ring, ringDefaults, diff --git a/src/routes/help/catalog/get.ts b/src/routes/help/catalog/get.ts index 836dec0..0a98457 100644 --- a/src/routes/help/catalog/get.ts +++ b/src/routes/help/catalog/get.ts @@ -55,6 +55,7 @@ export const catalogGet: Handler<"GET"> = (router, interaction, state) => { ["ringvc", "the home panel"], ["help", "getting started"], ["catalog", "this command catalog"], + ["about", "project links, policies, and feedback"], ["signup", "your signups panel (bare, outside a voice channel)"], ["filter", "your filter panel"], ["default_ring_recipients", "ring recipients and auto-ring panel"], From 022426a4f89972b8b964bee12ca8315db012f793 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:48:20 -0400 Subject: [PATCH 04/13] Shorten the feedback modal description Co-Authored-By: Claude Fable 5 --- src/routes/about/feedback/modal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/about/feedback/modal.ts b/src/routes/about/feedback/modal.ts index 840d543..ecdd94d 100644 --- a/src/routes/about/feedback/modal.ts +++ b/src/routes/about/feedback/modal.ts @@ -12,9 +12,7 @@ export const aboutFeedbackModal: Handler<"MODAL"> = (router) => .addLabelComponents( new LabelBuilder() .setLabel("Your feedback") - .setDescription( - "Submissions are anonymous. Nothing about who you are is stored.", - ) + .setDescription("Submissions are anonymous.") .setTextInputComponent( new TextInputBuilder() .setCustomId("content") From d7f05913be716d644d73ce8acef687a9787a3579 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:48:20 -0400 Subject: [PATCH 05/13] Split the About links into project and policy rows Co-Authored-By: Claude Fable 5 --- src/routes/about/get.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index c6f2f44..a1bf6ee 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -38,6 +38,8 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setLabel("Support Server") .setStyle(ButtonStyle.Link) .setURL(SUPPORT_URL), + ), + row( new ButtonBuilder() .setLabel("Privacy Policy") .setStyle(ButtonStyle.Link) From 76818aaa369bcc008fade50eca68a4285ef77dd9 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:49:06 -0400 Subject: [PATCH 06/13] Make the Give feedback button primary Co-Authored-By: Claude Fable 5 --- src/routes/about/get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index a1bf6ee..40e8596 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -52,7 +52,7 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { row( new RouteButtonBuilder(router) .setLabel("Give feedback") - .setStyle(ButtonStyle.Secondary) + .setStyle(ButtonStyle.Primary) .setTo(FEEDBACK, { method: "MODAL" }), ), navBar(router, interaction), From 953c1692f6212cd6d93de45226f7f1be4678c7cb Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:03:47 -0400 Subject: [PATCH 07/13] move feedhback button --- src/routes/about/get.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index 40e8596..8114ebb 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -38,6 +38,10 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setLabel("Support Server") .setStyle(ButtonStyle.Link) .setURL(SUPPORT_URL), + new RouteButtonBuilder(router) + .setLabel("Give feedback") + .setStyle(ButtonStyle.Primary) + .setTo(FEEDBACK, { method: "MODAL" }), ), row( new ButtonBuilder() @@ -49,12 +53,6 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setStyle(ButtonStyle.Link) .setURL(TERMS_URL), ), - row( - new RouteButtonBuilder(router) - .setLabel("Give feedback") - .setStyle(ButtonStyle.Primary) - .setTo(FEEDBACK, { method: "MODAL" }), - ), navBar(router, interaction), ], }; From 66d6492237b2eb596f272f1da7a30ebcd29ff135 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:19:15 -0400 Subject: [PATCH 08/13] Render nav emojis with Discord's artwork Emojis move from option labels (which render in the OS emoji font) to the dedicated emoji field, which Discord draws with its own artwork. Co-Authored-By: Claude Fable 5 --- src/routes/lib/components.ts | 41 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/routes/lib/components.ts b/src/routes/lib/components.ts index 447693b..7a5624f 100644 --- a/src/routes/lib/components.ts +++ b/src/routes/lib/components.ts @@ -50,12 +50,6 @@ export const editSelectRow = < return new ActionRowBuilder().addComponents(builder).toJSON(); }; -export const homeButton = (router: RingRouter): RingButton => - new RouteButtonBuilder(router) - .setLabel("🏠 Home") - .setStyle(ButtonStyle.Secondary) - .setTo("/"); - export const backButton = (router: RingRouter, path: string): RingButton => new RouteButtonBuilder(router) .setLabel("Back") @@ -73,18 +67,30 @@ export type Section = | "about" | "delete"; -type Tab = { section: Section; label: string; path: string }; +type Tab = { section: Section; emoji: string; label: string; path: string }; -// every section the bar offers, in display order +// every section the bar offers, in display order. The emoji rides the +// dedicated option field, not the label: labels render in the OS emoji font +// while the emoji field gets Discord's own artwork, matching embed titles const SECTIONS: readonly Tab[] = [ - { section: "home", label: "🏠 Home", path: "/" }, - { section: "signups", label: "🔔 Signups", path: "/signups" }, - { section: "filters", label: "đŸ›Ąī¸ Filters", path: "/filter/global" }, - { section: "ringees", label: "đŸ“Ŗ Ring", path: "/recipients/global" }, - { section: "mode", label: "💤 Mode", path: "/mode" }, - { section: "help", label: "📖 Help", path: "/help" }, - { section: "about", label: "â„šī¸ About", path: "/about" }, - { section: "delete", label: "đŸ—‘ī¸ Delete data", path: "/delete-data" }, + { section: "home", emoji: "🏠", label: "Home", path: "/" }, + { section: "signups", emoji: "🔔", label: "Signups", path: "/signups" }, + { section: "filters", emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, + { + section: "ringees", + emoji: "đŸ“Ŗ", + label: "Ring", + path: "/recipients/global", + }, + { section: "mode", emoji: "💤", label: "Mode", path: "/mode" }, + { section: "help", emoji: "📖", label: "Help", path: "/help" }, + { section: "about", emoji: "â„šī¸", label: "About", path: "/about" }, + { + section: "delete", + emoji: "đŸ—‘ī¸", + label: "Delete data", + path: "/delete-data", + }, ]; // the persistent section bar every panel ends on. A string select fits all @@ -102,10 +108,11 @@ export const navBar = ( interaction.member.voice.channel ); - const option = ({ section, label, path }: Tab) => { + const option = ({ section, emoji, label, path }: Tab) => { const target = section === "ringees" && inVoice ? "/ring" : path; return new RouteStringSelectMenuOptionBuilder(router) .setTo(target) + .setEmoji(emoji) .setLabel(label); }; From 431842345c418cfe3ddaf334c67e2303995d9cf6 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:20:12 -0400 Subject: [PATCH 09/13] Remove the home panel and the /ringvc command The getting-started view answers "/" so older messages' components and fallback targets keep working; the section switcher loses its Home tab. Co-Authored-By: Claude Fable 5 --- src/commands/commandNames.ts | 1 - src/commands/commands.ts | 2 -- src/commands/ringvc.ts | 18 ------------------ src/routes/get.ts | 27 --------------------------- src/routes/help/catalog/get.ts | 1 - src/routes/index.ts | 6 +++--- src/routes/lib/components.ts | 10 +--------- src/routes/lib/emoji.ts | 8 -------- 8 files changed, 4 insertions(+), 69 deletions(-) delete mode 100644 src/commands/ringvc.ts delete mode 100644 src/routes/get.ts delete mode 100644 src/routes/lib/emoji.ts diff --git a/src/commands/commandNames.ts b/src/commands/commandNames.ts index e17af9b..7b41641 100644 --- a/src/commands/commandNames.ts +++ b/src/commands/commandNames.ts @@ -1,5 +1,4 @@ export const commandNamesList = [ - "ringvc", "help", "catalog", "about", diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 74a7ffe..5048f8b 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -14,7 +14,6 @@ import { mode } from "@commands/mode"; import { quit } from "@commands/quit"; import { ring } from "@commands/ring"; import { ringDefaults } from "@commands/ringDefaults"; -import { ringvc } from "@commands/ringvc"; import { signup } from "@commands/signup"; import { signuprole } from "@commands/signuprole"; import { unsignup } from "@commands/unsignup"; @@ -32,7 +31,6 @@ export type CommandImplementation = { }; export const commands: CommandImplementation[] = [ - ringvc, help, catalog, about, diff --git a/src/commands/ringvc.ts b/src/commands/ringvc.ts deleted file mode 100644 index 0a84c74..0000000 --- a/src/commands/ringvc.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - ChatInputCommandInteraction, - MessageFlags, - SlashCommandBuilder, -} from "discord.js"; - -import { RingRouter } from "@routes/types"; - -export const ringvc = { - data: new SlashCommandBuilder() - .setName("ringvc") - .setDescription("Open the RingVC home panel"), - async execute(router: RingRouter, interaction: ChatInputCommandInteraction) { - await router.dispatch(interaction, "/", { - flags: [MessageFlags.Ephemeral], - }); - }, -}; diff --git a/src/routes/get.ts b/src/routes/get.ts deleted file mode 100644 index df9bc33..0000000 --- a/src/routes/get.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EmbedBuilder } from "discord.js"; - -import { navBar } from "@routes/lib/components"; -import { emojiIconURL, RINGVC_EMOJI_ID } from "@routes/lib/emoji"; -import { withFlash } from "@routes/lib/flash"; -import { Handler } from "@routes/types"; - -import { COLOR } from "./_shared"; - -export const homeGet: Handler<"GET"> = (router, interaction, state) => { - // the branded author line (icon + "RingVC") is the panel's header, so - // there is no separate title to duplicate it - const embed = new EmbedBuilder() - .setColor(COLOR) - .setAuthor({ name: "RingVC", iconURL: emojiIconURL(RINGVC_EMOJI_ID) }) - .setDescription( - withFlash( - state.queryParams, - "RingVC replicates group-chat voice calls in Discord servers: sign up for a voice channel and get pinged when someone starts a call there.", - ), - ); - - return { - embeds: [embed], - components: [navBar(router, interaction)], - }; -}; diff --git a/src/routes/help/catalog/get.ts b/src/routes/help/catalog/get.ts index 0a98457..baf42c4 100644 --- a/src/routes/help/catalog/get.ts +++ b/src/routes/help/catalog/get.ts @@ -52,7 +52,6 @@ export const catalogGet: Handler<"GET"> = (router, interaction, state) => { { title: "Panels", entries: [ - ["ringvc", "the home panel"], ["help", "getting started"], ["catalog", "this command catalog"], ["about", "project links, policies, and feedback"], diff --git a/src/routes/index.ts b/src/routes/index.ts index 1bc87c1..556aed9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -11,7 +11,6 @@ import { filterMembersPost } from "./filter/[scope]/members/post"; import { filterResetModal } from "./filter/[scope]/reset/modal"; import { filterResetPost } from "./filter/[scope]/reset/post"; import { filterTypePost } from "./filter/[scope]/type/post"; -import { homeGet } from "./get"; import { catalogGet } from "./help/catalog/get"; import { helpGet } from "./help/get"; import { modeGet } from "./mode/get"; @@ -45,8 +44,9 @@ import { rolesGet } from "./signups/roles/get"; // handlers live in files mirroring their route: the folder is the path (with // [param] segments) and the file is the method export const registerRoutes = (router: RingRouter) => { - router.get("/", homeGet); - router.get("/help", helpGet); + // "/" answers with the getting-started view: it stays the fallback target + // of older messages' components even though no panel links to it anymore + router.get(["/", "/help"], helpGet); router.get("/help/catalog", catalogGet); router.get("/about", aboutGet); router.route("/about/feedback", { diff --git a/src/routes/lib/components.ts b/src/routes/lib/components.ts index 7a5624f..1fb200a 100644 --- a/src/routes/lib/components.ts +++ b/src/routes/lib/components.ts @@ -58,14 +58,7 @@ export const backButton = (router: RingRouter, path: string): RingButton => // the top-level sections, one per entry in the section bar (SECTIONS below) export type Section = - | "home" - | "signups" - | "filters" - | "ringees" - | "mode" - | "help" - | "about" - | "delete"; + "signups" | "filters" | "ringees" | "mode" | "help" | "about" | "delete"; type Tab = { section: Section; emoji: string; label: string; path: string }; @@ -73,7 +66,6 @@ type Tab = { section: Section; emoji: string; label: string; path: string }; // dedicated option field, not the label: labels render in the OS emoji font // while the emoji field gets Discord's own artwork, matching embed titles const SECTIONS: readonly Tab[] = [ - { section: "home", emoji: "🏠", label: "Home", path: "/" }, { section: "signups", emoji: "🔔", label: "Signups", path: "/signups" }, { section: "filters", emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, { diff --git a/src/routes/lib/emoji.ts b/src/routes/lib/emoji.ts deleted file mode 100644 index ab7a6b3..0000000 --- a/src/routes/lib/emoji.ts +++ /dev/null @@ -1,8 +0,0 @@ -// branded custom emoji used as the panel author icon -export const RINGVC_EMOJI_ID = "1324809350899961877"; - -// the emoji image on the CDN, usable as an embed author or thumbnail icon. -// A plain image URL renders for anyone, so it needs no access check; an -// invalid id simply 404s and the icon is dropped -export const emojiIconURL = (id: string): string => - `https://cdn.discordapp.com/emojis/${id}.webp`; From ebee407902090f1de3bdf491be967bef391ffa25 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:25:59 -0400 Subject: [PATCH 10/13] Stop answering "/" Panels are ephemeral, so components targeting the old root are gone with their messages; the page-jump fallbacks now point at /help. Co-Authored-By: Claude Fable 5 --- src/routes/index.ts | 4 +--- src/routes/page-jump/modal.ts | 2 +- src/routes/page-jump/post.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/routes/index.ts b/src/routes/index.ts index 556aed9..88d3732 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -44,9 +44,7 @@ import { rolesGet } from "./signups/roles/get"; // handlers live in files mirroring their route: the folder is the path (with // [param] segments) and the file is the method export const registerRoutes = (router: RingRouter) => { - // "/" answers with the getting-started view: it stays the fallback target - // of older messages' components even though no panel links to it anymore - router.get(["/", "/help"], helpGet); + router.get("/help", helpGet); router.get("/help/catalog", catalogGet); router.get("/about", aboutGet); router.route("/about/feedback", { diff --git a/src/routes/page-jump/modal.ts b/src/routes/page-jump/modal.ts index a3b5ceb..b92f7e7 100644 --- a/src/routes/page-jump/modal.ts +++ b/src/routes/page-jump/modal.ts @@ -14,7 +14,7 @@ export const pageJumpModal: Handler<"MODAL"> = (router, interaction, state) => { .setTo(PAGE_JUMP, { method: "POST", queryParams: { - to: query.get("to") ?? "/", + to: query.get("to") ?? "/help", page: query.get("page") ?? "0", pageCount, }, diff --git a/src/routes/page-jump/post.ts b/src/routes/page-jump/post.ts index 76afc42..a4b4f70 100644 --- a/src/routes/page-jump/post.ts +++ b/src/routes/page-jump/post.ts @@ -3,7 +3,7 @@ import { Handler } from "@routes/types"; export const pageJumpPost: Handler<"POST"> = (router, interaction, state) => { const query = state.queryParams; - const to = query.get("to") ?? "/"; + const to = query.get("to") ?? "/help"; const input = state.fields?.getTextInputValue("page")?.trim() ?? ""; const parsed = /^\d+$/.test(input) ? Number(input) : NaN; const pageCount = parseInt(query.get("pageCount") ?? "1", 10) || 1; From 27e4fefeacb5674b68cacccff65ad51be57f2db9 Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:25:59 -0400 Subject: [PATCH 11/13] Put Ring first in the section switcher Co-Authored-By: Claude Fable 5 --- src/routes/lib/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/lib/components.ts b/src/routes/lib/components.ts index 1fb200a..d967dbd 100644 --- a/src/routes/lib/components.ts +++ b/src/routes/lib/components.ts @@ -66,14 +66,14 @@ type Tab = { section: Section; emoji: string; label: string; path: string }; // dedicated option field, not the label: labels render in the OS emoji font // while the emoji field gets Discord's own artwork, matching embed titles const SECTIONS: readonly Tab[] = [ - { section: "signups", emoji: "🔔", label: "Signups", path: "/signups" }, - { section: "filters", emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, { section: "ringees", emoji: "đŸ“Ŗ", label: "Ring", path: "/recipients/global", }, + { section: "signups", emoji: "🔔", label: "Signups", path: "/signups" }, + { section: "filters", emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, { section: "mode", emoji: "💤", label: "Mode", path: "/mode" }, { section: "help", emoji: "📖", label: "Help", path: "/help" }, { section: "about", emoji: "â„šī¸", label: "About", path: "/about" }, From cc171b084321febb60f5749d9987c67cbf0f4e7c Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:28:16 -0400 Subject: [PATCH 12/13] Open the About panel with what RingVC is Co-Authored-By: Claude Fable 5 --- src/routes/about/get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/about/get.ts b/src/routes/about/get.ts index 8114ebb..9231bcb 100644 --- a/src/routes/about/get.ts +++ b/src/routes/about/get.ts @@ -24,7 +24,7 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => { .setDescription( withFlash( state.queryParams, - "RingVC is free and open source. Star it on GitHub, or join the support server for help and updates.\n\nHave a bug report or feature idea? Give anonymous feedback below.", + "RingVC replicates group-chat voice calls in Discord servers: sign up for a voice channel and get pinged when someone starts a call there.\n\nIt's free and open source. Star it on GitHub, or join the support server for help and updates.\n\nHave a bug report or feature idea? Give anonymous feedback below.", ), ), ], From 2cb4166c403e9e0794fc28254b2fe83b7d26e84e Mon Sep 17 00:00:00 2001 From: altrup <51763643+altrup@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:35:27 -0400 Subject: [PATCH 13/13] Clean up leftovers flagged by review Deletes the orphaned root _shared.ts and the unused Section union (the ringees check keys off the path), and points the notice fallback at /help. Co-Authored-By: Claude Fable 5 --- src/routes/_shared.ts | 1 - src/routes/lib/components.ts | 34 ++++++++++------------------------ src/routes/notice/get.ts | 4 ++-- 3 files changed, 12 insertions(+), 27 deletions(-) delete mode 100644 src/routes/_shared.ts diff --git a/src/routes/_shared.ts b/src/routes/_shared.ts deleted file mode 100644 index ac30fab..0000000 --- a/src/routes/_shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const COLOR = "#a082c3"; diff --git a/src/routes/lib/components.ts b/src/routes/lib/components.ts index d967dbd..c43a642 100644 --- a/src/routes/lib/components.ts +++ b/src/routes/lib/components.ts @@ -56,33 +56,19 @@ export const backButton = (router: RingRouter, path: string): RingButton => .setStyle(ButtonStyle.Secondary) .setTo(path); -// the top-level sections, one per entry in the section bar (SECTIONS below) -export type Section = - "signups" | "filters" | "ringees" | "mode" | "help" | "about" | "delete"; - -type Tab = { section: Section; emoji: string; label: string; path: string }; +type Tab = { emoji: string; label: string; path: string }; // every section the bar offers, in display order. The emoji rides the // dedicated option field, not the label: labels render in the OS emoji font // while the emoji field gets Discord's own artwork, matching embed titles const SECTIONS: readonly Tab[] = [ - { - section: "ringees", - emoji: "đŸ“Ŗ", - label: "Ring", - path: "/recipients/global", - }, - { section: "signups", emoji: "🔔", label: "Signups", path: "/signups" }, - { section: "filters", emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, - { section: "mode", emoji: "💤", label: "Mode", path: "/mode" }, - { section: "help", emoji: "📖", label: "Help", path: "/help" }, - { section: "about", emoji: "â„šī¸", label: "About", path: "/about" }, - { - section: "delete", - emoji: "đŸ—‘ī¸", - label: "Delete data", - path: "/delete-data", - }, + { emoji: "đŸ“Ŗ", label: "Ring", path: "/recipients/global" }, + { emoji: "🔔", label: "Signups", path: "/signups" }, + { emoji: "đŸ›Ąī¸", label: "Filters", path: "/filter/global" }, + { emoji: "💤", label: "Mode", path: "/mode" }, + { emoji: "📖", label: "Help", path: "/help" }, + { emoji: "â„šī¸", label: "About", path: "/about" }, + { emoji: "đŸ—‘ī¸", label: "Delete data", path: "/delete-data" }, ]; // the persistent section bar every panel ends on. A string select fits all @@ -100,8 +86,8 @@ export const navBar = ( interaction.member.voice.channel ); - const option = ({ section, emoji, label, path }: Tab) => { - const target = section === "ringees" && inVoice ? "/ring" : path; + const option = ({ emoji, label, path }: Tab) => { + const target = path === "/recipients/global" && inVoice ? "/ring" : path; return new RouteStringSelectMenuOptionBuilder(router) .setTo(target) .setEmoji(emoji) diff --git a/src/routes/notice/get.ts b/src/routes/notice/get.ts index 5bcac44..fed56a9 100644 --- a/src/routes/notice/get.ts +++ b/src/routes/notice/get.ts @@ -21,8 +21,8 @@ const openLabel = (path: string): string => { }; export const noticeGet: Handler<"GET"> = (router, interaction, state) => { - const to = state.queryParams.get("to") ?? "/"; - const [path = "/", query = ""] = to.split("?"); + const to = state.queryParams.get("to") ?? "/help"; + const [path = "/help", query = ""] = to.split("?"); const open = new RouteButtonBuilder(router) .setLabel(openLabel(path)) .setStyle(ButtonStyle.Secondary)