Skip to content

Commit 65678d4

Browse files
committed
action row builder refactor
1 parent 86c936e commit 65678d4

13 files changed

Lines changed: 235 additions & 255 deletions

File tree

src/Disc.NET.Client.SDK/Messages/Components/Buttons/ButtonComponent.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
using Disc.NET.Client.SDK.Messages.Components.Enums;
1+
using Disc.NET.Client.SDK.Messages.Components.Enums;
22

33
namespace Disc.NET.Client.SDK.Messages.Components.Buttons
44
{
55
public class ButtonComponent : IMessageComponent
66
{
77
public MessageComponentType Type => MessageComponentType.Button;
8-
public string? Id { get; set; }
8+
99
public ButtonStyle Style { get; set; }
10+
1011
public string? Label { get; set; }
1112

1213
// emoji
1314

14-
public required string CustomId { get; set; }
15+
public string? CustomId { get; set; }
16+
1517
public string? SkuId { get; set; }
18+
1619
public string? Url { get; set; }
1720

1821
public bool Disabled { get; set; }
Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
namespace Disc.NET.Client.SDK.Messages.Components
1+
using Disc.NET.Client.SDK.Messages.Components.Buttons;
2+
using Disc.NET.Client.SDK.Messages.Components.Enums;
3+
using Disc.NET.Client.SDK.Messages.Components.Selects;
4+
using System.Text.Json.Serialization;
5+
6+
namespace Disc.NET.Client.SDK.Messages.Components
27
{
3-
public interface IMessageComponent
8+
[JsonDerivedType(typeof(ButtonComponent))]
9+
[JsonDerivedType(typeof(StringSelectComponent))]
10+
public interface IMessageComponent
411
{
5-
6-
//⚠️ Outro problema(importante)
7-
//Você está usando:
8-
9-
//CustomId = "test_button"
10-
//👉 Isso vai dar problema real quando:
11-
12-
//Dois usuários clicarem
13-
14-
//Ou você tiver múltiplas mensagens
15-
16-
//💥 Porque o callback vai sobrescrever no dictionary
17-
18-
//✅ Solução recomendada(muito importante)
19-
//Use CustomId único:
20-
21-
//CustomId = $"test_button:{Guid.NewGuid()}"
22-
23-
string? CustomId { get; set; }
12+
MessageComponentType Type { get; }
13+
string? CustomId { get; set; }
2414
}
2515
}
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
using Disc.NET.Client.SDK.Messages.Components.Enums;
1+
using Disc.NET.Client.SDK.Messages.Components.Enums;
22

33
namespace Disc.NET.Client.SDK.Messages.Components.Selects
44
{
55
public class StringSelectComponent : ISelectComponent
66
{
77
public MessageComponentType Type => MessageComponentType.SelectMenu;
88

9-
public int? Id { get; set; }
10-
11-
public required string CustomId { get; set; }
9+
public string? CustomId { get; set; }
1210

1311
public List<StringSelectOption> Options { get; set; } = [];
1412

1513
public string? Placeholder { get; set; }
14+
1615
public int? MinValues { get; set; }
16+
1717
public int? MaxValues { get; set; }
18+
1819
public bool? Disabled { get; set; }
1920
}
2021

2122
public class StringSelectOption
2223
{
2324
public required string Label { get; set; }
25+
2426
public required string Value { get; set; }
2527

2628
public string? Description { get; set; }
29+
2730
public bool Default { get; set; }
2831
}
2932
}

src/Disc.NET.Commands/Message.cs

Lines changed: 55 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using Disc.NET.Client.SDK.Enums;
1+
using Disc.NET.Client.SDK.Enums;
22
using Disc.NET.Client.SDK.Messages;
33
using Disc.NET.Client.SDK.Messages.Components;
4+
using Disc.NET.Client.SDK.Messages.Components.Enums;
45
using Disc.NET.Commands.Contexts;
56
using Disc.NET.Commands.MessageBuilders;
67
using Disc.NET.Shared.Constraints;
@@ -31,62 +32,42 @@ private List<object> MountActionRows()
3132

3233
var results = new List<object>();
3334

34-
NormalizeActionRows<ActionRowSelectMenuBuilder>(ActionRowConstraint.MAX_SELECT_MENUS_PER_ACTION_ROW, message =>
35-
new ActionRowSelectMenuBuilder().AddComponent<ContextBase>(message.First()));
35+
// Normalize for Select Menus (Max 1 per action row)
36+
NormalizeActionRows(ActionRowConstraint.MAX_SELECT_MENUS_PER_ACTION_ROW, components =>
37+
components.Any(c => c.Type == MessageComponentType.SelectMenu),
38+
message => new ActionRowBuilder().AddComponent<ContextBase>(message.First()));
39+
40+
// Normalize for Buttons (Max 5 per action row)
41+
NormalizeActionRows(ActionRowConstraint.MAX_BUTTONS_PER_ACTION_ROW, components =>
42+
components.All(c => c.Type == MessageComponentType.Button),
43+
message =>
44+
{
45+
var builder = new ActionRowBuilder();
46+
message.ForEach(c => builder.AddComponent<ContextBase>(c));
47+
return builder;
48+
});
3649

3750
ActionRows.ForEach(x => results.Add(x.Build()));
3851
return results;
3952
}
4053

41-
/// <summary>
42-
/// Normalizes action rows of a specific component type to comply with Discord message constraints.
43-
///
44-
/// This method validates the maximum number of action rows per message and the maximum number
45-
/// of components allowed per action row. If an action row exceeds the allowed number of components,
46-
/// it is automatically split into multiple valid action rows.
47-
/// </summary>
48-
/// <typeparam name="T">
49-
/// The type of action row builder to process (e.g., select menu rows or button rows).
50-
/// </typeparam>
51-
/// <param name="quantityComponentPerActionRow">
52-
/// The maximum number of components allowed per action row for the given component type.
53-
/// </param>
54-
/// <param name="createBuilderFunc">
55-
/// A factory function responsible for creating a new action row builder from a list
56-
/// of message components.
57-
/// </param>
58-
/// <exception cref="DiscNetClientSdkException">
59-
/// Thrown when the message exceeds Discord constraints, such as:
60-
/// - Maximum number of action rows per message
61-
/// - Insufficient available action row slots to split invalid rows
62-
/// </exception>
63-
/// <remarks>
64-
/// This method mutates the <see cref="ActionRow"/> collection by:
65-
/// - Removing excess components from invalid action rows
66-
/// - Adding newly created action rows to the message
67-
/// </remarks>
68-
69-
private void NormalizeActionRows<T>(int quantityComponentPerActionRow,
54+
private void NormalizeActionRows(int quantityComponentPerActionRow,
55+
Func<List<IMessageComponent>, bool> predicate,
7056
Func<List<IMessageComponent>, IActionRowBuilder> createBuilderFunc)
71-
where T : IActionRowBuilder
7257
{
73-
var actionRows = ActionRows.Where(x => x is T).ToList();
58+
var actionRowsToProcess = ActionRows.Where(x => predicate(x.Components)).ToList();
7459

75-
if (actionRows.Count == 0) return;
60+
if (actionRowsToProcess.Count == 0) return;
61+
7662
int actionRowsPerMessage = ActionRowConstraint.MAX_ACTION_ROWS_PER_MESSAGE;
77-
int actionRowsCount = actionRows.Count;
78-
if (actionRowsCount > actionRowsPerMessage)
79-
{
80-
throw new DiscNetGenericException(
81-
$"The message cannot contain more than {actionRowsPerMessage} top-level components.");
82-
}
83-
63+
8464
int availableActionRowSlots = actionRowsPerMessage - ActionRows.Count;
85-
var invalidActionRows = actionRows.Where(x => x.Components.Count > quantityComponentPerActionRow).ToList();
65+
var invalidActionRows = actionRowsToProcess.Where(x => x.Components.Count > quantityComponentPerActionRow).ToList();
8666
var containsActionRowsInvalids = invalidActionRows.Count > 0;
8767

8868
if (!containsActionRowsInvalids) return;
89-
if (availableActionRowSlots == 0 && containsActionRowsInvalids)
69+
70+
if (availableActionRowSlots <= 0)
9071
{
9172
throw new DiscNetGenericException($"The message cannot contain more than {actionRowsPerMessage} top-level components.");
9273
}
@@ -103,19 +84,16 @@ private void NormalizeActionRows<T>(int quantityComponentPerActionRow,
10384
.Sum(x => x.Components.Count - 1);
10485
}
10586

106-
10787
if (numberNecessaryToCreateNewActionRows > availableActionRowSlots)
10888
{
10989
throw new DiscNetGenericException(
110-
$"The message cannot contain more than {actionRowsPerMessage} top-level components.");
90+
$"The message cannot contain more than {actionRowsPerMessage} top-level components and there is no space to split components into new Action Rows.");
11191
}
11292

11393
int newActionRowsCount = 0;
11494
foreach (var actionRow in invalidActionRows)
11595
{
116-
var messageComponents = actionRow.Components
117-
.OfType<IMessageComponent>()
118-
.ToList();
96+
var messageComponents = actionRow.Components.ToList();
11997

12098
int index = 0;
12199
while (messageComponents.Count - index > quantityComponentPerActionRow &&
@@ -125,28 +103,44 @@ private void NormalizeActionRows<T>(int quantityComponentPerActionRow,
125103

126104
if (quantityComponentPerActionRow > 1)
127105
{
128-
var components = messageComponents
129-
.Skip(index)
106+
var componentsToMove = messageComponents
107+
.Skip(index + quantityComponentPerActionRow) // Keep the first N in current row, move others
130108
.Take(quantityComponentPerActionRow)
131109
.ToList();
132-
components.ForEach(x => actionRow.Components.Remove(x));
110+
111+
componentsToMove.ForEach(x => actionRow.Components.Remove(x));
133112
index += quantityComponentPerActionRow;
134-
newActionRow = createBuilderFunc.Invoke(components);
113+
newActionRow = createBuilderFunc.Invoke(componentsToMove);
135114
}
136115
else
137116
{
138-
var component = messageComponents[index];
139-
140-
actionRow.Components.Remove(component);
141-
index++;
142-
newActionRow = createBuilderFunc.Invoke([component]);
117+
// For Select Menus (max 1), move everything after the first one
118+
var componentsToMove = messageComponents.Skip(1).ToList();
119+
componentsToMove.ForEach(x => actionRow.Components.Remove(x));
120+
121+
// We need to create a new action row for each extra select menu
122+
foreach(var comp in componentsToMove)
123+
{
124+
if (newActionRowsCount < availableActionRowSlots)
125+
{
126+
ActionRows.Add(createBuilderFunc.Invoke([comp]));
127+
newActionRowsCount++;
128+
}
129+
else
130+
{
131+
throw new DiscNetGenericException("Not enough slots for all select menus.");
132+
}
133+
}
134+
break; // Already handled all components for this row
143135
}
144136

145-
ActionRows.Add(newActionRow);
146-
newActionRowsCount++;
137+
if (newActionRow != null)
138+
{
139+
ActionRows.Add(newActionRow);
140+
newActionRowsCount++;
141+
}
147142
}
148143
}
149144
}
150-
151145
}
152146
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Disc.NET.Client.SDK.Messages.Components;
2+
using Disc.NET.Client.SDK.Messages.Components.Buttons;
3+
using Disc.NET.Client.SDK.Messages.Components.Enums;
4+
using Disc.NET.Client.SDK.Messages.Components.Selects;
5+
using Disc.NET.Commands.Contexts;
6+
7+
namespace Disc.NET.Commands.MessageBuilders
8+
{
9+
public sealed class ActionRowBuilder : ActionRowBuilderBase, IActionRowBuilder
10+
{
11+
public List<IMessageComponent> Components { get; } = [];
12+
13+
public ActionRowBuilder()
14+
{
15+
}
16+
17+
public IActionRowBuilder AddComponent(IMessageComponent component)
18+
{
19+
Components.Add(component);
20+
return this;
21+
}
22+
23+
public IActionRowBuilder AddComponent<T>(IMessageComponent component, T? context = null, Func<T, Task>? callback = null) where T : ContextBase
24+
{
25+
if (context != null && callback != null)
26+
{
27+
if (string.IsNullOrEmpty(component.CustomId))
28+
throw new ArgumentException("CustomId must be set for the component when a callback is provided.");
29+
30+
RegisterComponentCallback(component, context, callback);
31+
}
32+
return AddComponent(component);
33+
}
34+
35+
public IActionRowBuilder AddButton(ButtonComponent button)
36+
{
37+
return AddComponent(button);
38+
}
39+
40+
public IActionRowBuilder AddButton<T>(ButtonComponent button, T? context = null, Func<T, Task>? callback = null) where T : ContextBase
41+
{
42+
return AddComponent(button, context, callback);
43+
}
44+
45+
public IActionRowBuilder AddSelectMenu(StringSelectComponent selectMenu)
46+
{
47+
return AddComponent(selectMenu);
48+
}
49+
50+
public IActionRowBuilder AddSelectMenu<T>(StringSelectComponent selectMenu, T? context = null, Func<T, Task>? callback = null) where T : ContextBase
51+
{
52+
return AddComponent(selectMenu, context, callback);
53+
}
54+
55+
public object Build()
56+
{
57+
return new
58+
{
59+
Type = MessageComponentType.ActionRow,
60+
Components = Components
61+
};
62+
}
63+
}
64+
}

src/Disc.NET.Commands/MessageBuilders/ActiorRowBuilderBase.cs renamed to src/Disc.NET.Commands/MessageBuilders/ActionRowBuilderBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
using Disc.NET.Client.SDK.Messages.Components;
1+
using Disc.NET.Client.SDK.Messages.Components;
22
using Disc.NET.Commands.Contexts;
33
using Disc.NET.Shared.Constraints;
44

55
namespace Disc.NET.Commands.MessageBuilders
66
{
7-
public abstract class ActiorRowBuilderBase
7+
public abstract class ActionRowBuilderBase
88
{
99
protected void RegisterComponentCallback<T>(IMessageComponent component, T context, Func<T, Task>? callback) where T : ContextBase
1010
{

0 commit comments

Comments
 (0)