diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt index 06b3e9bf2e..11e0f882c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt @@ -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" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt new file mode 100644 index 0000000000..4841f134ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt @@ -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? = 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index 2707283325..d75cd7f8dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt index c4cbde98eb..622250da4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt index c66d1b9770..d43c9f6a0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt @@ -23,4 +23,5 @@ interface RelationContent { val type: String? val eventId: String? val inReplyTo: ReplyToContent? + val option: Int? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt index 853a381740..892fc61dee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt @@ -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 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 ac1b50bbcb..d3d4b13cc0 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 @@ -61,6 +61,13 @@ interface SendService { */ fun sendMedias(attachments: List): 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index 98cf9e234e..03b2c9d41d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -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) 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 30247ade12..6e303db93b 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,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)) { 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 bfa1d380ae..0cfd36b8e9 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 @@ -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, 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 d0716bd047..ba0c187856 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,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?, 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 710c70a948..f39ac66941 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,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() 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 65a6f5f244..d24cb8a19b 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 @@ -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, 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 new file mode 100644 index 0000000000..fcabb44ba6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt @@ -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() { + + @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? = null + var callback: TimelineEventController.Callback? = null + + val button1 by bind