From 9a7bd35ddc850738206bfd3e6579a1e55c362342 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Feb 2020 14:46:32 +0100 Subject: [PATCH] Add m.buttons support (a.k.a bot buttons) --- .../api/session/room/send/SendService.kt | 2 +- .../session/room/send/DefaultSendService.kt | 4 +- .../room/send/LocalEchoEventFactory.kt | 8 +- .../home/room/detail/RoomDetailAction.kt | 3 +- .../home/room/detail/RoomDetailViewModel.kt | 11 ++- .../timeline/factory/MessageItemFactory.kt | 40 +++++++--- .../timeline/item/MessageOptionsItem.kt | 79 +++++++++++++++++++ .../detail/timeline/item/MessagePollItem.kt | 2 +- .../color/button_bot_background_selector.xml | 5 ++ .../color/button_bot_text_color_selector.xml | 5 ++ vector/src/main/res/drawable/ic_poll.xml | 30 +++++++ .../res/layout/item_timeline_event_base.xml | 7 ++ ...tem_timeline_event_option_buttons_stub.xml | 35 ++++++++ .../layout/item_timeline_event_poll_stub.xml | 27 +++++-- vector/src/main/res/layout/option_buttons.xml | 6 ++ vector/src/main/res/values/colors_riot.xml | 2 + vector/src/main/res/values/colors_riotx.xml | 1 + vector/src/main/res/values/styles_riot.xml | 5 ++ 18 files changed, 241 insertions(+), 31 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt create mode 100644 vector/src/main/res/color/button_bot_background_selector.xml create mode 100644 vector/src/main/res/color/button_bot_text_color_selector.xml create mode 100644 vector/src/main/res/drawable/ic_poll.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_option_buttons_stub.xml create mode 100644 vector/src/main/res/layout/option_buttons.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index a49828c0d1..33bbdc3072 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -68,7 +68,7 @@ interface SendService { * @param optionValue The option value (for compatibility) * @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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index f0a600cc67..761b4cb207 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -84,8 +84,8 @@ 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 { + override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { + val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also { saveLocalEcho(it) } return sendEvent(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 13691412f7..a445c96acf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -136,10 +136,10 @@ internal class LocalEchoEventFactory @Inject constructor( )) } - fun createPollReplyEvent(roomId: String, - pollEventId: String, - optionIndex: Int, - optionLabel: String): Event { + fun createOptionsReplyEvent(roomId: String, + pollEventId: String, + optionIndex: Int, + optionLabel: String): Event { return createEvent(roomId, MessagePollResponseContent( body = optionLabel, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index ba0c187856..48d7f15e53 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -53,7 +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 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( val eventId: String, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index adb410d83e..4b067a8151 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -199,7 +199,8 @@ 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.ReplyToOptionsPoll -> replyToPoll(action) + is RoomDetailAction.ReplyToOptionsButtons -> replyToButtons(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action) @@ -861,8 +862,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun replyToPoll(action: RoomDetailAction.ReplyToPoll) { - room.sendPollReply(action.eventId, action.optionIndex, action.optionValue) + private fun replyToPoll(action: RoomDetailAction.ReplyToOptionsPoll) { + 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() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 1a94e87988..381e3711cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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.MessageVerificationRequestContent 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.timeline.TimelineEvent 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.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.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 MessageAudioContent -> buildAudioMessageItem(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) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } - 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 buildOptionsMessageItem(messageContent: MessageOptionsContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { + if (messageContent.optionType == OptionsType.POLL.value) { + return MessagePollItem_() + .attributes(attributes) + .callback(callback) + .informationData(informationData) + .leftGuideline(avatarSizeProvider.leftGuideline) + .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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt new file mode 100644 index 0000000000..e13a28deaa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt @@ -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() { + + @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(R.id.optionLabelText) + + val buttonContainer by bind(R.id.optionsButtonContainer) + } + + companion object { + private const val STUB_ID = R.id.messageOptionsStub + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt index 13b899fec0..8bcba9be62 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt @@ -145,7 +145,7 @@ abstract class MessagePollItem : AbsMessageItem() { val optionIndex = buttons.indexOf(it) if (optionIndex != -1 && pollId != 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")) } }) diff --git a/vector/src/main/res/color/button_bot_background_selector.xml b/vector/src/main/res/color/button_bot_background_selector.xml new file mode 100644 index 0000000000..f2aa7abc53 --- /dev/null +++ b/vector/src/main/res/color/button_bot_background_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/color/button_bot_text_color_selector.xml b/vector/src/main/res/color/button_bot_text_color_selector.xml new file mode 100644 index 0000000000..84558ed582 --- /dev/null +++ b/vector/src/main/res/color/button_bot_text_color_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_poll.xml b/vector/src/main/res/drawable/ic_poll.xml new file mode 100644 index 0000000000..581343eb2b --- /dev/null +++ b/vector/src/main/res/drawable/ic_poll.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 85c3be71e8..77eaeae05c 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -112,6 +112,13 @@ android:layout_marginEnd="56dp" android:layout="@layout/item_timeline_event_poll_stub" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_poll_stub.xml b/vector/src/main/res/layout/item_timeline_event_poll_stub.xml index 3553785879..0ee6b089da 100644 --- a/vector/src/main/res/layout/item_timeline_event_poll_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_poll_stub.xml @@ -5,14 +5,29 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:orientation="horizontal"> + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml index fb1becc939..85e3a6d31a 100644 --- a/vector/src/main/res/values/colors_riot.xml +++ b/vector/src/main/res/values/colors_riot.xml @@ -125,6 +125,8 @@ #FFFFFFFF #FF4B55 #FF4B55 + #FF368BD6 + #61708B diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 7adf3e9111..7c0e57be93 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -10,6 +10,7 @@ #FFFF4B55 #1EFF4B55 + #14368BD6 #03B381 diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 1b4a540280..76d1988f61 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -151,6 +151,11 @@ @color/button_positive_text_color_selector + +