Skip to content

Commit

Permalink
feat: implement loading gif for leaderboard (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
HeadTriXz authored Oct 25, 2023
1 parent a8a41e5 commit 1e4415e
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 44 deletions.
Binary file added apps/barry/assets/images/leaderboard-loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion apps/barry/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<LevelingModule> {
/**
* The loading image to use for the leaderboard.
*/
#loadingImage?: Buffer;

/**
* Represents a slash command that shows a leaderboard of the most active members.
*
Expand Down Expand Up @@ -90,9 +96,19 @@ export default class extends SlashCommand<LevelingModule> {
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([
Expand Down Expand Up @@ -342,4 +358,20 @@ export default class extends SlashCommand<LevelingModule> {
}]
};
}

/**
* Loads the loading image for the leaderboard.
*
* @returns The loading image.
*/
async #loadLoadingImage(): Promise<Buffer> {
if (this.#loadingImage !== undefined) {
return this.#loadingImage;
}

const buffer = await readFile(join(process.cwd(), "./assets/images/leaderboard-loading.gif"));
this.#loadingImage = buffer;

return buffer;
}
}
16 changes: 7 additions & 9 deletions apps/barry/src/utils/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export interface BasePaginationOptions {
*/
interaction: ReplyableInteraction;

/**
* A function that is called when the pagination is refreshed.
*/
onRefresh?: (interaction: ReplyableInteraction) => Awaitable<void>;

/**
* The amount of items to show per page.
*/
Expand All @@ -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).
*/
Expand Down Expand Up @@ -212,10 +212,8 @@ export class PaginationMessage<T = unknown> {
* Refreshes the paginated message by updating the content.
*/
async refresh(): Promise<void> {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ vi.mock("canvas-constructor/napi-rs", () => {
};
});

vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
return {
...original,
readFile: vi.fn().mockResolvedValue(Buffer.from("Hello World"))
};
});

describe("/leaderboard", () => {
const guildID = "68239102456844360";
const userID = "257522665441460225";
Expand Down Expand Up @@ -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;
Expand Down
39 changes: 7 additions & 32 deletions apps/barry/tests/utils/pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down

0 comments on commit 1e4415e

Please # to comment.