Add m.buttons support (a.k.a bot buttons)

This commit is contained in:
Valere 2020-02-05 14:46:32 +01:00 committed by Benoit Marty
parent 3ac54c51f6
commit 9a7bd35ddc
18 changed files with 241 additions and 31 deletions

View file

@ -68,7 +68,7 @@ interface SendService {
* @param optionValue The option value (for compatibility) * @param optionValue The option value (for compatibility)
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendPollReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable
/** /**
* @param options list of (label, value) * @param options list of (label, value)

View file

@ -84,8 +84,8 @@ internal class DefaultSendService @AssistedInject constructor(
return sendEvent(event) return sendEvent(event)
} }
override fun sendPollReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
val event = localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, optionIndex, optionValue).also { val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
return sendEvent(event) return sendEvent(event)

View file

@ -136,10 +136,10 @@ internal class LocalEchoEventFactory @Inject constructor(
)) ))
} }
fun createPollReplyEvent(roomId: String, fun createOptionsReplyEvent(roomId: String,
pollEventId: String, pollEventId: String,
optionIndex: Int, optionIndex: Int,
optionLabel: String): Event { optionLabel: String): Event {
return createEvent(roomId, return createEvent(roomId,
MessagePollResponseContent( MessagePollResponseContent(
body = optionLabel, body = optionLabel,

View file

@ -53,7 +53,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(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 ReplyToOptionsPoll(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()
data class ReplyToOptionsButtons(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()
data class ReportContent( data class ReportContent(
val eventId: String, val eventId: String,

View file

@ -199,7 +199,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToPoll -> replyToPoll(action) is RoomDetailAction.ReplyToOptionsPoll -> replyToPoll(action)
is RoomDetailAction.ReplyToOptionsButtons -> replyToButtons(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
@ -861,8 +862,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
private fun replyToPoll(action: RoomDetailAction.ReplyToPoll) { private fun replyToPoll(action: RoomDetailAction.ReplyToOptionsPoll) {
room.sendPollReply(action.eventId, action.optionIndex, action.optionValue) room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
}
private fun replyToButtons(action: RoomDetailAction.ReplyToOptionsButtons) {
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
} }
private fun observeSyncState() { private fun observeSyncState() {

View file

@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.OptionsType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -66,6 +67,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageOptionsItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessagePollItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessagePollItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
@ -139,24 +141,36 @@ class MessageItemFactory @Inject constructor(
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildPollMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
} }
private fun buildPollMessageItem(messageContent: MessageOptionsContent, private fun buildOptionsMessageItem(messageContent: MessageOptionsContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
return MessagePollItem_() if (messageContent.optionType == OptionsType.POLL.value) {
.attributes(attributes) return MessagePollItem_()
.callback(callback) .attributes(attributes)
.informationData(informationData) .callback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline) .informationData(informationData)
.optionsContent(messageContent) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight) .optionsContent(messageContent)
.highlighted(highlight)
} else if (messageContent.optionType == OptionsType.BUTTONS.value) {
return MessageOptionsItem_()
.attributes(attributes)
.callback(callback)
.informationData(informationData)
.leftGuideline(avatarSizeProvider.leftGuideline)
.optionsContent(messageContent)
.highlighted(highlight)
} else {
return null
}
} }
private fun buildAudioMessageItem(messageContent: MessageAudioContent, private fun buildAudioMessageItem(messageContent: MessageAudioContent,

View file

@ -0,0 +1,79 @@
/*
* 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.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.button.MaterialButton
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.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 MessageOptionsItem : AbsMessageItem<MessageOptionsItem.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)
renderSendState(holder.view, holder.labelText)
holder.labelText.setTextOrHide(optionsContent?.label)
holder.buttonContainer.removeAllViews()
val relatedEventId = informationData?.eventId ?: return
val options = optionsContent?.options?.takeIf { it.isNotEmpty() } ?: return
// Now add back the buttons
options.forEachIndexed { index, option ->
val materialButton = LayoutInflater.from(holder.view.context).inflate(R.layout.option_buttons, holder.buttonContainer, false)
as MaterialButton
holder.buttonContainer.addView(materialButton, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
materialButton.text = option.label
materialButton.setOnClickListener {
callback?.onTimelineItemAction(RoomDetailAction.ReplyToOptionsButtons(relatedEventId, index, option.value
?: "$index"))
}
}
}
class Holder : AbsMessageItem.Holder(STUB_ID) {
val labelText by bind<TextView>(R.id.optionLabelText)
val buttonContainer by bind<ViewGroup>(R.id.optionsButtonContainer)
}
companion object {
private const val STUB_ID = R.id.messageOptionsStub
}
}

View file

@ -145,7 +145,7 @@ abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
val optionIndex = buttons.indexOf(it) val optionIndex = buttons.indexOf(it)
if (optionIndex != -1 && pollId != null) { if (optionIndex != -1 && pollId != null) {
val compatValue = if (optionIndex < optionValues?.size ?: 0) optionValues?.get(optionIndex) else null val compatValue = if (optionIndex < optionValues?.size ?: 0) optionValues?.get(optionIndex) else null
callback?.onTimelineItemAction(RoomDetailAction.ReplyToPoll(pollId!!, optionIndex, compatValue callback?.onTimelineItemAction(RoomDetailAction.ReplyToOptionsPoll(pollId!!, optionIndex, compatValue
?: "$optionIndex")) ?: "$optionIndex"))
} }
}) })

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/riotx_button_disabled_alpha12" android:state_enabled="false" />
<item android:color="@color/riotx_button_primary_accent_alpha12" android:state_enabled="true" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/button_bot_disabled_text_color" android:state_enabled="false" />
<item android:color="@color/button_bot_enabled_text_color" />
</selector>

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.5,0.5L20.5,0.5A3,3 0,0 1,23.5 3.5L23.5,20.5A3,3 0,0 1,20.5 23.5L3.5,23.5A3,3 0,0 1,0.5 20.5L0.5,3.5A3,3 0,0 1,3.5 0.5z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M5.5,12L6.5,12A1.5,1.5 0,0 1,8 13.5L8,19.5A1.5,1.5 0,0 1,6.5 21L5.5,21A1.5,1.5 0,0 1,4 19.5L4,13.5A1.5,1.5 0,0 1,5.5 12z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.5,9L12.5,9A1.5,1.5 0,0 1,14 10.5L14,19.5A1.5,1.5 0,0 1,12.5 21L11.5,21A1.5,1.5 0,0 1,10 19.5L10,10.5A1.5,1.5 0,0 1,11.5 9z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M17.5,6L18.5,6A1.5,1.5 0,0 1,20 7.5L20,19.5A1.5,1.5 0,0 1,18.5 21L17.5,21A1.5,1.5 0,0 1,16 19.5L16,7.5A1.5,1.5 0,0 1,17.5 6z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

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

View file

@ -0,0 +1,35 @@
<?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"
android:orientation="vertical">
<TextView
android:id="@+id/optionLabelText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:layout_marginBottom="8dp"
android:textStyle="normal"
tools:text="What would you like to do?" />
<LinearLayout
android:id="@+id/optionsButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Filled at runtime with buttons -->
<!--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" /-->
</LinearLayout>
</LinearLayout>

View file

@ -5,14 +5,29 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/pollLabelText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?riotx_text_primary" android:orientation="horizontal">
android:textSize="14sp"
android:textStyle="bold" <ImageView
tools:text="What would you like to do?" /> android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:layout_margin="4dp"
android:tint="@color/riotx_accent"
android:src="@drawable/ic_poll" />
<TextView
android:id="@+id/pollLabelText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="What would you like to do?" />
</LinearLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/pollButton1" android:id="@+id/pollButton1"

View file

@ -0,0 +1,6 @@
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/VectorButtonStyleInlineBot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Create Github issue" />

View file

@ -125,6 +125,8 @@
<color name="button_disabled_text_color">#FFFFFFFF</color> <color name="button_disabled_text_color">#FFFFFFFF</color>
<color name="button_destructive_enabled_text_color">#FF4B55</color> <color name="button_destructive_enabled_text_color">#FF4B55</color>
<color name="button_destructive_disabled_text_color">#FF4B55</color> <color name="button_destructive_disabled_text_color">#FF4B55</color>
<color name="button_bot_enabled_text_color">#FF368BD6</color>
<color name="button_bot_disabled_text_color">#61708B</color>
<!-- Link color --> <!-- Link color -->

View file

@ -10,6 +10,7 @@
<color name="riotx_destructive_accent">#FFFF4B55</color> <color name="riotx_destructive_accent">#FFFF4B55</color>
<color name="riotx_destructive_accent_alpha12">#1EFF4B55</color> <color name="riotx_destructive_accent_alpha12">#1EFF4B55</color>
<color name="riotx_button_primary_accent_alpha12">#14368BD6</color>
<color name="riotx_positive_accent">#03B381</color> <color name="riotx_positive_accent">#03B381</color>

View file

@ -151,6 +151,11 @@
<item name="android:textColor">@color/button_positive_text_color_selector</item> <item name="android:textColor">@color/button_positive_text_color_selector</item>
</style> </style>
<style name="VectorButtonStyleInlineBot" parent="VectorButtonStyleDestructive">
<item name="backgroundTint">@color/button_bot_background_selector</item>
<item name="android:textColor">@color/button_bot_enabled_text_color</item>
</style>
<!--Widget.AppCompat.Button.Borderless.Colored, which sets the text color to colorAccent, <!--Widget.AppCompat.Button.Borderless.Colored, which sets the text color to colorAccent,
using colorControlHighlight as an overlay for focused and pressed states.--> using colorControlHighlight as an overlay for focused and pressed states.-->
<style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton"> <style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton">