diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 14e89870..69999741 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -43,7 +43,6 @@ export const CasesModCmd = modActionsCmd({ const prefix = getGuildPrefix(pluginData); createPaginatedMessage( - pluginData.client, msg.channel, totalPages, async (page) => { diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index b2b417b7..9ba26b85 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -1,4 +1,5 @@ import { + Formatters, GuildMember, Message, MessageActionRow, @@ -35,6 +36,15 @@ export enum SearchType { BanSearch, } +interface SearchResult { + results: T[]; + totalResults: number; + page: number; + lastPage: number; + from: number; + to: number; +} + class SearchError extends Error {} type MemberSearchParams = ArgsFromSignatureOrArray; @@ -90,7 +100,7 @@ export async function displaySearch( const perPage = args.ids ? SEARCH_ID_RESULTS_PER_PAGE : SEARCH_RESULTS_PER_PAGE; - const loadSearchPage = async (page) => { + const loadSearchPage = async (page: number) => { if (searching) return; searching = true; @@ -146,11 +156,9 @@ export async function displaySearch( : formatSearchResultList(searchResult.results); const result = trimLines(` - ${headerText} - \`\`\`js - ${resultList} - \`\`\` - `); + ${headerText} + ${Formatters.codeBlock("js", resultList)} + `); const searchMsg = await searchMsgPromise; @@ -298,7 +306,7 @@ async function performMemberSearch( args: MemberSearchParams, page = 1, perPage = SEARCH_RESULTS_PER_PAGE, -): Promise<{ results: GuildMember[]; totalResults: number; page: number; lastPage: number; from: number; to: number }> { +): Promise> { await refreshMembersIfNeeded(pluginData.guild); let matchingMembers = Array.from(pluginData.guild.members.cache.values()); @@ -426,7 +434,7 @@ async function performBanSearch( args: BanSearchParams, page = 1, perPage = SEARCH_RESULTS_PER_PAGE, -): Promise<{ results: User[]; totalResults: number; page: number; lastPage: number; from: number; to: number }> { +): Promise> { const member = pluginData.guild.members.cache.get(pluginData.client.user!.id); if (member && !hasDiscordPermissions(member.permissions, Permissions.FLAGS.BAN_MEMBERS)) { throw new SearchError(`Unable to search bans: missing "Ban Members" permission`); @@ -454,7 +462,7 @@ async function performBanSearch( }); } - const [, sortDir, sortBy] = (args.sort && args.sort.match(/^(-?)(.*)$/)) ?? [null, "ASC", "name"]; + const [, sortDir, sortBy] = args.sort?.match(/^(-?)(.*)$/) ?? [null, "ASC", "name"]; const realSortDir = sortDir === "-" ? "DESC" : "ASC"; if (sortBy === "id") { diff --git a/backend/src/utils/createPaginatedMessage.ts b/backend/src/utils/createPaginatedMessage.ts index e711c620..af95be6a 100644 --- a/backend/src/utils/createPaginatedMessage.ts +++ b/backend/src/utils/createPaginatedMessage.ts @@ -1,6 +1,9 @@ import { Client, + Constants, Message, + MessageActionRow, + MessageButton, MessageEditOptions, MessageOptions, MessageReaction, @@ -25,8 +28,10 @@ const defaultOpts: PaginateMessageOpts = { limitToUserId: null, }; +const forwardId = "forward" as const; +const backwardId = "backward" as const; + export async function createPaginatedMessage( - client: Client, channel: TextChannel | User, totalPages: number, loadPageFn: LoadPageFn, @@ -34,30 +39,37 @@ export async function createPaginatedMessage( ): Promise { const fullOpts = { ...defaultOpts, ...opts } as PaginateMessageOpts; const firstPageContent = await loadPageFn(1); - const message = await channel.send(firstPageContent); + + const components: MessageButton[] = [ + new MessageButton({ + customId: backwardId, + emoji: "⬅", + style: Constants.MessageButtonStyles.SECONDARY, + }), + new MessageButton({ + customId: forwardId, + emoji: "➡", + style: Constants.MessageButtonStyles.SECONDARY, + }), + ]; + + const message = await channel.send({ ...firstPageContent, components: [new MessageActionRow({ components })] }); let page = 1; - let pageLoadId = 0; // Used to avoid race conditions when rapidly switching pages - const reactionListener = async ( - reactionMessage: MessageReaction | PartialMessageReaction, - reactor: User | PartialUser, - ) => { - if (reactionMessage.message.id !== message.id) { - return; + + const collector = message.createMessageComponentCollector({ time: fullOpts.timeout }); + + collector.on("collect", async interaction => { + if (fullOpts.limitToUserId && interaction.user.id !== fullOpts.limitToUserId) { + return interaction.reply({ content: `You are not permitted to use these buttons.`, ephemeral: true }); } - if (fullOpts.limitToUserId && reactor.id !== fullOpts.limitToUserId) { - return; - } - - if (reactor.id === client.user!.id) { - return; - } + await interaction.deferUpdate(); let pageDelta = 0; - if (reactionMessage.emoji.name === "⬅️") { + if (interaction.customId === backwardId) { pageDelta = -1; - } else if (reactionMessage.emoji.name === "➡️") { + } else if (interaction.customId === forwardId) { pageDelta = 1; } @@ -71,34 +83,12 @@ export async function createPaginatedMessage( } page = newPage; - const thisPageLoadId = ++pageLoadId; - const newPageContent = await loadPageFn(page); - if (thisPageLoadId !== pageLoadId) { - return; - } + void message.edit(await loadPageFn(page)).catch(noop); + }); - message.edit(newPageContent).catch(noop); - reactionMessage.users.remove(reactor.id).catch(noop); - refreshTimeout(); - }; - client.on("messageReactionAdd", reactionListener); - - // The timeout after which reactions are removed and the pagination stops working - // is refreshed each time the page is changed - let timeout: Timeout; - const refreshTimeout = () => { - clearTimeout(timeout); - timeout = setTimeout(() => { - message.reactions.removeAll().catch(noop); - client.off("messageReactionAdd", reactionListener); - }, fullOpts.timeout); - }; - - refreshTimeout(); - - // Add reactions - message.react("⬅️").catch(noop); - message.react("➡️").catch(noop); + collector.on("end", () => { + message.edit({ components: [] }); + }); return message; }