EdJoPaTo / grammy-inline-menu

Inline Menus for Telegram made simple. Successor of telegraf-inline-menu.
MIT License
354 stars 46 forks source link

How to set up pagination for multiple submenus? #87

Closed yakovenkodenis closed 4 years ago

yakovenkodenis commented 4 years ago

I'm making a bot for ordering drinks in a coffeeshop. The expected sequence of user actions is the following:

  1. Users hits the command /start
  2. the list of drinks appears
  3. user hits on a drink item
  4. user is taken to another submenu where they choose the quantity they need (from 1 to 5)
  5. user hits go back back button and gets taken to the drinks list
  6. user repeats these actions until they complete the order
  7. user hits the confirm button and the collected data gets sent to the server.

The list of drinks is stored in an array. The list of orders from different users is also stored locally (in memory) as I'm not expecting many orders at the same time. For sessions I use the telegraf-session-local package.

The structure of the orders object is the following:

const orders = {
   tg_username: {
       "drink1": {"count": 1},
       "drink2": {"count": 3},
       ......
   }
}

So I generate the menu for the list of drinks in forEach and create a submenu for each drink, and then attach a submenu for choosing the quantity to each drink submenu.

Given that the list of drinks is quite long, I need pagination so that on each page the user could see not more than 10 items.

However, after reading the issues and the docs I still can't understand how to implement such functionality in my case.

My current code is below:

(menuItems is an array like this [{name: "Drink1", key: "d", price: "10"}, {...},...])

menuItems.forEach((item, index) => {
  function countSelectText(ctx) {
    const count = ctx.match[1];
    let chosenCount = false;

    if (
      orders[ctx.update.callback_query.from.username] &&
      orders[ctx.update.callback_query.from.username][item.key]
    ) {
      chosenCount =
        orders[ctx.update.callback_query.from.username][item.key].count;
    }

    if (!chosenCount) {
      return `Выберите количество`;
    }

    return `Вы выбрали ${count} ${item.name}`;
  }

  // that's the submenu for choosing the quantity of a given drink.
  const countSelectSubmenu = new TelegrafInlineMenu(countSelectText).select(
    item.key,
    [0, 1, 2, 3, 4, 5], {
      setFunc: (ctx, key) => {
        const count = ctx.match[1];

        if (!orders[ctx.update.callback_query.from.username]) {
          orders[ctx.update.callback_query.from.username] = {};
        }

        if (!orders[ctx.update.callback_query.from.username][item.key]) {
          orders[ctx.update.callback_query.from.username][item.key] = {};
        }

        orders[ctx.update.callback_query.from.username][item.key].count = key;
      },
      isSetFunc: (ctx, key) => {
        const count = ctx.match[1];

        if (
          orders[ctx.update.callback_query.from.username] &&
          orders[ctx.update.callback_query.from.username][item.key]
        ) {
          return (
            orders[ctx.update.callback_query.from.username][item.key]
            .count === key
          );
        } else return false;
      },
    }
  );

  menu.submenu(item.name, item.key, countSelectSubmenu, {
    columns: 5,
  });
});

// this piece of code I just copied from the docs
menu.pagination("page", {
  setPage: (ctx, page) => {
    ctx.session.page = page;
  },
  getTotalPages: (ctx) => 3,
  getCurrentPage: (ctx) => ctx.session.page,
});

// that's the order confirmation button
menu.button("Показать мой заказ", "show", {
  doFunc: (ctx) => {
    ctx.reply(orders[ctx.update.callback_query.from.username]);
  },
});

Could you please help me to get the pagination right?


This is how the code works right now: image

EdJoPaTo commented 4 years ago

Thanks for your interest! I think I understand your idea of doing it. The approach of the menu is a bit different: The menu structure itself tries to be static, not generated by code. Only the content is dynamic. I think in your situation the selectSubmenu (version 5 calls it chooseIntoSubmenu) is fitted quite well for your needs.

That way you will only have one submenu representing the amount selection:

const amountMenu = new TelegrafInlineMenu(ctx => {
  return 'How many ' + ctx.match[1] + ' do you want to get?'
})

amountMenu.select('pickAmount', [0, 1, 2, 3, 4, 5], {
  setParentMenuAfter: true, // When the user hits a button the parent menu is opened -> no need to hit the back button then
  setFunc: (ctx, key) => {
    const product = ctx.match[1]
    // pick one of the following, they are the same
    const amount = Number(ctx.match[2])
    const amount = Number(key)

    // do your logic of storing the information
  }
})

This menu is now independent from the actual drink. It works based on the ctx.match information.

Now you can create a drink selection menu which enters the submenus.

const drinkMenu = new TelegrafInlineMenu('Want a drink?')

drinkMenu.selectSubmenu('drink', ['applejuice', 'mate', 'Tschunk', 'whatever drinks you have…'], amountMenu, {…})

That way you basically have another menu with a dynamic list of buttons, in this case of the array of your drinks (or a function returning the array).

Back to the pagination: You could use menu.pagination but the pagination feature is already built into select and selectSubmenu:

drinkMenu.selectSubmenu('drink', ctx => drinksCurrentlyInStock(), amountMenu, {
  getCurrentPage: ctx => ctx.session.page,
  setPage: (ctx, page) => {
    ctx.session.page = page
  }
})

That way the menu calls the getCurrentPage whenever it needs to display the buttons and will call setPage when the user hits a button to go to a different page.

Also another tip for you: Telegraf simplifies the usage of the context for you. You can simplify ctx.update.callback_query to ctx.callbackQuery or ctx.update.callback_query.from to ctx.from which is a lot shorter to write.

Also you should not use the username as not all users have one and they can change it anytime. I suggest using ctx.from.id instead as it is unique to the user.

Hope it helps :)

yakovenkodenis commented 4 years ago

@EdJoPaTo Yes, it totally helped me, thanks a lot for such a detailed answer! In fact, you've made my code three times shorter and have given me some very useful tips.

Your library is by far the best out there for making bots with inline menus!