Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: implement loading gif for leaderboard #93

Merged
merged 2 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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