Skip to content

Commit

Permalink
feat(PaginatedMessage): add support for all select menus (#589)
Browse files Browse the repository at this point in the history

---------

Co-authored-by: Jeroen Claassens <support@favware.tech>
  • Loading branch information
killbasa and favna committed May 5, 2023
1 parent b917236 commit 4858486
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 90 deletions.
@@ -1,12 +1,12 @@
import type { Ctor } from '@sapphire/utilities';
import type { CollectorFilter, EmojiResolvable, Message, MessageReaction, User } from 'discord.js';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from './constants';
import type {
IMessagePrompterExplicitConfirmReturn,
IMessagePrompterExplicitMessageReturn,
IMessagePrompterExplicitNumberReturn,
IMessagePrompterExplicitReturnBase
} from './ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from './constants';
import { MessagePrompterBaseStrategy } from './strategies/MessagePrompterBaseStrategy';
import { MessagePrompterConfirmStrategy } from './strategies/MessagePrompterConfirmStrategy';
import { MessagePrompterMessageStrategy } from './strategies/MessagePrompterMessageStrategy';
Expand Down
@@ -1,6 +1,6 @@
export type { MessagePrompterChannelTypes, MessagePrompterMessage } from './constants';
export * from './ExplicitReturnTypes';
export * from './MessagePrompter';
export type { MessagePrompterChannelTypes, MessagePrompterMessage } from './constants';
export * from './strategies/MessagePrompterBaseStrategy';
export * from './strategies/MessagePrompterConfirmStrategy';
export * from './strategies/MessagePrompterMessageStrategy';
Expand Down
@@ -1,8 +1,8 @@
import { isNullish, type ArgumentTypes, type Awaitable } from '@sapphire/utilities';
import type { CollectorFilter, CollectorOptions, EmojiIdentifierResolvable, Message, MessageReaction, User } from 'discord.js';
import { isStageChannel, isTextBasedChannel } from '../../type-guards';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterExplicitReturnBase } from '../ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterStrategyOptions } from '../strategyOptions';

export abstract class MessagePrompterBaseStrategy {
Expand Down
@@ -1,6 +1,6 @@
import type { CollectorFilter, EmojiResolvable, MessageReaction, User } from 'discord.js';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterExplicitConfirmReturn } from '../ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterConfirmStrategyOptions } from '../strategyOptions';
import { MessagePrompterBaseStrategy } from './MessagePrompterBaseStrategy';

Expand Down
@@ -1,8 +1,8 @@
import { isNullish, type ArgumentTypes } from '@sapphire/utilities';
import type { CollectorFilter, CollectorOptions, Message, User } from 'discord.js';
import { isStageChannel, isTextBasedChannel } from '../../type-guards';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterExplicitMessageReturn } from '../ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterStrategyOptions } from '../strategyOptions';
import { MessagePrompterBaseStrategy } from './MessagePrompterBaseStrategy';

Expand Down
@@ -1,6 +1,6 @@
import type { CollectorFilter, EmojiIdentifierResolvable, MessageReaction, User } from 'discord.js';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterExplicitNumberReturn } from '../ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterNumberStrategyOptions } from '../strategyOptions';
import { MessagePrompterBaseStrategy } from './MessagePrompterBaseStrategy';

Expand Down
@@ -1,6 +1,6 @@
import type { CollectorFilter, EmojiIdentifierResolvable, EmojiResolvable, MessageReaction, User } from 'discord.js';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterExplicitReturnBase } from '../ExplicitReturnTypes';
import type { MessagePrompterChannelTypes, MessagePrompterMessage } from '../constants';
import type { IMessagePrompterReactionStrategyOptions } from '../strategyOptions';
import { MessagePrompterBaseStrategy } from './MessagePrompterBaseStrategy';

Expand Down
@@ -1,35 +1,37 @@
import { Time } from '@sapphire/duration';
import { deepClone, isFunction, isNullish, isObject } from '@sapphire/utilities';
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ChannelSelectMenuBuilder,
ComponentType,
EmbedBuilder,
GatewayIntentBits,
IntentsBitField,
InteractionCollector,
InteractionType,
MentionableSelectMenuBuilder,
Partials,
RoleSelectMenuBuilder,
StringSelectMenuBuilder,
StringSelectMenuInteraction,
UserSelectMenuBuilder,
isJSONEncodable,
userMention,
type APIActionRowComponent,
type APIEmbed,
type APIMessage,
type APIMessageActionRowComponent,
type BaseMessageOptions,
type ButtonInteraction,
type Collection,
type InteractionReplyOptions,
type JSONEncodable,
type Message,
type MessageActionRowComponentBuilder,
type Snowflake,
type TextBasedChannel,
type User,
type WebhookMessageEditOptions,
ActionRowBuilder,
type APIActionRowComponent,
type APIButtonComponent,
type APIStringSelectComponent
type WebhookMessageEditOptions
} from 'discord.js';
import { MessageBuilder } from '../builders/MessageBuilder';
import { isAnyInteraction, isGuildBasedChannel, isMessageInstance, isStageChannel } from '../type-guards';
Expand All @@ -38,6 +40,7 @@ import type {
PaginatedMessageAction,
PaginatedMessageComponentUnion,
PaginatedMessageEmbedResolvable,
PaginatedMessageInteractionUnion,
PaginatedMessageInternationalizationContext,
PaginatedMessageMessageOptionsUnion,
PaginatedMessageOptions,
Expand All @@ -46,7 +49,17 @@ import type {
PaginatedMessageStopReasons,
PaginatedMessageWrongUserInteractionReplyFunction
} from './PaginatedMessageTypes';
import { actionIsButtonOrMenu, createPartitionedMessageRow, isMessageButtonInteractionData, safelyReplyToInteraction } from './utils';
import {
actionIsButtonOrMenu,
createPartitionedMessageRow,
isMessageButtonInteractionData,
isMessageChannelSelectInteractionData,
isMessageMentionableSelectInteractionData,
isMessageRoleSelectInteractionData,
isMessageStringSelectInteractionData,
isMessageUserSelectInteractionData,
safelyReplyToInteraction
} from './utils';

/**
* This is a {@link PaginatedMessage}, a utility to paginate messages (usually embeds).
Expand Down Expand Up @@ -140,7 +153,7 @@ export class PaginatedMessage {
/**
* The collector used for handling component interactions.
*/
public collector: InteractionCollector<ButtonInteraction | StringSelectMenuInteraction> | null = null;
public collector: InteractionCollector<PaginatedMessageInteractionUnion> | null = null;

/**
* The pages which were converted from {@link PaginatedMessage.pages}
Expand Down Expand Up @@ -276,6 +289,10 @@ export class PaginatedMessage {

/**
* Sets the {@link PaginatedMessage.selectMenuPlaceholder} for this instance of {@link PaginatedMessage}.
*
* This applies only to the string select menu from the {@link PaginatedMessage.defaultActions}
* that offers "go to page" (we internally check the customId for this)
*
* This will only apply to this one instance and no others.
* @param placeholder The new placeholder to set
* @returns The current instance of {@link PaginatedMessage}
Expand Down Expand Up @@ -946,7 +963,7 @@ export class PaginatedMessage {
*
* @param messageOrInteraction The message or interaction that triggered this {@link PaginatedMessage}.
* Generally this will be the command message or an interaction
* (either a {@link CommandInteraction}, a {@link ContextMenuInteraction}, a {@link StringSelectMenuInteraction} or a {@link ButtonInteraction}),
* (either a {@link CommandInteraction}, {@link ContextMenuInteraction}, or an interaction from {@link PaginatedMessageInteractionUnion}),
* but it can also be another message from your client, i.e. to indicate a loading state.
*
* @param target The user who will be able to interact with the buttons of this {@link PaginatedMessage}.
Expand Down Expand Up @@ -1126,15 +1143,15 @@ export class PaginatedMessage {
* Get the components of a page.
* @param index The index of the page that has the components.
*/
public async getComponents(index: number): Promise<ActionRowBuilder<ButtonBuilder | StringSelectMenuBuilder>[] | undefined> {
public async getComponents(index: number): Promise<ActionRowBuilder<MessageActionRowComponentBuilder>[] | undefined> {
const page = this.messages.at(index);
if (isNullish(page)) return undefined;

const options = isFunction(page) ? await page(this.index, this.pages, this) : page;
if (isNullish(options.components)) return undefined;

return options.components.map((row) => {
return ActionRowBuilder.from(row as APIActionRowComponent<APIButtonComponent | APIStringSelectComponent>);
return ActionRowBuilder.from(row as APIActionRowComponent<APIMessageActionRowComponent>);
});
}

Expand All @@ -1143,7 +1160,7 @@ export class PaginatedMessage {
*
* @param messageOrInteraction The message or interaction that triggered this {@link PaginatedMessage}.
* Generally this will be the command message or an interaction
* (either a {@link CommandInteraction}, a {@link ContextMenuInteraction}, a {@link StringSelectMenuInteraction} or a {@link ButtonInteraction}),
* (either a {@link CommandInteraction}, {@link ContextMenuInteraction}, or an interaction from {@link PaginatedMessageInteractionUnion}),
* but it can also be another message from your client, i.e. to indicate a loading state.
*/
protected async setUpMessage(messageOrInteraction: Message | AnyInteractableInteraction): Promise<void> {
Expand Down Expand Up @@ -1185,7 +1202,7 @@ export class PaginatedMessage {
*/
protected setUpCollector(channel: TextBasedChannel, targetUser: User): void {
if (this.pages.length > 1) {
this.collector = new InteractionCollector<ButtonInteraction | StringSelectMenuInteraction>(targetUser.client, {
this.collector = new InteractionCollector<PaginatedMessageInteractionUnion>(targetUser.client, {
filter: (interaction) =>
!isNullish(this.response) && //
interaction.isMessageComponent() &&
Expand Down Expand Up @@ -1239,28 +1256,52 @@ export class PaginatedMessage {
actions: PaginatedMessageAction[],
messageOrInteraction: Message | AnyInteractableInteraction,
targetUser: User
): Promise<(ButtonBuilder | StringSelectMenuBuilder)[]> {
): Promise<MessageActionRowComponentBuilder[]> {
return Promise.all(
actions.map<Promise<ButtonBuilder | StringSelectMenuBuilder>>(async (interaction) => {
return isMessageButtonInteractionData(interaction)
? new ButtonBuilder(interaction)
: new StringSelectMenuBuilder({
...(interaction.customId === '@sapphire/paginated-messages.goToPage' && {
options: await Promise.all(
this.pages.map(async (_, index) => {
return {
...(await this.selectMenuOptions(
index + 1,
this.resolvePaginatedMessageInternationalizationContext(messageOrInteraction, targetUser)
)),
value: index.toString()
};
})
),
placeholder: this.selectMenuPlaceholder
}),
...interaction
});
actions.map<Promise<MessageActionRowComponentBuilder>>(async (interaction) => {
if (isMessageButtonInteractionData(interaction)) {
return new ButtonBuilder(interaction);
}

if (isMessageUserSelectInteractionData(interaction)) {
return new UserSelectMenuBuilder(interaction);
}

if (isMessageRoleSelectInteractionData(interaction)) {
return new RoleSelectMenuBuilder(interaction);
}

if (isMessageMentionableSelectInteractionData(interaction)) {
return new MentionableSelectMenuBuilder(interaction);
}

if (isMessageChannelSelectInteractionData(interaction)) {
return new ChannelSelectMenuBuilder(interaction);
}

if (isMessageStringSelectInteractionData(interaction)) {
return new StringSelectMenuBuilder({
...interaction,
...(interaction.customId === '@sapphire/paginated-messages.goToPage' && {
options: await Promise.all(
this.pages.map(async (_, index) => {
return {
...(await this.selectMenuOptions(
index + 1,
this.resolvePaginatedMessageInternationalizationContext(messageOrInteraction, targetUser)
)),
value: index.toString()
};
})
),
placeholder: this.selectMenuPlaceholder
})
});
}

throw new Error(
"Unsupported message component type detected. Validate your code and if you're sure this is a bug in Sapphire make a report in the server"
);
})
);
}
Expand All @@ -1271,11 +1312,7 @@ export class PaginatedMessage {
* @param channel The channel the handler is running at.
* @param interaction The button interaction that was received.
*/
protected async handleCollect(
targetUser: User,
channel: Message['channel'],
interaction: ButtonInteraction | StringSelectMenuInteraction
): Promise<void> {
protected async handleCollect(targetUser: User, channel: Message['channel'], interaction: PaginatedMessageInteractionUnion): Promise<void> {
if (interaction.user.id === targetUser.id) {
// Update the response to the latest interaction
this.response = interaction;
Expand Down Expand Up @@ -1329,10 +1366,7 @@ export class PaginatedMessage {
* Handles the `end` event from the collector.
* @param reason The reason for which the collector was ended.
*/
protected async handleEnd(
_: Collection<Snowflake, ButtonInteraction | StringSelectMenuInteraction>,
reason: PaginatedMessageStopReasons
): Promise<void> {
protected async handleEnd(_: Collection<Snowflake, PaginatedMessageInteractionUnion>, reason: PaginatedMessageStopReasons): Promise<void> {
// Ensure no race condition can occur where interacting with the message when the paginated message closes would otherwise result in a DiscordAPIError
if (
(reason === 'time' || reason === 'idle') &&
Expand Down Expand Up @@ -1498,6 +1532,7 @@ export class PaginatedMessage {
{
customId: '@sapphire/paginated-messages.goToPage',
type: ComponentType.StringSelect,
options: [],
run: ({ handler, interaction }) => interaction.isStringSelectMenu() && (handler.index = parseInt(interaction.values[0], 10))
},
{
Expand Down

0 comments on commit 4858486

Please sign in to comment.