using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; using TelegramBotBase.Args; using TelegramBotBase.Base; using TelegramBotBase.DataSources; using TelegramBotBase.Enums; using TelegramBotBase.Exceptions; using TelegramBotBase.Form; using TelegramBotBase.Localizations; using static TelegramBotBase.Base.Async; namespace TelegramBotBase.Controls.Hybrid { public class ButtonGrid : ControlBase { public string Title { get; set; } = Default.Language["ButtonGrid_Title"]; public string ConfirmationText { get; set; } = ""; private bool _renderNecessary = true; private static readonly object EvButtonClicked = new object(); private readonly EventHandlerList _events = new EventHandlerList(); /// /// /// [Obsolete("This property is obsolete. Please use the DataSource property instead.")] public ButtonForm ButtonsForm { get => DataSource.ButtonForm; set => DataSource = new ButtonFormDataSource(value); } /// /// Data source of the items. /// public ButtonFormDataSource DataSource { get; set; } public int? MessageId { get; set; } /// /// Optional. Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). Defaults to false, in which case the custom keyboard is always of the same height as the app's standard keyboard. /// Source: https://core.telegram.org/bots/api#replykeyboardmarkup /// public bool ResizeKeyboard { get; set; } = false; public bool OneTimeKeyboard { get; set; } = false; public bool HideKeyboardOnCleanup { get; set; } = true; public bool DeletePreviousMessage { get; set; } = true; /// /// Removes the reply message from a user. /// public bool DeleteReplyMessage { get; set; } = true; /// /// Parsemode of the message. /// public ParseMode MessageParseMode { get; set; } = ParseMode.Markdown; /// /// Enables automatic paging of buttons when the amount of rows is exceeding the limits. /// public bool EnablePaging { get; set; } = false; /// /// Enabled a search function. /// public bool EnableSearch { get; set; } = false; public string SearchQuery { get; set; } public ENavigationBarVisibility NavigationBarVisibility { get; set; } = ENavigationBarVisibility.always; /// /// Index of the current page /// public int CurrentPageIndex { get; set; } public string PreviousPageLabel = Default.Language["ButtonGrid_PreviousPage"]; public string NextPageLabel = Default.Language["ButtonGrid_NextPage"]; public string NoItemsLabel = Default.Language["ButtonGrid_NoItems"]; public string SearchLabel = Default.Language["ButtonGrid_SearchFeature"]; /// /// Layout of the buttons which should be displayed always on top. /// public ButtonRow HeadLayoutButtonRow { get; set; } /// /// Layout of columns which should be displayed below the header /// public ButtonRow SubHeadLayoutButtonRow { get; set; } /// /// Defines which type of Button Keyboard should be rendered. /// public EKeyboardType KeyboardType { get => _mEKeyboardType; set { if (_mEKeyboardType != value) { _renderNecessary = true; Cleanup().Wait(); _mEKeyboardType = value; } } } private EKeyboardType _mEKeyboardType = EKeyboardType.ReplyKeyboard; public ButtonGrid() { DataSource = new ButtonFormDataSource(); } public ButtonGrid(EKeyboardType type) : this() { _mEKeyboardType = type; } public ButtonGrid(ButtonForm form) { DataSource = new ButtonFormDataSource(form); } public event AsyncEventHandler ButtonClicked { add => _events.AddHandler(EvButtonClicked, value); remove => _events.RemoveHandler(EvButtonClicked, value); } public async Task OnButtonClicked(ButtonClickedEventArgs e) { var handler = _events[EvButtonClicked]?.GetInvocationList().Cast>(); if (handler == null) return; foreach (var h in handler) { await h.InvokeAllAsync(this, e); } } public override void Init() { Device.MessageDeleted += Device_MessageDeleted; } private void Device_MessageDeleted(object sender, MessageDeletedEventArgs e) { if (MessageId == null) return; if (e.MessageId != MessageId) return; MessageId = null; } public override async Task Load(MessageResult result) { if (KeyboardType != EKeyboardType.ReplyKeyboard) return; if (!result.IsFirstHandler) return; if (result.MessageText == null || result.MessageText == "") return; var matches = new List(); ButtonRow match = null; var index = -1; if (HeadLayoutButtonRow?.Matches(result.MessageText) ?? false) { match = HeadLayoutButtonRow; goto check; } if (SubHeadLayoutButtonRow?.Matches(result.MessageText) ?? false) { match = SubHeadLayoutButtonRow; goto check; } var br = DataSource.FindRow(result.MessageText); if (br != null) { match = br.Item1; index = br.Item2; } //var button = HeadLayoutButtonRow?. .FirstOrDefault(a => a.Text.Trim() == result.MessageText) // ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Text.Trim() == result.MessageText); // bf.ToList().FirstOrDefault(a => a.Text.Trim() == result.MessageText) //var index = bf.FindRowByButton(button); check: //Remove button click message if (DeleteReplyMessage) await Device.DeleteMessage(result.MessageId); if (match != null) { await OnButtonClicked(new ButtonClickedEventArgs(match.GetButtonMatch(result.MessageText), index, match)); result.Handled = true; return; } if (result.MessageText == PreviousPageLabel) { if (CurrentPageIndex > 0) CurrentPageIndex--; Updated(); } else if (result.MessageText == NextPageLabel) { if (CurrentPageIndex < PageCount - 1) CurrentPageIndex++; Updated(); } else if (EnableSearch) { if (result.MessageText.StartsWith("🔍")) { //Sent note about searching if (SearchQuery == null) { await Device.Send(SearchLabel); } SearchQuery = null; Updated(); return; } SearchQuery = result.MessageText; if (SearchQuery != null && SearchQuery != "") { CurrentPageIndex = 0; Updated(); } } } public override async Task Action(MessageResult result, string value = null) { if (result.Handled) return; if (!result.IsFirstHandler) return; //Find clicked button depending on Text or Value (depending on markup type) if (KeyboardType != EKeyboardType.InlineKeyBoard) return; await result.ConfirmAction(ConfirmationText ?? ""); ButtonRow match = null; var index = -1; if (HeadLayoutButtonRow?.Matches(result.RawData, false) ?? false) { match = HeadLayoutButtonRow; goto check; } if (SubHeadLayoutButtonRow?.Matches(result.RawData, false) ?? false) { match = SubHeadLayoutButtonRow; goto check; } var br = DataSource.FindRow(result.RawData, false); if (br != null) { match = br.Item1; index = br.Item2; } //var bf = DataSource.ButtonForm; //var button = HeadLayoutButtonRow?.FirstOrDefault(a => a.Value == result.RawData) // ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Value == result.RawData) // ?? bf.ToList().FirstOrDefault(a => a.Value == result.RawData); //var index = bf.FindRowByButton(button); check: if (match != null) { await OnButtonClicked(new ButtonClickedEventArgs(match.GetButtonMatch(result.RawData, false), index, match)); result.Handled = true; return; } switch (result.RawData) { case "$previous$": if (CurrentPageIndex > 0) CurrentPageIndex--; Updated(); break; case "$next$": if (CurrentPageIndex < PageCount - 1) CurrentPageIndex++; Updated(); break; } } /// /// This method checks of the amount of buttons /// private void CheckGrid() { switch (_mEKeyboardType) { case EKeyboardType.InlineKeyBoard: if (DataSource.RowCount > Constants.Telegram.MaxInlineKeyBoardRows && !EnablePaging) { throw new MaximumRowsReachedException { Value = DataSource.RowCount, Maximum = Constants.Telegram.MaxInlineKeyBoardRows }; } if (DataSource.ColumnCount > Constants.Telegram.MaxInlineKeyBoardCols) { throw new MaximumColsException { Value = DataSource.ColumnCount, Maximum = Constants.Telegram.MaxInlineKeyBoardCols }; } break; case EKeyboardType.ReplyKeyboard: if (DataSource.RowCount > Constants.Telegram.MaxReplyKeyboardRows && !EnablePaging) { throw new MaximumRowsReachedException { Value = DataSource.RowCount, Maximum = Constants.Telegram.MaxReplyKeyboardRows }; } if (DataSource.ColumnCount > Constants.Telegram.MaxReplyKeyboardCols) { throw new MaximumColsException { Value = DataSource.ColumnCount, Maximum = Constants.Telegram.MaxReplyKeyboardCols }; } break; } } public override async Task Render(MessageResult result) { if (!_renderNecessary) return; //Check for rows and column limits CheckGrid(); _renderNecessary = false; var form = DataSource.PickItems(CurrentPageIndex * ItemRowsPerPage, ItemRowsPerPage, (EnableSearch ? SearchQuery : null)); //if (this.EnableSearch && this.SearchQuery != null && this.SearchQuery != "") //{ // form = form.FilterDuplicate(this.SearchQuery, true); //} //else //{ // form = form.Duplicate(); //} if (EnablePaging) { IntegratePagingView(form); } if (HeadLayoutButtonRow != null && HeadLayoutButtonRow.Count > 0) { form.InsertButtonRow(0, HeadLayoutButtonRow); } if (SubHeadLayoutButtonRow != null && SubHeadLayoutButtonRow.Count > 0) { if (IsNavigationBarVisible) { form.InsertButtonRow(2, SubHeadLayoutButtonRow); } else { form.InsertButtonRow(1, SubHeadLayoutButtonRow); } } Message m = null; switch (KeyboardType) { //Reply Keyboard could only be updated with a new keyboard. case EKeyboardType.ReplyKeyboard: if (form.Count == 0) { if (MessageId != null) { await Device.HideReplyKeyboard(); MessageId = null; } return; } //if (this.MessageId != null) //{ // if (form.Count == 0) // { // await this.Device.HideReplyKeyboard(); // this.MessageId = null; // return; // } //} //if (form.Count == 0) // return; var rkm = (ReplyKeyboardMarkup)form; rkm.ResizeKeyboard = ResizeKeyboard; rkm.OneTimeKeyboard = OneTimeKeyboard; m = await Device.Send(Title, rkm, disableNotification: true, parseMode: MessageParseMode, markdownV2AutoEscape: false); //Prevent flicker of keyboard if (DeletePreviousMessage && MessageId != null) await Device.DeleteMessage(MessageId.Value); break; case EKeyboardType.InlineKeyBoard: //Try to edit message if message id is available //When the returned message is null then the message has been already deleted, resend it if (MessageId != null) { m = await Device.Edit(MessageId.Value, Title, (InlineKeyboardMarkup)form); if (m != null) { MessageId = m.MessageId; return; } } //When no message id is available or it has been deleted due the use of AutoCleanForm re-render automatically m = await Device.Send(Title, (InlineKeyboardMarkup)form, disableNotification: true, parseMode: MessageParseMode, markdownV2AutoEscape: false); break; } if (m != null) { MessageId = m.MessageId; } } private void IntegratePagingView(ButtonForm dataForm) { //No Items if (dataForm.Rows == 0) { dataForm.AddButtonRow(new ButtonBase(NoItemsLabel, "$")); } if (IsNavigationBarVisible) { //🔍 var row = new ButtonRow(); row.Add(new ButtonBase(PreviousPageLabel, "$previous$")); row.Add(new ButtonBase(string.Format(Default.Language["ButtonGrid_CurrentPage"], CurrentPageIndex + 1, PageCount), "$site$")); row.Add(new ButtonBase(NextPageLabel, "$next$")); if (EnableSearch) { row.Insert(2, new ButtonBase("🔍 " + (SearchQuery ?? ""), "$search$")); } dataForm.InsertButtonRow(0, row); dataForm.AddButtonRow(row); } } public bool PagingNecessary { get { if (KeyboardType == EKeyboardType.InlineKeyBoard && TotalRows > Constants.Telegram.MaxInlineKeyBoardRows) { return true; } if (KeyboardType == EKeyboardType.ReplyKeyboard && TotalRows > Constants.Telegram.MaxReplyKeyboardRows) { return true; } return false; } } public bool IsNavigationBarVisible { get { if (NavigationBarVisibility == ENavigationBarVisibility.always | (NavigationBarVisibility == ENavigationBarVisibility.auto && PagingNecessary)) { return true; } return false; } } /// /// Returns the maximum number of rows /// public int MaximumRow { get { return KeyboardType switch { EKeyboardType.InlineKeyBoard => Constants.Telegram.MaxInlineKeyBoardRows, EKeyboardType.ReplyKeyboard => Constants.Telegram.MaxReplyKeyboardRows, _ => 0 }; } } /// /// Returns the number of all rows (layout + navigation + content); /// public int TotalRows => LayoutRows + DataSource.RowCount; /// /// Contains the Number of Rows which are used by the layout. /// private int LayoutRows { get { var layoutRows = 0; if (NavigationBarVisibility == ENavigationBarVisibility.always | NavigationBarVisibility == ENavigationBarVisibility.auto) layoutRows += 2; if (HeadLayoutButtonRow != null && HeadLayoutButtonRow.Count > 0) layoutRows++; if (SubHeadLayoutButtonRow != null && SubHeadLayoutButtonRow.Count > 0) layoutRows++; return layoutRows; } } /// /// Returns the number of item rows per page. /// public int ItemRowsPerPage => MaximumRow - LayoutRows; /// /// Return the number of pages. /// public int PageCount { get { if (DataSource.RowCount == 0) return 1; //var bf = this.DataSource.PickAllItems(this.EnableSearch ? this.SearchQuery : null); var max = DataSource.CalculateMax(EnableSearch ? SearchQuery : null); //if (this.EnableSearch && this.SearchQuery != null && this.SearchQuery != "") //{ // bf = bf.FilterDuplicate(this.SearchQuery); //} if (max == 0) return 1; return (int)Math.Ceiling(max / (decimal)ItemRowsPerPage); } } public override Task Hidden(bool formClose) { //Prepare for opening Modal, and comming back if (!formClose) { Updated(); } else { //Remove event handler Device.MessageDeleted -= Device_MessageDeleted; } return Task.CompletedTask; } /// /// Tells the control that it has been updated. /// public void Updated() { _renderNecessary = true; } public override async Task Cleanup() { if (MessageId == null) return; switch (KeyboardType) { case EKeyboardType.InlineKeyBoard: await Device.DeleteMessage(MessageId.Value); MessageId = null; break; case EKeyboardType.ReplyKeyboard: if (HideKeyboardOnCleanup) { await Device.HideReplyKeyboard(); } MessageId = null; break; } } } }