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

Added Support for Opening Modals as an MCI Response #173

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 1 addition & 2 deletions examples/feature_showcase/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ mod collector;
mod context_menu;
mod inherit_checks;
mod localization;
mod mci_modal_response;
mod modal;
mod paginate;
mod panic_handler;
Expand Down Expand Up @@ -57,6 +56,7 @@ async fn main() {
inherit_checks::parent_checks(),
localization::welcome(),
modal::modal(),
modal::component_modal(),
paginate::paginate(),
panic_handler::div(),
parameter_attributes::addmultiple(),
Expand All @@ -70,7 +70,6 @@ async fn main() {
subcommand_required::parent_subcommand_required(),
track_edits::test_reuse_response(),
track_edits::add(),
mci_modal_response::mci_modal_response(),
],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()),
Expand Down
39 changes: 0 additions & 39 deletions examples/feature_showcase/mci_modal_response.rs

This file was deleted.

33 changes: 33 additions & 0 deletions examples/feature_showcase/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,36 @@ pub async fn modal(ctx: poise::ApplicationContext<'_, Data, Error>) -> Result<()

Ok(())
}

/// Tests the Modal trait with component interactions.
///
/// Should be both prefix and slash to make sure it works without any slash command interaction
/// present.
#[poise::command(prefix_command, slash_command)]
pub async fn component_modal(ctx: crate::Context<'_>) -> Result<(), Error> {
ctx.send(|m| {
m.content("Click the button below to open the modal")
.components(|c| {
c.create_action_row(|a| {
a.create_button(|b| {
b.custom_id("open_modal")
.label("Open modal")
.style(poise::serenity_prelude::ButtonStyle::Success)
})
})
})
})
.await?;

while let Some(mci) =
poise::serenity_prelude::CollectComponentInteraction::new(ctx.serenity_context())
.timeout(std::time::Duration::from_secs(120))
.filter(move |mci| mci.data.custom_id == "open_modal")
.await
{
let data =
poise::execute_modal_on_component_interaction::<MyModal>(ctx, mci, None, None).await?;
println!("Got data: {:?}", data);
}
Ok(())
}
Comment on lines +18 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, makes sense to me to have these live in the same file. Again just didn't want to complicate things, was mostly trying to maintain the existing structures.

136 changes: 66 additions & 70 deletions src/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,24 @@ pub fn find_modal_text(
None
}

/// Convenience function for showing the modal and waiting for a response.
///
/// If the user doesn't submit before the timeout expires, `None` is returned.
///
/// Note: a modal must be the first response to a command. You cannot send any messages before,
/// or the modal will fail.
///
/// This function:
/// 1. sends the modal via [`Modal::create()`]
/// 2. waits for the user to submit via [`serenity::CollectModalInteraction`]
/// 3. acknowledges the submitted data so that Discord closes the pop-up for the user
/// 4. parses the submitted data via [`Modal::parse()`], wrapping errors in [`serenity::Error::Other`]
///
/// If you need more specialized behavior, you can copy paste the implementation of this function
/// and adjust to your needs. The code of this function is just a starting point.
pub async fn execute_modal<U: Send + Sync, E, M: Modal>(
ctx: crate::ApplicationContext<'_, U, E>,
/// Underlying code for the modal spawning convenience function which abstracts over the kind of
/// interaction
async fn execute_modal_generic<
M: Modal,
F: std::future::Future<Output = Result<(), serenity::Error>>,
>(
ctx: &serenity::Context,
create_interaction_response: impl FnOnce(serenity::CreateInteractionResponse<'static>) -> F,
modal_custom_id: String,
defaults: Option<M>,
timeout: Option<std::time::Duration>,
) -> Result<Option<M>, serenity::Error> {
let interaction = ctx.interaction.unwrap();
let interaction_id = interaction.id.to_string();

// Send modal
interaction
.create_interaction_response(ctx.serenity_context, |b| {
*b = M::create(defaults, interaction_id.clone());
b
})
.await?;
ctx.has_sent_initial_response
.store(true, std::sync::atomic::Ordering::SeqCst);
create_interaction_response(M::create(defaults, modal_custom_id.clone())).await?;

// Wait for user to submit
let response = serenity::CollectModalInteraction::new(&ctx.serenity_context.shard)
.filter(move |d| d.data.custom_id == interaction_id)
let response = serenity::CollectModalInteraction::new(&ctx.shard)
.filter(move |d| d.data.custom_id == modal_custom_id)
.timeout(timeout.unwrap_or(std::time::Duration::from_secs(3600)))
.await;
let response = match response {
Expand All @@ -82,7 +64,7 @@ pub async fn execute_modal<U: Send + Sync, E, M: Modal>(

// Send acknowledgement so that the pop-up is closed
response
.create_interaction_response(ctx.serenity_context, |b| {
.create_interaction_response(ctx, |b| {
b.kind(serenity::InteractionResponseType::DeferredUpdateMessage)
})
.await?;
Expand All @@ -92,56 +74,76 @@ pub async fn execute_modal<U: Send + Sync, E, M: Modal>(
))
}

/// Convenience function for showing the modal on a message interaction and waiting for a response.
/// Convenience function for showing the modal and waiting for a response.
///
/// If the user doesn't submit before the timeout expires, `None` is returned.
///
/// Note: a modal must be the first response to a command. You cannot send any messages before,
/// or the modal will fail.
///
/// This function:
/// 1. sends the modal via [`Modal::create()`] as a mci interaction response
/// 1. sends the modal via [`Modal::create()`]
/// 2. waits for the user to submit via [`serenity::CollectModalInteraction`]
/// 3. acknowledges the submitted data so that Discord closes the pop-up for the user
/// 4. parses the submitted data via [`Modal::parse()`], wrapping errors in [`serenity::Error::Other`]
///
/// If you need more specialized behavior, you can copy paste the implementation of this function
/// and adjust to your needs. The code of this function is just a starting point.
pub async fn execute_component_interaction<U: Send + Sync, E, M: Modal>(
pub async fn execute_modal<U: Send + Sync, E, M: Modal>(
ctx: crate::ApplicationContext<'_, U, E>,
mci: Arc<serenity::MessageComponentInteraction>,
defaults: Option<M>,
timeout: Option<std::time::Duration>,
) -> Result<Option<M>, serenity::Error> {
let interaction = ctx.interaction.unwrap();
let interaction_id = interaction.id.to_string();

// Send modal
mci.create_interaction_response(ctx.serenity_context(), |b| {
*b = M::create(defaults, interaction_id.clone());
b
})
let response = execute_modal_generic(
ctx.serenity_context,
|resp| {
interaction.create_interaction_response(ctx.http(), |b| {
*b = resp;
b
})
},
interaction.id.to_string(),
defaults,
timeout,
)
.await?;
ctx.has_sent_initial_response
.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(response)
}

// Wait for user to submit
let response = serenity::CollectModalInteraction::new(&ctx.serenity_context.shard)
.filter(move |d| d.data.custom_id == interaction_id)
.timeout(timeout.unwrap_or(std::time::Duration::from_secs(3600)))
.await;
let response = match response {
Some(x) => x,
None => return Ok(None),
};

// Send acknowledgement so that the pop-up is closed
response
.create_interaction_response(ctx.serenity_context, |b| {
b.kind(serenity::InteractionResponseType::DeferredUpdateMessage)
})
.await?;

Ok(Some(
M::parse(response.data.clone()).map_err(serenity::Error::Other)?,
))
/// Convenience function for showing the modal on a message interaction and waiting for a response.
///
/// If the user doesn't submit before the timeout expires, `None` is returned.
///
/// This function:
/// 1. sends the modal via [`Modal::create()`] as a mci interaction response
/// 2. waits for the user to submit via [`serenity::CollectModalInteraction`]
/// 3. acknowledges the submitted data so that Discord closes the pop-up for the user
/// 4. parses the submitted data via [`Modal::parse()`], wrapping errors in [`serenity::Error::Other`]
///
/// If you need more specialized behavior, you can copy paste the implementation of this function
/// and adjust to your needs. The code of this function is just a starting point.
pub async fn execute_modal_on_component_interaction<M: Modal>(
ctx: impl AsRef<serenity::Context>,
interaction: Arc<serenity::MessageComponentInteraction>,
defaults: Option<M>,
timeout: Option<std::time::Duration>,
) -> Result<Option<M>, serenity::Error> {
execute_modal_generic(
ctx.as_ref(),
|resp| {
interaction.create_interaction_response(ctx.as_ref(), |b| {
*b = resp;
b
})
},
interaction.id.to_string(),
defaults,
timeout,
)
.await
}

/// Derivable trait for modal interactions, Discords version of interactive forms
Expand Down Expand Up @@ -196,21 +198,15 @@ pub trait Modal: Sized {
fn parse(data: serenity::ModalSubmitInteractionData) -> Result<Self, &'static str>;

/// Calls `execute_modal(ctx, None, None)`. See [`execute_modal`]
///
/// For a variant that is triggered on component interactions, see [`execute_modal_on_component_interaction`].
// TODO: add execute_with_defaults? Or add a `defaults: Option<Self>` param?
async fn execute<U: Send + Sync, E>(
ctx: crate::ApplicationContext<'_, U, E>,
) -> Result<Option<Self>, serenity::Error> {
execute_modal(ctx, None::<Self>, None).await
}

/// Calls `execute_component_interaction(ctx, mic, None, None)`. See [`execute_modal`]
async fn execute_component_interaction<U: Send + Sync, E>(
ctx: crate::ApplicationContext<'_, U, E>,
mci: Arc<serenity::MessageComponentInteraction>,
) -> Result<Option<Self>, serenity::Error> {
execute_component_interaction(ctx, mci, None::<Self>, None).await
}

/// Calls `execute_modal(ctx, Some(defaults), None)`. See [`execute_modal`]
// TODO: deprecate this in favor of execute_modal()?
async fn execute_with_defaults<U: Send + Sync, E>(
Expand Down
7 changes: 7 additions & 0 deletions src/structs/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,13 @@ impl<U, E> AsRef<serenity::ShardMessenger> for Context<'_, U, E> {
&self.serenity_context().shard
}
}
// Originally added as part of component interaction modals; not sure if this impl is really
// required by anything else... It makes sense to have though imo
impl<U, E> AsRef<serenity::Context> for Context<'_, U, E> {
fn as_ref(&self) -> &serenity::Context {
self.serenity_context()
}
}
impl<U: Sync, E> serenity::CacheHttp for Context<'_, U, E> {
fn http(&self) -> &serenity::Http {
&self.serenity_context().http
Expand Down