Message Poll UX, and model

This commit is contained in:
Valere 2020-01-03 01:22:05 +01:00 committed by Benoit Marty
parent 7c5bb4ff5b
commit a0aebed3f7
21 changed files with 502 additions and 7 deletions

View file

@ -26,4 +26,6 @@ object RelationType {
const val REPLACE = "m.replace"
/** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference"
/** Lets you define an event which references an existing event.*/
const val RESPONSE = "m.response"
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
enum class OptionsType(val value: String) {
POLL("m.pool"),
BUTTONS("m.buttons"),
}
/**
* Polls and bot buttons are m.room.message events with a msgtype of m.options,
*/
@JsonClass(generateAdapter = true)
data class MessageOptionsContent(
@Json(name = "msgtype") override val type: String,
@Json(name = "type") val optionType: String? = null,
@Json(name = "body") override val body: String,
@Json(name = "label") val label: String?,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "options") val options: List<OptionItems>? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent
@JsonClass(generateAdapter = true)
data class OptionItems(
@Json(name = "label") val label: String?,
@Json(name = "value") val value: String?
)
@JsonClass(generateAdapter = true)
data class MessagePollResponseContent(
@Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_RESPONSE,
@Json(name = "body") override val body: String,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent

View file

@ -25,6 +25,9 @@ object MessageType {
const val MSGTYPE_VIDEO = "m.video"
const val MSGTYPE_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_OPTIONS = "m.options"
const val MSGTYPE_RESPONSE = "m.response"
const val MSGTYPE_POLL_CLOSED = "m.poll_closed"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field

View file

@ -25,5 +25,6 @@ data class ReactionInfo(
@Json(name = "event_id") override val eventId: String,
val key: String,
// always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null
) : RelationContent

View file

@ -23,4 +23,5 @@ interface RelationContent {
val type: String?
val eventId: String?
val inReplyTo: ReplyToContent?
val option: Int?
}

View file

@ -22,5 +22,6 @@ import com.squareup.moshi.JsonClass
data class RelationDefaultContent(
@Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String?,
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null
) : RelationContent

View file

@ -61,6 +61,13 @@ interface SendService {
*/
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
/**
* Method to send a list of media asynchronously.
* @param attachments the list of media to send
* @return a [Cancelable]
*/
fun sendPollReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable
/**
* Redacts (delete) the given event.
* @param event The event to redact

View file

@ -46,6 +46,7 @@ object MoshiProvider {
.registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO)
.registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION)
.registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE)
.registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS)
.registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST)
)
.add(SerializeNulls.JSON_ADAPTER_FACTORY)

View file

@ -84,6 +84,13 @@ internal class DefaultSendService @AssistedInject constructor(
return sendEvent(event)
}
override fun sendPollReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
val event = localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, optionIndex, optionValue).also {
saveLocalEcho(it)
}
return sendEvent(event)
}
private fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(roomId)) {

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
@ -132,6 +133,21 @@ internal class LocalEchoEventFactory @Inject constructor(
))
}
fun createPollReplyEvent(roomId: String,
pollEventId: String,
optionIndex: Int,
optionLabel: String): Event {
return createEvent(roomId,
MessagePollResponseContent(
body = optionLabel,
relatesTo = RelationDefaultContent(
type = RelationType.RESPONSE,
option = optionIndex,
eventId = pollEventId)
))
}
fun createReplaceTextOfReply(roomId: String,
eventReplaced: TimelineEvent,
originalEvent: TimelineEvent,

View file

@ -53,6 +53,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
data class ReplyToPoll(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()
data class ReportContent(
val eventId: String,
val senderId: String?,

View file

@ -199,6 +199,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToPoll -> replyToPoll(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
@ -855,6 +856,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
private fun replyToPoll(action: RoomDetailAction.ReplyToPoll) {
room.sendPollReply(action.eventId, action.optionIndex, action.optionValue)
}
private fun observeSyncState() {
session.rx()
.liveSyncState()

View file

@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageEmoteConte
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
@ -57,7 +58,6 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
@ -137,10 +137,21 @@ class MessageItemFactory @Inject constructor(
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildPollMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
}
}
private fun buildPollMessageItem(messageContent: MessageOptionsContent, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
return MessagePollItem_()
.attributes(attributes)
.callback(callback)
.informationData(informationData)
.leftGuideline(avatarSizeProvider.leftGuideline)
.optionsContent(messageContent)
.highlighted(highlight)
}
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
@ -228,9 +239,10 @@ class MessageItemFactory @Inject constructor(
private fun buildNotHandledMessageItem(messageContent: MessageContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem? {
val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.msgType)
return defaultItemFactory.create(text, informationData, highlight, callback)
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
// For compatibility reason we should display the body
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
private fun buildImageMessageItem(messageContent: MessageImageInfoContent,

View file

@ -0,0 +1,131 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.room.detail.RoomDetailAction
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
@EpoxyAttribute
var optionsContent: MessageOptionsContent? = null
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var informationData: MessageInformationData? = null
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
holder.pollId = informationData?.eventId
holder.callback = callback
holder.optionValues = optionsContent?.options?.map { it.value ?: it.label }
renderSendState(holder.view, holder.labelText)
holder.labelText.setTextOrHide(optionsContent?.label)
val buttons = listOf(holder.button1, holder.button2, holder.button3, holder.button4, holder.button5)
buttons.forEach { it.isVisible = false }
optionsContent?.options?.forEachIndexed { index, item ->
if (index < buttons.size) {
buttons[index].let {
it.text = item.label
it.isVisible = true
}
}
}
val resultLines = listOf(holder.result1, holder.result2, holder.result3, holder.result4, holder.result5)
resultLines.forEach { it.isVisible = false }
optionsContent?.options?.forEachIndexed { index, item ->
if (index < resultLines.size) {
resultLines[index].let {
it.label = item.label
it.optionSelected = index == 0
it.percent = "20%"
it.isVisible = true
}
}
}
holder.infoText.text = holder.view.context.resources.getQuantityString(R.plurals.poll_info, 0, 0)
}
override fun unbind(holder: Holder) {
holder.pollId = null
holder.callback = null
holder.optionValues = null
super.unbind(holder)
}
class Holder : AbsMessageItem.Holder(STUB_ID) {
var pollId: String? = null
var optionValues : List<String?>? = null
var callback: TimelineEventController.Callback? = null
val button1 by bind<Button>(R.id.pollButton1)
val button2 by bind<Button>(R.id.pollButton2)
val button3 by bind<Button>(R.id.pollButton3)
val button4 by bind<Button>(R.id.pollButton4)
val button5 by bind<Button>(R.id.pollButton5)
val result1 by bind<PollResultLineView>(R.id.pollResult1)
val result2 by bind<PollResultLineView>(R.id.pollResult2)
val result3 by bind<PollResultLineView>(R.id.pollResult3)
val result4 by bind<PollResultLineView>(R.id.pollResult4)
val result5 by bind<PollResultLineView>(R.id.pollResult5)
val labelText by bind<TextView>(R.id.pollLabelText)
val infoText by bind<TextView>(R.id.pollInfosText)
override fun bindView(itemView: View) {
super.bindView(itemView)
val buttons = listOf(button1, button2, button3, button4, button5)
val clickListener = DebouncedClickListener(View.OnClickListener {
val optionIndex = buttons.indexOf(it)
if (optionIndex != -1 && pollId != null) {
val compatValue = if (optionIndex < optionValues?.size ?: 0) optionValues?.get(optionIndex) else null
callback?.onAction(RoomDetailAction.ReplyToPoll(pollId!!, optionIndex, compatValue ?: "$optionIndex"))
}
})
buttons.forEach { it.setOnClickListener(clickListener) }
}
}
companion object {
private const val STUB_ID = R.id.messagePollStub
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.item
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
class PollResultLineView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
@BindView(R.id.pollResultItemLabel)
lateinit var labelTextView: TextView
@BindView(R.id.pollResultItemPercent)
lateinit var percentTextView: TextView
@BindView(R.id.pollResultItemSelectedIcon)
lateinit var selectedIcon: ImageView
var label: String? = null
set(value) {
field = value
labelTextView.setTextOrHide(value)
}
var percent: String? = null
set(value) {
field = value
percentTextView.setTextOrHide(value)
}
var optionSelected: Boolean = false
set(value) {
field = value
selectedIcon.visibility = if (value) View.VISIBLE else View.INVISIBLE
}
init {
inflate(context, R.layout.item_timeline_event_poll_result_item, this)
orientation = HORIZONTAL
ButterKnife.bind(this)
val typedArray = context.obtainStyledAttributes(attrs,
R.styleable.PollResultLineView, 0, 0)
label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: ""
percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: ""
optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false)
}
}

View file

@ -105,6 +105,13 @@
android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messagePollStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_poll_stub" />
</FrameLayout>
<LinearLayout

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<ImageView
android:id="@+id/pollResultItemSelectedIcon"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:layout_gravity="center_vertical"
android:paddingEnd="2dp"
android:src="@drawable/ic_check_white_24dp"
android:tint="?riotx_text_secondary"
android:contentDescription="@string/poll_item_selected_aria" />
<TextView
android:id="@+id/pollResultItemLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="Open a Github Issue" />
<TextView
android:id="@+id/pollResultItemPercent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="47%" />
</merge>

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:id="@+id/pollLabelText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="What would you like to do?" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pollButton1"
style="@style/Style.Vector.Poll.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Create Github issue" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pollButton2"
style="@style/Style.Vector.Poll.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Search Github" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pollButton3"
style="@style/Style.Vector.Poll.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Logout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pollButton4"
style="@style/Style.Vector.Poll.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="Option 4" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pollButton5"
style="@style/Style.Vector.Poll.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="Option 5" />
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
android:id="@+id/pollResult1"
android:layout_width="match_parent"
android:layout_height="40dp"
tools:optionName="Create Github issue"
tools:optionCount="40%"
tools:optionSelected="true"
/>
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
android:id="@+id/pollResult2"
android:layout_width="match_parent"
android:layout_height="40dp"
tools:optionName="Search Github"
tools:optionCount="60%"
tools:optionSelected="false"
/>
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
android:id="@+id/pollResult3"
android:layout_width="match_parent"
android:layout_height="40dp"
tools:optionName="Logout"
tools:optionCount="0%"
tools:optionSelected="false"
/>
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
android:id="@+id/pollResult4"
android:layout_width="match_parent"
android:layout_height="40dp"
tools:optionName="Option 4"
tools:optionCount="0%"
tools:optionSelected="false"
/>
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
android:id="@+id/pollResult5"
android:layout_width="match_parent"
android:layout_height="40dp"
tools:optionName="Option 5"
tools:optionCount="0%"
tools:optionSelected="false"
/>
<TextView
android:id="@+id/pollInfosText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_secondary"
android:gravity="start"
android:textSize="12sp"
tools:text="12 votes - Final Results" />
</LinearLayout>

View file

@ -97,4 +97,11 @@
<attr name="riotx_highlighted_message_background" format="reference" />
</declare-styleable>
<declare-styleable name="PollResultLineView">
<attr name="optionName" format="string" localization="suggested" />
<attr name="optionCount" format="string" />
<attr name="optionSelected" format="boolean" />
</declare-styleable>
</resources>

View file

@ -6,7 +6,15 @@
<!-- Sections has been created to avoid merge conflict. Let's see if it's better -->
<!-- BEGIN Strings added by Valere -->
<plurals name="poll_info">
<item quantity="zero">%d vote</item>
<item quantity="other">%d votes</item>
</plurals>
<plurals name="poll_info_final">
<item quantity="zero">%d vote - Final results</item>
<item quantity="other">%d votes - Final results</item>
</plurals>
<string name="poll_item_selected_aria">Selected Option</string>
<!-- END Strings added by Valere -->

View file

@ -183,6 +183,14 @@
<item name="colorControlHighlight">@android:color/white</item>
</style>
<style name="Style.Vector.Poll.Button" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:minHeight">44dp</item>
<item name="android:textAllCaps">false</item>
<item name="cornerRadius">10dp</item>
</style>
<style name="VectorSearchView" parent="Widget.AppCompat.SearchView">
<item name="searchIcon">@drawable/ic_search</item>
<item name="closeIcon">@drawable/ic_x_green</item>