mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-12 18:02:59 +00:00
discord: Add new select components
This commit is contained in:
parent
e5aabda660
commit
1b31249626
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,8 +18,12 @@ const (
|
||||||
_ ComponentType = iota
|
_ ComponentType = iota
|
||||||
ActionRowComponentType
|
ActionRowComponentType
|
||||||
ButtonComponentType
|
ButtonComponentType
|
||||||
SelectComponentType
|
StringSelectComponentType
|
||||||
TextInputComponentType
|
TextInputComponentType
|
||||||
|
UserSelectComponentType
|
||||||
|
RoleSelectComponentType
|
||||||
|
MentionableSelectComponentType
|
||||||
|
ChannelSelectComponentType
|
||||||
)
|
)
|
||||||
|
|
||||||
// String formats Type's name as a string.
|
// String formats Type's name as a string.
|
||||||
|
@ -30,10 +33,18 @@ func (t ComponentType) String() string {
|
||||||
return "ActionRow"
|
return "ActionRow"
|
||||||
case ButtonComponentType:
|
case ButtonComponentType:
|
||||||
return "Button"
|
return "Button"
|
||||||
case SelectComponentType:
|
case StringSelectComponentType:
|
||||||
return "Select"
|
return "StringSelect"
|
||||||
case TextInputComponentType:
|
case TextInputComponentType:
|
||||||
return "TextInput"
|
return "TextInput"
|
||||||
|
case UserSelectComponentType:
|
||||||
|
return "User"
|
||||||
|
case RoleSelectComponentType:
|
||||||
|
return "Role"
|
||||||
|
case MentionableSelectComponentType:
|
||||||
|
return "Mentionable"
|
||||||
|
case ChannelSelectComponentType:
|
||||||
|
return "Channel"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("ComponentType(%d)", int(t))
|
return fmt.Sprintf("ComponentType(%d)", int(t))
|
||||||
}
|
}
|
||||||
|
@ -142,8 +153,8 @@ func (c *ContainerComponents) Unmarshal(v interface{}) error {
|
||||||
|
|
||||||
switch component := component.(type) {
|
switch component := component.(type) {
|
||||||
case *TextInputComponent:
|
case *TextInputComponent:
|
||||||
v = component.Value.Val
|
v = component.Value
|
||||||
case *SelectComponent:
|
case *StringSelectComponent:
|
||||||
switch len(component.Options) {
|
switch len(component.Options) {
|
||||||
case 0:
|
case 0:
|
||||||
// ok
|
// ok
|
||||||
|
@ -186,7 +197,7 @@ func (c *ContainerComponents) Unmarshal(v interface{}) error {
|
||||||
switch elemt.Kind() {
|
switch elemt.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
switch component := component.(type) {
|
switch component := component.(type) {
|
||||||
case *SelectComponent:
|
case *StringSelectComponent:
|
||||||
fieldv.Set(reflect.MakeSlice(fieldt, len(component.Options), len(component.Options)))
|
fieldv.Set(reflect.MakeSlice(fieldt, len(component.Options), len(component.Options)))
|
||||||
for i, option := range component.Options {
|
for i, option := range component.Options {
|
||||||
fieldv.Index(i).Set(reflect.ValueOf(option.Value).Convert(elemt))
|
fieldv.Index(i).Set(reflect.ValueOf(option.Value).Convert(elemt))
|
||||||
|
@ -241,6 +252,10 @@ func (c *ContainerComponents) UnmarshalJSON(b []byte) error {
|
||||||
// - *ButtonComponent
|
// - *ButtonComponent
|
||||||
// - *SelectComponent
|
// - *SelectComponent
|
||||||
// - *TextInputComponent
|
// - *TextInputComponent
|
||||||
|
// - *UserSelectComponent
|
||||||
|
// - *RoleSelectComponent
|
||||||
|
// - *MentionableSelectComponent
|
||||||
|
// - *ChannelSelectComponent
|
||||||
//
|
//
|
||||||
type Component interface {
|
type Component interface {
|
||||||
// Type returns the type of the underlying component.
|
// Type returns the type of the underlying component.
|
||||||
|
@ -257,6 +272,10 @@ type Component interface {
|
||||||
// - *ButtonComponent
|
// - *ButtonComponent
|
||||||
// - *SelectComponent
|
// - *SelectComponent
|
||||||
// - *TextInputComponent
|
// - *TextInputComponent
|
||||||
|
// - *UserSelectComponent
|
||||||
|
// - *RoleSelectComponent
|
||||||
|
// - *MentionableSelectComponent
|
||||||
|
// - *ChannelSelectComponent
|
||||||
//
|
//
|
||||||
type InteractiveComponent interface {
|
type InteractiveComponent interface {
|
||||||
Component
|
Component
|
||||||
|
@ -296,8 +315,8 @@ func ParseComponent(b []byte) (Component, error) {
|
||||||
c = &ActionRowComponent{}
|
c = &ActionRowComponent{}
|
||||||
case ButtonComponentType:
|
case ButtonComponentType:
|
||||||
c = &ButtonComponent{}
|
c = &ButtonComponent{}
|
||||||
case SelectComponentType:
|
case StringSelectComponentType:
|
||||||
c = &SelectComponent{}
|
c = &StringSelectComponent{}
|
||||||
case TextInputComponentType:
|
case TextInputComponentType:
|
||||||
c = &TextInputComponent{}
|
c = &TextInputComponent{}
|
||||||
default:
|
default:
|
||||||
|
@ -562,9 +581,9 @@ func (b *ButtonComponent) UnmarshalJSON(j []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select is a clickable button that may be added to an interaction
|
// StringSelectComponent is a clickable button that may be added to an interaction
|
||||||
// response.
|
// response.
|
||||||
type SelectComponent struct {
|
type StringSelectComponent struct {
|
||||||
// Options are the choices in the select.
|
// Options are the choices in the select.
|
||||||
Options []SelectOption `json:"options"`
|
Options []SelectOption `json:"options"`
|
||||||
// CustomID is the custom unique ID.
|
// CustomID is the custom unique ID.
|
||||||
|
@ -595,19 +614,19 @@ type SelectOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID implements the Component interface.
|
// ID implements the Component interface.
|
||||||
func (s *SelectComponent) ID() ComponentID { return s.CustomID }
|
func (s *StringSelectComponent) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
// Type implements the Component interface.
|
// Type implements the Component interface.
|
||||||
func (s *SelectComponent) Type() ComponentType {
|
func (s *StringSelectComponent) Type() ComponentType {
|
||||||
return SelectComponentType
|
return StringSelectComponentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SelectComponent) _cmp() {}
|
func (s *StringSelectComponent) _cmp() {}
|
||||||
func (s *SelectComponent) _icp() {}
|
func (s *StringSelectComponent) _icp() {}
|
||||||
|
|
||||||
// MarshalJSON marshals the select in the format Discord expects.
|
// MarshalJSON marshals the select in the format Discord expects.
|
||||||
func (s *SelectComponent) MarshalJSON() ([]byte, error) {
|
func (s *StringSelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
type sel SelectComponent
|
type sel StringSelectComponent
|
||||||
|
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
Type ComponentType `json:"type"`
|
Type ComponentType `json:"type"`
|
||||||
|
@ -617,7 +636,7 @@ func (s *SelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := Msg{
|
msg := Msg{
|
||||||
Type: SelectComponentType,
|
Type: StringSelectComponentType,
|
||||||
sel: (*sel)(s),
|
sel: (*sel)(s),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,9 +673,9 @@ type TextInputComponent struct {
|
||||||
// Required dictates whether or not the user must fill out the component
|
// Required dictates whether or not the user must fill out the component
|
||||||
Required bool `json:"required"`
|
Required bool `json:"required"`
|
||||||
// Value is the pre-filled value of this component (max 4000 chars)
|
// Value is the pre-filled value of this component (max 4000 chars)
|
||||||
Value option.NullableString `json:"value,omitempty"`
|
Value string `json:"value,omitempty"`
|
||||||
// Placeholder is the text that appears when the input is empty (max 100 chars)
|
// Placeholder is the text that appears when the input is empty (max 100 chars)
|
||||||
Placeholder option.NullableString `json:"placeholder,omitempty"`
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TextInputComponent) _cmp() {}
|
func (s *TextInputComponent) _cmp() {}
|
||||||
|
@ -695,6 +714,212 @@ func (i *TextInputComponent) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(m)
|
return json.Marshal(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserSelectComponent struct {
|
||||||
|
// CustomID is the custom unique ID.
|
||||||
|
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||||
|
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||||
|
// 100 characters.
|
||||||
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
|
// ValueLimits is the minimum and maximum number of items that can be
|
||||||
|
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||||
|
ValueLimits [2]int `json:"-"`
|
||||||
|
// Disabled disables the select if true.
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements the Component interface.
|
||||||
|
func (s *UserSelectComponent) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
|
// Type implements the Component interface.
|
||||||
|
func (s *UserSelectComponent) Type() ComponentType {
|
||||||
|
return UserSelectComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSelectComponent) _cmp() {}
|
||||||
|
func (s *UserSelectComponent) _icp() {}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the select in the format Discord expects.
|
||||||
|
func (s *UserSelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
|
type sel UserSelectComponent
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Type ComponentType `json:"type"`
|
||||||
|
*sel
|
||||||
|
MinValues *int `json:"min_values,omitempty"`
|
||||||
|
MaxValues *int `json:"max_values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := Msg{
|
||||||
|
Type: UserSelectComponentType,
|
||||||
|
sel: (*sel)(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ValueLimits != [2]int{0, 0} {
|
||||||
|
msg.MinValues = new(int)
|
||||||
|
msg.MaxValues = new(int)
|
||||||
|
|
||||||
|
*msg.MinValues = s.ValueLimits[0]
|
||||||
|
*msg.MaxValues = s.ValueLimits[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleSelectComponent struct {
|
||||||
|
// CustomID is the custom unique ID.
|
||||||
|
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||||
|
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||||
|
// 100 characters.
|
||||||
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
|
// ValueLimits is the minimum and maximum number of items that can be
|
||||||
|
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||||
|
ValueLimits [2]int `json:"-"`
|
||||||
|
// Disabled disables the select if true.
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements the Component interface.
|
||||||
|
func (s *RoleSelectComponent) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
|
// Type implements the Component interface.
|
||||||
|
func (s *RoleSelectComponent) Type() ComponentType {
|
||||||
|
return RoleSelectComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RoleSelectComponent) _cmp() {}
|
||||||
|
func (s *RoleSelectComponent) _icp() {}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the select in the format Discord expects.
|
||||||
|
func (s *RoleSelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
|
type sel RoleSelectComponent
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Type ComponentType `json:"type"`
|
||||||
|
*sel
|
||||||
|
MinValues *int `json:"min_values,omitempty"`
|
||||||
|
MaxValues *int `json:"max_values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := Msg{
|
||||||
|
Type: RoleSelectComponentType,
|
||||||
|
sel: (*sel)(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ValueLimits != [2]int{0, 0} {
|
||||||
|
msg.MinValues = new(int)
|
||||||
|
msg.MaxValues = new(int)
|
||||||
|
|
||||||
|
*msg.MinValues = s.ValueLimits[0]
|
||||||
|
*msg.MaxValues = s.ValueLimits[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MentionableSelectComponent struct {
|
||||||
|
// CustomID is the custom unique ID.
|
||||||
|
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||||
|
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||||
|
// 100 characters.
|
||||||
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
|
// ValueLimits is the minimum and maximum number of items that can be
|
||||||
|
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||||
|
ValueLimits [2]int `json:"-"`
|
||||||
|
// Disabled disables the select if true.
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements the Component interface.
|
||||||
|
func (s *MentionableSelectComponent) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
|
// Type implements the Component interface.
|
||||||
|
func (s *MentionableSelectComponent) Type() ComponentType {
|
||||||
|
return MentionableSelectComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MentionableSelectComponent) _cmp() {}
|
||||||
|
func (s *MentionableSelectComponent) _icp() {}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the select in the format Discord expects.
|
||||||
|
func (s *MentionableSelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
|
type sel MentionableSelectComponent
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Type ComponentType `json:"type"`
|
||||||
|
*sel
|
||||||
|
MinValues *int `json:"min_values,omitempty"`
|
||||||
|
MaxValues *int `json:"max_values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := Msg{
|
||||||
|
Type: MentionableSelectComponentType,
|
||||||
|
sel: (*sel)(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ValueLimits != [2]int{0, 0} {
|
||||||
|
msg.MinValues = new(int)
|
||||||
|
msg.MaxValues = new(int)
|
||||||
|
|
||||||
|
*msg.MinValues = s.ValueLimits[0]
|
||||||
|
*msg.MaxValues = s.ValueLimits[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelSelectComponent struct {
|
||||||
|
// CustomID is the custom unique ID.
|
||||||
|
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||||
|
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||||
|
// 100 characters.
|
||||||
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
|
// ValueLimits is the minimum and maximum number of items that can be
|
||||||
|
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||||
|
ValueLimits [2]int `json:"-"`
|
||||||
|
// Disabled disables the select if true.
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
// ChannelTypes is the types of channels that can be chosen from.
|
||||||
|
ChannelTypes []ChannelType `json:"channel_types,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements the Component interface.
|
||||||
|
func (s *ChannelSelectComponent) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
|
// Type implements the Component interface.
|
||||||
|
func (s *ChannelSelectComponent) Type() ComponentType {
|
||||||
|
return ChannelSelectComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChannelSelectComponent) _cmp() {}
|
||||||
|
func (s *ChannelSelectComponent) _icp() {}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the select in the format Discord expects.
|
||||||
|
func (s *ChannelSelectComponent) MarshalJSON() ([]byte, error) {
|
||||||
|
type sel ChannelSelectComponent
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Type ComponentType `json:"type"`
|
||||||
|
*sel
|
||||||
|
MinValues *int `json:"min_values,omitempty"`
|
||||||
|
MaxValues *int `json:"max_values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := Msg{
|
||||||
|
Type: ChannelSelectComponentType,
|
||||||
|
sel: (*sel)(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ValueLimits != [2]int{0, 0} {
|
||||||
|
msg.MinValues = new(int)
|
||||||
|
msg.MaxValues = new(int)
|
||||||
|
|
||||||
|
*msg.MinValues = s.ValueLimits[0]
|
||||||
|
*msg.MaxValues = s.ValueLimits[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
// Unknown is reserved for components with unknown or not yet implemented
|
// Unknown is reserved for components with unknown or not yet implemented
|
||||||
// components types. It can also be used in place of a ComponentInteraction.
|
// components types. It can also be used in place of a ComponentInteraction.
|
||||||
type UnknownComponent struct {
|
type UnknownComponent struct {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/v3/discord"
|
"github.com/diamondburned/arikawa/v3/discord"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleContainerComponents_Unmarshal() {
|
func ExampleContainerComponents_Unmarshal() {
|
||||||
|
@ -14,21 +13,21 @@ func ExampleContainerComponents_Unmarshal() {
|
||||||
&discord.ActionRowComponent{
|
&discord.ActionRowComponent{
|
||||||
&discord.TextInputComponent{
|
&discord.TextInputComponent{
|
||||||
CustomID: "text1",
|
CustomID: "text1",
|
||||||
Value: option.NewNullableString("hello"),
|
Value: "hello",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&discord.ActionRowComponent{
|
&discord.ActionRowComponent{
|
||||||
&discord.TextInputComponent{
|
&discord.TextInputComponent{
|
||||||
CustomID: "text2",
|
CustomID: "text2",
|
||||||
Value: option.NewNullableString("hello 2"),
|
Value: "hello 2",
|
||||||
},
|
},
|
||||||
&discord.TextInputComponent{
|
&discord.TextInputComponent{
|
||||||
CustomID: "text3",
|
CustomID: "text3",
|
||||||
Value: option.NewNullableString("hello 3"),
|
Value: "hello 3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&discord.ActionRowComponent{
|
&discord.ActionRowComponent{
|
||||||
&discord.SelectComponent{
|
&discord.StringSelectComponent{
|
||||||
CustomID: "select1",
|
CustomID: "select1",
|
||||||
Options: []discord.SelectOption{
|
Options: []discord.SelectOption{
|
||||||
{Value: "option 1"},
|
{Value: "option 1"},
|
||||||
|
@ -40,7 +39,7 @@ func ExampleContainerComponents_Unmarshal() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&discord.ActionRowComponent{
|
&discord.ActionRowComponent{
|
||||||
&discord.SelectComponent{
|
&discord.StringSelectComponent{
|
||||||
CustomID: "select2",
|
CustomID: "select2",
|
||||||
Options: []discord.SelectOption{
|
Options: []discord.SelectOption{
|
||||||
{Value: "option 1"},
|
{Value: "option 1"},
|
||||||
|
|
|
@ -305,7 +305,7 @@ type SelectInteraction struct {
|
||||||
func (s *SelectInteraction) ID() ComponentID { return s.CustomID }
|
func (s *SelectInteraction) ID() ComponentID { return s.CustomID }
|
||||||
|
|
||||||
// Type implements ComponentInteraction.
|
// Type implements ComponentInteraction.
|
||||||
func (s *SelectInteraction) Type() ComponentType { return SelectComponentType }
|
func (s *SelectInteraction) Type() ComponentType { return StringSelectComponentType }
|
||||||
|
|
||||||
// InteractionType implements InteractionData.
|
// InteractionType implements InteractionData.
|
||||||
func (s *SelectInteraction) InteractionType() InteractionDataType {
|
func (s *SelectInteraction) InteractionType() InteractionDataType {
|
||||||
|
@ -351,7 +351,7 @@ func ParseComponentInteraction(b []byte) (ComponentInteraction, error) {
|
||||||
switch t.Type {
|
switch t.Type {
|
||||||
case ButtonComponentType:
|
case ButtonComponentType:
|
||||||
d = &ButtonInteraction{CustomID: t.CustomID}
|
d = &ButtonInteraction{CustomID: t.CustomID}
|
||||||
case SelectComponentType:
|
case StringSelectComponentType:
|
||||||
d = &SelectInteraction{CustomID: t.CustomID}
|
d = &SelectInteraction{CustomID: t.CustomID}
|
||||||
default:
|
default:
|
||||||
d = &UnknownComponent{
|
d = &UnknownComponent{
|
||||||
|
|
Loading…
Reference in a new issue