diff --git a/packages/client/src/types/bots/index.ts b/packages/client/src/types/bots/index.ts index df7516c8..b2650ec3 100644 --- a/packages/client/src/types/bots/index.ts +++ b/packages/client/src/types/bots/index.ts @@ -5,3 +5,4 @@ export * from './inline-query' export * from './callback-query' export * from './game-high-score' export * from './command-scope' +export * from './keyboard-builder' diff --git a/packages/client/src/types/bots/keyboard-builder.ts b/packages/client/src/types/bots/keyboard-builder.ts new file mode 100644 index 00000000..d624852f --- /dev/null +++ b/packages/client/src/types/bots/keyboard-builder.ts @@ -0,0 +1,105 @@ +import type { InlineKeyboardMarkup, ReplyKeyboardMarkup } from './keyboards' +import { tl } from '@mtcute/tl' + +type ButtonLike = tl.TypeKeyboardButton | false | null | undefined | void + +/** + * Builder for bot keyboards + */ +export class BotKeyboardBuilder { + private _buttons: tl.TypeKeyboardButton[][] = [] + + constructor(readonly maxRowWidth: number | null = 3) {} + + /** + * Add buttons, wrapping them once {@link maxRowWidth} is reached + * + * @param buttons Buttons to add + */ + push(...buttons: (ButtonLike | (() => ButtonLike))[]): this { + if (!buttons.length) return this + + let row: tl.TypeKeyboardButton[] = [] + buttons.forEach((btn) => { + if (typeof btn === 'function') btn = btn() + if (!btn) return + + row.push(btn) + if (row.length === this.maxRowWidth) { + this._buttons.push(row) + row = [] + } + }) + + if (row.length) { + this._buttons.push(row) + } + + return this + } + + /** + * Add a row of buttons. Will not be wrapped. + * + * @param row Row or a function that will populate it + */ + row(row: ButtonLike[] | ((arr: ButtonLike[]) => void)): this { + if (typeof row === 'function') { + const fn = row + row = [] + fn(row) + } + + const normal = row.filter(Boolean) as tl.TypeKeyboardButton[] + if (normal.length) this._buttons.push(normal) + + return this + } + + /** + * Append a button to the last row, wrapping if needed. + * + * @param btn Button to add + * @param force Whether to forcefully add the button (i.e. do not wrap) + */ + append(btn: ButtonLike | (() => ButtonLike), force = false): this { + if (typeof btn === 'function') btn = btn() + if (!btn) return this + + if ( + this._buttons.length && + (this.maxRowWidth === null || + force || + this._buttons[this._buttons.length - 1].length < + this.maxRowWidth) + ) { + this._buttons[this._buttons.length - 1].push() + } else { + this._buttons.push([btn]) + } + + return this + } + + /** + * Return contents of this builder as an inline keyboard + */ + asInline(): InlineKeyboardMarkup { + return { + type: 'inline', + buttons: this._buttons, + } + } + + /** + * Return contents of this builder as a reply keyboard + */ + asReply( + params: Omit = {} + ): ReplyKeyboardMarkup { + const ret = params as tl.Mutable + ret.type = 'reply' + ret.buttons = this._buttons + return ret + } +} diff --git a/packages/client/src/types/bots/keyboards.ts b/packages/client/src/types/bots/keyboards.ts index 5a229f50..c00b3c82 100644 --- a/packages/client/src/types/bots/keyboards.ts +++ b/packages/client/src/types/bots/keyboards.ts @@ -1,4 +1,5 @@ import { tl } from '@mtcute/tl' +import { BotKeyboardBuilder } from './keyboard-builder' /** * Reply keyboard markup @@ -57,6 +58,10 @@ export type ReplyMarkup = * > in the description. */ export namespace BotKeyboard { + export function builder(maxRowWidth?: number | null): BotKeyboardBuilder { + return new BotKeyboardBuilder(maxRowWidth) + } + /** * Create an inline keyboard markup *