pagination: make editing generic over a CanEdit interface

This commit is contained in:
Natsu Kagami 2025-02-20 22:54:38 +01:00
parent 5fde2f343a
commit a35563801d
Signed by: nki
GPG key ID: 55A032EB38B49ADB
11 changed files with 233 additions and 207 deletions

View file

@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use codeforces::Contest;
use pagination::paginate_from_fn;
use serenity::{
builder::{CreateMessage, EditMessage},
builder::CreateMessage,
framework::standard::{
macros::{command, group},
Args, CommandResult,
@ -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, _, _, btns| {
paginate_from_fn(move |page, btns| {
use Align::*;
let ranks = ranks.clone();
Box::pin(async move {
@ -222,7 +222,9 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),
@ -317,7 +319,7 @@ pub(crate) async fn contest_rank_table(
let ranks = Arc::new(ranks);
paginate_reply(
paginate_from_fn(move |page, _, _, btns| {
paginate_from_fn(move |page, btns| {
let contest = contest.clone();
let problems = problems.clone();
let ranks = ranks.clone();
@ -388,7 +390,9 @@ pub(crate) async fn contest_rank_table(
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),

View file

@ -1,5 +1,4 @@
use serenity::{
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
model::{
channel::{Message, ReactionType},
@ -44,7 +43,7 @@ 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, _, _, btns| {
paginate_from_fn(|page, btns| {
let roles = roles.clone();
Box::pin(async move {
let page = page as usize;
@ -77,7 +76,9 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
.push_line(format!("Page **{}/{}**", page + 1, pages))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(pages),

View file

@ -1,5 +1,4 @@
use serde::Deserialize;
use serenity::builder::EditMessage;
use serenity::framework::standard::CommandError as Error;
use serenity::{
framework::standard::{
@ -66,7 +65,7 @@ async fn message_command(
}
let images = std::sync::Arc::new(images);
paginate_reply(
paginate_from_fn(|page, _, _, btns| {
paginate_from_fn(|page, btns| {
let images = images.clone();
Box::pin(async move {
let page = page as usize;
@ -74,7 +73,7 @@ async fn message_command(
Ok(None)
} else {
Ok(Some(
EditMessage::new()
CreateReply::default()
.content(format!(
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
page + 1,

View file

@ -294,11 +294,14 @@ async fn handle_listing<U: HasOsuEnv>(
listing_kind,
user.mention()
))
.await?
.into_message()
.await?;
style
.display_scores(plays, ctx.serenity_context(), ctx.guild_id(), reply)
.display_scores(
plays,
ctx.clone().serenity_context(),
ctx.guild_id(),
(reply, ctx),
)
.await?;
}
}
@ -368,16 +371,14 @@ async fn beatmap<U: HasOsuEnv>(
let msg = ctx
.clone()
.reply(format!("Information for {}", b0.beatmapset_mention()))
.await?
.into_message()
.await?;
display_beatmapset(
ctx.serenity_context().clone(),
ctx.clone().serenity_context(),
vec,
mode,
mods,
ctx.guild_id(),
msg,
(msg, ctx),
)
.await?;
}
@ -470,8 +471,6 @@ async fn check<U: HasOsuEnv>(
args.user.mention(),
display
))
.await?
.into_message()
.await?;
let style = style.unwrap_or(if scores.len() <= 5 {
@ -481,7 +480,12 @@ async fn check<U: HasOsuEnv>(
});
style
.display_scores(scores, ctx.serenity_context(), ctx.guild_id(), msg)
.display_scores(
scores,
ctx.clone().serenity_context(),
ctx.guild_id(),
(msg, ctx),
)
.await?;
Ok(())
@ -577,13 +581,13 @@ async fn leaderboard<U: HasOsuEnv>(
.await?;
}
ScoreListStyle::Grid => {
let reply = ctx.reply(header).await?.into_message().await?;
let reply = ctx.reply(header).await?;
style
.display_scores(
scores.into_iter().map(|s| s.score).collect(),
ctx.serenity_context(),
Some(guild.id),
reply,
(reply, ctx),
)
.await?;
}

View file

@ -3,7 +3,7 @@ pub use scores::ScoreListStyle;
mod scores {
use poise::ChoiceParameter;
use serenity::{all::GuildId, model::channel::Message};
use serenity::all::GuildId;
use youmubot_prelude::*;
@ -42,7 +42,7 @@ mod scores {
scores: Vec<Score>,
ctx: &Context,
guild_id: Option<GuildId>,
m: Message,
m: impl CanEdit,
) -> Result<()> {
match self {
ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await,
@ -54,8 +54,6 @@ mod scores {
mod grid {
use pagination::paginate_with_first_message;
use serenity::all::{CreateActionRow, GuildId};
use serenity::builder::EditMessage;
use serenity::model::channel::Message;
use youmubot_prelude::*;
@ -67,16 +65,23 @@ mod scores {
scores: Vec<Score>,
ctx: &Context,
guild_id: Option<GuildId>,
mut on: Message,
mut on: impl CanEdit,
) -> Result<()> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let channel_id = on.get_message().await?.channel_id;
if scores.is_empty() {
on.edit(&ctx, EditMessage::new().content("No plays found"))
on.apply_edit(CreateReply::default().content("No plays found"))
.await?;
return Ok(());
}
paginate_with_first_message(
Paginate { scores, guild_id },
Paginate {
env,
scores,
guild_id,
channel_id,
},
ctx,
on,
std::time::Duration::from_secs(60),
@ -86,8 +91,10 @@ mod scores {
}
pub struct Paginate {
env: OsuEnv,
scores: Vec<Score>,
guild_id: Option<GuildId>,
channel_id: serenity::all::ChannelId,
}
#[async_trait]
@ -95,11 +102,9 @@ mod scores {
async fn render(
&mut self,
page: u8,
ctx: &Context,
msg: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
) -> Result<Option<CreateReply>> {
let env = &self.env;
let page = page as usize;
let score = &self.scores[page];
@ -120,9 +125,9 @@ mod scores {
.await?
.ok_or_else(|| Error::msg("user not found"))?;
save_beatmap(&env, msg.channel_id, &bm).await?;
save_beatmap(&env, self.channel_id, &bm).await?;
Ok(Some(
EditMessage::new()
CreateReply::default()
.embed({
crate::discord::embeds::score_embed(score, &bm, &content, &user)
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
@ -148,8 +153,6 @@ mod scores {
use pagination::paginate_with_first_message;
use serenity::all::CreateActionRow;
use serenity::builder::EditMessage;
use serenity::model::channel::Message;
use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::table_format::{table_formatting, Align};
@ -162,17 +165,18 @@ mod scores {
pub async fn display_scores_table(
scores: Vec<Score>,
ctx: &Context,
mut on: Message,
mut on: impl CanEdit,
) -> Result<()> {
if scores.is_empty() {
on.edit(&ctx, EditMessage::new().content("No plays found"))
on.apply_edit(CreateReply::default().content("No plays found"))
.await?;
return Ok(());
}
paginate_with_first_message(
Paginate {
header: on.content.clone(),
env: ctx.data.read().await.get::<OsuEnv>().unwrap().clone(),
header: on.get_message().await?.content.clone(),
scores,
},
ctx,
@ -184,6 +188,7 @@ mod scores {
}
pub struct Paginate {
env: OsuEnv,
header: String,
scores: Vec<Score>,
}
@ -201,11 +206,9 @@ mod scores {
async fn render(
&mut self,
page: u8,
ctx: &Context,
_: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
) -> Result<Option<CreateReply>> {
let env = &self.env;
let meta_cache = &env.beatmaps;
let oppai = &env.oppai;
@ -331,7 +334,9 @@ mod scores {
.push_line("[?] means pp was predicted by oppai-rs.")
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
}
fn len(&self) -> Option<usize> {
@ -344,8 +349,8 @@ mod scores {
mod beatmapset {
use serenity::{
all::{CreateActionRow, CreateButton, GuildId},
builder::{CreateEmbedFooter, EditMessage},
model::channel::{Message, ReactionType},
builder::CreateEmbedFooter,
model::channel::ReactionType,
};
use youmubot_prelude::*;
@ -363,12 +368,12 @@ mod beatmapset {
const SHOW_ALL: &str = "youmubot_osu::discord::display::show_all";
pub async fn display_beatmapset(
ctx: Context,
ctx: &Context,
mut beatmapset: Vec<Beatmap>,
mode: Option<Mode>,
mods: Option<UnparsedMods>,
guild_id: Option<GuildId>,
target: Message,
target: impl CanEdit,
) -> Result<bool> {
assert!(!beatmapset.is_empty(), "Beatmapset should not be empty");
@ -381,6 +386,8 @@ mod beatmapset {
});
let p = Paginate {
env: ctx.data.read().await.get::<OsuEnv>().unwrap().clone(),
channel_id: target.get_message().await?.channel_id,
infos: vec![None; beatmapset.len()],
maps: beatmapset,
mode,
@ -389,20 +396,20 @@ mod beatmapset {
};
let ctx = ctx.clone();
spawn_future(async move {
pagination::paginate_with_first_message(
p,
&ctx,
target,
std::time::Duration::from_secs(60),
)
.await
.pls_ok();
});
pagination::paginate_with_first_message(
p,
&ctx,
target,
std::time::Duration::from_secs(60),
)
.await
.pls_ok();
Ok(true)
}
struct Paginate {
env: OsuEnv,
channel_id: serenity::all::ChannelId,
maps: Vec<Beatmap>,
infos: Vec<Option<BeatmapInfoWithPP>>,
mode: Option<Mode>,
@ -411,15 +418,9 @@ mod beatmapset {
}
impl Paginate {
async fn get_beatmap_info(
&self,
ctx: &Context,
b: &Beatmap,
mods: &Mods,
) -> Result<BeatmapInfoWithPP> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
env.oppai
async fn get_beatmap_info(&self, b: &Beatmap, mods: &Mods) -> Result<BeatmapInfoWithPP> {
self.env
.oppai
.get_beatmap(b.beatmap_id)
.await
.map(move |v| v.get_possible_pp_with(b.mode.with_override(self.mode), &mods))
@ -435,14 +436,12 @@ mod beatmapset {
async fn render(
&mut self,
page: u8,
ctx: &Context,
msg: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
) -> Result<Option<CreateReply>> {
let page = page as usize;
if page == self.maps.len() {
return Ok(Some(
EditMessage::new()
CreateReply::default()
.embed(crate::discord::embeds::beatmapset_embed(
&self.maps[..],
self.mode,
@ -464,21 +463,20 @@ mod beatmapset {
let info = match &self.infos[page] {
Some(info) => info,
None => {
let info = self.get_beatmap_info(ctx, map, &mods).await?;
let info = self.get_beatmap_info(map, &mods).await?;
self.infos[page].insert(info)
}
};
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
save_beatmap(
&env,
msg.channel_id,
&self.env,
self.channel_id,
&BeatmapWithMode(map.clone(), self.mode),
)
.await
.pls_ok();
Ok(Some(
EditMessage::new().embed(
CreateReply::default().embed(
crate::discord::embeds::beatmap_embed(
map,
self.mode.unwrap_or(map.mode),
@ -512,16 +510,16 @@ mod beatmapset {
async fn handle_reaction(
&mut self,
page: u8,
ctx: &Context,
message: &mut serenity::model::channel::Message,
_ctx: &Context,
message: &mut impl CanEdit,
reaction: &str,
) -> Result<Option<u8>> {
// Render the old style.
if reaction == SHOW_ALL {
pagination::do_render(self, self.maps.len() as u8, ctx, message).await?;
pagination::do_render(self, self.maps.len() as u8, message).await?;
return Ok(Some(self.maps.len() as u8));
}
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
pagination::handle_pagination_reaction(page, self, message, reaction)
.await
.map(Some)
}

View file

@ -2,9 +2,7 @@ use std::sync::Arc;
use futures_util::stream::FuturesOrdered;
use pagination::paginate_from_fn;
use serenity::{
all::EditMessage, builder::CreateMessage, model::channel::Message, utils::MessageBuilder,
};
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
use stream::Stream;
use youmubot_prelude::*;
@ -192,16 +190,16 @@ pub fn dot_osu_hook<'a>(
} else {
let osu_embeds = Arc::new(osu_embeds);
paginate_reply(
paginate_from_fn(|page, _, _, btns| {
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()
let mut edit = CreateReply::default()
.content(format!("Attached beatmaps ({}/{})", page + 1, embed_len))
.embed(embed.clone())
.components(btns);
for att in attachments {
edit = edit.new_attachment(att.clone());
edit = edit.attachment(att.clone());
}
Ok(Some(edit))
})
@ -334,12 +332,12 @@ async fn handle_beatmapset<'a, 'b>(
)
.await?;
crate::discord::display::display_beatmapset(
ctx.clone(),
ctx,
beatmaps,
mode,
None,
reply_to.guild_id,
reply,
(reply, ctx),
)
.await
.pls_ok();

View file

@ -95,27 +95,22 @@ pub fn handle_check_button<'a>(
return Ok(());
}
let reply = comp
.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Here are the scores by [`{}`](<https://osu.ppy.sh/users/{}>) on {}!",
user.username,
user.id,
embed.mention()
)),
)
.await?;
comp.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Here are the scores by [`{}`](<https://osu.ppy.sh/users/{}>) on {}!",
user.username,
user.id,
embed.mention()
)),
)
.await?;
let ctx = ctx.clone();
let guild_id = comp.guild_id;
spawn_future(async move {
ScoreListStyle::Grid
.display_scores(scores, &ctx, guild_id, reply)
.await
.pls_ok();
});
ScoreListStyle::Grid
.display_scores(scores, &ctx, guild_id, (comp, ctx))
.await
.pls_ok();
Ok(())
})
}
@ -326,24 +321,21 @@ async fn handle_last_req(
.await
.unwrap();
let content_type = format!("Information for {}", embed.mention());
match embed {
EmbedType::Beatmapset(beatmapset, mode) => {
let reply = comp
.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Beatmapset `{}`",
beatmapset[0].beatmapset_mention()
)),
)
.await?;
comp.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(content_type),
)
.await?;
super::display::display_beatmapset(
ctx.clone(),
ctx,
beatmapset,
mode,
None,
comp.guild_id,
reply,
(comp, ctx),
)
.await?;
return Ok(());
@ -357,7 +349,7 @@ async fn handle_last_req(
comp.create_followup(
&ctx,
serenity::all::CreateInteractionResponseFollowup::new()
.content(format!("Information for beatmap {}", b.mention(m, &mods)))
.content(content_type)
.embed(beatmap_embed(&*b, m.unwrap_or(b.mode), &mods, &info))
.components(vec![beatmap_components(m.unwrap_or(b.mode), comp.guild_id)]),
)

View file

@ -689,7 +689,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
)
.await?;
style
.display_scores(plays, ctx, reply.guild_id, reply)
.display_scores(plays, ctx, reply.guild_id, (reply, ctx))
.await?;
}
Nth::Nth(nth) => {
@ -762,7 +762,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
)
.await?;
style
.display_scores(plays, ctx, reply.guild_id, reply)
.display_scores(plays, ctx, reply.guild_id, (reply, ctx))
.await?;
}
Nth::Nth(nth) => {
@ -966,7 +966,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let reply = msg
.reply(&ctx, format!("Information for {}", content_type))
.await?;
display::display_beatmapset(ctx.clone(), beatmaps, mode, umods, msg.guild_id, reply)
display::display_beatmapset(ctx, beatmaps, mode, umods, msg.guild_id, (reply, ctx))
.await?;
}
}
@ -1018,7 +1018,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
)
.await?;
style
.display_scores(scores, ctx, msg.guild_id, reply)
.display_scores(scores, ctx, msg.guild_id, (reply, ctx))
.await?;
Ok(())
@ -1130,7 +1130,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
)
.await?;
style
.display_scores(plays, ctx, msg.guild_id, reply)
.display_scores(plays, ctx, msg.guild_id, (reply, ctx))
.await?;
}
}

View file

@ -11,7 +11,6 @@ use chrono::DateTime;
use pagination::paginate_with_first_message;
use serenity::{
all::{GuildId, Member, PartialGuild},
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
model::channel::Message,
utils::MessageBuilder,
@ -236,7 +235,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, _: &Context, _: &Message, btns| {
paginate_from_fn(move |page: u8, btns| {
let header = header.clone();
use Align::*;
let users = users.clone();
@ -317,12 +316,14 @@ where
last_update.format("<t:%s:R>"),
))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),
ctx,
msg,
(msg, ctx),
std::time::Duration::from_secs(60),
)
.await?;
@ -425,7 +426,7 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C
scores.into_iter().map(|s| s.score).collect(),
ctx,
Some(guild),
reply,
(reply, ctx),
)
.await?;
}
@ -614,7 +615,7 @@ pub async fn display_rankings_table(
let header = Arc::new(to.content.clone());
paginate_with_first_message(
paginate_from_fn(move |page: u8, _, _, btns| {
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 {
@ -717,12 +718,14 @@ pub async fn display_rankings_table(
))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),
ctx,
to,
(to, ctx),
std::time::Duration::from_secs(60),
)
.await?;

View file

@ -18,7 +18,8 @@ pub use debugging_ok::OkPrint;
pub use flags::Flags;
pub use hook::Hook;
pub use member_cache::MemberCache;
pub use pagination::{paginate, paginate_from_fn, paginate_reply, Paginate};
pub use pagination::{paginate, paginate_from_fn, paginate_reply, CanEdit, Paginate};
pub use poise::CreateReply;
pub mod announcer;
pub mod args;

View file

@ -1,9 +1,10 @@
use crate::{Context, OkPrint, Result};
use crate::{CmdContext, Context, OkPrint, Result};
use futures_util::future::Future;
use poise::{CreateReply, ReplyHandle};
use serenity::{
all::{
CreateActionRow, CreateButton, CreateInteractionResponse, EditMessage, Interaction,
MessageId,
ComponentInteraction, CreateActionRow, CreateButton, CreateInteractionResponse,
EditInteractionResponse, EditMessage, Interaction, MessageId,
},
builder::CreateMessage,
model::{
@ -15,28 +16,68 @@ use serenity::{
use std::{convert::TryFrom, sync::Arc};
use tokio::time as tokio_time;
const ARROW_RIGHT: &str = "➡️";
const ARROW_LEFT: &str = "⬅️";
const REWIND: &str = "";
const FAST_FORWARD: &str = "";
// const ARROW_RIGHT: &str = "➡️";
// 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";
pub trait CanEdit: Send {
fn get_message(&self) -> impl Future<Output = Result<Message>> + Send;
fn apply_edit(&mut self, edit: CreateReply) -> impl Future<Output = Result<()>> + Send;
}
impl<'a> CanEdit for (Message, &'a Context) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.clone())
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0
.edit(&self.1, edit.to_prefix_edit(EditMessage::new()))
.await?;
Ok(())
}
}
impl<'a, 'b> CanEdit for (&'a ComponentInteraction, &'b Context) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.get_response(&self.1.http).await?)
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0
.edit_response(
&self.1,
edit.to_slash_initial_response_edit(EditInteractionResponse::new()),
)
.await?;
Ok(())
}
}
impl<'a, 'e, Env: Send + Sync> CanEdit for (ReplyHandle<'a>, CmdContext<'e, Env>) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.message().await?.into_owned())
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0.edit(self.1, edit).await?;
Ok(())
}
}
/// A trait that provides the implementation of a paginator.
#[async_trait::async_trait]
pub trait Paginate: Send + Sized {
/// Render the given page.
/// Remember to add the [[interaction_buttons]] as an action row!
async fn render(
&mut self,
page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>>;
async fn render(&mut self, page: u8, btns: Vec<CreateActionRow>)
-> Result<Option<CreateReply>>;
// /// The [[CreateActionRow]] for pagination.
// fn pagination_row(&self) -> CreateActionRow {
@ -54,11 +95,11 @@ pub trait Paginate: Send + Sized {
async fn handle_reaction(
&mut self,
page: u8,
ctx: &Context,
message: &mut Message,
_ctx: &Context,
message: &mut impl CanEdit,
reaction: &str,
) -> Result<Option<u8>> {
handle_pagination_reaction(page, self, ctx, message, reaction)
handle_pagination_reaction(page, self, message, reaction)
.await
.map(Some)
}
@ -82,25 +123,19 @@ pub trait Paginate: Send + Sized {
}
}
pub async fn do_render(
p: &mut impl Paginate,
page: u8,
ctx: &Context,
m: &mut Message,
) -> Result<bool> {
pub async fn do_render(p: &mut impl Paginate, page: u8, m: &mut impl CanEdit) -> Result<bool> {
let btns = vec![CreateActionRow::Buttons(p.interaction_buttons())];
do_render_with_btns(p, page, ctx, m, btns).await
do_render_with_btns(p, page, m, btns).await
}
async fn do_render_with_btns(
p: &mut impl Paginate,
page: u8,
ctx: &Context,
m: &mut Message,
m: &mut impl CanEdit,
btns: Vec<CreateActionRow>,
) -> Result<bool> {
if let Some(edit) = p.render(page, ctx, m, btns).await? {
m.edit(ctx, edit).await?;
if let Some(edit) = p.render(page, btns).await? {
m.apply_edit(edit).await?;
Ok(true)
} else {
Ok(false)
@ -108,29 +143,24 @@ async fn do_render_with_btns(
}
pub fn paginate_from_fn(
pager: impl for<'m> FnMut(
pager: impl FnMut(
u8,
&'m Context,
&'m Message,
Vec<CreateActionRow>,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>,
> + Send,
)
-> std::pin::Pin<Box<dyn Future<Output = Result<Option<CreateReply>>> + Send>>
+ Send,
) -> impl Paginate {
pager
}
pub fn default_buttons(p: &impl Paginate) -> Vec<CreateButton> {
let mut btns = vec![
CreateButton::new(PREV).emoji(ReactionType::try_from(ARROW_LEFT).unwrap()),
CreateButton::new(NEXT).emoji(ReactionType::try_from(ARROW_RIGHT).unwrap()),
CreateButton::new(PREV).label("<"),
CreateButton::new(NEXT).label(">"),
];
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.insert(0, CreateButton::new(FAST_PREV).label("<<"));
btns.push(CreateButton::new(FAST_NEXT).label(">>"))
}
btns
}
@ -145,21 +175,19 @@ impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
async fn render(
&mut self,
page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
) -> Result<Option<CreateReply>> {
if page as usize >= self.page_count {
return Ok(None);
}
self.inner.render(page, ctx, m, btns).await
self.inner.render(page, btns).await
}
async fn handle_reaction(
&mut self,
page: u8,
ctx: &Context,
message: &mut Message,
message: &mut impl CanEdit,
reaction: &str,
) -> Result<Option<u8>> {
self.inner
@ -179,23 +207,19 @@ impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
#[async_trait::async_trait]
impl<T> Paginate for T
where
T: for<'m> FnMut(
T: FnMut(
u8,
&'m Context,
&'m Message,
Vec<CreateActionRow>,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>,
> + Send,
)
-> std::pin::Pin<Box<dyn Future<Output = Result<Option<CreateReply>>> + Send>>
+ Send,
{
async fn render(
&mut self,
page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
self(page, ctx, m, btns).await
) -> Result<Option<CreateReply>> {
self(page, btns).await
}
}
@ -210,6 +234,7 @@ pub async fn paginate_reply(
let message = reply_to
.reply(&ctx, "Youmu is loading the first page...")
.await?;
let message = (message, ctx);
paginate_with_first_message(pager, ctx, message, timeout).await
}
@ -227,6 +252,7 @@ pub async fn paginate(
CreateMessage::new().content("Youmu is loading the first page..."),
)
.await?;
let message = (message, ctx);
paginate_with_first_message(pager, ctx, message, timeout).await
}
@ -234,13 +260,14 @@ pub async fn paginate(
pub async fn paginate_with_first_message(
mut pager: impl Paginate,
ctx: &Context,
mut message: Message,
mut message: impl CanEdit,
timeout: std::time::Duration,
) -> Result<()> {
let msg_id = message.get_message().await?.id;
let (send, recv) = flume::unbounded::<String>();
Paginator::push(ctx, &message, send).await?;
Paginator::push(ctx, msg_id, send).await?;
do_render(&mut pager, 0, ctx, &mut message).await?;
do_render(&mut pager, 0, &mut message).await?;
// Just quit if there is only one page
if pager.len().filter(|&v| v == 1).is_some() {
return Ok(());
@ -266,10 +293,10 @@ pub async fn paginate_with_first_message(
};
// Render one last time with no buttons
do_render_with_btns(&mut pager, page, ctx, &mut message, vec![])
do_render_with_btns(&mut pager, page, &mut message, vec![])
.await
.pls_ok();
Paginator::pop(ctx, &message).await?;
Paginator::pop(ctx, msg_id).await?;
res
}
@ -278,8 +305,7 @@ pub async fn paginate_with_first_message(
pub async fn handle_pagination_reaction(
page: u8,
pager: &mut impl Paginate,
ctx: &Context,
message: &mut Message,
message: &mut impl CanEdit,
reaction: &str,
) -> Result<u8> {
let pages = pager.len();
@ -299,7 +325,7 @@ pub async fn handle_pagination_reaction(
FAST_NEXT => (pages.unwrap() as u8 - 1).min(page + fast),
_ => return Ok(page),
};
Ok(if do_render(pager, new_page, ctx, message).await? {
Ok(if do_render(pager, new_page, message).await? {
new_page
} else {
page
@ -318,25 +344,25 @@ impl Paginator {
channels: Arc::new(dashmap::DashMap::new()),
}
}
async fn push(ctx: &Context, msg: &Message, channel: flume::Sender<String>) -> Result<()> {
async fn push(ctx: &Context, msg: MessageId, channel: flume::Sender<String>) -> Result<()> {
ctx.data
.write()
.read()
.await
.get_mut::<Paginator>()
.get::<Paginator>()
.unwrap()
.channels
.insert(msg.id, channel);
.insert(msg, channel);
Ok(())
}
async fn pop(ctx: &Context, msg: &Message) -> Result<()> {
async fn pop(ctx: &Context, msg: MessageId) -> Result<()> {
ctx.data
.write()
.read()
.await
.get_mut::<Paginator>()
.get::<Paginator>()
.unwrap()
.channels
.remove(&msg.id);
.remove(&msg);
Ok(())
}
}