diff --git a/apps/barry/assets/images/leaderboard-loading.gif b/apps/barry/assets/images/leaderboard-loading.gif new file mode 100644 index 0000000..983119e Binary files /dev/null and b/apps/barry/assets/images/leaderboard-loading.gif differ diff --git a/apps/barry/src/config.ts b/apps/barry/src/config.ts index 18abe7f..0ae5425 100644 --- a/apps/barry/src/config.ts +++ b/apps/barry/src/config.ts @@ -74,7 +74,6 @@ export default { // Other check: new Emoji("check", "1004436175307669659"), error: new Emoji("error", "1004436176859578510"), - loading: new Emoji("loading", "1135668500728397855", true), menu: new Emoji("hamburger", "1136294229405077564"), next: new Emoji("next", "1124406938738905098"), previous: new Emoji("previous", "1124406936188768357"), diff --git a/apps/barry/src/modules/leveling/commands/chatinput/leaderboard/index.ts b/apps/barry/src/modules/leveling/commands/chatinput/leaderboard/index.ts index 1e8a93c..0b77a67 100644 --- a/apps/barry/src/modules/leveling/commands/chatinput/leaderboard/index.ts +++ b/apps/barry/src/modules/leveling/commands/chatinput/leaderboard/index.ts @@ -20,6 +20,7 @@ import { LeaderboardCanvas } from "./LeaderboardCanvas.js"; import { PaginationMessage } from "../../../../../utils/pagination.js"; import { join } from "node:path"; import { loadFont } from "canvas-constructor/napi-rs"; +import { readFile } from "node:fs/promises"; import config from "../../../../../config.js"; @@ -54,6 +55,11 @@ export enum LeaderboardMenuOption { * Represents a slash command that shows a leaderboard of the most active members. */ export default class extends SlashCommand { + /** + * The loading image to use for the leaderboard. + */ + #loadingImage?: Buffer; + /** * Represents a slash command that shows a leaderboard of the most active members. * @@ -90,9 +96,19 @@ export default class extends SlashCommand { content: (index) => this.#getPageContent(interaction.guildID, index, sortOptions), count: count, interaction: interaction, + onRefresh: async (interaction) => { + const buffer = await this.#loadLoadingImage(); + await interaction.editOriginalMessage({ + attachments: [{ id: "0" }], + files: [{ + contentType: "image/gif", + data: buffer, + name: "loading.gif" + }] + }); + }, pageSize: PAGE_SIZE, - preLoadPages: 1, - showLoading: true + preLoadPages: 1 }); await Promise.all([ @@ -342,4 +358,20 @@ export default class extends SlashCommand { }] }; } + + /** + * Loads the loading image for the leaderboard. + * + * @returns The loading image. + */ + async #loadLoadingImage(): Promise { + if (this.#loadingImage !== undefined) { + return this.#loadingImage; + } + + const buffer = await readFile(join(process.cwd(), "./assets/images/leaderboard-loading.gif")); + this.#loadingImage = buffer; + + return buffer; + } } diff --git a/apps/barry/src/utils/pagination.ts b/apps/barry/src/utils/pagination.ts index 1df3094..242d9ee 100644 --- a/apps/barry/src/utils/pagination.ts +++ b/apps/barry/src/utils/pagination.ts @@ -54,6 +54,11 @@ export interface BasePaginationOptions { */ interaction: ReplyableInteraction; + /** + * A function that is called when the pagination is refreshed. + */ + onRefresh?: (interaction: ReplyableInteraction) => Awaitable; + /** * The amount of items to show per page. */ @@ -64,11 +69,6 @@ export interface BasePaginationOptions { */ preLoadPages?: number; - /** - * Whether to show a message indicating the page is still loading. - */ - showLoading?: boolean; - /** * The timeout duration in milliseconds (default: 10 minutes). */ @@ -212,10 +212,8 @@ export class PaginationMessage { * Refreshes the paginated message by updating the content. */ async refresh(): Promise { - if (this.#options.showLoading) { - await this.#options.interaction.editOriginalMessage({ - content: `## ${config.emotes.loading} Loading...` - }); + if (this.#options.onRefresh !== undefined) { + await this.#options.onRefresh(this.#options.interaction); } const content = await this.#getPageContent(); diff --git a/apps/barry/tests/modules/leveling/commands/chatinput/leaderboard/index.test.ts b/apps/barry/tests/modules/leveling/commands/chatinput/leaderboard/index.test.ts index ecef618..bfaae03 100644 --- a/apps/barry/tests/modules/leveling/commands/chatinput/leaderboard/index.test.ts +++ b/apps/barry/tests/modules/leveling/commands/chatinput/leaderboard/index.test.ts @@ -49,6 +49,14 @@ vi.mock("canvas-constructor/napi-rs", () => { }; }); +vi.mock("node:fs/promises", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + readFile: vi.fn().mockResolvedValue(Buffer.from("Hello World")) + }; +}); + describe("/leaderboard", () => { const guildID = "68239102456844360"; const userID = "257522665441460225"; @@ -108,6 +116,24 @@ describe("/leaderboard", () => { }); }); + it("should show the loading gif while generating the leaderboard", async () => { + const editSpy = vi.spyOn(interaction, "editOriginalMessage"); + vi.spyOn(interaction, "awaitMessageComponent") + .mockResolvedValue(undefined); + + await command.execute(interaction); + + expect(editSpy).toHaveBeenCalledTimes(3); + expect(editSpy).toHaveBeenCalledWith({ + attachments: [{ id: "0" }], + files: [{ + contentType: "image/gif", + data: Buffer.from("Hello World"), + name: "loading.gif" + }] + }); + }); + it("should ignore if the command was invoked outside a guild", async () => { const editSpy = vi.spyOn(interaction, "editOriginalMessage"); interaction.guildID = undefined; diff --git a/apps/barry/tests/utils/pagination.test.ts b/apps/barry/tests/utils/pagination.test.ts index edc4587..0641cca 100644 --- a/apps/barry/tests/utils/pagination.test.ts +++ b/apps/barry/tests/utils/pagination.test.ts @@ -135,42 +135,17 @@ describe("PaginationMessage", () => { expect(loadSpy).toHaveBeenCalledWith([1, 2], 0); }); - it("should show a loading message if 'showLoading' is true", async () => { - const initialEditSpy = vi.spyOn(interaction, "editOriginalMessage"); + it("should call the 'onRefresh' callback when refreshing the message", async () => { vi.spyOn(interaction, "awaitMessageComponent") - .mockResolvedValueOnce(nextInteraction); - - const nextEditSpy = vi.spyOn(nextInteraction, "editOriginalMessage"); - vi.spyOn(nextInteraction, "awaitMessageComponent") - .mockResolvedValue(undefined); - - await PaginationMessage.create({ ...indexOptions, showLoading: true }); + .mockResolvedValueOnce(undefined); - expect(initialEditSpy).toHaveBeenCalledWith({ - content: expect.stringContaining("Loading...") - }); - expect(nextEditSpy).toHaveBeenCalledWith({ - content: expect.stringContaining("Loading...") + const refreshSpy = vi.fn(); + await PaginationMessage.create({ + ...indexOptions, + onRefresh: refreshSpy }); - }); - it("should not show a loading message if 'showLoading' is false", async () => { - const initialEditSpy = vi.spyOn(interaction, "editOriginalMessage"); - vi.spyOn(interaction, "awaitMessageComponent") - .mockResolvedValueOnce(nextInteraction); - - const nextEditSpy = vi.spyOn(nextInteraction, "editOriginalMessage"); - vi.spyOn(nextInteraction, "awaitMessageComponent") - .mockResolvedValue(undefined); - - await PaginationMessage.create(indexOptions); - - expect(initialEditSpy).not.toHaveBeenCalledWith({ - content: expect.stringContaining("Loading...") - }); - expect(nextEditSpy).not.toHaveBeenCalledWith({ - content: expect.stringContaining("Loading...") - }); + expect(refreshSpy).toHaveBeenCalledOnce(); }); });