Repository: slack-go/slack
Pull request #669 (2020-02-28 00:58:38)

Support views interactions
Adding support for view interactions as well as input, file, and multi select block elements

We already reviewed this in
https://github.com/kanata2/slack/pull/1


Patch:
diff --git a/block.go b/block.go
index 32ff260..dbc3449 100644
--- a/block.go
+++ b/block.go
@@ -14,6 +14,7 @@ const (
 	MBTImage   MessageBlockType = "image"
 	MBTAction  MessageBlockType = "actions"
 	MBTContext MessageBlockType = "context"
+	MBTFile    MessageBlockType = "file"
 	MBTInput   MessageBlockType = "input"
 )
 
diff --git a/block_conv.go b/block_conv.go
index ce48ce1..43c0c96 100644
--- a/block_conv.go
+++ b/block_conv.go
@@ -56,20 +56,16 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
 			block = &ContextBlock{}
 		case "divider":
 			block = &DividerBlock{}
+		case "file":
+			block = &FileBlock{}
 		case "image":
 			block = &ImageBlock{}
 		case "input":
 			block = &InputBlock{}
 		case "section":
 			block = &SectionBlock{}
-		case "rich_text":
-			// for now ignore the (complex) content of rich_text blocks until we can fully support it
-			continue
-		case "file":
-			// for now ignore the file blocks until we can fully support it
-			continue
 		default:
-			return errors.New("unsupported block type")
+			block = &UnknownBlock{}
 		}
 
 		err = json.Unmarshal(r, block)
@@ -253,12 +249,36 @@ func (a *Accessory) UnmarshalJSON(data []byte) error {
 			return err
 		}
 		a.DatePickerElement = element.(*DatePickerBlockElement)
-	case "static_select":
+	case "plain_text_input":
+		element, err := unmarshalBlockElement(r, &PlainTextInputBlockElement{})
+		if err != nil {
+			return err
+		}
+		a.PlainTextInputElement = element.(*PlainTextInputBlockElement)
+	case "radio_buttons":
+		element, err := unmarshalBlockElement(r, &RadioButtonsBlockElement{})
+		if err != nil {
+			return err
+		}
+		a.RadioButtonsElement = element.(*RadioButtonsBlockElement)
+	case "static_select", "external_select", "users_select", "conversations_select", "channels_select":
 		element, err := unmarshalBlockElement(r, &SelectBlockElement{})
 		if err != nil {
 			return err
 		}
 		a.SelectElement = element.(*SelectBlockElement)
+	case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select":
+		element, err := unmarshalBlockElement(r, &MultiSelectBlockElement{})
+		if err != nil {
+			return err
+		}
+		a.MultiSelectElement = element.(*MultiSelectBlockElement)
+	default:
+		element, err := unmarshalBlockElement(r, &UnknownBlockElement{})
+		if err != nil {
+			return err
+		}
+		a.UnknownElement = element.(*UnknownBlockElement)
 	}
 
 	return nil
@@ -285,9 +305,18 @@ func toBlockElement(element *Accessory) BlockElement {
 	if element.DatePickerElement != nil {
 		return element.DatePickerElement
 	}
+	if element.PlainTextInputElement != nil {
+		return element.PlainTextInputElement
+	}
+	if element.RadioButtonsElement != nil {
+		return element.RadioButtonsElement
+	}
 	if element.SelectElement != nil {
 		return element.SelectElement
 	}
+	if element.MultiSelectElement != nil {
+		return element.MultiSelectElement
+	}
 
 	return nil
 }
diff --git a/block_element.go b/block_element.go
index e0a7bf9..8460e95 100644
--- a/block_element.go
+++ b/block_element.go
@@ -8,6 +8,7 @@ const (
 	METOverflow       MessageElementType = "overflow"
 	METDatepicker     MessageElementType = "datepicker"
 	METPlainTextInput MessageElementType = "plain_text_input"
+	METRadioButtons   MessageElementType = "radio_buttons"
 
 	MixedElementImage MixedElementType = "mixed_image"
 	MixedElementText  MixedElementType = "mixed_text"
@@ -17,6 +18,12 @@ const (
 	OptTypeUser          string = "users_select"
 	OptTypeConversations string = "conversations_select"
 	OptTypeChannels      string = "channels_select"
+
+	MultiOptTypeStatic        string = "multi_static_select"
+	MultiOptTypeExternal      string = "multi_external_select"
+	MultiOptTypeUser          string = "multi_users_select"
+	MultiOptTypeConversations string = "multi_conversations_select"
+	MultiOptTypeChannels      string = "multi_channels_select"
 )
 
 type MessageElementType string
@@ -32,11 +39,15 @@ type MixedElement interface {
 }
 
 type Accessory struct {
-	ImageElement      *ImageBlockElement
-	ButtonElement     *ButtonBlockElement
-	OverflowElement   *OverflowBlockElement
-	DatePickerElement *DatePickerBlockElement
-	SelectElement     *SelectBlockElement
+	ImageElement          *ImageBlockElement
+	ButtonElement         *ButtonBlockElement
+	OverflowElement       *OverflowBlockElement
+	DatePickerElement     *DatePickerBlockElement
+	PlainTextInputElement *PlainTextInputBlockElement
+	RadioButtonsElement   *RadioButtonsBlockElement
+	SelectElement         *SelectBlockElement
+	MultiSelectElement    *MultiSelectBlockElement
+	UnknownElement        *UnknownBlockElement
 }
 
 // NewAccessory returns a new Accessory for a given block element
@@ -50,11 +61,17 @@ func NewAccessory(element BlockElement) *Accessory {
 		return &Accessory{OverflowElement: element.(*OverflowBlockElement)}
 	case *DatePickerBlockElement:
 		return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)}
+	case *PlainTextInputBlockElement:
+		return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)}
+	case *RadioButtonsBlockElement:
+		return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)}
 	case *SelectBlockElement:
 		return &Accessory{SelectElement: element.(*SelectBlockElement)}
+	case *MultiSelectBlockElement:
+		return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)}
+	default:
+		return &Accessory{UnknownElement: element.(*UnknownBlockElement)}
 	}
-
-	return nil
 }
 
 // BlockElements is a convenience struct defined to allow dynamic unmarshalling of
@@ -63,6 +80,20 @@ type BlockElements struct {
 	ElementSet []BlockElement `json:"elements,omitempty"`
 }
 
+// UnknownBlockElement any block element that this library does not directly support.
+// See the "Rich Elements" section at the following URL:
+// https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less
+// New block element types may be introduced by Slack at any time; this is a catch-all for any such block elements.
+type UnknownBlockElement struct {
+	Type     MessageElementType `json:"type"`
+	Elements BlockElements
+}
+
+// ElementType returns the type of the Element
+func (s UnknownBlockElement) ElementType() MessageElementType {
+	return s.Type
+}
+
 // ImageBlockElement An element to insert an image - this element can be used
 // in section and context blocks only. If you want a block with only an image
 // in it, you're looking for the image block.
@@ -135,6 +166,20 @@ func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *Butto
 	}
 }
 
+// OptionsResponse defines the response used for select block typahead.
+//
+// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select
+type OptionsResponse struct {
+	Options []*OptionBlockObject `json:"options,omitempty"`
+}
+
+// OptionGroupsResponse defines the response used for select block typahead.
+//
+// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select
+type OptionGroupsResponse struct {
+	OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"`
+}
+
 // SelectBlockElement defines the simplest form of select menu, with a static list
 // of options passed in when defining the element.
 //
@@ -149,7 +194,7 @@ type SelectBlockElement struct {
 	InitialUser         string                    `json:"initial_user,omitempty"`
 	InitialConversation string                    `json:"initial_conversation,omitempty"`
 	InitialChannel      string                    `json:"initial_channel,omitempty"`
-	MinQueryLength      int                       `json:"min_query_length,omitempty"`
+	MinQueryLength      *int                      `json:"min_query_length,omitempty"`
 	Confirm             *ConfirmationBlockObject  `json:"confirm,omitempty"`
 }
 
@@ -185,6 +230,56 @@ func NewOptionsGroupSelectBlockElement(
 	}
 }
 
+// MultiSelectBlockElement defines a multiselect menu, with a static list
+// of options passed in when defining the element.
+//
+// More Information: https://api.slack.com/reference/messaging/block-elements#multi_select
+type MultiSelectBlockElement struct {
+	Type                 string                    `json:"type,omitempty"`
+	Placeholder          *TextBlockObject          `json:"placeholder,omitempty"`
+	ActionID             string                    `json:"action_id,omitempty"`
+	Options              []*OptionBlockObject      `json:"options,omitempty"`
+	OptionGroups         []*OptionGroupBlockObject `json:"option_groups,omitempty"`
+	InitialOptions       []*OptionBlockObject      `json:"initial_options,omitempty"`
+	InitialUsers         []string                  `json:"initial_users,omitempty"`
+	InitialConversations []string                  `json:"initial_conversations,omitempty"`
+	InitialChannels      []string                  `json:"initial_channels,omitempty"`
+	MinQueryLength       *int                      `json:"min_query_length,omitempty"`
+	Confirm              *ConfirmationBlockObject  `json:"confirm,omitempty"`
+}
+
+// ElementType returns the type of the Element
+func (s MultiSelectBlockElement) ElementType() MessageElementType {
+	return MessageElementType(s.Type)
+}
+
+// NewOptionsMultiSelectBlockElement returns a new instance of SelectBlockElement for use with
+// the Options object only.
+func NewOptionsMultiSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *MultiSelectBlockElement {
+	return &MultiSelectBlockElement{
+		Type:        optType,
+		Placeholder: placeholder,
+		ActionID:    actionID,
+		Options:     options,
+	}
+}
+
+// NewOptionsGroupMultiSelectBlockElement returns a new instance of MultiSelectBlockElement for use with
+// the Options object only.
+func NewOptionsGroupMultiSelectBlockElement(
+	optType string,
+	placeholder *TextBlockObject,
+	actionID string,
+	optGroups ...*OptionGroupBlockObject,
+) *MultiSelectBlockElement {
+	return &MultiSelectBlockElement{
+		Type:         optType,
+		Placeholder:  placeholder,
+		ActionID:     actionID,
+		OptionGroups: optGroups,
+	}
+}
+
 // OverflowBlockElement defines the fields needed to use an overflow element.
 // And Overflow Element is like a cross between a button and a select menu -
 // when a user clicks on this overflow button, they will be presented with a
@@ -238,10 +333,11 @@ func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement {
 	}
 }
 
-// PlainTextInputBlockElement creates a field where a user can enter freeform data.
+// PlainTextInputBlockElement creates a field where a user can enter freeform
+// data.
 // Plain-text input elements are currently only available in modals.
 //
-// More Information: https://api.slack.com/reference/messaging/block-elements#input
+// More Information: https://api.slack.com/reference/block-kit/block-elements#input
 type PlainTextInputBlockElement struct {
 	Type         MessageElementType `json:"type"`
 	ActionID     string             `json:"action_id"`
@@ -257,7 +353,8 @@ func (s PlainTextInputBlockElement) ElementType() MessageElementType {
 	return s.Type
 }
 
-// NewPlainTextInputBlockElement returns an instance of a plain-text input element
+// NewPlainTextInputBlockElement returns an instance of a plain-text input
+// element
 func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string) *PlainTextInputBlockElement {
 	return &PlainTextInputBlockElement{
 		Type:        METPlainTextInput,
@@ -265,3 +362,29 @@ func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string
 		Placeholder: placeholder,
 	}
 }
+
+// RadioButtonsBlockElement defines an element which lets users choose one item
+// from a list of possible options.
+//
+// More Information: https://api.slack.com/reference/block-kit/block-elements#radio
+type RadioButtonsBlockElement struct {
+	Type          MessageElementType       `json:"type"`
+	ActionID      string                   `json:"action_id"`
+	Options       []*OptionBlockObject     `json:"options"`
+	InitialOption *OptionBlockObject       `json:"initial_option,omitempty"`
+	Confirm       *ConfirmationBlockObject `json:"confirm,omitempty"`
+}
+
+// ElementType returns the type of the Element
+func (s RadioButtonsBlockElement) ElementType() MessageElementType {
+	return s.Type
+}
+
+// NewRadioButtonsBlockElement returns an instance of a radio buttons element.
+func NewRadioButtonsBlockElement(actionID string, options ...*OptionBlockObject) *RadioButtonsBlockElement {
+	return &RadioButtonsBlockElement{
+		Type:     METRadioButtons,
+		ActionID: actionID,
+		Options:  options,
+	}
+}
diff --git a/block_file.go b/block_file.go
new file mode 100644
index 0000000..ac4453f
--- /dev/null
+++ b/block_file.go
@@ -0,0 +1,26 @@
+package slack
+
+// FileBlock defines data that is used to display a remote file.
+//
+// More Information: https://api.slack.com/reference/block-kit/blocks#file
+type FileBlock struct {
+	Type       MessageBlockType `json:"type"`
+	BlockID    string           `json:"block_id,omitempty"`
+	ExternalID string           `json:"external_id"`
+	Source     string           `json:"source"`
+}
+
+// BlockType returns the type of the block
+func (s FileBlock) BlockType() MessageBlockType {
+	return s.Type
+}
+
+// NewFileBlock returns a new instance of a file block
+func NewFileBlock(blockID string, externalID string, source string) *FileBlock {
+	return &FileBlock{
+		Type:       MBTFile,
+		BlockID:    blockID,
+		ExternalID: externalID,
+		Source:     source,
+	}
+}
diff --git a/block_input.go b/block_input.go
index 9d08203..10638cd 100644
--- a/block_input.go
+++ b/block_input.go
@@ -1,10 +1,8 @@
 package slack
 
-// InputBlock defines data that is used to collect information from users -
-// it can hold a plain-text input element, a select menu element,
-// a multi-select menu element, or a datepicker.
+// InputBlock defines data that is used to display user input fields.
 //
-// More Information: https://api.slack.com/reference/messaging/blocks#input
+// More Information: https://api.slack.com/reference/block-kit/blocks#input
 type InputBlock struct {
 	Type     MessageBlockType `json:"type"`
 	BlockID  string           `json:"block_id,omitempty"`
@@ -19,7 +17,7 @@ func (s InputBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
-// NewInputBlock returns a new instance of an Input Block
+// NewInputBlock returns a new instance of an input block
 func NewInputBlock(blockID string, label *TextBlockObject, element BlockElement) *InputBlock {
 	return &InputBlock{
 		Type:    MBTInput,
diff --git a/block_unknown.go b/block_unknown.go
new file mode 100644
index 0000000..97054c7
--- /dev/null
+++ b/block_unknown.go
@@ -0,0 +1,13 @@
+package slack
+
+// UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing
+// new and unknown block types that break this library.
+type UnknownBlock struct {
+	Type    MessageBlockType `json:"type"`
+	BlockID string           `json:"block_id,omitempty"`
+}
+
+// BlockType returns the type of the block
+func (b UnknownBlock) BlockType() MessageBlockType {
+	return b.Type
+}
diff --git a/go.sum b/go.sum
index 7a0ae46..0068bd4 100644
--- a/go.sum
+++ b/go.sum
@@ -9,4 +9,4 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
\ No newline at end of file
diff --git a/interactions.go b/interactions.go
index 26a8b6d..c56a34f 100644
--- a/interactions.go
+++ b/interactions.go
@@ -24,6 +24,9 @@ const (
 	InteractionTypeInteractionMessage = InteractionType("interactive_message")
 	InteractionTypeMessageAction      = InteractionType("message_action")
 	InteractionTypeBlockActions       = InteractionType("block_actions")
+	InteractionTypeBlockSuggestion    = InteractionType("block_suggestion")
+	InteractionTypeViewSubmission     = InteractionType("view_submission")
+	InteractionTypeViewClosed         = InteractionType("view_closed")
 )
 
 // InteractionCallback is sent from slack when a user interactions with a button or dialog.
@@ -44,8 +47,19 @@ type InteractionCallback struct {
 	MessageTs       string          `json:"message_ts"`
 	AttachmentID    string          `json:"attachment_id"`
 	ActionCallback  ActionCallbacks `json:"actions"`
+	View            View            `json:"view"`
+	ActionID        string          `json:"action_id"`
 	APIAppID        string          `json:"api_app_id"`
+	BlockID         string          `json:"block_id"`
+	Container       Container       `json:"container"`
 	DialogSubmissionCallback
+	ViewSubmissionCallback
+	ViewClosedCallback
+}
+
+type Container struct {
+	Type   string `json:"type"`
+	ViewID string `json:"view_id"`
 }
 
 // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of
diff --git a/views.go b/views.go
index afe391b..c34feec 100644
--- a/views.go
+++ b/views.go
@@ -12,6 +12,10 @@ const (
 
 type ViewType string
 
+type ViewState struct {
+	Values map[string]map[string]BlockAction `json:"values"`
+}
+
 type View struct {
 	SlackResponse
 	ID              string           `json:"id"`
@@ -23,7 +27,7 @@ type View struct {
 	Blocks          Blocks           `json:"blocks"`
 	PrivateMetadata string           `json:"private_metadata"`
 	CallbackID      string           `json:"callback_id"`
-	State           interface{}      `json:"state"`
+	State           *ViewState       `json:"state"`
 	Hash            string           `json:"hash"`
 	ClearOnClose    bool             `json:"clear_on_close"`
 	NotifyOnClose   bool             `json:"notify_on_close"`
@@ -34,17 +38,67 @@ type View struct {
 	BotID           string           `json:"bot_id"`
 }
 
+type ViewSubmissionCallback struct {
+	Hash string `json:"hash"`
+}
+
+type ViewClosedCallback struct {
+	IsCleared bool `json:"is_cleared"`
+}
+
+const (
+	RAClear  ViewResponseAction = "clear"
+	RAUpdate ViewResponseAction = "update"
+	RAPush   ViewResponseAction = "push"
+	RAErrors ViewResponseAction = "errors"
+)
+
+type ViewResponseAction string
+
+type ViewSubmissionResponse struct {
+	ResponseAction ViewResponseAction `json:"response_action"`
+	View           *ModalViewRequest  `json:"view,omitempty"`
+	Errors         map[string]string  `json:"errors,omitempty"`
+}
+
+func NewClearViewSubmissionResponse() *ViewSubmissionResponse {
+	return &ViewSubmissionResponse{
+		ResponseAction: RAClear,
+	}
+}
+
+func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
+	return &ViewSubmissionResponse{
+		ResponseAction: RAUpdate,
+		View:           view,
+	}
+}
+
+func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
+	return &ViewSubmissionResponse{
+		ResponseAction: RAPush,
+		View:           view,
+	}
+}
+
+func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionResponse {
+	return &ViewSubmissionResponse{
+		ResponseAction: RAErrors,
+		Errors:         errors,
+	}
+}
+
 type ModalViewRequest struct {
 	Type            ViewType         `json:"type"`
 	Title           *TextBlockObject `json:"title"`
 	Blocks          Blocks           `json:"blocks"`
-	Close           *TextBlockObject `json:"close"`
-	Submit          *TextBlockObject `json:"submit"`
-	PrivateMetadata string           `json:"private_metadata"`
-	CallbackID      string           `json:"callback_id"`
-	ClearOnClose    bool             `json:"clear_on_close"`
-	NotifyOnClose   bool             `json:"notify_on_close"`
-	ExternalID      string           `json:"external_id"`
+	Close           *TextBlockObject `json:"close,omitempty"`
+	Submit          *TextBlockObject `json:"submit,omitempty"`
+	PrivateMetadata string           `json:"private_metadata,omitempty"`
+	CallbackID      string           `json:"callback_id,omitempty"`
+	ClearOnClose    bool             `json:"clear_on_close,omitempty"`
+	NotifyOnClose   bool             `json:"notify_on_close,omitempty"`
+	ExternalID      string           `json:"external_id,omitempty"`
 }
 
 func (v *ModalViewRequest) ViewType() ViewType {
@@ -54,9 +108,9 @@ func (v *ModalViewRequest) ViewType() ViewType {
 type HomeTabViewRequest struct {
 	Type            ViewType `json:"type"`
 	Blocks          Blocks   `json:"blocks"`
-	PrivateMetadata string   `json:"private_metadata"`
-	CallbackID      string   `json:"callback_id"`
-	ExternalID      string   `json:"external_id"`
+	PrivateMetadata string   `json:"private_metadata,omitempty"`
+	CallbackID      string   `json:"callback_id,omitempty"`
+	ExternalID      string   `json:"external_id,omitempty"`
 }
 
 func (v *HomeTabViewRequest) ViewType() ViewType {
@@ -71,7 +125,7 @@ type openViewRequest struct {
 type publishViewRequest struct {
 	UserID string             `json:"user_id"`
 	View   HomeTabViewRequest `json:"view"`
-	Hash   string             `json:"hash"`
+	Hash   string             `json:"hash,omitempty"`
 }
 
 type pushViewRequest struct {
@@ -81,9 +135,9 @@ type pushViewRequest struct {
 
 type updateViewRequest struct {
 	View       ModalViewRequest `json:"view"`
-	ExternalID string           `json:"external_id"`
-	Hash       string           `json:"hash"`
-	ViewID     string           `json:"view_id"`
+	ExternalID string           `json:"external_id,omitempty"`
+	Hash       string           `json:"hash,omitempty"`
+	ViewID     string           `json:"view_id,omitempty"`
 }
 
 type ViewResponse struct {


Test patch:
diff --git a/block_element_test.go b/block_element_test.go
index d101f10..651930c 100644
--- a/block_element_test.go
+++ b/block_element_test.go
@@ -55,6 +55,32 @@ func TestNewOptionsGroupSelectBlockElement(t *testing.T) {
 
 }
 
+func TestNewOptionsMultiSelectBlockElement(t *testing.T) {
+
+	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
+	testOption := NewOptionBlockObject("test", testOptionText)
+
+	option := NewOptionsMultiSelectBlockElement("static_select", nil, "test", testOption)
+	assert.Equal(t, option.Type, "static_select")
+	assert.Equal(t, len(option.Options), 1)
+	assert.Nil(t, option.OptionGroups)
+
+}
+
+func TestNewOptionsGroupMultiSelectBlockElement(t *testing.T) {
+
+	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
+	testOption := NewOptionBlockObject("test", testOptionText)
+	testLabel := NewTextBlockObject("plain_text", "Test Label", false, false)
+	testGroupOption := NewOptionGroupBlockElement(testLabel, testOption)
+
+	optGroup := NewOptionsGroupMultiSelectBlockElement("static_select", nil, "test", testGroupOption)
+
+	assert.Equal(t, string(optGroup.Type), "static_select")
+	assert.Equal(t, optGroup.ActionID, "test")
+	assert.Equal(t, len(optGroup.OptionGroups), 1)
+
+}
 func TestNewOverflowBlockElement(t *testing.T) {
 
 	// Build Text Objects associated with each option
@@ -86,8 +112,31 @@ func TestNewDatePickerBlockElement(t *testing.T) {
 }
 
 func TestNewPlainTextInputBlockElement(t *testing.T) {
-	inputElement := NewPlainTextInputBlockElement(nil, "test")
 
-	assert.Equal(t, string(inputElement.Type), "plain_text_input")
-	assert.Equal(t, inputElement.ActionID, "test")
+	plainTextInputElemnet := NewPlainTextInputBlockElement(nil, "test")
+
+	assert.Equal(t, string(plainTextInputElemnet.Type), "plain_text_input")
+	assert.Equal(t, plainTextInputElemnet.ActionID, "test")
+
+}
+
+func TestNewRadioButtonsBlockElement(t *testing.T) {
+
+	// Build Text Objects associated with each option
+	radioButtonsOptionTextOne := NewTextBlockObject("plain_text", "Option One", false, false)
+	radioButtonsOptionTextTwo := NewTextBlockObject("plain_text", "Option Two", false, false)
+	radioButtonsOptionTextThree := NewTextBlockObject("plain_text", "Option Three", false, false)
+
+	// Build each option, providing a value for the option
+	radioButtonsOptionOne := NewOptionBlockObject("value-0", radioButtonsOptionTextOne)
+	radioButtonsOptionTwo := NewOptionBlockObject("value-1", radioButtonsOptionTextTwo)
+	radioButtonsOptionThree := NewOptionBlockObject("value-2", radioButtonsOptionTextThree)
+
+	// Build overflow section
+	radioButtonsElement := NewRadioButtonsBlockElement("test", radioButtonsOptionOne, radioButtonsOptionTwo, radioButtonsOptionThree)
+
+	assert.Equal(t, string(radioButtonsElement.Type), "radio_buttons")
+	assert.Equal(t, radioButtonsElement.ActionID, "test")
+	assert.Equal(t, len(radioButtonsElement.Options), 3)
+
 }
diff --git a/block_file_test.go b/block_file_test.go
new file mode 100644
index 0000000..c51a93e
--- /dev/null
+++ b/block_file_test.go
@@ -0,0 +1,15 @@
+package slack
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewFileBlock(t *testing.T) {
+	fileBlock := NewFileBlock("test", "external_id", "source")
+	assert.Equal(t, string(fileBlock.Type), "file")
+	assert.Equal(t, fileBlock.BlockID, "test")
+	assert.Equal(t, fileBlock.ExternalID, "external_id")
+	assert.Equal(t, fileBlock.Source, "source")
+}
diff --git a/block_input_test.go b/block_input_test.go
index ab94590..10c3081 100644
--- a/block_input_test.go
+++ b/block_input_test.go
@@ -7,11 +7,12 @@ import (
 )
 
 func TestNewInputBlock(t *testing.T) {
-	label := NewTextBlockObject("plain_text", "Input", false, false)
-	inputElement := NewPlainTextInputBlockElement(nil, "input_123")
+	label := NewTextBlockObject("plain_text", "label", false, false)
+	element := NewDatePickerBlockElement("action_id")
 
-	inputBlock := NewInputBlock("test", label, inputElement)
+	inputBlock := NewInputBlock("test", label, element)
 	assert.Equal(t, string(inputBlock.Type), "input")
 	assert.Equal(t, inputBlock.BlockID, "test")
-	assert.Equal(t, string(inputBlock.Element.ElementType()), "plain_text_input")
+	assert.Equal(t, inputBlock.Label, label)
+	assert.Equal(t, inputBlock.Element, element)
 }
diff --git a/interactions_test.go b/interactions_test.go
index b1ad45a..f9e549d 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -9,34 +9,115 @@ import (
 
 const (
 	dialogSubmissionCallback = `{
-  "type": "dialog_submission",
-  "submission": {
-      "name": "Sigourney Dreamweaver",
-      "email": "sigdre@example.com",
-      "phone": "+1 800-555-1212",
-      "meal": "burrito",
-      "comment": "No sour cream please",
-      "team_channel": "C0LFFBKPB",
-      "who_should_sing": "U0MJRG1AL"
-  },
-  "callback_id": "employee_offsite_1138b",
-  "team": {
-      "id": "T1ABCD2E12",
-      "domain": "coverbands"
-  },
-  "user": {
-      "id": "W12A3BCDEF",
-      "name": "dreamweaver"
-  },
-  "channel": {
-      "id": "C1AB2C3DE",
-      "name": "coverthon-1999"
-  },
-  "action_ts": "936893340.702759",
-  "token": "M1AqUUw3FqayAbqNtsGMch72",
-  "response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ"
-}`
-	actionCallback = `{}`
+		"type": "dialog_submission",
+		"submission": {
+			"name": "Sigourney Dreamweaver",
+			"email": "sigdre@example.com",
+			"phone": "+1 800-555-1212",
+			"meal": "burrito",
+			"comment": "No sour cream please",
+			"team_channel": "C0LFFBKPB",
+			"who_should_sing": "U0MJRG1AL"
+		},
+		"callback_id": "employee_offsite_1138b",
+		"team": {
+			"id": "T1ABCD2E12",
+			"domain": "coverbands"
+		},
+		"user": {
+			"id": "W12A3BCDEF",
+			"name": "dreamweaver"
+		},
+		"channel": {
+			"id": "C1AB2C3DE",
+			"name": "coverthon-1999"
+		},
+		"action_ts": "936893340.702759",
+		"token": "M1AqUUw3FqayAbqNtsGMch72",
+		"response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ"
+	}`
+	actionCallback     = `{}`
+	viewClosedCallback = `{
+		"type": "view_closed",
+		"team": {
+			"id": "T1ABCD2E12",
+			"domain": "coverbands"
+		},
+		"user": {
+			"id": "W12A3BCDEF",
+			"name": "dreamweaver"
+		},
+		"view": {
+			"type": "modal",
+			"title": {
+				"type": "plain_text",
+				"text": "launch project"
+			},
+			"blocks": [{
+				"type": "section",
+				"text": {
+				  "text": "*Sally* has requested you set the deadline for the Nano launch project",
+				  "type": "mrkdwn"
+				},
+				"accessory": {
+				  "type": "datepicker",
+				  "action_id": "datepicker123",
+				  "initial_date": "1990-04-28",
+				  "placeholder": {
+					"type": "plain_text",
+					"text": "Select a date"
+				  }
+				}
+			}]
+		},
+		"api_app_id": "A123ABC",
+		"is_cleared": false
+	}`
+	viewSubmissionCallback = `{
+		"type": "view_submission",
+		"team": {
+			"id": "T1ABCD2E12",
+			"domain": "coverbands"
+		},
+		"user": {
+			"id": "W12A3BCDEF",
+			"name": "dreamweaver"
+		},
+		"channel": {
+			"id": "C1AB2C3DE",
+			"name": "coverthon-1999"
+		},
+		"view": {
+			"type": "modal",
+			"title": {
+				"type": "plain_text",
+				"text": "meal choice"
+			},
+			"blocks": [{
+				"type": "input",
+				"block_id": "multi-line",
+				"label": {
+					"type": "plain_text",
+					"text": "dietary restrictions"
+				},
+				"element": {
+					"type": "plain_text_input",
+					"multiline": true,
+					"action_id": "ml-value"
+				}
+			}],
+			"state": {
+				"values": {
+					"multi-line": {
+						"ml-value": {
+							"type": "plain_text_input",
+							"value": "No onions"
+						}
+					}
+				}
+			}
+		}
+	}`
 )
 
 func assertInteractionCallback(t *testing.T, callback InteractionCallback, encoded string) {
@@ -84,6 +165,89 @@ func TestActionCallback(t *testing.T) {
 	assertInteractionCallback(t, InteractionCallback{}, actionCallback)
 }
 
+func TestViewClosedck(t *testing.T) {
+	expected := InteractionCallback{
+		Type: InteractionTypeViewClosed,
+		Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"},
+		User: User{
+			ID:   "W12A3BCDEF",
+			Name: "dreamweaver",
+		},
+		View: View{
+			Type:  VTModal,
+			Title: NewTextBlockObject("plain_text", "launch project", false, false),
+			Blocks: Blocks{
+				BlockSet: []Block{
+					NewSectionBlock(
+						NewTextBlockObject("mrkdwn", "*Sally* has requested you set the deadline for the Nano launch project", false, false),
+						nil,
+						NewAccessory(&DatePickerBlockElement{
+							Type:        METDatepicker,
+							ActionID:    "datepicker123",
+							InitialDate: "1990-04-28",
+							Placeholder: NewTextBlockObject("plain_text", "Select a date", false, false),
+						}),
+					),
+				},
+			},
+		},
+		APIAppID: "A123ABC",
+	}
+	assertInteractionCallback(t, expected, viewClosedCallback)
+}
+
+func TestViewSubmissionCallback(t *testing.T) {
+	expected := InteractionCallback{
+		Type: InteractionTypeViewSubmission,
+		Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"},
+		Channel: Channel{
+			GroupConversation: GroupConversation{
+				Conversation: Conversation{
+					ID: "C1AB2C3DE",
+				},
+				Name: "coverthon-1999",
+			},
+		},
+		User: User{
+			ID:   "W12A3BCDEF",
+			Name: "dreamweaver",
+		},
+		View: View{
+			Type:  VTModal,
+			Title: NewTextBlockObject("plain_text", "meal choice", false, false),
+			Blocks: Blocks{
+				BlockSet: []Block{
+					NewInputBlock(
+						"multi-line",
+						NewTextBlockObject(
+							"plain_text",
+							"dietary restrictions",
+							false,
+							false,
+						),
+						&PlainTextInputBlockElement{
+							Type:      "plain_text_input",
+							ActionID:  "ml-value",
+							Multiline: true,
+						},
+					),
+				},
+			},
+			State: &ViewState{
+				Values: map[string]map[string]BlockAction{
+					"multi-line": map[string]BlockAction{
+						"ml-value": BlockAction{
+							Type:  "plain_text_input",
+							Value: "No onions",
+						},
+					},
+				},
+			},
+		},
+	}
+	assertInteractionCallback(t, expected, viewSubmissionCallback)
+}
+
 func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 	cb := &InteractionCallback{
 		Type:        InteractionTypeBlockActions,
@@ -92,14 +256,33 @@ func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 		ResponseURL: "responseURL",
 		TriggerID:   "triggerID",
 		ActionTs:    "actionTS",
-		Team:        Team{ID: "teamid", Name: "teamname"},
-		Channel: Channel{GroupConversation: GroupConversation{
-			Name: "channelname", Conversation: Conversation{ID: "channelid"}}},
-		User: User{ID: "userid", Name: "username",
-			Profile: UserProfile{RealName: "userrealname"}},
-		OriginalMessage: Message{Msg: Msg{Text: "ogmsg text",
-			Timestamp: "ogmsg ts"}},
-		Message:      Message{Msg: Msg{Text: "text", Timestamp: "ts"}},
+		Team: Team{
+			ID:   "teamid",
+			Name: "teamname",
+		},
+		Channel: Channel{
+			GroupConversation: GroupConversation{
+				Name:         "channelname",
+				Conversation: Conversation{ID: "channelid"},
+			},
+		},
+		User: User{
+			ID:      "userid",
+			Name:    "username",
+			Profile: UserProfile{RealName: "userrealname"},
+		},
+		OriginalMessage: Message{
+			Msg: Msg{
+				Text:      "ogmsg text",
+				Timestamp: "ogmsg ts",
+			},
+		},
+		Message: Message{
+			Msg: Msg{
+				Text:      "text",
+				Timestamp: "ts",
+			},
+		},
 		Name:         "name",
 		Value:        "value",
 		MessageTs:    "messageTs",
@@ -108,6 +291,13 @@ func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 			AttachmentActions: []*AttachmentAction{{Value: "value"}},
 			BlockActions:      []*BlockAction{{ActionID: "id123"}},
 		},
+		View: View{
+			Type:  VTModal,
+			Title: NewTextBlockObject("plain_text", "title", false, false),
+			Blocks: Blocks{
+				BlockSet: []Block{NewDividerBlock()},
+			},
+		},
 		DialogSubmissionCallback: DialogSubmissionCallback{State: "dsstate"},
 	}
 
@@ -145,6 +335,9 @@ func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 		len(jsonCB.ActionCallback.AttachmentActions))
 	assert.Equal(t, len(cb.ActionCallback.BlockActions),
 		len(jsonCB.ActionCallback.BlockActions))
+	assert.Equal(t, cb.View.Type, jsonCB.View.Type)
+	assert.Equal(t, cb.View.Title, jsonCB.View.Title)
+	assert.Equal(t, cb.View.Blocks, jsonCB.View.Blocks)
 	assert.Equal(t, cb.DialogSubmissionCallback.State,
 		jsonCB.DialogSubmissionCallback.State)
 }
diff --git a/views_test.go b/views_test.go
index 561c8ce..ce91da3 100644
--- a/views_test.go
+++ b/views_test.go
@@ -1,10 +1,12 @@
 package slack
 
 import (
+	"encoding/json"
 	"net/http"
 	"testing"
 
 	"github.com/slack-go/slack/internal/errorsx"
+	"github.com/stretchr/testify/assert"
 )
 
 var dummySlackErr = errorsx.String("dummy_error_from_slack")
@@ -94,7 +96,7 @@ func TestSlack_OpenView(t *testing.T) {
 					"callback_id": "identify_your_modals",
 					"external_id": "",
 					"state": {
-						"values": []
+						"values": {}
 					},
 					"hash": "156772938.1827394",
 					"clear_on_close": false,
@@ -211,7 +213,7 @@ func TestSlack_View_PublishView(t *testing.T) {
 					"private_metadata": "Shh it is a secret",
 					"callback_id": "identify_your_home_tab",
 					"state": {
-						"values": []
+						"values": {}
 					},
 					"hash": "156772938.1827394",
 					"clear_on_close": false,
@@ -342,7 +344,7 @@ func TestSlack_PushView(t *testing.T) {
 					"callback_id": "identify_your_modals",
 					"external_id": "",
 					"state": {
-						"values": []
+						"values": {}
 					},
 					"hash": "156772938.1827394",
 					"clear_on_close": false,
@@ -475,7 +477,7 @@ func TestSlack_UpdateView(t *testing.T) {
 					"callback_id": "identify_your_modals",
 					"external_id": "",
 					"state": {
-						"values": []
+						"values": {}
 					},
 					"hash": "156772938.1827394",
 					"clear_on_close": false,
@@ -527,3 +529,107 @@ func TestSlack_UpdateView(t *testing.T) {
 		})
 	}
 }
+
+func assertViewSubmissionResponse(t *testing.T, resp *ViewSubmissionResponse, encoded string) {
+	var decoded *ViewSubmissionResponse
+	assert.Nil(t, json.Unmarshal([]byte(encoded), &decoded))
+	assert.Equal(t, decoded, resp)
+}
+
+func TestSlack_ClearViewSubmissionResponse(t *testing.T) {
+	resp := NewClearViewSubmissionResponse()
+	rawResp := `{
+		"response_action": "clear"
+	}`
+
+	assertViewSubmissionResponse(t, resp, rawResp)
+}
+
+func TestSlack_UpdateViewSubmissionResponse(t *testing.T) {
+	resp := NewUpdateViewSubmissionResponse(&ModalViewRequest{
+		Type:   VTModal,
+		Title:  NewTextBlockObject("plain_text", "Test update view submission response", false, false),
+		Blocks: Blocks{BlockSet: []Block{NewFileBlock("file_block_id", "external_string", "source_string")}},
+	})
+	rawResp := `{
+		"response_action": "update",
+		"view": {
+			"type": "modal",
+			"title": {
+				"type": "plain_text",
+				"text": "Test update view submission response"
+			},
+			"blocks": [
+				{
+					"type": "file",
+					"block_id": "file_block_id",
+					"external_id": "external_string",
+					"source": "source_string"
+				}
+			]
+		}
+	}`
+
+	assertViewSubmissionResponse(t, resp, rawResp)
+}
+
+func TestSlack_PushViewSubmissionResponse(t *testing.T) {
+	resp := NewPushViewSubmissionResponse(&ModalViewRequest{
+		Type:  VTModal,
+		Title: NewTextBlockObject("plain_text", "Test update view submission response", false, false),
+		Blocks: Blocks{
+			BlockSet: []Block{
+				NewContextBlock(
+					"context_block_id",
+					NewTextBlockObject("plain_text", "Context text", false, false),
+					NewImageBlockElement("image_url", "alt_text"),
+				),
+			},
+		},
+	})
+	rawResp := `{
+		"response_action": "push",
+		"view": {
+			"type": "modal",
+			"title": {
+				"type": "plain_text",
+				"text": "Test update view submission response"
+			},
+			"blocks": [
+				{
+					"type": "context",
+					"block_id": "context_block_id",
+					"elements": [
+						{
+							"type": "plain_text",
+							"text": "Context text"
+						},
+						{
+							"type": "image",
+							"image_url": "image_url",
+							"alt_text": "alt_text"
+						}
+					]
+				}
+			]
+		}
+	}`
+
+	assertViewSubmissionResponse(t, resp, rawResp)
+}
+
+func TestSlack_ErrorsViewSubmissionResponse(t *testing.T) {
+	resp := NewErrorsViewSubmissionResponse(map[string]string{
+		"input_text_action_id": "Please input a name that's at least 6 characters long",
+		"file_action_id":       "File exceeded size limit of 5 KB",
+	})
+	rawResp := `{
+		"response_action": "errors",
+		"errors": {
+			"input_text_action_id": "Please input a name that's at least 6 characters long",
+			"file_action_id": "File exceeded size limit of 5 KB"
+		}
+	}`
+
+	assertViewSubmissionResponse(t, resp, rawResp)
+}