v1.0.0-alpha4.1
This introduces an entire new paginator that is lightweight, faster, and barebones compared to the plain paginator which will allow pagination that queries the database on the next page, previous page, etc.
Warning
Before proceeding, I would like to warn you that all the code samples here are written in Kotlin since I am more familiar and comfortable with Kotlin now compared to Java. It should still be easy to understand despite that.
🪶 Feather Paging
Feather Paging is the name of the lightweight paginator of Nexus which is more barebones and provides minor abstractions that many might like. I always found the normal paginator a bit too limiting and bulky and wanted something that was light and allowed for more possibilities and this is what I came up with.
There was also the issue where the paginators won't work anymore after restarting the bot application which was because all the data was being stored in the memory and also a potential issue of blockage in the memory because the chances of the data not being cleared were decently high until you destroyed the paginator instances. This solves all those problems.
To summarize the differences:
- Feather doesn't create the buttons for you.
- Feather doesn't create events such as
FeatherNextListener
or anything similar. Instead, we are opting for a singular listener that contains the abstractions of Nexus such asgetUserId
, etc. with additional methods that use the Paging API. (NexusFeatherView
). - Feather paginators don't break apart after every restart but instead continue functioning. (this is a key difference but this also requires your code to be able to query new data, etc.)
- Feather is incredibly lightweight.
- Feather is almost barebones.
- Feather has no extra functionality other than to assist with pagination, no fancy stuff.
🔑 Key Terminologies
To understand how Feather works, let's understand the key terminologies that Feather uses:
key
: A key is basically a unique identifier in a sense of the item being paginated. If you are using a bucket pattern (which is the most common and RECOMMENDED method of paging items in MongoDB) then this key will be either the last item's or the next item's key. If you are using skip-limit or offset then this will be the page.type
: A "sorta-unique" name for the Feather View that will be handling the events for this paginator. For example, a paginator for inventory can have something likeinventory.pager
.pager
: This contains both thekey
andtype
which can be accessed viaevent.pager
in Kotlin orevent.getPager()
in Java.action
: The action performed by the user.
📖 Understanding Feather
After the terminologies come the concept of Feather and how it maintains availability even after restarts. To understand truly how Feather works, it abuses the customId
field of Discord buttons and tries to store as much data in the field as possible. A sample of a customId
made by Feather would look like this:
your.type.here[$;your_key[$;action
All the data are delimited with a little [$;
to ensure that your key won't be caught up accidentally while splitting into three parts. This is also a vital part that you have to ensure since this will cause issues if your key actually contains [$;
. After splitting, it takes the data and distributes them to the event. That's basically the entirety of Feather.
Note
Key notes from above:
- Ensure the key doesn't include the following sequence `[$;`` since that is the delimiter of Feather.
- Feather simply adds data into your buttons custom id in the schema of
type[$;key[$;action
.
📲 Actually Paging
After understanding the concept of Feather, let's start paging!
To create a paginator, you first have to create a Feather View which can be done via:
val pager = NexusFeatherPaging.register("your.type.here", YourNexusFeatherViewHere)
object YourNexusFeatherViewHere: NexusFeatherView {
override fun onEvent(event: NexusFeatherViewEvent) {}
}
In this part, you are creating the handler that will be dispatched every time a button that matches the type and schema of Feather is clicked. After creating the View, you need to create a pager before you can actually use Feather.
val items = ...
val pager = NexusFeatherPaging.pager(initialKey = items.last()._id.toString() + ";{${event.userId}", type = "inventory.pager")
The above is a sample of a project that I am working on which uses Feather. The above simply stores the last item's _id
value (the unique id in MongoDB) and the user who invoked the command (delimited with ;{
to prevent collision with Feather). Adding the user field is completely optional but not adding it will prevent you from knowing who originally invoked the pagination.
Note
Some key notes from the above.
- The code sample includes the user id to identify who the invoker of the command is, this is completely optional if you want to allow other users to use the paginator as well.
This pager contains vital information such as the initial key and the type of the paginator. You can then use the pager instance that was created to actually create the button custom ids. An example that I am using on a project is:
updater.addEmbed(InventoryView.embed(event.user, items)).addComponents(
ActionRow.of(
pager.makeWithCurrentKey(action = "next").setLabel("Next (❀❛ ֊ ❛„)♡").setStyle(ButtonStyle.SECONDARY).build(),
pager.makeWithCurrentKey(action = "delete").setEmoji("").setLabel("🗑️").setStyle(ButtonStyle.PRIMARY).build(),
pager.makeWithCurrentKey(action = "reverse").setLabel("૮₍ ˶•⤙•˶ ₎ა Previous").setStyle(ButtonStyle.SECONDARY).build()
)
) .update()
As you can see, we are creating the buttons with the method pager.makeWithCurrentKey(action)
which creates a ButtonBuilder
with the customId
field already pre-filled with the generated custom id. The action
field can be anything. You can then send that message and it will work as intended and show the buttons on Discord.
Now comes the fun part, actually performing the pagination. To do this, let's head back to our Feather View and actually write out the code that we want.
object InventoryView: NexusFeatherView {
override fun onEvent(event: NexusFeatherViewEvent) {
// We are acknowledging the event before actually performing anything since we are just editing the message and don't need to respond later or anything.
exceptionally(event.interaction.acknowledge())
// This is how we acquire the key and invoker, remember what I mentioned earlier.
val key = event.pager.key.substringBefore(";{")
val invoker = event.pager.key.substringAfter(";{")
// This is done to prevent other users other than the invoker from using the paginator.
if (event.userId != invoker.toLong()) return
if (event.action == "delete") {
event.message.delete()
return
}
MyThreadPool.submit {
var items: List<UserItem> = emptyList()
when (event.action) {
"next" -> {
items = ItemDatabase.findAfter(event.userId, key, 20).join()
}
"reverse" -> {
items = ItemDatabase.findBefore(event.userId, key, 20).join()
}
}
if (items.isEmpty()) return@submit
exceptionally(
event.message.createUpdater()
.removeAllEmbeds()
.removeAllComponents()
.addEmbed(embed(event.user, items))
.addComponents(ActionRow.of(makeFeatherComponents(event, items.last()._id.toString() + ";{$invoker"))) // Read more below
.applyChanges()
)
}
}
fun embed(user: User, items: List<UserItem>): EmbedBuilder {
// ... Imagine code that builds the embed builder here
}
}
You may have noticed the makeFeatherComponents
method and may be confused. To summarize the function of that method, it's a custom utility method of mine that copies all the buttons of the first action row and updates the custom id.
fun makeFeatherComponents(event: NexusFeatherViewEvent, newKey: String): List<Button> {
return event.message.components.map {
it.asActionRow().orElseThrow().components.map { component ->
component.asButton().orElseThrow()
}
}.map {
it.map { button ->
val buttonBuilder = event.pager.makeWith(
newKey,
button.customId.orElseThrow().substringAfterLast("[$;")
).setStyle(button.style)
if (button.label.isPresent) buttonBuilder.setLabel(button.label.get())
if (button.emoji.isPresent) buttonBuilder.setEmoji(button.emoji.get())
if (button.url.isPresent) buttonBuilder.setLabel(button.url.get())
if (button.isDisabled.isPresent) buttonBuilder.setDisabled(button.isDisabled.get())
buttonBuilder.build()
}
}.first()
}
In a sense, it shows how barebones the Feather API really is. It doesn't handle any of the buttons or anything even, it simply provides a very TINY abstraction that helps with pagination. You can then run the code and BAM paginator! It's as simple as that.