EdJoPaTo / grammy-inline-menu

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

Fetching items dynamically in submenu #109

Closed kentnek closed 4 years ago

kentnek commented 4 years ago

Hi! First off, thanks for the awesome work! I’m very new to Telegram bots, and your project made it super easy for me to build an interactive inline bot.

Back to my question, I want to create a submenu (/items/) whose contents are loaded asynchronously from an API. Each item has a fixed submenu containing one button action:

main
└── items/
    ├── item-0/
    │   └── action
    ├── item-1/
    │   └── action
    ├── item 2/
    │   └── action
    └── ...

Since the API to fetch items is expensive, I want to call it only once, i.e. going from main to items. Going from item-i back to items should not trigger the API call again.

To load the items dynamically, I found two ways:

  1. Fetch the items in choices argument:

    itemListMenu.chooseIntoSubmenu(
      ‘item’,
      async (ctx) => { /* fetch items */ }, 
      itemActionMenu
    );
    
    mainMenu.submenu(‘Show items’, ‘items’, itemListMenu);

    This works, but the fetch operation is called whenever itemListMenu is rendered, including going from item-i back to items.

  2. Fetch the items then open the menu manually:

    let items = [];
    
    itemListMenu.chooseIntoSubmenu(
      ‘item’,
      () => items, 
      itemActionMenu
    );
    
    mainMenu.interact(‘Show items’, ‘items’, {
      do: async (ctx) => {
        items = await fetch();
        await editMenuOnContext(itemListMenu, ctx, ‘/items/`);
        return false; // without this the menu would go back to “/“
      }
    });

    What I want to achieve here is to call fetch() and update items only when the button Show items is clicked from the main menu. This shows the items too, but clicking on the item doesn’t lead to the itemActionMenu, but back to /.

Could you explain why method (2) doesn’t work? And if there is any better way to achieve this, please let me know.

Thanks!

EdJoPaTo commented 4 years ago

Personally i go with the first variant and use caching there.

function getOptions(ctx) {
  if (isNotInCache || cacheIsOld) {
    cache = await fetch()
  }

  return cache
}

Also you can disable the check if the option is still there before entering the submenu:

itemListMenu.chooseIntoSubmenu('item', getItemsCached, itemActionMenu, {
    disableChoiceExistsCheck: true
})

For your second approach it took me a moment to realize the problem: You probably only have a MenuMiddleware for the mainMenu. As the itemListMenu is not a submenu of the mainMenu the MenuMiddleware will not respond with the menu automatically. It will see a path below the root path ('/items/' is below '/'). But as this path doesnt exist currently it shows you the main menu (so the user gets why the path isnt there, no button is leading there).

You should be able to have two MenuMiddlewares which handle that to achieve the second way but I still think the first way is way simpler. Just cache the requests.

I hope it helps :)

kentnek commented 4 years ago

@EdJoPaTo thank you so much for the detailed explanations! I didn't know about the disableChoiceExistsCheck flag either, that explains the refetching behavior.

You should be able to have two MenuMiddlewares which handle that to achieve the second way

If I understand this correctly, the MenuMiddleware handles path registration, and since editMenuOnContext doesn't explicitly register the new path with the main menu's middleware, it will default to the main menu right?

EdJoPaTo commented 4 years ago

editMenuOnContext uses that path only to create the buttons but does not register any handling on pressed buttons. So once a button is pressed the main menu MenuMiddleware will see its below the main menu path and tries to handle that. But as it does not know about that submenu it will default to the main menu so the user is not stuck on unreachable menus.

kentnek commented 4 years ago

thanks!