[Feature] Select menu default values (#2776)

* initial commit

* docs & readonly
This commit is contained in:
Mihail Gribkov
2023-11-18 23:44:16 +03:00
committed by GitHub
parent 8060dcf4ae
commit ac274d4b76
15 changed files with 302 additions and 8 deletions

View File

@@ -1,4 +1,5 @@
using Discord.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -129,7 +130,8 @@ namespace Discord
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns></returns>
public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null)
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu,
ChannelType[] channelTypes = null, SelectMenuDefaultValue[] defaultValues = null)
{
return WithSelectMenu(new SelectMenuBuilder()
.WithCustomId(customId)
@@ -139,7 +141,8 @@ namespace Discord
.WithMinValues(minValues)
.WithDisabled(disabled)
.WithType(type)
.WithChannelTypes(channelTypes),
.WithChannelTypes(channelTypes)
.WithDefaultValues(defaultValues),
row);
}
@@ -891,12 +894,25 @@ namespace Discord
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }
public List<SelectMenuDefaultValue> DefaultValues
{
get => _defaultValues;
set
{
if (value != null)
Preconditions.AtMost(value.Count, MaxOptionCount, nameof(DefaultValues));
_defaultValues = value;
}
}
private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>();
private int _minValues = 1;
private int _maxValues = 1;
private string _placeholder;
private string _customId;
private ComponentType _type = ComponentType.SelectMenu;
private List<SelectMenuDefaultValue> _defaultValues = new();
/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
@@ -916,6 +932,7 @@ namespace Discord
Options = selectMenu.Options?
.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault))
.ToList();
DefaultValues = selectMenu.DefaultValues?.ToList();
}
/// <summary>
@@ -929,7 +946,8 @@ namespace Discord
/// <param name="isDisabled">Disabled this select menu or not.</param>
/// <param name="type">The <see cref="ComponentType"/> of this select menu.</param>
/// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param>
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null)
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1,
bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null, List<SelectMenuDefaultValue> defaultValues = null)
{
CustomId = customId;
Options = options;
@@ -939,6 +957,7 @@ namespace Discord
MinValues = minValues;
Type = type;
ChannelTypes = channelTypes ?? new();
DefaultValues = defaultValues ?? new();
}
/// <summary>
@@ -1046,6 +1065,48 @@ namespace Discord
return this;
}
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="id">The id of an entity to add.</param>
/// <param name="type">The type of an entity to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(ulong id, SelectDefaultValueType type)
=> AddDefaultValue(new(id, type));
/// <summary>
/// Add one default value to menu options.
/// </summary>
/// <param name="value">The default value to add.</param>
/// <exception cref="InvalidOperationException">Default values count reached <see cref="MaxOptionCount"/>.</exception>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder AddDefaultValue(SelectMenuDefaultValue value)
{
if (DefaultValues.Count >= MaxOptionCount)
throw new InvalidOperationException($"Options count reached {MaxOptionCount}.");
DefaultValues.Add(value);
return this;
}
/// <summary>
/// Sets the field default values.
/// </summary>
/// <param name="defaultValues">The value to set the field default values to.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithDefaultValues(params SelectMenuDefaultValue[] defaultValues)
{
DefaultValues = defaultValues.ToList();
return this;
}
/// <summary>
/// Sets whether the current menu is disabled.
/// </summary>
@@ -1108,7 +1169,7 @@ namespace Discord
{
var options = Options?.Select(x => x.Build()).ToList();
return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes);
return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues);
}
}

View File

@@ -0,0 +1,22 @@
namespace Discord;
/// <summary>
/// Type of a <see cref="SelectDefaultValueType" />.
/// </summary>
public enum SelectDefaultValueType
{
/// <summary>
/// The select menu default value is a user.
/// </summary>
User = 0,
/// <summary>
/// The select menu default value is a role.
/// </summary>
Role = 1,
/// <summary>
/// The select menu default value is a channel.
/// </summary>
Channel = 2
}

View File

@@ -45,6 +45,11 @@ namespace Discord
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }
/// <summary>
/// Gets default values for auto-populated select menu components.
/// </summary>
public IReadOnlyCollection<SelectMenuDefaultValue> DefaultValues { get; }
/// <summary>
/// Turns this select menu into a builder.
/// </summary>
@@ -58,9 +63,11 @@ namespace Discord
Placeholder,
MaxValues,
MinValues,
IsDisabled, Type, ChannelTypes.ToList());
IsDisabled, Type, ChannelTypes.ToList(),
DefaultValues.ToList());
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null)
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues,
bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null, IEnumerable<SelectMenuDefaultValue> defaultValues = null)
{
CustomId = customId;
Options = options;
@@ -70,6 +77,7 @@ namespace Discord
IsDisabled = disabled;
Type = type;
ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>();
DefaultValues = defaultValues?.ToArray() ?? Array.Empty<SelectMenuDefaultValue>();
}
}
}

View File

@@ -0,0 +1,46 @@
namespace Discord;
/// <summary>
/// Represents a default value of an auto-populated select menu.
/// </summary>
public readonly struct SelectMenuDefaultValue
{
/// <summary>
/// Gets the id of entity this default value refers to.
/// </summary>
public ulong Id { get; }
/// <summary>
/// Gets the type of this default value.
/// </summary>
public SelectDefaultValueType Type { get; }
/// <summary>
/// Creates a new default value.
/// </summary>
/// <param name="id">Id of the target object.</param>
/// <param name="type">Type of the target entity.</param>
public SelectMenuDefaultValue(ulong id, SelectDefaultValueType type)
{
Id = id;
Type = type;
}
/// <summary>
/// Creates a new default value from a <see cref="IChannel"/>.
/// </summary>
public static SelectMenuDefaultValue FromChannel(IChannel channel)
=> new(channel.Id, SelectDefaultValueType.Channel);
/// <summary>
/// Creates a new default value from a <see cref="IRole"/>.
/// </summary>
public static SelectMenuDefaultValue FromRole(IRole role)
=> new(role.Id, SelectDefaultValueType.Role);
/// <summary>
/// Creates a new default value from a <see cref="IUser"/>.
/// </summary>
public static SelectMenuDefaultValue FromUser(IUser user)
=> new(user.Id, SelectDefaultValueType.User);
}

View File

@@ -8,6 +8,11 @@ namespace Discord
/// </summary>
public interface IUserMessage : IMessage
{
/// <summary>
/// Gets the resolved data if the message has components. <see langword="null"/> otherwise.
/// </summary>
MessageResolvedData ResolvedData { get; }
/// <summary>
/// Gets the referenced message if it is a crosspost, channel follow add, pin, or reply message.
/// </summary>

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Discord;
public class MessageResolvedData
{
/// <summary>
/// Gets a collection of <see cref="IUser"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IUser> Users { get; }
/// <summary>
/// Gets a collection of <see cref="IGuildUser"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IGuildUser> Members { get; }
/// <summary>
/// Gets a collection of <see cref="IRole"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IRole> Roles { get; }
/// <summary>
/// Gets a collection of <see cref="IChannel"/> resolved in the message.
/// </summary>
public IReadOnlyCollection<IChannel> Channels { get; }
internal MessageResolvedData(IReadOnlyCollection<IUser> users, IReadOnlyCollection<IGuildUser> members, IReadOnlyCollection<IRole> roles, IReadOnlyCollection<IChannel> channels)
{
Users = users;
Members = members;
Roles = roles;
Channels = channels;
}
}

View File

@@ -67,5 +67,8 @@ namespace Discord.API
[JsonProperty("thread")]
public Optional<Channel> Thread { get; set; }
[JsonProperty("resolved")]
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; }
}
}

View File

@@ -34,6 +34,10 @@ namespace Discord.API
[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
[JsonProperty("default_values")]
public Optional<SelectMenuDefaultValue[]> DefaultValues { get; set; }
public SelectMenuComponent() { }
public SelectMenuComponent(Discord.SelectMenuComponent component)
@@ -46,6 +50,7 @@ namespace Discord.API
MaxValues = component.MaxValues;
Disabled = component.IsDisabled;
ChannelTypes = component.ChannelTypes.ToArray();
DefaultValues = component.DefaultValues.Select(x => new SelectMenuDefaultValue {Id = x.Id, Type = x.Type}).ToArray();
}
}
}

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class SelectMenuDefaultValue
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("type")]
public SelectDefaultValueType Type { get; set; }
}

View File

@@ -203,7 +203,10 @@ namespace Discord.Rest
parsed.MaxValues,
parsed.Disabled,
parsed.Type,
parsed.ChannelTypes.GetValueOrDefault()
parsed.ChannelTypes.GetValueOrDefault(),
parsed.DefaultValues.IsSpecified
? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type))
: Array.Empty<SelectMenuDefaultValue>()
);
}
default:

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Message;
@@ -47,6 +48,9 @@ namespace Discord.Rest
/// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage;
/// <inheritdoc />
public MessageResolvedData ResolvedData { get; internal set; }
internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
: base(discord, id, channel, author, source)
{
@@ -130,6 +134,34 @@ namespace Discord.Rest
else
_stickers = ImmutableArray.Create<StickerItem>();
}
if (model.Resolved.IsSpecified)
{
var users = model.Resolved.Value.Users.IsSpecified
? model.Resolved.Value.Users.Value.Select(x => RestUser.Create(Discord, x.Value)).ToImmutableArray()
: ImmutableArray<RestUser>.Empty;
var members = model.Resolved.Value.Members.IsSpecified
? model.Resolved.Value.Members.Value.Select(x =>
{
x.Value.User = model.Resolved.Value.Users.Value.TryGetValue(x.Key, out var user)
? user
: null;
return RestGuildUser.Create(Discord, guild, x.Value, guildId);
}).ToImmutableArray()
: ImmutableArray<RestGuildUser>.Empty;
var roles = model.Resolved.Value.Roles.IsSpecified
? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray()
: ImmutableArray<RestRole>.Empty;
var channels = model.Resolved.Value.Channels.IsSpecified
? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray()
: ImmutableArray<RestChannel>.Empty;
ResolvedData = new MessageResolvedData(users, members, roles, channels);
}
}
/// <inheritdoc />

View File

@@ -79,6 +79,8 @@ namespace Discord.Net.Converters
return UserStatusConverter.Instance;
if (type == typeof(EmbedType))
return EmbedTypeConverter.Instance;
if (type == typeof(SelectDefaultValueType))
return SelectMenuDefaultValueTypeConverter.Instance;
//Special
if (type == typeof(API.Image))

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System;
using System.Globalization;
namespace Discord.Net.Converters;
internal class SelectMenuDefaultValueTypeConverter : JsonConverter
{
public static readonly SelectMenuDefaultValueTypeConverter Instance = new ();
public override bool CanConvert(Type objectType) => true;
public override bool CanRead => true;
public override bool CanWrite => true;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return Enum.TryParse<SelectDefaultValueType>((string)reader.Value, true, out var result)
? result
: null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((SelectDefaultValueType)value).ToString().ToLower(CultureInfo.InvariantCulture));
}
}

View File

@@ -237,7 +237,10 @@ namespace Discord.WebSocket
parsed.MaxValues,
parsed.Disabled,
parsed.Type,
parsed.ChannelTypes.GetValueOrDefault()
parsed.ChannelTypes.GetValueOrDefault(),
parsed.DefaultValues.IsSpecified
? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type))
: Array.Empty<SelectMenuDefaultValue>()
);
}
default:

View File

@@ -49,6 +49,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage;
/// <inheritdoc />
public MessageResolvedData ResolvedData { get; internal set; }
internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
: base(discord, id, channel, author, source)
{
@@ -172,6 +175,34 @@ namespace Discord.WebSocket
else
_stickers = ImmutableArray.Create<SocketSticker>();
}
if (model.Resolved.IsSpecified)
{
var users = model.Resolved.Value.Users.IsSpecified
? model.Resolved.Value.Users.Value.Select(x => RestUser.Create(Discord, x.Value)).ToImmutableArray()
: ImmutableArray<RestUser>.Empty;
var members = model.Resolved.Value.Members.IsSpecified
? model.Resolved.Value.Members.Value.Select(x =>
{
x.Value.User = model.Resolved.Value.Users.Value.TryGetValue(x.Key, out var user)
? user
: null;
return RestGuildUser.Create(Discord, guild, x.Value);
}).ToImmutableArray()
: ImmutableArray<RestGuildUser>.Empty;
var roles = model.Resolved.Value.Roles.IsSpecified
? model.Resolved.Value.Roles.Value.Select(x => RestRole.Create(Discord, guild, x.Value)).ToImmutableArray()
: ImmutableArray<RestRole>.Empty;
var channels = model.Resolved.Value.Channels.IsSpecified
? model.Resolved.Value.Channels.Value.Select(x => RestChannel.Create(Discord, x.Value, guild)).ToImmutableArray()
: ImmutableArray<RestChannel>.Empty;
ResolvedData = new MessageResolvedData(users, members, roles, channels);
}
}
/// <inheritdoc />