Florian Zevedei cb5fa35269 Fix #60
2024-01-30 16:07:08 +01:00

741 lines
20 KiB
C#

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
{
private static readonly object EvButtonClicked = new();
private readonly EventHandlerList _events = new();
private EKeyboardType _mEKeyboardType = EKeyboardType.ReplyKeyboard;
private bool _renderNecessary = true;
public string NextPageLabel = Default.Language["ButtonGrid_NextPage"];
public string NoItemsLabel = Default.Language["ButtonGrid_NoItems"];
public string PreviousPageLabel = Default.Language["ButtonGrid_PreviousPage"];
public string SearchLabel = Default.Language["ButtonGrid_SearchFeature"];
public ButtonGrid()
{
DataSource = new ButtonFormDataSource();
}
public ButtonGrid(EKeyboardType type) : this()
{
_mEKeyboardType = type;
}
public ButtonGrid(ButtonForm form)
{
DataSource = new ButtonFormDataSource(form);
}
public string Title { get; set; } = Default.Language["ButtonGrid_Title"];
public string ConfirmationText { get; set; } = "";
/// <summary>
/// Data source of the items.
/// </summary>
public ButtonFormDataSource DataSource { get; set; }
public int? MessageId { get; set; }
/// <summary>
/// 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
/// </summary>
public bool ResizeKeyboard { get; set; } = false;
public bool OneTimeKeyboard { get; set; } = false;
public bool HideKeyboardOnCleanup { get; set; } = true;
public bool DeletePreviousMessage { get; set; } = true;
/// <summary>
/// Removes the reply message from a user.
/// </summary>
public bool DeleteReplyMessage { get; set; } = true;
/// <summary>
/// Parsemode of the message.
/// </summary>
public ParseMode MessageParseMode { get; set; } = ParseMode.Markdown;
/// <summary>
/// Enables automatic paging of buttons when the amount of rows is exceeding the limits.
/// </summary>
public bool EnablePaging { get; set; } = false;
/// <summary>
/// Enabled a search function.
/// </summary>
public bool EnableSearch { get; set; } = false;
public string SearchQuery { get; set; }
public ENavigationBarVisibility NavigationBarVisibility { get; set; } = ENavigationBarVisibility.always;
/// <summary>
/// Index of the current page
/// </summary>
public int CurrentPageIndex { get; set; }
/// <summary>
/// Layout of the buttons which should be displayed always on top.
/// </summary>
public ButtonRow HeadLayoutButtonRow { get; set; }
/// <summary>
/// Layout of columns which should be displayed below the header
/// </summary>
public ButtonRow SubHeadLayoutButtonRow { get; set; }
/// <summary>
/// Defines which type of Button Keyboard should be rendered.
/// </summary>
public EKeyboardType KeyboardType
{
get => _mEKeyboardType;
set
{
if (_mEKeyboardType != value)
{
_renderNecessary = true;
Cleanup().Wait();
_mEKeyboardType = value;
}
}
}
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;
}
}
/// <summary>
/// Returns the maximum number of rows
/// </summary>
public int MaximumRow
{
get
{
return KeyboardType switch
{
EKeyboardType.InlineKeyBoard => Constants.Telegram.MaxInlineKeyBoardRows,
EKeyboardType.ReplyKeyboard => Constants.Telegram.MaxReplyKeyboardRows,
_ => 0
};
}
}
/// <summary>
/// Returns the number of all rows (layout + navigation + content);
/// </summary>
public int TotalRows => LayoutRows + DataSource.RowCount;
/// <summary>
/// Contains the Number of Rows which are used by the layout.
/// </summary>
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;
}
}
/// <summary>
/// Returns the number of item rows per page.
/// </summary>
public int ItemRowsPerPage => MaximumRow - LayoutRows;
/// <summary>
/// Return the number of pages.
/// </summary>
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 event AsyncEventHandler<ButtonClickedEventArgs> ButtonClicked
{
add => _events.AddHandler(EvButtonClicked, value);
remove => _events.RemoveHandler(EvButtonClicked, value);
}
public async Task OnButtonClicked(ButtonClickedEventArgs e)
{
var handler = _events[EvButtonClicked]?.GetInvocationList()
.Cast<AsyncEventHandler<ButtonClickedEventArgs>>();
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>();
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;
}
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 result.ConfirmAction(ConfirmationText ?? "");
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;
}
}
/// <summary>
/// This method checks of the amount of buttons
/// </summary>
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 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;
}
/// <summary>
/// Tells the control that it has been updated.
/// </summary>
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;
}
}
}