diff --git a/src/bot/commands/announce.ts b/src/bot/commands/announce.ts index 13027e9..9f6f81c 100644 --- a/src/bot/commands/announce.ts +++ b/src/bot/commands/announce.ts @@ -1,7 +1,9 @@ import { - ActionRowBuilder, + ChannelSelectMenuBuilder, + ChannelType, ComponentType, EmbedBuilder, + LabelBuilder, MessageFlags, ModalBuilder, SlashCommandBuilder, @@ -20,6 +22,7 @@ const ANNOUNCE_COLOR = 0xf59e0b; const ANNOUNCE_MODAL_PREFIX = "ptf-announce:"; const TITLE_INPUT_ID = "ptf-announce-title"; const BODY_INPUT_ID = "ptf-announce-body"; +const CHANNEL_INPUT_ID = "ptf-announce-channel"; /** Modal paragraph inputs cap at 4000 chars; the embed description limit is 4096. */ const MAX_BODY_LENGTH = 4000; @@ -33,6 +36,12 @@ const modalText = (fields: ModalSubmitInteraction["fields"], customId: string): return field?.type === ComponentType.TextInput ? field.value : ""; }; +const modalChannelId = (fields: ModalSubmitInteraction["fields"]): string | null => { + const field = fields.fields.get(CHANNEL_INPUT_ID); + if (field?.type !== ComponentType.ChannelSelect) return null; + return field.channels?.first()?.id ?? null; +}; + const postAnnouncement = async ( channel: TextBasedChannelFields, title: string, @@ -75,33 +84,33 @@ export const announceCommand: BotCommand = { const message = (interaction.options.getString("message") ?? "").trim(); const option = interaction.options.getChannel("channel"); - const channelId = option?.id ?? botConfig.announceChannelId ?? botConfig.opsChannelId; + const channelId = option?.id ?? botConfig.announceChannelId; if (!channelId) { await interaction.reply( - ephemeral("No announce channel is configured and no channel was given."), + ephemeral( + "No announce channel is configured (set DISCORD_ANNOUNCE_CHANNEL_ID) and no channel was given.", + ), ); return; } if (!message) { - // Compose in a modal. The resolved channel rides along in the custom id, - // because modal submit interactions do not carry the command's options. + // customId carries the default channel as the fallback: modal submits + // don't carry the command's options. const modal = new ModalBuilder() .setCustomId(`${ANNOUNCE_MODAL_PREFIX}${channelId}`) .setTitle("Post an announcement"); - modal.addComponents( - new ActionRowBuilder().addComponents( + modal.addLabelComponents( + new LabelBuilder().setLabel("Title (optional)").setTextInputComponent( new TextInputBuilder() .setCustomId(TITLE_INPUT_ID) - .setLabel("Title (optional)") .setStyle(TextInputStyle.Short) .setMaxLength(256) .setRequired(false), ), - new ActionRowBuilder().addComponents( + new LabelBuilder().setLabel("Announcement text").setTextInputComponent( new TextInputBuilder() .setCustomId(BODY_INPUT_ID) - .setLabel("Announcement text") .setStyle(TextInputStyle.Paragraph) .setMaxLength(MAX_BODY_LENGTH) .setRequired(true) @@ -109,6 +118,16 @@ export const announceCommand: BotCommand = { "Paste your announcement. Markdown (headings, lists, bold, code) is supported.", ), ), + new LabelBuilder() + .setLabel("Channel (optional)") + .setDescription("Defaults to the configured app updates channel.") + .setChannelSelectMenuComponent( + new ChannelSelectMenuBuilder() + .setCustomId(CHANNEL_INPUT_ID) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) + .setDefaultChannels(channelId) + .setRequired(false), + ), ); await interaction.showModal(modal); return; @@ -145,9 +164,11 @@ export const handleAnnounceModal = async ( } // The modal custom id is "ptf-announce:", written when the modal - // was opened from the slash command. Fall back to the configured channel. + // was opened. A channel picked in the modal overrides it. const customChannelId = interaction.customId.slice(ANNOUNCE_MODAL_PREFIX.length); - const resolvedId = customChannelId || botConfig.announceChannelId || botConfig.opsChannelId; + const resolvedId = + modalChannelId(interaction.fields) ?? + (customChannelId || botConfig.announceChannelId); if (!resolvedId) { await interaction.reply(ephemeral("No announce channel is configured.")); return;