mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
Change pagination to use interactions instead of reactions
This commit is contained in:
parent
460624c7fe
commit
d1f3aa5fa9
8 changed files with 281 additions and 227 deletions
|
@ -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();
|
let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap();
|
||||||
|
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
paginate_from_fn(move |page, ctx, msg| {
|
paginate_from_fn(move |page, _, _, btns| {
|
||||||
use Align::*;
|
use Align::*;
|
||||||
let ranks = ranks.clone();
|
let ranks = ranks.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -188,7 +188,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
let start = ITEMS_PER_PAGE * page;
|
let start = ITEMS_PER_PAGE * page;
|
||||||
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let ranks = &ranks[start..end];
|
let ranks = &ranks[start..end];
|
||||||
|
|
||||||
|
@ -222,8 +222,9 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(content)).await?;
|
Ok(Some(
|
||||||
Ok(true)
|
EditMessage::new().content(content).components(vec![btns]),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(total_pages),
|
.with_page_count(total_pages),
|
||||||
|
@ -318,7 +319,7 @@ pub(crate) async fn contest_rank_table(
|
||||||
let ranks = Arc::new(ranks);
|
let ranks = Arc::new(ranks);
|
||||||
|
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
paginate_from_fn(move |page, ctx, msg| {
|
paginate_from_fn(move |page, _, _, btns| {
|
||||||
let contest = contest.clone();
|
let contest = contest.clone();
|
||||||
let problems = problems.clone();
|
let problems = problems.clone();
|
||||||
let ranks = ranks.clone();
|
let ranks = ranks.clone();
|
||||||
|
@ -327,7 +328,7 @@ pub(crate) async fn contest_rank_table(
|
||||||
let start = page * ITEMS_PER_PAGE;
|
let start = page * ITEMS_PER_PAGE;
|
||||||
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let ranks = &ranks[start..end];
|
let ranks = &ranks[start..end];
|
||||||
|
|
||||||
|
@ -389,8 +390,9 @@ pub(crate) async fn contest_rank_table(
|
||||||
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
|
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(content)).await?;
|
Ok(Some(
|
||||||
Ok(true)
|
EditMessage::new().content(content).components(vec![btns]),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(total_pages),
|
.with_page_count(total_pages),
|
||||||
|
|
|
@ -44,14 +44,14 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
||||||
|
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
paginate_from_fn(|page, ctx, msg| {
|
paginate_from_fn(|page, _, _, btns| {
|
||||||
let roles = roles.clone();
|
let roles = roles.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let start = page * ROLES_PER_PAGE;
|
let start = page * ROLES_PER_PAGE;
|
||||||
let end = roles.len().min(start + ROLES_PER_PAGE);
|
let end = roles.len().min(start + ROLES_PER_PAGE);
|
||||||
if end <= start {
|
if end <= start {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roles = &roles[start..end];
|
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))
|
.push_line(format!("Page **{}/{}**", page + 1, pages))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(content)).await?;
|
Ok(Some(
|
||||||
Ok(true)
|
EditMessage::new().content(content).components(vec![btns]),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(pages),
|
.with_page_count(pages),
|
||||||
|
|
|
@ -66,25 +66,23 @@ async fn message_command(
|
||||||
}
|
}
|
||||||
let images = std::sync::Arc::new(images);
|
let images = std::sync::Arc::new(images);
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
paginate_from_fn(|page, ctx, msg: &mut Message| {
|
paginate_from_fn(|page, _, _, btns| {
|
||||||
let images = images.clone();
|
let images = images.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
if page >= images.len() {
|
if page >= images.len() {
|
||||||
Ok(false)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
msg.edit(
|
Ok(Some(
|
||||||
ctx,
|
EditMessage::new()
|
||||||
EditMessage::new().content(format!(
|
.content(format!(
|
||||||
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
|
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
|
||||||
page + 1,
|
page + 1,
|
||||||
images.len(),
|
images.len(),
|
||||||
images[page]
|
images[page]
|
||||||
)),
|
))
|
||||||
)
|
.components(vec![btns]),
|
||||||
.await
|
))
|
||||||
.map(|_| true)
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -92,7 +92,12 @@ mod scores {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl pagination::Paginate for Paginate {
|
impl pagination::Paginate for Paginate {
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
async fn render(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
msg: &Message,
|
||||||
|
) -> Result<Option<EditMessage>> {
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let score = &self.scores[page];
|
let score = &self.scores[page];
|
||||||
|
@ -115,22 +120,17 @@ mod scores {
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| Error::msg("user not found"))?;
|
.ok_or_else(|| Error::msg("user not found"))?;
|
||||||
|
|
||||||
msg.edit(
|
hourglass.delete(ctx).await?;
|
||||||
ctx,
|
save_beatmap(&env, msg.channel_id, &bm).await?;
|
||||||
|
Ok(Some(
|
||||||
EditMessage::new()
|
EditMessage::new()
|
||||||
.embed({
|
.embed({
|
||||||
crate::discord::embeds::score_embed(score, &bm, &content, &user)
|
crate::discord::embeds::score_embed(score, &bm, &content, &user)
|
||||||
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
|
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.components(vec![score_components(self.guild_id)]),
|
.components(vec![score_components(self.guild_id), self.pagination_row()]),
|
||||||
)
|
))
|
||||||
.await?;
|
|
||||||
save_beatmap(&env, msg.channel_id, &bm).await?;
|
|
||||||
|
|
||||||
// End
|
|
||||||
hourglass.delete(ctx).await?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> Option<usize> {
|
fn len(&self) -> Option<usize> {
|
||||||
|
@ -193,7 +193,12 @@ mod scores {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl pagination::Paginate for Paginate {
|
impl pagination::Paginate for Paginate {
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
async fn render(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
msg: &Message,
|
||||||
|
) -> Result<Option<EditMessage>> {
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
|
|
||||||
let meta_cache = &env.beatmaps;
|
let meta_cache = &env.beatmaps;
|
||||||
|
@ -202,7 +207,7 @@ mod scores {
|
||||||
let start = page * ITEMS_PER_PAGE;
|
let start = page * ITEMS_PER_PAGE;
|
||||||
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
|
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hourglass = msg.react(ctx, '⌛').await?;
|
let hourglass = msg.react(ctx, '⌛').await?;
|
||||||
|
@ -321,9 +326,12 @@ mod scores {
|
||||||
.push_line("[?] means pp was predicted by oppai-rs.")
|
.push_line("[?] means pp was predicted by oppai-rs.")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(content)).await?;
|
|
||||||
hourglass.delete(ctx).await?;
|
hourglass.delete(ctx).await?;
|
||||||
Ok(true)
|
Ok(Some(
|
||||||
|
EditMessage::new()
|
||||||
|
.content(content)
|
||||||
|
.components(vec![self.pagination_row()]),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> Option<usize> {
|
fn len(&self) -> Option<usize> {
|
||||||
|
@ -335,7 +343,7 @@ mod scores {
|
||||||
|
|
||||||
mod beatmapset {
|
mod beatmapset {
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::{GuildId, Reaction},
|
all::{CreateButton, GuildId},
|
||||||
builder::{CreateEmbedFooter, EditMessage},
|
builder::{CreateEmbedFooter, EditMessage},
|
||||||
model::channel::{Message, ReactionType},
|
model::channel::{Message, ReactionType},
|
||||||
};
|
};
|
||||||
|
@ -352,6 +360,7 @@ mod beatmapset {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHOW_ALL_EMOTE: &str = "🗒️";
|
const SHOW_ALL_EMOTE: &str = "🗒️";
|
||||||
|
const SHOW_ALL: &str = "youmubot_osu::discord::display::show_all";
|
||||||
|
|
||||||
pub async fn display_beatmapset(
|
pub async fn display_beatmapset(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
|
@ -377,8 +386,6 @@ mod beatmapset {
|
||||||
mode,
|
mode,
|
||||||
mods,
|
mods,
|
||||||
guild_id,
|
guild_id,
|
||||||
|
|
||||||
all_reaction: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
|
@ -401,8 +408,6 @@ mod beatmapset {
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
mods: Option<UnparsedMods>,
|
mods: Option<UnparsedMods>,
|
||||||
guild_id: Option<GuildId>,
|
guild_id: Option<GuildId>,
|
||||||
|
|
||||||
all_reaction: Option<Reaction>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Paginate {
|
impl Paginate {
|
||||||
|
@ -427,21 +432,25 @@ mod beatmapset {
|
||||||
Some(self.maps.len())
|
Some(self.maps.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
async fn render(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
msg: &Message,
|
||||||
|
) -> Result<Option<EditMessage>> {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
if page == self.maps.len() {
|
if page == self.maps.len() {
|
||||||
msg.edit(
|
return Ok(Some(
|
||||||
ctx,
|
EditMessage::new()
|
||||||
EditMessage::new().embed(crate::discord::embeds::beatmapset_embed(
|
.embed(crate::discord::embeds::beatmapset_embed(
|
||||||
&self.maps[..],
|
&self.maps[..],
|
||||||
self.mode,
|
self.mode,
|
||||||
)),
|
))
|
||||||
)
|
.components(vec![self.pagination_row()]),
|
||||||
.await?;
|
));
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
if page > self.maps.len() {
|
if page > self.maps.len() {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let map = &self.maps[page];
|
let map = &self.maps[page];
|
||||||
|
@ -458,7 +467,16 @@ mod beatmapset {
|
||||||
self.infos[page].insert(info)
|
self.infos[page].insert(info)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
msg.edit(ctx,
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
|
save_beatmap(
|
||||||
|
&env,
|
||||||
|
msg.channel_id,
|
||||||
|
&BeatmapWithMode(map.clone(), self.mode),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.pls_ok();
|
||||||
|
|
||||||
|
Ok(Some(
|
||||||
EditMessage::new().embed(
|
EditMessage::new().embed(
|
||||||
crate::discord::embeds::beatmap_embed(
|
crate::discord::embeds::beatmap_embed(
|
||||||
map,
|
map,
|
||||||
|
@ -475,31 +493,19 @@ mod beatmapset {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.components(vec![beatmap_components(map.mode, self.guild_id)]),
|
.components(vec![beatmap_components(map.mode, self.guild_id), self.pagination_row()]),
|
||||||
)
|
))
|
||||||
.await?;
|
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
||||||
save_beatmap(
|
|
||||||
&env,
|
|
||||||
msg.channel_id,
|
|
||||||
&BeatmapWithMode(map.clone(), self.mode),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.pls_ok();
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prerender(
|
fn interaction_buttons(&self) -> Vec<CreateButton> {
|
||||||
&mut self,
|
let mut btns = pagination::default_buttons(self);
|
||||||
ctx: &Context,
|
btns.insert(
|
||||||
m: &mut serenity::model::channel::Message,
|
0,
|
||||||
) -> Result<()> {
|
CreateButton::new(SHOW_ALL)
|
||||||
self.all_reaction = Some(
|
.emoji(ReactionType::try_from(SHOW_ALL_EMOTE).unwrap())
|
||||||
m.react(&ctx, SHOW_ALL_EMOTE.parse::<ReactionType>().unwrap())
|
.label("Show all"),
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
Ok(())
|
btns
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_reaction(
|
async fn handle_reaction(
|
||||||
|
@ -507,26 +513,16 @@ mod beatmapset {
|
||||||
page: u8,
|
page: u8,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut serenity::model::channel::Message,
|
message: &mut serenity::model::channel::Message,
|
||||||
reaction: &Reaction,
|
reaction: &str,
|
||||||
) -> Result<Option<u8>> {
|
) -> Result<Option<u8>> {
|
||||||
// Render the old style.
|
// Render the old style.
|
||||||
if let ReactionType::Unicode(s) = &reaction.emoji {
|
if reaction == SHOW_ALL {
|
||||||
if s == SHOW_ALL_EMOTE {
|
pagination::do_render(self, self.maps.len() as u8, ctx, message).await?;
|
||||||
self.render(self.maps.len() as u8, ctx, message).await?;
|
return Ok(Some(self.maps.len() as u8));
|
||||||
return Ok(Some(self.maps.len() as u8));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
|
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
|
||||||
.await
|
.await
|
||||||
.map(Some)
|
.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,18 +192,18 @@ pub fn dot_osu_hook<'a>(
|
||||||
} else {
|
} else {
|
||||||
let osu_embeds = Arc::new(osu_embeds);
|
let osu_embeds = Arc::new(osu_embeds);
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
paginate_from_fn(|page, ctx, msg| {
|
paginate_from_fn(|page, _, _, btns| {
|
||||||
let osu_embeds = osu_embeds.clone();
|
let osu_embeds = osu_embeds.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (embed, attachments) = &osu_embeds[page as usize];
|
let (embed, attachments) = &osu_embeds[page as usize];
|
||||||
let mut edit = EditMessage::new()
|
let mut edit = EditMessage::new()
|
||||||
.content(format!("Attached beatmaps ({}/{})", page + 1, embed_len))
|
.content(format!("Attached beatmaps ({}/{})", page + 1, embed_len))
|
||||||
.embed(embed.clone());
|
.embed(embed.clone())
|
||||||
|
.components(vec![btns]);
|
||||||
for att in attachments {
|
for att in attachments {
|
||||||
edit = edit.new_attachment(att.clone());
|
edit = edit.new_attachment(att.clone());
|
||||||
}
|
}
|
||||||
msg.edit(&ctx, edit).await?;
|
Ok(Some(edit))
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(embed_len),
|
.with_page_count(embed_len),
|
||||||
|
|
|
@ -236,7 +236,7 @@ where
|
||||||
let total_len = users.len();
|
let total_len = users.len();
|
||||||
let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||||
paginate_with_first_message(
|
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();
|
let header = header.clone();
|
||||||
use Align::*;
|
use Align::*;
|
||||||
let users = users.clone();
|
let users = users.clone();
|
||||||
|
@ -244,7 +244,7 @@ where
|
||||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
let end = (start + ITEMS_PER_PAGE).min(users.len());
|
let end = (start + ITEMS_PER_PAGE).min(users.len());
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let users = &users[start..end];
|
let users = &users[start..end];
|
||||||
let table = match query {
|
let table = match query {
|
||||||
|
@ -317,8 +317,9 @@ where
|
||||||
last_update.format("<t:%s:R>"),
|
last_update.format("<t:%s:R>"),
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
m.edit(ctx, EditMessage::new().content(content)).await?;
|
Ok(Some(
|
||||||
Ok(true)
|
EditMessage::new().content(content).components(vec![btns]),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(total_pages),
|
.with_page_count(total_pages),
|
||||||
|
@ -617,11 +618,11 @@ pub async fn display_rankings_table(
|
||||||
let header = Arc::new(to.content.clone());
|
let header = Arc::new(to.content.clone());
|
||||||
|
|
||||||
paginate_with_first_message(
|
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 start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
let end = (start + ITEMS_PER_PAGE).min(scores.len());
|
let end = (start + ITEMS_PER_PAGE).min(scores.len());
|
||||||
if start >= end {
|
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 scores = scores[start..end].to_vec();
|
||||||
let header = header.clone();
|
let header = header.clone();
|
||||||
|
@ -720,8 +721,9 @@ pub async fn display_rankings_table(
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
m.edit(&ctx, EditMessage::new().content(content)).await?;
|
Ok(Some(
|
||||||
Ok(true)
|
EditMessage::new().content(content).components(vec![btns]),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.with_page_count(total_pages),
|
.with_page_count(total_pages),
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
use crate::{Context, OkPrint, Result};
|
use crate::{Context, Result};
|
||||||
use futures_util::{future::Future, StreamExt as _};
|
use futures_util::future::Future;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
|
all::{
|
||||||
|
CreateActionRow, CreateButton, CreateInteractionResponse, EditMessage, Interaction,
|
||||||
|
MessageId,
|
||||||
|
},
|
||||||
builder::CreateMessage,
|
builder::CreateMessage,
|
||||||
collector,
|
|
||||||
model::{
|
model::{
|
||||||
channel::{Message, Reaction, ReactionType},
|
channel::{Message, ReactionType},
|
||||||
id::ChannelId,
|
id::ChannelId,
|
||||||
},
|
},
|
||||||
|
prelude::TypeMapKey,
|
||||||
};
|
};
|
||||||
use std::convert::TryFrom;
|
use std::{convert::TryFrom, sync::Arc};
|
||||||
use tokio::time as tokio_time;
|
use tokio::time as tokio_time;
|
||||||
|
|
||||||
const ARROW_RIGHT: &str = "➡️";
|
const ARROW_RIGHT: &str = "➡️";
|
||||||
|
@ -16,19 +20,28 @@ const ARROW_LEFT: &str = "⬅️";
|
||||||
const REWIND: &str = "⏪";
|
const REWIND: &str = "⏪";
|
||||||
const FAST_FORWARD: &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.
|
/// A trait that provides the implementation of a paginator.
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Paginate: Send + Sized {
|
pub trait Paginate: Send + Sized {
|
||||||
/// Render the given page.
|
/// Render the given page.
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool>;
|
/// Remember to add the [[interaction_buttons]] as an action row!
|
||||||
|
async fn render(&mut self, page: u8, ctx: &Context, m: &Message)
|
||||||
|
-> Result<Option<EditMessage>>;
|
||||||
|
|
||||||
/// Any setting-up before the rendering stage.
|
/// The [[CreateActionRow]] for pagination.
|
||||||
async fn prerender(&mut self, _ctx: &Context, _m: &mut Message) -> Result<()> {
|
fn pagination_row(&self) -> CreateActionRow {
|
||||||
Ok(())
|
CreateActionRow::Buttons(self.interaction_buttons())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cleans up after the pagination has timed out.
|
/// A list of buttons to create that would interact with pagination logic.
|
||||||
async fn cleanup(&mut self, _ctx: &Context, _m: &mut Message) -> () {}
|
fn interaction_buttons(&self) -> Vec<CreateButton> {
|
||||||
|
default_buttons(self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling
|
/// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling
|
||||||
/// before handing the functionality over.
|
/// before handing the functionality over.
|
||||||
|
@ -39,7 +52,7 @@ pub trait Paginate: Send + Sized {
|
||||||
page: u8,
|
page: u8,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut Message,
|
message: &mut Message,
|
||||||
reaction: &Reaction,
|
reaction: &str,
|
||||||
) -> Result<Option<u8>> {
|
) -> Result<Option<u8>> {
|
||||||
handle_pagination_reaction(page, self, ctx, message, reaction)
|
handle_pagination_reaction(page, self, ctx, message, reaction)
|
||||||
.await
|
.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<bool> {
|
||||||
|
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(
|
pub fn paginate_from_fn(
|
||||||
pager: impl for<'m> FnMut(
|
pager: impl for<'m> FnMut(
|
||||||
u8,
|
u8,
|
||||||
&'m Context,
|
&'m Context,
|
||||||
&'m mut Message,
|
&'m Message,
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
CreateActionRow,
|
||||||
+ Send,
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>,
|
||||||
|
> + Send,
|
||||||
) -> impl Paginate {
|
) -> impl Paginate {
|
||||||
pager
|
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()),
|
||||||
|
];
|
||||||
|
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> {
|
struct WithPageCount<Inner> {
|
||||||
inner: Inner,
|
inner: Inner,
|
||||||
page_count: usize,
|
page_count: usize,
|
||||||
|
@ -83,33 +127,28 @@ struct WithPageCount<Inner> {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
|
impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool> {
|
async fn render(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
m: &Message,
|
||||||
|
) -> Result<Option<EditMessage>> {
|
||||||
if page as usize >= self.page_count {
|
if page as usize >= self.page_count {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
self.inner.render(page, ctx, m).await
|
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(
|
async fn handle_reaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
page: u8,
|
page: u8,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut Message,
|
message: &mut Message,
|
||||||
reaction: &Reaction,
|
reaction: &str,
|
||||||
) -> Result<Option<u8>> {
|
) -> Result<Option<u8>> {
|
||||||
// handle normal reactions first, then fallback to the inner one
|
self.inner
|
||||||
let new_page = handle_pagination_reaction(page, self, ctx, message, reaction).await?;
|
.handle_reaction(page, ctx, message, reaction)
|
||||||
|
.await
|
||||||
if new_page != page {
|
|
||||||
Ok(Some(new_page))
|
|
||||||
} else {
|
|
||||||
self.inner
|
|
||||||
.handle_reaction(page, ctx, message, reaction)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> Option<usize> {
|
fn len(&self) -> Option<usize> {
|
||||||
|
@ -119,10 +158,6 @@ impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
|
||||||
fn is_empty(&self) -> Option<bool> {
|
fn is_empty(&self) -> Option<bool> {
|
||||||
Some(self.page_count == 0)
|
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]
|
#[async_trait::async_trait]
|
||||||
|
@ -131,12 +166,20 @@ where
|
||||||
T: for<'m> FnMut(
|
T: for<'m> FnMut(
|
||||||
u8,
|
u8,
|
||||||
&'m Context,
|
&'m Context,
|
||||||
&'m mut Message,
|
&'m Message,
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
CreateActionRow,
|
||||||
+ Send,
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>,
|
||||||
|
> + Send,
|
||||||
{
|
{
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool> {
|
async fn render(
|
||||||
self(page, ctx, m).await
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
m: &Message,
|
||||||
|
) -> Result<Option<EditMessage>> {
|
||||||
|
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,
|
mut message: Message,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
pager.prerender(ctx, &mut message).await?;
|
let (send, recv) = flume::unbounded::<String>();
|
||||||
pager.render(0, ctx, &mut message).await?;
|
Paginator::push(ctx, &message, send).await?;
|
||||||
|
|
||||||
|
do_render(&mut pager, 0, ctx, &mut message).await?;
|
||||||
// Just quit if there is only one page
|
// Just quit if there is only one page
|
||||||
if pager.len().filter(|&v| v == 1).is_some() {
|
if pager.len().filter(|&v| v == 1).is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// React to the message
|
|
||||||
let large_count = pager.len().filter(|&p| p > 10).is_some();
|
|
||||||
let reactions = {
|
|
||||||
let mut rs = Vec::<Reaction>::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;
|
let mut page = 0;
|
||||||
|
|
||||||
// Loop the handler function.
|
// Loop the handler function.
|
||||||
let res: Result<()> = loop {
|
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(()),
|
Err(_) => break Ok(()),
|
||||||
Ok(None) => break Ok(()),
|
Ok(Err(_)) => break Ok(()),
|
||||||
Ok(Some(reaction)) => {
|
Ok(Ok(reaction)) => {
|
||||||
page = match pager
|
page = match pager
|
||||||
.handle_reaction(page, ctx, &mut message, &reaction)
|
.handle_reaction(page, ctx, &mut message, &reaction)
|
||||||
.await
|
.await
|
||||||
|
@ -247,14 +249,7 @@ pub async fn paginate_with_first_message(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pager.cleanup(ctx, &mut message).await;
|
Paginator::pop(ctx, &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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -265,35 +260,90 @@ pub async fn handle_pagination_reaction(
|
||||||
pager: &mut impl Paginate,
|
pager: &mut impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut Message,
|
message: &mut Message,
|
||||||
reaction: &Reaction,
|
reaction: &str,
|
||||||
) -> Result<u8> {
|
) -> Result<u8> {
|
||||||
let pages = pager.len();
|
let pages = pager.len();
|
||||||
let fast = pages.map(|v| v / 10).unwrap_or(5).max(5) as u8;
|
let fast = pages.map(|v| v / 10).unwrap_or(5).max(5) as u8;
|
||||||
match &reaction.emoji {
|
let new_page = match reaction {
|
||||||
ReactionType::Unicode(ref s) => {
|
PREV | FAST_PREV if page == 0 => return Ok(page),
|
||||||
let new_page = match s.as_str() {
|
PREV => page - 1,
|
||||||
ARROW_LEFT | REWIND if page == 0 => return Ok(page),
|
FAST_PREV => {
|
||||||
ARROW_LEFT => page - 1,
|
if page < fast {
|
||||||
REWIND => {
|
0
|
||||||
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
|
|
||||||
} else {
|
} 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<dashmap::DashMap<MessageId, flume::Sender<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
channels: Arc::new(dashmap::DashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn push(ctx: &Context, msg: &Message, channel: flume::Sender<String>) -> Result<()> {
|
||||||
|
ctx.data
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.get_mut::<Paginator>()
|
||||||
|
.unwrap()
|
||||||
|
.channels
|
||||||
|
.insert(msg.id, channel);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pop(ctx: &Context, msg: &Message) -> Result<()> {
|
||||||
|
ctx.data
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.get_mut::<Paginator>()
|
||||||
|
.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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,6 +236,11 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Paginator
|
||||||
|
let paginator = youmubot_prelude::pagination::Paginator::new();
|
||||||
|
handler.push_interaction_hook(paginator.clone());
|
||||||
|
data.insert::<youmubot_prelude::pagination::Paginator>(paginator);
|
||||||
|
|
||||||
data.insert::<Env>(env.clone());
|
data.insert::<Env>(env.clone());
|
||||||
|
|
||||||
#[cfg(feature = "core")]
|
#[cfg(feature = "core")]
|
||||||
|
|
Loading…
Add table
Reference in a new issue