Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

401 add pagination helper #527

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions docs/COMMAND-WIKI.md
Original file line number Diff line number Diff line change
@@ -258,6 +258,13 @@
- ``uwid``: The Quest ID of the user.
- **Subcommands:** None

## pg
- **Aliases:** `pagination`, `paginationtest`, `pgtest`
- **Description:** Test the pagination feature.
- **Examples:**<br> `.pg`<br> `.pagination`<br> `.pagination`<br> `.pgtest`
- **Options:** None
- **Subcommands:** None

## ping
- **Aliases:** `pong`
- **Description:** Ping the bot to see if it is alive. :ping_pong:
159 changes: 159 additions & 0 deletions src/commandDetails/miscellaneous/pagination-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { container } from '@sapphire/framework';
import { CodeyCommandDetails, getUserFromMessage } from '../../codeyCommand';
import { EmbedBuilder } from 'discord.js';
import { PaginationBuilder, PaginationBuilderFromText } from '../../utils/pagination';
import { SapphireMessageExecuteType, SapphireMessageResponse } from '../../codeyCommand';
import { Colors } from 'discord.js';

const PaginationTestExecuteCommand: SapphireMessageExecuteType = async (
_client,
messageFromUser,
_args,
): Promise<SapphireMessageResponse> => {
const message = messageFromUser;
const author = getUserFromMessage(message).id;

const embedData: { title: string; description: string; color: number }[] = [
{
title: 'Page 1',
description: 'Hey there! This is the content of Page 1 💙',
color: 0xff0000,
},
{
title: 'Page 2',
description: 'Hey there! This is the content of Page 2 🎉',
color: 0x00ff00,
},
{
title: 'Page 3',
description: 'Hey there! This is the content of Page 3 👽',
color: 0x0000ff,
},
{
title: 'Page 4',
description: 'Hey there! This is the content of Page 4 🎃',
color: 0xff00ff,
},
{
title: 'Page 5',
description: 'Hey there! This is the content of Page 5 🎄',
color: 0xffff00,
},
];

const embeds: EmbedBuilder[] = embedData.map((data) =>
new EmbedBuilder()
.setColor(Colors.Blue)
.setTitle(data.title)
.setDescription(data.description)
.setColor(data.color),
);

try {
await PaginationBuilder(message, author, embeds); // 1. Test Embed List Pagination

// 2. Test Large Text Pagination
// await PaginationBuilderFromText(
// message,
// author,
// `Lorem Ipsum is simply dummy text of the printing and typesetting industry. \
// Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make \
// a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, \
// remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
// of Lorem Ipsum. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, \
// consectetur, from a \
// Lorem \
// Ipsum passage, and going through the cites of the word in classical literature, \
// discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and \
// 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, \
// written in 45 BC. This book is a treatise on the theory of ethics, \
// very popular \
// during the Renaissance. The first \
// line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`,
// );

// 3. Test Empty String Case
// await PaginationBuilderFromText(message, author, "") // no content test case

// 4. Test Large Text Pagination without ignoreNewLines (Spaces needed after \)
await PaginationBuilderFromText(
message,
author,
`Lorem Ipsum is simply dummy text of the printing and typesetting industry. \
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make \
a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions \
of Lorem Ipsum. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, \
consectetur, from a \
Lorem \
Ipsum passage, and going through the cites of the word in classical literature, \
discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and \
1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, \
written in 45 BC. This book is a treatise on the theory of ethics, \
very popular \
during the Renaissance. The first \
line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`,
);

// 5. Test Leaderboard Text Pagination with ignoreNewLines
await PaginationBuilderFromText(
message,
author,
`1. palepinkroses#0 - 69749 :codey_coin: \
2. cho_c0.#0 - 49700 :codey_coin: \
3. infinit3e#0 - 47952 :codey_coin: \
4. picowchew#0 - 29696 :codey_coin: \
5. redapple410#0 - 20237 :codey_coin: \
6. mcpenguin6194#0 - 19240 :codey_coin: \
7. fylixz#0 - 18580 :codey_coin: \
8. antangelo#0 - 16037 :codey_coin: \
9. elegy2333#0 - 15842 :codey_coin: \
10. icanc#0 - 15828 :codey_coin: \
11. sagar1#0 - 15700 :codey_coin: \
12. sagar2#0 - 15600 :codey_coin: \
13. sagar3#0 - 15500 :codey_coin: \
14. sagar4#0 - 15400 :codey_coin: \
15. sagar5#0 - 15300 :codey_coin: \
16. sagar6#0 - 15200 :codey_coin: \
17. sagar7#0 - 15100 :codey_coin: \
18. sagar8#0 - 15000 :codey_coin: \
19. sagar9#0 - 14900 :codey_coin: \
20. sagar10#0 - 14800 :codey_coin: \
21. sagar11#0 - 14700 :codey_coin: \
22. sagar12#0 - 14600 :codey_coin: \
Your Position \
You are currently #213 in the leaderboard with 553 :codey_coin:.`,
true,
);
} catch (error) {
await message.reply('Error or timeout occurred during navigation.');
}
};

export const paginationTestCommandDetails: CodeyCommandDetails = {
name: 'pg',
aliases: ['pagination', 'paginationtest', 'pgtest'],
description: 'Test the pagination feature.',
detailedDescription: `**Examples:**
\`${container.botPrefix}pg\`
\`${container.botPrefix}pagination\`
\`${container.botPrefix}pagination\`
\`${container.botPrefix}pgtest\``,

isCommandResponseEphemeral: false,
messageWhenExecutingCommand: 'Testing pagination...',
executeCommand: PaginationTestExecuteCommand,
messageIfFailure: 'Could not test pagination.',
options: [],
subcommandDetails: {},
};
16 changes: 16 additions & 0 deletions src/commands/miscellaneous/pagination-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from '@sapphire/framework';
import { CodeyCommand } from '../../codeyCommand';
import { paginationTestCommandDetails } from '../../commandDetails/miscellaneous/pagination-test';

export class MiscellaneousPaginationTestCommand extends CodeyCommand {
details = paginationTestCommandDetails;

public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
aliases: paginationTestCommandDetails.aliases,
description: paginationTestCommandDetails.description,
detailedDescription: paginationTestCommandDetails.detailedDescription,
});
}
}
205 changes: 205 additions & 0 deletions src/utils/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import {
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
ButtonInteraction,
ComponentType,
Message,
ChatInputCommandInteraction,
CacheType,
} from 'discord.js';

const COLLECTOR_TIMEOUT = 300000;
const MAX_CHARS_PER_PAGE = 2048;
const MAX_PAGES = 25;
const MAX_NEWLINES_PER_PAGE = 10;
const getRandomColor = (): number => Math.floor(Math.random() * 16777215);

const textToPages = (text: string, maxChars: number, ignoreNewLines: boolean): string[] => {
const pages: string[] = [];
let currentPage = '';
let newLineCount = 0;
let charCount = 0;

for (let i = 0; i < text.length; i++) {
currentPage += text[i];
charCount++;
if (text[i] === '\n') {
newLineCount++;
}

if (
(text[i] === '\n' && !ignoreNewLines) ||
charCount >= maxChars ||
newLineCount === MAX_NEWLINES_PER_PAGE
) {
pages.push(currentPage.trim());
currentPage = '';
charCount = 0;
newLineCount = 0;
}
}

if (currentPage.trim()) {
pages.push(currentPage.trim());
}
return pages;
};

export const PaginationBuilder = async (
originalMessage: Message<boolean> | ChatInputCommandInteraction<CacheType>,
author: string,
embedPages: EmbedBuilder[],
timeout: number = COLLECTOR_TIMEOUT,
): Promise<Message<boolean> | undefined> => {
try {
if (!embedPages || !embedPages.length) {
await originalMessage.reply({
embeds: [new EmbedBuilder().setColor(0xff0000).setDescription('No pages to display.')],
});
return;
}
if (embedPages.length > MAX_PAGES) {
await originalMessage.reply({
embeds: [
new EmbedBuilder()
.setColor(0xff0000)
.setDescription(
`Too much content to display. Limit is ${MAX_PAGES} pages. \nCurrent content produces ${embedPages.length} pages.`,
),
],
});
return;
}
let currentPage = 0;
const firstButton = new ButtonBuilder()
.setCustomId('first')
.setEmoji('⏮️')
// .setLabel('First')
.setStyle(ButtonStyle.Primary)
.setDisabled(true);
const previousButton = new ButtonBuilder()
.setCustomId('previous')
.setEmoji('⬅️')
// .setLabel('Previous')
.setStyle(ButtonStyle.Primary)
.setDisabled(true);
const pageCount = new ButtonBuilder()
.setCustomId('pagecount')
.setLabel(`${currentPage + 1}/${embedPages.length}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
const nextButton = new ButtonBuilder()
.setCustomId('next')
.setEmoji('➡️')
// .setLabel('Next')
.setStyle(ButtonStyle.Primary)
.setDisabled(embedPages.length <= 1);
const lastButton = new ButtonBuilder()
.setCustomId('last')
.setEmoji('⏭️')
// .setLabel('Last')
.setStyle(ButtonStyle.Primary)
.setDisabled(embedPages.length <= 1);
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
firstButton,
previousButton,
pageCount,
nextButton,
lastButton,
);

const message = await originalMessage.reply({
embeds: [embedPages[currentPage]],
components: [actionRow],
fetchReply: true,
});

await message.edit({
embeds: [embedPages[currentPage]],
components: [actionRow],
});

const collector = message.createMessageComponentCollector({
filter: (interaction) => interaction.user.id === author,
componentType: ComponentType.Button,
idle: timeout,
});

collector.on('collect', async (buttonInteraction: ButtonInteraction) => {
await buttonInteraction.deferUpdate();

switch (buttonInteraction.customId) {
case 'first':
currentPage = 0;
break;
case 'previous':
currentPage = Math.max(0, currentPage - 1);
break;
case 'next':
currentPage = Math.min(embedPages.length - 1, currentPage + 1);
break;
case 'last':
currentPage = embedPages.length - 1;
break;
}

firstButton.setDisabled(currentPage === 0);
previousButton.setDisabled(currentPage === 0);
nextButton.setDisabled(currentPage === embedPages.length - 1);
lastButton.setDisabled(currentPage === embedPages.length - 1);
pageCount.setLabel(`${currentPage + 1}/${embedPages.length}`);

await message.edit({
embeds: [embedPages[currentPage]],
components: [actionRow],
});
});

collector.on('end', async () => {
firstButton.setDisabled(true);
previousButton.setDisabled(true);
nextButton.setDisabled(true);
lastButton.setDisabled(true);
pageCount.setDisabled(true);

await message.edit({
components: [actionRow],
});

setTimeout(async () => {
await message.edit({
components: [],
});
}, 3000);
});

return message;
} catch (error) {
return undefined;
}
};

export const PaginationBuilderFromText = async (
originalMessage: Message<boolean> | ChatInputCommandInteraction<CacheType>,
author: string,
text: string,
ignoreNewLines = false,
textPageSize: number = MAX_CHARS_PER_PAGE,
timeout: number = COLLECTOR_TIMEOUT,
): Promise<Message<boolean> | undefined> => {
try {
const textPages = textToPages(text, textPageSize, ignoreNewLines);
const embedPages = textPages.map((text, index) =>
new EmbedBuilder()
.setColor(getRandomColor())
.setTitle('Page ' + (index + 1))
.setDescription(text),
);

return PaginationBuilder(originalMessage, author, embedPages, timeout);
} catch (error) {
return undefined;
}
};