Multipage inline menu for Telegram Bots API

225-2

February 21, 2018

After implementing InlineKeyboardBuilder I thought if is the menu had very much items to display all of them together. It might not be convenient.

So I decide to implement a manager for rendering the menu.

Minimal code to run our bot

According to very simple lesson we must create our Bot class and start it like this:


public class Main {
    public static void main(String[] args) {
        // Initialize Api Context
        ApiContextInitializer.init();

        // Instantiate Telegram Bots API
        TelegramBotsApi botsApi = new TelegramBotsApi();

        // Register our bot
        MenuBot bot = new MenuBot();
        bot.init();
        try {
            botsApi.registerBot(bot);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }
}



public class MenuBot extends TelegramLongPollingBot {

    public void init() {
        // accessory initialization
    }

    @Override
    public void onUpdateReceived(Update update) {

        long chat_id = update.getMessage().getChatId();
        
        // We check if the update has a message and the message has text
        if (update.hasMessage() && update.getMessage().hasText()) {

            if (update.getMessage().getText().equals("/menu")) {

                // lets render the menu
                
            } else {

            }

        } else if (update.hasCallbackQuery()) {
        
            // Set variables
            String call_data = update.getCallbackQuery().getData();
            long message_id = update.getCallbackQuery().getMessage().getMessageId();

            // here will be menu buttons callbacks
            
        }
    }

    @Override
    public String getBotUsername() {
        // Return bot username
        // If bot username is @MyAmazingBot, it must return 'MyAmazingBot'
        return "MenuBot";
    }

    @Override
    public String getBotToken() {
        // Return bot token from BotFather
        return "12345:qwertyuiopASDGFHKMK";
    }
}

Multipage menu manager

Lets create our manager. We might be able to initialize it with menu items. And then we want to render concrete page of menu items.

Our menu item will be a class:


public class MenuItem {

    private String name;
    private String action;

    // getters, setters here
    // all args constructor here

}

And now the manager will be:


public class MenuManager {

    public static final String PREV_ACTION = "page-prev";
    public static final String NEXT_ACTION = "page-next";
    public static final String CANCEL_ACTION = "cancel";

    private int buttonsPerPage = 6;
    public void setButtonsPerPage(int buttonsPerPage) {
        this.buttonsPerPage = buttonsPerPage;
    }

    private int total;
    private int lastPage;

    private MenuItem btnPrev = new MenuItem("<<", PREV_ACTION);
    private MenuItem btnNext = new MenuItem(">>", NEXT_ACTION);
    private MenuItem btnCancel = new MenuItem("Cancel", CANCEL_ACTION);

    private List<MenuItem> menu = new ArrayList<>();

    private int columnsCount;
    public void setColumnsCount(int columnsCount) {
        this.columnsCount = columnsCount;
    }

    public void init() {
        this.total = menu.size();
        this.lastPage = (int) Math.ceil((double) total / buttonsPerPage) - 1;
    }

    public void addMenuItem(String name, String action) {
        this.menu.add(new MenuItem(name, action));
    }

    private List<MenuItem> getPage(int page) {
        List<MenuItem> pageMenu = new ArrayList<>();

        if (page > lastPage) {
            return pageMenu;
        }

        int start = page* buttonsPerPage;
        int end = (page+1)* buttonsPerPage -1;

        if (start < 0) start = 0;
        if (end >= total) end = total-1;

        for (int i = start; i <= end; i++) {
            pageMenu.add(menu.get(i));
        }

        return pageMenu;
    }

    private List<MenuItem> getControlButtonsForPage(int page, boolean hasCancel) {
        List<MenuItem> buttons = new ArrayList<>();
        if (page > 0) {
            buttons.add(btnPrev);
        }
        if (hasCancel) {
            buttons.add(btnCancel);
        }
        if (page < lastPage) {
            buttons.add(btnNext);
        }
        return buttons;
    }

    public InlineKeyboardBuilder createMenuForPage(int page, boolean hasCancel) {
        List<MenuItem> pageButtons = getPage(page);
        List<MenuItem> controlButtons = getControlButtonsForPage(page, hasCancel);

        InlineKeyboardBuilder builder = InlineKeyboardBuilder.create();
        int col = 0;
        int num = 0;
        builder.row();
        for (MenuItem button : pageButtons) {
            builder.button(button.getName(), button.getAction());
            if (++col >= columnsCount) {
                col = 0;
                builder.endRow();
                if (num++ <= pageButtons.size()) {
                    builder.row();
                }
            }
        }
        builder.endRow();

        builder.row();
        for (MenuItem button : controlButtons) {
            if (button.getAction().equals(PREV_ACTION)) {
                builder.button(button.getName(), button.getAction()+":"+(page-1));
            } else if (button.getAction().equals(NEXT_ACTION)) {
                builder.button(button.getName(), button.getAction()+":"+(page+1));
            } else {
                builder.button(button.getName(), button.getAction());
            }
        }
        builder.endRow();

        return builder;
    }

}

Usage

Lets use our manager:


public class MenuBot extends TelegramLongPollingBot {

    private MenuManager menuManager = new MenuManager();
    
    public void init() {
        menuManager.setColumnsCount(2);

        menuManager.addMenuItem("Action 1", "action 1");
        menuManager.addMenuItem("Action 2", "action 2");
        menuManager.addMenuItem("Action 3", "action 3");
        menuManager.addMenuItem("Action 4", "action 4");
        menuManager.addMenuItem("Action 5", "action 5");
        menuManager.addMenuItem("Action 6", "action 6");
        menuManager.addMenuItem("Action 7", "action 7");
        menuManager.addMenuItem("Action 8", "action 8");
        menuManager.addMenuItem("Action 9", "action 9");
        menuManager.addMenuItem("Action 10", "action 10");
        menuManager.addMenuItem("Action 11", "action 11");
        menuManager.addMenuItem("Action 12", "action 12");
        menuManager.addMenuItem("Action 13", "action 13");
        menuManager.addMenuItem("Action 14", "action 14");
        menuManager.addMenuItem("Action 15", "action 15");
        menuManager.addMenuItem("Action 16", "action 16");
        menuManager.addMenuItem("Action 17", "action 17");
        menuManager.addMenuItem("Action 18", "action 18");
        menuManager.addMenuItem("Action 19", "action 19");
        menuManager.addMenuItem("Action 20", "action 20");

        menuManager.init();
    }
    
    
    private void replaceMessageWithText(long chatId, long messageId, String text) {
        EditMessageText newMessage = new EditMessageText()
                .setChatId(chatId)
                .setMessageId(Math.toIntExact(messageId))
                .setText(text);
        try {
            execute(newMessage);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }
    
    
    private void replaceMessage(long chatId, long messageId, SendMessage message) {
        EditMessageText newMessage = new EditMessageText()
                .setChatId(chatId)
                .setMessageId(Math.toIntExact(messageId))
                .setText(message.getText())
                .setReplyMarkup((InlineKeyboardMarkup) message.getReplyMarkup());
        try {
            execute(newMessage);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }
    

    @Override
    public void onUpdateReceived(Update update) {

        // We check if the update has a message and the message has text
        if (update.hasMessage() && update.getMessage().hasText()) {

            if (update.getMessage().getText().equals("/menu")) {
                long chatId = update.getMessage().getChatId();
                
                // lets render the menu
                InlineKeyboardBuilder builder = menuManager.createMenuForPage(0, true);

                builder.setChatId(chatId).setText("Choose action:");
                SendMessage message = builder.build();
                
                try {
                    // Send the message
                    execute(message);
                } catch (TelegramApiException e) {
                    e.printStackTrace();
                }
                
            } else {

            }

        } else if (update.hasCallbackQuery()) {
        
            // Set variables
            long chatId = update.getCallbackQuery().getMessage().getChatId();
            String callData = update.getCallbackQuery().getData();
            long messageId = update.getCallbackQuery().getMessage().getMessageId();

            // here will be menu buttons callbacks
            
            if (callData.equals(MenuManager.CANCEL_ACTION)) {
                replaceMessageWithText(chatId, messageId, "Cancelled.");

            
            } else if (callData.startsWith(MenuManager.PREV_ACTION) || callData.startsWith(MenuManager.NEXT_ACTION)) {
            
                String pageNum = "0";
                if (callData.startsWith(MenuManager.PREV_ACTION)) {
                    pageNum = callData.replace(MenuManager.PREV_ACTION+":", "");
                } else {
                    pageNum = callData.replace(MenuManager.NEXT_ACTION+":", "");
                }

                InlineKeyboardBuilder builder = menuManager.createMenuForPage(Integer.parseInt(pageNum), true);

                builder.setChatId(chatId).setText("Choose action:");
                SendMessage message = builder.build();

                replaceMessage(chatId, messageId, message);

            }
            
        }
    }

    // ...
    
}

Result

Using this manager we can make multipage menu like this:

Pretty simple and much useful, don't think so?

You can find working example in my GitHub repo.