From d1f3aa5fa9978e0e0633826868c2df1d225678ff Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 20 Feb 2025 19:34:14 +0100 Subject: [PATCH] Change pagination to use interactions instead of reactions --- youmubot-cf/src/lib.rs | 18 +- youmubot-core/src/community/roles.rs | 9 +- youmubot-core/src/fun/images.rs | 26 +-- youmubot-osu/src/discord/display.rs | 132 ++++++----- youmubot-osu/src/discord/hook.rs | 8 +- youmubot-osu/src/discord/server_rank.rs | 18 +- youmubot-prelude/src/pagination.rs | 292 ++++++++++++++---------- youmubot/src/main.rs | 5 + 8 files changed, 281 insertions(+), 227 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index d49da9b..49d0531 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -180,7 +180,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap(); paginate_reply( - paginate_from_fn(move |page, ctx, msg| { + paginate_from_fn(move |page, _, _, btns| { use Align::*; let ranks = ranks.clone(); Box::pin(async move { @@ -188,7 +188,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { let start = ITEMS_PER_PAGE * page; let end = ranks.len().min(start + ITEMS_PER_PAGE); if start >= end { - return Ok(false); + return Ok(None); } let ranks = &ranks[start..end]; @@ -222,8 +222,9 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { )) .build(); - msg.edit(ctx, EditMessage::new().content(content)).await?; - Ok(true) + Ok(Some( + EditMessage::new().content(content).components(vec![btns]), + )) }) }) .with_page_count(total_pages), @@ -318,7 +319,7 @@ pub(crate) async fn contest_rank_table( let ranks = Arc::new(ranks); paginate_reply( - paginate_from_fn(move |page, ctx, msg| { + paginate_from_fn(move |page, _, _, btns| { let contest = contest.clone(); let problems = problems.clone(); let ranks = ranks.clone(); @@ -327,7 +328,7 @@ pub(crate) async fn contest_rank_table( let start = page * ITEMS_PER_PAGE; let end = ranks.len().min(start + ITEMS_PER_PAGE); if start >= end { - return Ok(false); + return Ok(None); } let ranks = &ranks[start..end]; @@ -389,8 +390,9 @@ pub(crate) async fn contest_rank_table( .push_line(format!("Page **{}/{}**", page + 1, total_pages)) .build(); - msg.edit(ctx, EditMessage::new().content(content)).await?; - Ok(true) + Ok(Some( + EditMessage::new().content(content).components(vec![btns]), + )) }) }) .with_page_count(total_pages), diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index 37887bd..06f9db8 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -44,14 +44,14 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult { let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE; paginate_reply( - paginate_from_fn(|page, ctx, msg| { + paginate_from_fn(|page, _, _, btns| { let roles = roles.clone(); Box::pin(async move { let page = page as usize; let start = page * ROLES_PER_PAGE; let end = roles.len().min(start + ROLES_PER_PAGE); if end <= start { - return Ok(false); + return Ok(None); } let roles = &roles[start..end]; @@ -77,8 +77,9 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult { .push_line(format!("Page **{}/{}**", page + 1, pages)) .build(); - msg.edit(ctx, EditMessage::new().content(content)).await?; - Ok(true) + Ok(Some( + EditMessage::new().content(content).components(vec![btns]), + )) }) }) .with_page_count(pages), diff --git a/youmubot-core/src/fun/images.rs b/youmubot-core/src/fun/images.rs index 4257fe2..733e84a 100644 --- a/youmubot-core/src/fun/images.rs +++ b/youmubot-core/src/fun/images.rs @@ -66,25 +66,23 @@ async fn message_command( } let images = std::sync::Arc::new(images); paginate_reply( - paginate_from_fn(|page, ctx, msg: &mut Message| { + paginate_from_fn(|page, _, _, btns| { let images = images.clone(); Box::pin(async move { let page = page as usize; if page >= images.len() { - Ok(false) + Ok(None) } else { - msg.edit( - ctx, - EditMessage::new().content(format!( - "[🖼️ **{}/{}**] Here's the image you requested!\n\n{}", - page + 1, - images.len(), - images[page] - )), - ) - .await - .map(|_| true) - .map_err(|e| e.into()) + Ok(Some( + EditMessage::new() + .content(format!( + "[🖼️ **{}/{}**] Here's the image you requested!\n\n{}", + page + 1, + images.len(), + images[page] + )) + .components(vec![btns]), + )) } }) }) diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 1cfc51d..33162f7 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -92,7 +92,12 @@ mod scores { #[async_trait] impl pagination::Paginate for Paginate { - async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result { + async fn render( + &mut self, + page: u8, + ctx: &Context, + msg: &Message, + ) -> Result> { let env = ctx.data.read().await.get::().unwrap().clone(); let page = page as usize; let score = &self.scores[page]; @@ -115,22 +120,17 @@ mod scores { .await? .ok_or_else(|| Error::msg("user not found"))?; - msg.edit( - ctx, + hourglass.delete(ctx).await?; + save_beatmap(&env, msg.channel_id, &bm).await?; + Ok(Some( EditMessage::new() .embed({ crate::discord::embeds::score_embed(score, &bm, &content, &user) .footer(format!("Page {}/{}", page + 1, self.scores.len())) .build() }) - .components(vec![score_components(self.guild_id)]), - ) - .await?; - save_beatmap(&env, msg.channel_id, &bm).await?; - - // End - hourglass.delete(ctx).await?; - Ok(true) + .components(vec![score_components(self.guild_id), self.pagination_row()]), + )) } fn len(&self) -> Option { @@ -193,7 +193,12 @@ mod scores { #[async_trait] impl pagination::Paginate for Paginate { - async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result { + async fn render( + &mut self, + page: u8, + ctx: &Context, + msg: &Message, + ) -> Result> { let env = ctx.data.read().await.get::().unwrap().clone(); let meta_cache = &env.beatmaps; @@ -202,7 +207,7 @@ mod scores { let start = page * ITEMS_PER_PAGE; let end = self.scores.len().min(start + ITEMS_PER_PAGE); if start >= end { - return Ok(false); + return Ok(None); } let hourglass = msg.react(ctx, '⌛').await?; @@ -321,9 +326,12 @@ mod scores { .push_line("[?] means pp was predicted by oppai-rs.") .build(); - msg.edit(ctx, EditMessage::new().content(content)).await?; hourglass.delete(ctx).await?; - Ok(true) + Ok(Some( + EditMessage::new() + .content(content) + .components(vec![self.pagination_row()]), + )) } fn len(&self) -> Option { @@ -335,7 +343,7 @@ mod scores { mod beatmapset { use serenity::{ - all::{GuildId, Reaction}, + all::{CreateButton, GuildId}, builder::{CreateEmbedFooter, EditMessage}, model::channel::{Message, ReactionType}, }; @@ -352,6 +360,7 @@ mod beatmapset { }; const SHOW_ALL_EMOTE: &str = "🗒️"; + const SHOW_ALL: &str = "youmubot_osu::discord::display::show_all"; pub async fn display_beatmapset( ctx: Context, @@ -377,8 +386,6 @@ mod beatmapset { mode, mods, guild_id, - - all_reaction: None, }; let ctx = ctx.clone(); @@ -401,8 +408,6 @@ mod beatmapset { mode: Option, mods: Option, guild_id: Option, - - all_reaction: Option, } impl Paginate { @@ -427,21 +432,25 @@ mod beatmapset { Some(self.maps.len()) } - async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result { + async fn render( + &mut self, + page: u8, + ctx: &Context, + msg: &Message, + ) -> Result> { let page = page as usize; if page == self.maps.len() { - msg.edit( - ctx, - EditMessage::new().embed(crate::discord::embeds::beatmapset_embed( - &self.maps[..], - self.mode, - )), - ) - .await?; - return Ok(true); + return Ok(Some( + EditMessage::new() + .embed(crate::discord::embeds::beatmapset_embed( + &self.maps[..], + self.mode, + )) + .components(vec![self.pagination_row()]), + )); } if page > self.maps.len() { - return Ok(false); + return Ok(None); } let map = &self.maps[page]; @@ -458,7 +467,16 @@ mod beatmapset { self.infos[page].insert(info) } }; - msg.edit(ctx, + let env = ctx.data.read().await.get::().unwrap().clone(); + save_beatmap( + &env, + msg.channel_id, + &BeatmapWithMode(map.clone(), self.mode), + ) + .await + .pls_ok(); + + Ok(Some( EditMessage::new().embed( crate::discord::embeds::beatmap_embed( map, @@ -475,31 +493,19 @@ mod beatmapset { )) }) ) - .components(vec![beatmap_components(map.mode, self.guild_id)]), - ) - .await?; - let env = ctx.data.read().await.get::().unwrap().clone(); - save_beatmap( - &env, - msg.channel_id, - &BeatmapWithMode(map.clone(), self.mode), - ) - .await - .pls_ok(); - - Ok(true) + .components(vec![beatmap_components(map.mode, self.guild_id), self.pagination_row()]), + )) } - async fn prerender( - &mut self, - ctx: &Context, - m: &mut serenity::model::channel::Message, - ) -> Result<()> { - self.all_reaction = Some( - m.react(&ctx, SHOW_ALL_EMOTE.parse::().unwrap()) - .await?, + fn interaction_buttons(&self) -> Vec { + let mut btns = pagination::default_buttons(self); + btns.insert( + 0, + CreateButton::new(SHOW_ALL) + .emoji(ReactionType::try_from(SHOW_ALL_EMOTE).unwrap()) + .label("Show all"), ); - Ok(()) + btns } async fn handle_reaction( @@ -507,26 +513,16 @@ mod beatmapset { page: u8, ctx: &Context, message: &mut serenity::model::channel::Message, - reaction: &Reaction, + reaction: &str, ) -> Result> { // Render the old style. - if let ReactionType::Unicode(s) = &reaction.emoji { - if s == SHOW_ALL_EMOTE { - self.render(self.maps.len() as u8, ctx, message).await?; - return Ok(Some(self.maps.len() as u8)); - } + if reaction == SHOW_ALL { + pagination::do_render(self, self.maps.len() as u8, ctx, message).await?; + return Ok(Some(self.maps.len() as u8)); } pagination::handle_pagination_reaction(page, self, ctx, message, reaction) .await .map(Some) } - - async fn cleanup(&mut self, ctx: &Context, _msg: &mut Message) { - if let Some(r) = self.all_reaction.take() { - if !r.delete_all(&ctx).await.is_ok() { - r.delete(&ctx).await.pls_ok(); - } - } - } } } diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 2d08043..2cf7bc3 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -192,18 +192,18 @@ pub fn dot_osu_hook<'a>( } else { let osu_embeds = Arc::new(osu_embeds); paginate_reply( - paginate_from_fn(|page, ctx, msg| { + paginate_from_fn(|page, _, _, btns| { let osu_embeds = osu_embeds.clone(); Box::pin(async move { let (embed, attachments) = &osu_embeds[page as usize]; let mut edit = EditMessage::new() .content(format!("Attached beatmaps ({}/{})", page + 1, embed_len)) - .embed(embed.clone()); + .embed(embed.clone()) + .components(vec![btns]); for att in attachments { edit = edit.new_attachment(att.clone()); } - msg.edit(&ctx, edit).await?; - Ok(true) + Ok(Some(edit)) }) }) .with_page_count(embed_len), diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index ad0b166..502829e 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -236,7 +236,7 @@ where let total_len = users.len(); let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; paginate_with_first_message( - paginate_from_fn(move |page: u8, ctx: &Context, m: &mut Message| { + paginate_from_fn(move |page: u8, _: &Context, _: &Message, btns| { let header = header.clone(); use Align::*; let users = users.clone(); @@ -244,7 +244,7 @@ where let start = (page as usize) * ITEMS_PER_PAGE; let end = (start + ITEMS_PER_PAGE).min(users.len()); if start >= end { - return Ok(false); + return Ok(None); } let users = &users[start..end]; let table = match query { @@ -317,8 +317,9 @@ where last_update.format(""), )) .build(); - m.edit(ctx, EditMessage::new().content(content)).await?; - Ok(true) + Ok(Some( + EditMessage::new().content(content).components(vec![btns]), + )) }) }) .with_page_count(total_pages), @@ -617,11 +618,11 @@ pub async fn display_rankings_table( let header = Arc::new(to.content.clone()); paginate_with_first_message( - paginate_from_fn(move |page: u8, ctx: &Context, m: &mut Message| { + paginate_from_fn(move |page: u8, _, _, btns| { let start = (page as usize) * ITEMS_PER_PAGE; let end = (start + ITEMS_PER_PAGE).min(scores.len()); if start >= end { - return Box::pin(future::ready(Ok(false))); + return Box::pin(future::ready(Ok(None))); } let scores = scores[start..end].to_vec(); let header = header.clone(); @@ -720,8 +721,9 @@ pub async fn display_rankings_table( )) .build(); - m.edit(&ctx, EditMessage::new().content(content)).await?; - Ok(true) + Ok(Some( + EditMessage::new().content(content).components(vec![btns]), + )) }) }) .with_page_count(total_pages), diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index f1817b5..01213d2 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -1,14 +1,18 @@ -use crate::{Context, OkPrint, Result}; -use futures_util::{future::Future, StreamExt as _}; +use crate::{Context, Result}; +use futures_util::future::Future; use serenity::{ + all::{ + CreateActionRow, CreateButton, CreateInteractionResponse, EditMessage, Interaction, + MessageId, + }, builder::CreateMessage, - collector, model::{ - channel::{Message, Reaction, ReactionType}, + channel::{Message, ReactionType}, id::ChannelId, }, + prelude::TypeMapKey, }; -use std::convert::TryFrom; +use std::{convert::TryFrom, sync::Arc}; use tokio::time as tokio_time; const ARROW_RIGHT: &str = "➡️"; @@ -16,19 +20,28 @@ const ARROW_LEFT: &str = "⬅️"; const REWIND: &str = "⏪"; const FAST_FORWARD: &str = "⏩"; +const NEXT: &str = "youmubot_pagination_next"; +const PREV: &str = "youmubot_pagination_prev"; +const FAST_NEXT: &str = "youmubot_pagination_fast_next"; +const FAST_PREV: &str = "youmubot_pagination_fast_prev"; + /// A trait that provides the implementation of a paginator. #[async_trait::async_trait] pub trait Paginate: Send + Sized { /// Render the given page. - async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result; + /// Remember to add the [[interaction_buttons]] as an action row! + async fn render(&mut self, page: u8, ctx: &Context, m: &Message) + -> Result>; - /// Any setting-up before the rendering stage. - async fn prerender(&mut self, _ctx: &Context, _m: &mut Message) -> Result<()> { - Ok(()) + /// The [[CreateActionRow]] for pagination. + fn pagination_row(&self) -> CreateActionRow { + CreateActionRow::Buttons(self.interaction_buttons()) } - /// Cleans up after the pagination has timed out. - async fn cleanup(&mut self, _ctx: &Context, _m: &mut Message) -> () {} + /// A list of buttons to create that would interact with pagination logic. + fn interaction_buttons(&self) -> Vec { + default_buttons(self) + } /// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling /// before handing the functionality over. @@ -39,7 +52,7 @@ pub trait Paginate: Send + Sized { page: u8, ctx: &Context, message: &mut Message, - reaction: &Reaction, + reaction: &str, ) -> Result> { handle_pagination_reaction(page, self, ctx, message, reaction) .await @@ -65,17 +78,48 @@ pub trait Paginate: Send + Sized { } } +pub async fn do_render( + p: &mut impl Paginate, + page: u8, + ctx: &Context, + m: &mut Message, +) -> Result { + if let Some(edit) = p.render(page, ctx, m).await? { + m.edit(ctx, edit).await?; + Ok(true) + } else { + Ok(false) + } +} + pub fn paginate_from_fn( pager: impl for<'m> FnMut( u8, &'m Context, - &'m mut Message, - ) -> std::pin::Pin> + Send + 'm>> - + Send, + &'m Message, + CreateActionRow, + ) -> std::pin::Pin< + Box>> + Send + 'm>, + > + Send, ) -> impl Paginate { pager } +pub fn default_buttons(p: &impl Paginate) -> Vec { + let mut btns = vec![ + CreateButton::new(PREV).emoji(ReactionType::try_from(ARROW_LEFT).unwrap()), + CreateButton::new(NEXT).emoji(ReactionType::try_from(ARROW_RIGHT).unwrap()), + ]; + if p.len().is_some_and(|v| v > 5) { + btns.insert( + 0, + CreateButton::new(FAST_PREV).emoji(ReactionType::try_from(REWIND).unwrap()), + ); + btns.push(CreateButton::new(FAST_NEXT).emoji(ReactionType::try_from(FAST_FORWARD).unwrap())) + } + btns +} + struct WithPageCount { inner: Inner, page_count: usize, @@ -83,33 +127,28 @@ struct WithPageCount { #[async_trait::async_trait] impl Paginate for WithPageCount { - async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result { + async fn render( + &mut self, + page: u8, + ctx: &Context, + m: &Message, + ) -> Result> { if page as usize >= self.page_count { - return Ok(false); + return Ok(None); } self.inner.render(page, ctx, m).await } - async fn prerender(&mut self, ctx: &Context, m: &mut Message) -> Result<()> { - self.inner.prerender(ctx, m).await - } async fn handle_reaction( &mut self, page: u8, ctx: &Context, message: &mut Message, - reaction: &Reaction, + reaction: &str, ) -> Result> { - // handle normal reactions first, then fallback to the inner one - let new_page = handle_pagination_reaction(page, self, ctx, message, reaction).await?; - - if new_page != page { - Ok(Some(new_page)) - } else { - self.inner - .handle_reaction(page, ctx, message, reaction) - .await - } + self.inner + .handle_reaction(page, ctx, message, reaction) + .await } fn len(&self) -> Option { @@ -119,10 +158,6 @@ impl Paginate for WithPageCount { fn is_empty(&self) -> Option { Some(self.page_count == 0) } - - async fn cleanup(&mut self, ctx: &Context, msg: &mut Message) { - self.inner.cleanup(ctx, msg).await; - } } #[async_trait::async_trait] @@ -131,12 +166,20 @@ where T: for<'m> FnMut( u8, &'m Context, - &'m mut Message, - ) -> std::pin::Pin> + Send + 'm>> - + Send, + &'m Message, + CreateActionRow, + ) -> std::pin::Pin< + Box>> + Send + 'm>, + > + Send, { - async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result { - self(page, ctx, m).await + async fn render( + &mut self, + page: u8, + ctx: &Context, + m: &Message, + ) -> Result> { + let btns = self.pagination_row(); + self(page, ctx, m, btns).await } } @@ -178,63 +221,22 @@ pub async fn paginate_with_first_message( mut message: Message, timeout: std::time::Duration, ) -> Result<()> { - pager.prerender(ctx, &mut message).await?; - pager.render(0, ctx, &mut message).await?; + let (send, recv) = flume::unbounded::(); + Paginator::push(ctx, &message, send).await?; + + do_render(&mut pager, 0, ctx, &mut message).await?; // Just quit if there is only one page if pager.len().filter(|&v| v == 1).is_some() { return Ok(()); } - // React to the message - let large_count = pager.len().filter(|&p| p > 10).is_some(); - let reactions = { - let mut rs = Vec::::with_capacity(4); - if large_count { - // add >> and << buttons - rs.push(message.react(&ctx, ReactionType::try_from(REWIND)?).await?); - } - rs.push( - message - .react(&ctx, ReactionType::try_from(ARROW_LEFT)?) - .await?, - ); - rs.push( - message - .react(&ctx, ReactionType::try_from(ARROW_RIGHT)?) - .await?, - ); - if large_count { - // add >> and << buttons - rs.push( - message - .react(&ctx, ReactionType::try_from(FAST_FORWARD)?) - .await?, - ); - } - rs - }; - // Build a reaction collector - let mut reaction_collector = { - // message.await_reactions(ctx).removed(true).build(); - let message_id = message.id; - let me = message.author.id; - collector::collect(&ctx.shard, move |event| { - match event { - serenity::all::Event::ReactionAdd(r) => Some(r.reaction.clone()), - serenity::all::Event::ReactionRemove(r) => Some(r.reaction.clone()), - _ => None, - } - .filter(|r| r.message_id == message_id) - .filter(|r| r.user_id.is_some_and(|id| id != me)) - }) - }; let mut page = 0; // Loop the handler function. let res: Result<()> = loop { - match tokio_time::timeout(timeout, reaction_collector.next()).await { + match tokio_time::timeout(timeout, recv.clone().into_recv_async()).await { Err(_) => break Ok(()), - Ok(None) => break Ok(()), - Ok(Some(reaction)) => { + Ok(Err(_)) => break Ok(()), + Ok(Ok(reaction)) => { page = match pager .handle_reaction(page, ctx, &mut message, &reaction) .await @@ -247,14 +249,7 @@ pub async fn paginate_with_first_message( } }; - pager.cleanup(ctx, &mut message).await; - - for reaction in reactions { - if reaction.delete_all(&ctx).await.pls_ok().is_none() { - // probably no permission to delete all reactions, fall back to delete my own. - reaction.delete(&ctx).await.pls_ok(); - } - } + Paginator::pop(ctx, &message).await?; res } @@ -265,35 +260,90 @@ pub async fn handle_pagination_reaction( pager: &mut impl Paginate, ctx: &Context, message: &mut Message, - reaction: &Reaction, + reaction: &str, ) -> Result { let pages = pager.len(); let fast = pages.map(|v| v / 10).unwrap_or(5).max(5) as u8; - match &reaction.emoji { - ReactionType::Unicode(ref s) => { - let new_page = match s.as_str() { - ARROW_LEFT | REWIND if page == 0 => return Ok(page), - ARROW_LEFT => page - 1, - REWIND => { - if page < fast { - 0 - } else { - page - fast - } - } - ARROW_RIGHT if pages.filter(|&pages| page as usize + 1 >= pages).is_some() => { - return Ok(page) - } - ARROW_RIGHT => page + 1, - FAST_FORWARD => (pages.unwrap() as u8 - 1).min(page + fast), - _ => return Ok(page), - }; - Ok(if pager.render(new_page, ctx, message).await? { - new_page + let new_page = match reaction { + PREV | FAST_PREV if page == 0 => return Ok(page), + PREV => page - 1, + FAST_PREV => { + if page < fast { + 0 } else { - page - }) + page - fast + } + } + NEXT if pages.filter(|&pages| page as usize + 1 >= pages).is_some() => return Ok(page), + NEXT => page + 1, + FAST_NEXT => (pages.unwrap() as u8 - 1).min(page + fast), + _ => return Ok(page), + }; + Ok( + if let Some(edit) = pager.render(new_page, ctx, message).await? { + message.edit(ctx, edit).await?; + new_page + } else { + page + }, + ) +} + +#[derive(Debug, Clone)] +/// Handles distributing pagination interaction to the handlers. +pub struct Paginator { + pub(crate) channels: Arc>>, +} + +impl Paginator { + pub fn new() -> Self { + Self { + channels: Arc::new(dashmap::DashMap::new()), + } + } + async fn push(ctx: &Context, msg: &Message, channel: flume::Sender) -> Result<()> { + ctx.data + .write() + .await + .get_mut::() + .unwrap() + .channels + .insert(msg.id, channel); + Ok(()) + } + + async fn pop(ctx: &Context, msg: &Message) -> Result<()> { + ctx.data + .write() + .await + .get_mut::() + .unwrap() + .channels + .remove(&msg.id); + Ok(()) + } +} + +impl TypeMapKey for Paginator { + type Value = Paginator; +} + +#[async_trait::async_trait] +impl crate::hook::InteractionHook for Paginator { + async fn call(&self, ctx: &Context, interaction: &Interaction) -> Result<()> { + match interaction { + Interaction::Component(component_interaction) => { + if let Some(ch) = self.channels.get(&component_interaction.message.id) { + component_interaction + .create_response(ctx, CreateInteractionResponse::Acknowledge) + .await?; + ch.send_async(component_interaction.data.custom_id.clone()) + .await + .ok(); + } + Ok(()) + } + _ => Ok(()), } - _ => Ok(page), } } diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 054bdc4..4139c9c 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -236,6 +236,11 @@ async fn main() { } }; + // Paginator + let paginator = youmubot_prelude::pagination::Paginator::new(); + handler.push_interaction_hook(paginator.clone()); + data.insert::(paginator); + data.insert::(env.clone()); #[cfg(feature = "core")]