[CV2] QoL & fixes (#3153)

This commit is contained in:
Mihail Gribkov
2025-07-01 22:11:27 +03:00
committed by GitHub
parent c343ce95a5
commit 800a23430d
6 changed files with 226 additions and 33 deletions

View File

@@ -2,6 +2,7 @@ using Discord.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -13,6 +14,18 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponentContainer public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponentContainer
{ {
/// <inheritdoc />
public ImmutableArray<ComponentType> SupportedComponentTypes { get; } =
[
ComponentType.Button,
ComponentType.SelectMenu,
ComponentType.UserSelect,
ComponentType.RoleSelect,
ComponentType.ChannelSelect,
ComponentType.MentionableSelect,
ComponentType.TextInput
];
/// <inheritdoc /> /// <inheritdoc />
public ComponentType Type => ComponentType.ActionRow; public ComponentType Type => ComponentType.ActionRow;
@@ -207,7 +220,10 @@ public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponent
{ {
Preconditions.AtLeast(Components.Count, 1, nameof(Components), "There must be at least 1 component in a row."); Preconditions.AtLeast(Components.Count, 1, nameof(Components), "There must be at least 1 component in a row.");
Preconditions.AtMost(Components.Count, MaxChildCount, nameof(Components), $"Action row can only contain {MaxChildCount} child components!"); Preconditions.AtMost(Components.Count, MaxChildCount, nameof(Components), $"Action row can only contain {MaxChildCount} child components!");
if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
return new ActionRowComponent(_components.Select(x => x.Build()).ToList(), Id); return new ActionRowComponent(_components.Select(x => x.Build()).ToList(), Id);
} }
IMessageComponent IMessageComponentBuilder.Build() => Build(); IMessageComponent IMessageComponentBuilder.Build() => Build();
@@ -242,6 +258,8 @@ public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponent
/// <inheritdoc /> /// <inheritdoc />
IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components); IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components);
/// <inheritdoc />
int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ActionRowBuilder)}: {this.ComponentCount()} child components."; private string DebuggerDisplay => $"{nameof(ActionRowBuilder)}: {this.ComponentCount()} child components.";
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -9,10 +10,22 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ComponentBuilderV2 : IStaticComponentContainer public class ComponentBuilderV2 : IStaticComponentContainer
{ {
/// <inheritdoc />
public ImmutableArray<ComponentType> SupportedComponentTypes { get; } =
[
ComponentType.ActionRow,
ComponentType.Section,
ComponentType.MediaGallery,
ComponentType.Separator,
ComponentType.Container,
ComponentType.File,
ComponentType.TextDisplay
];
/// <summary> /// <summary>
/// Gets the maximum number of components that can be added to a message. /// Gets the maximum number of components that can be added to a message.
/// </summary> /// </summary>
public const int MaxComponents = 40; public const int MaxChildCount = 40;
private List<IMessageComponentBuilder> _components = new(); private List<IMessageComponentBuilder> _components = new();
@@ -74,21 +87,14 @@ public class ComponentBuilderV2 : IStaticComponentContainer
{ {
Preconditions.NotNull(Components, nameof(Components)); Preconditions.NotNull(Components, nameof(Components));
Preconditions.AtLeast(Components.Count, 1, nameof(Components.Count), "At least 1 component must be added to this container."); Preconditions.AtLeast(Components.Count, 1, nameof(Components.Count), "At least 1 component must be added to this container.");
Preconditions.AtMost(this.ComponentCount(), MaxComponents, nameof(Components.Count), $"A message must contain {MaxComponents} components or less."); Preconditions.AtMost(this.ComponentCount(), MaxChildCount, nameof(Components.Count), $"A message must contain {MaxChildCount} components or less.");
var ids = this.GetComponentIds().ToList(); var ids = this.GetComponentIds().ToList();
if (ids.Count != ids.Distinct().Count()) if (ids.Count != ids.Distinct().Count())
throw new InvalidOperationException("Components must have unique ids."); throw new InvalidOperationException("Components must have unique ids.");
if (_components.Any(x => if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
x is not ActionRowBuilder throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
and not SectionBuilder
and not TextDisplayBuilder
and not MediaGalleryBuilder
and not FileComponentBuilder
and not SeparatorBuilder
and not ContainerBuilder))
throw new InvalidOperationException($"Only the following components can be at the top level: {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components.");
return new MessageComponent(Components.Select(x => x.Build()).ToList()); return new MessageComponent(Components.Select(x => x.Build()).ToList());
} }
@@ -101,6 +107,8 @@ public class ComponentBuilderV2 : IStaticComponentContainer
/// <inheritdoc/> /// <inheritdoc/>
IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components); IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components);
/// <inheritdoc/>
int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ComponentBuilderV2)}: {this.ComponentCount()} child components."; private string DebuggerDisplay => $"{nameof(ComponentBuilderV2)}: {this.ComponentCount()} child components.";
} }

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -52,6 +53,21 @@ public static class ComponentContainerExtensions
.WithContent(content) .WithContent(content)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="TextDisplayBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithTextDisplay<BuilderT>(this BuilderT container,
Action<TextDisplayBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new TextDisplayBuilder();
options(comp);
return container.WithTextDisplay(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="SectionBuilder"/> to the container. /// Adds a <see cref="SectionBuilder"/> to the container.
/// </summary> /// </summary>
@@ -65,6 +81,21 @@ public static class ComponentContainerExtensions
return container; return container;
} }
/// <summary>
/// Adds a <see cref="SectionBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithSection<BuilderT>(this BuilderT container,
Action<SectionBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new SectionBuilder();
options(comp);
return container.WithSection(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="SectionBuilder"/> to the container. /// Adds a <see cref="SectionBuilder"/> to the container.
/// </summary> /// </summary>
@@ -122,6 +153,21 @@ public static class ComponentContainerExtensions
.WithItems(urls.Select(x => new MediaGalleryItemProperties(new UnfurledMediaItemProperties(x)))) .WithItems(urls.Select(x => new MediaGalleryItemProperties(new UnfurledMediaItemProperties(x))))
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="MediaGalleryBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithMediaGallery<BuilderT>(this BuilderT container,
Action<MediaGalleryBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new MediaGalleryBuilder();
options(comp);
return container.WithMediaGallery(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="SeparatorBuilder"/> to the container. /// Adds a <see cref="SeparatorBuilder"/> to the container.
/// </summary> /// </summary>
@@ -151,6 +197,21 @@ public static class ComponentContainerExtensions
.WithIsDivider(isDivider) .WithIsDivider(isDivider)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="SeparatorBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithSeparator<BuilderT>(this BuilderT container,
Action<SeparatorBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new SeparatorBuilder();
options(comp);
return container.WithSeparator(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="FileComponentBuilder"/> to the container. /// Adds a <see cref="FileComponentBuilder"/> to the container.
/// </summary> /// </summary>
@@ -180,6 +241,21 @@ public static class ComponentContainerExtensions
.WithIsSpoiler(isSpoiler) .WithIsSpoiler(isSpoiler)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="FileComponentBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithFile<BuilderT>(this BuilderT container,
Action<FileComponentBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new FileComponentBuilder();
options(comp);
return container.WithFile(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="ContainerBuilder"/> to the container. /// Adds a <see cref="ContainerBuilder"/> to the container.
/// </summary> /// </summary>
@@ -223,6 +299,21 @@ public static class ComponentContainerExtensions
=> container.WithContainer(new ContainerBuilder() => container.WithContainer(new ContainerBuilder()
.WithComponents(components)); .WithComponents(components));
/// <summary>
/// Adds a <see cref="ContainerBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithContainer<BuilderT>(this BuilderT container,
Action<ContainerBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var comp = new ContainerBuilder();
options(comp);
return container.WithContainer(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the container. /// Adds a <see cref="ButtonBuilder"/> to the container.
/// </summary> /// </summary>
@@ -262,6 +353,21 @@ public static class ComponentContainerExtensions
.WithSkuId(skuId) .WithSkuId(skuId)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithButton<BuilderT>(this BuilderT container,
Action<ButtonBuilder> options)
where BuilderT : class, IInteractableComponentContainer
{
var comp = new ButtonBuilder();
options(comp);
return container.WithButton(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the container. /// Adds a <see cref="SelectMenuBuilder"/> to the container.
/// </summary> /// </summary>
@@ -306,6 +412,21 @@ public static class ComponentContainerExtensions
.WithDefaultValues(defaultValues) .WithDefaultValues(defaultValues)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithSelectMenu<BuilderT>(this BuilderT container,
Action<SelectMenuBuilder> options)
where BuilderT : class, IInteractableComponentContainer
{
var comp = new SelectMenuBuilder();
options(comp);
return container.WithSelectMenu(comp);
}
/// <summary> /// <summary>
/// Adds a <see cref="ActionRowBuilder"/> to the container. /// Adds a <see cref="ActionRowBuilder"/> to the container.
/// </summary> /// </summary>
@@ -333,6 +454,21 @@ public static class ComponentContainerExtensions
.WithComponents(components) .WithComponents(components)
.WithId(id)); .WithId(id));
/// <summary>
/// Adds a <see cref="SectionBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithActionRow<BuilderT>(this BuilderT container,
Action<ActionRowBuilder> options)
where BuilderT : class, IStaticComponentContainer
{
var cont = new ActionRowBuilder();
options(cont);
return container.WithActionRow(cont);
}
/// <summary> /// <summary>
/// Finds the first <see cref="IMessageComponentBuilder"/> in the <see cref="IComponentContainer"/> /// Finds the first <see cref="IMessageComponentBuilder"/> in the <see cref="IComponentContainer"/>
/// or any of its child <see cref="IComponentContainer"/>s with matching id. /// or any of its child <see cref="IComponentContainer"/>s with matching id.

View File

@@ -9,9 +9,25 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContainer public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContainer
{ {
/// <inheritdoc />
public ImmutableArray<ComponentType> SupportedComponentTypes { get; } =
[
ComponentType.ActionRow,
ComponentType.Section,
ComponentType.Button,
ComponentType.MediaGallery,
ComponentType.Separator,
ComponentType.File,
ComponentType.SelectMenu,
ComponentType.TextDisplay
];
/// <inheritdoc /> /// <inheritdoc />
public ComponentType Type => ComponentType.Container; public ComponentType Type => ComponentType.Container;
/// <inheritdoc cref="IComponentContainer.MaxChildCount"/>
public const int MaxChildCount = 39;
/// <inheritdoc /> /// <inheritdoc />
public int? Id { get; set; } public int? Id { get; set; }
@@ -46,7 +62,7 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
{ {
Components = components?.ToList(); Components = components?.ToList();
} }
/// <summary> /// <summary>
/// Initializes a new <see cref="ContainerBuilder"/> from existing component. /// Initializes a new <see cref="ContainerBuilder"/> from existing component.
/// </summary> /// </summary>
@@ -107,14 +123,11 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
/// <inheritdoc cref="IMessageComponentBuilder.Build"/> /// <inheritdoc cref="IMessageComponentBuilder.Build"/>
public ContainerComponent Build() public ContainerComponent Build()
{ {
if (_components.Any(x => x Preconditions.NotNull(Components, nameof(Components));
is not ActionRowBuilder Preconditions.AtLeast(Components.Count, 1, nameof(Components.Count), "At least 1 component must be added to this container.");
and not TextDisplayBuilder
and not SectionBuilder if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
and not MediaGalleryBuilder throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
and not SeparatorBuilder
and not FileComponentBuilder))
throw new InvalidOperationException($"A container can only contain {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components.");
return new(Components.ConvertAll(x => x.Build()).ToImmutableArray(), AccentColor, IsSpoiler, Id); return new(Components.ConvertAll(x => x.Build()).ToImmutableArray(), AccentColor, IsSpoiler, Id);
} }
@@ -127,6 +140,8 @@ public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContai
IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components);
/// <inheritdoc /> /// <inheritdoc />
IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components); IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components);
/// <inheritdoc/>
int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(ContainerBuilder)}: {this.ComponentCount()} child components."; private string DebuggerDisplay => $"{nameof(ContainerBuilder)}: {this.ComponentCount()} child components.";
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
namespace Discord; namespace Discord;
@@ -7,6 +8,16 @@ namespace Discord;
/// </summary> /// </summary>
public interface IComponentContainer public interface IComponentContainer
{ {
/// <summary>
/// Gets the types of child components supported by this container.
/// </summary>
ImmutableArray<ComponentType> SupportedComponentTypes { get; }
/// <summary>
/// Gets the maximum number of components allowed in the container.
/// </summary>
int MaxChildCount { get; }
/// <summary> /// <summary>
/// Gets the components in the container. /// Gets the components in the container.
/// </summary> /// </summary>

View File

@@ -10,10 +10,14 @@ namespace Discord;
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContainer public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContainer
{ {
/// <summary> /// <inheritdoc />
/// Gets the maximum number of components allowed in this container. public ImmutableArray<ComponentType> SupportedComponentTypes { get; } =
/// </summary> [
public const int MaxComponents = 3; ComponentType.TextDisplay
];
/// <inheritdoc cref="IComponentContainer.MaxChildCount"/>
public const int MaxChildCount = 3;
/// <inheritdoc/> /// <inheritdoc/>
public ComponentType Type => ComponentType.Section; public ComponentType Type => ComponentType.Section;
@@ -45,7 +49,7 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
/// Initializes a new <see cref="SectionBuilder"/>. /// Initializes a new <see cref="SectionBuilder"/>.
/// </summary> /// </summary>
public SectionBuilder() { } public SectionBuilder() { }
/// <summary> /// <summary>
/// Initializes a new <see cref="SectionBuilder"/>. /// Initializes a new <see cref="SectionBuilder"/>.
/// </summary> /// </summary>
@@ -108,17 +112,17 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
/// <inheritdoc cref="IMessageComponentBuilder.Build"/> /// <inheritdoc cref="IMessageComponentBuilder.Build"/>
public SectionComponent Build() public SectionComponent Build()
{ {
if (_components.Count is 0 or > MaxComponents) if (_components.Count is 0 or > MaxChildCount)
throw new InvalidOperationException($"Section component can only contain {MaxComponents} child components!"); throw new InvalidOperationException($"Section component can only contain {MaxChildCount} child components.");
if (_components.Any(x => x is not TextDisplayBuilder)) if (Components.Any(x => !SupportedComponentTypes.Contains(x.Type)))
throw new InvalidOperationException($"Section component can only contain {nameof(TextDisplayBuilder)}!"); throw new InvalidOperationException($"This component container only supports components of types: {string.Join(", ", SupportedComponentTypes)}");
if (Accessory is null) if (Accessory is null)
throw new ArgumentNullException(nameof(Accessory), "A section must have an accessory"); throw new ArgumentNullException(nameof(Accessory), "A section must have an accessory.");
if (Accessory is not ButtonBuilder and not ThumbnailBuilder) if (Accessory is not ButtonBuilder and not ThumbnailBuilder)
throw new InvalidOperationException($"Accessory component can only be {nameof(ButtonBuilder)} or {nameof(ThumbnailBuilder)}!"); throw new InvalidOperationException($"Accessory component can only be {nameof(ButtonBuilder)} or {nameof(ThumbnailBuilder)}.");
return new(Id, Components.Select(x => x.Build()).ToImmutableArray(), Accessory?.Build()); return new(Id, Components.Select(x => x.Build()).ToImmutableArray(), Accessory?.Build());
} }
@@ -131,6 +135,7 @@ public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContaine
IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components);
/// <inheritdoc/> /// <inheritdoc/>
IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components.ToList()); IComponentContainer IComponentContainer.WithComponents(IEnumerable<IMessageComponentBuilder> components) => WithComponents(components.ToList());
/// <inheritdoc/>
int IComponentContainer.MaxChildCount => MaxChildCount;
private string DebuggerDisplay => $"{nameof(SectionBuilder)}: {this.ComponentCount()} child components."; private string DebuggerDisplay => $"{nameof(SectionBuilder)}: {this.ComponentCount()} child components.";
} }