Skip to content

Feather Pagination

Miu edited this page Aug 18, 2023 · 1 revision

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 as getUserId, 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 like inventory.pager.
  • pager: This contains both the key and type which can be accessed via event.pager in Kotlin or event.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.

To get started with Nexus, we recommend reading the following in chronological:

  1. Installation & Preparing Nexus
  2. Designing Commands
  3. Command Interceptors
  4. Additional Features (Subcommand Router, Option Validation)
  5. Context Menus
  6. Command Synchronization

You may want to read a specific part of handling command and middleware responses:

You can also read about additional features of Nexus:

You can read about synchronizing commands to Discord:

For more additional performance:

Additional configurations:

Clone this wiki locally