diff --git a/changelog.d/4367.feature b/changelog.d/4367.feature
new file mode 100644
index 00000000000..04db0bd6be2
--- /dev/null
+++ b/changelog.d/4367.feature
@@ -0,0 +1 @@
+Poll Feature - Create Poll Screen
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml b/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml
new file mode 100644
index 00000000000..343c42ebf33
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml b/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml
new file mode 100644
index 00000000000..7079cc271b3
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml
index cc1041a2ce0..a1a9f6a7e85 100644
--- a/library/ui-styles/src/main/res/values/styles_edit_text.xml
+++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml
@@ -19,4 +19,9 @@
- ?vctr_message_text_color
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_polls.xml b/library/ui-styles/src/main/res/values/styles_polls.xml
new file mode 100644
index 00000000000..ac99a6514b2
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/styles_polls.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index d0ce55070e6..a39ca5b4f48 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -102,6 +102,9 @@ object EventType {
// Relation Events
const val REACTION = "m.reaction"
+ // Poll
+ const val POLL_START = "org.matrix.msc3381.poll.start"
+
// Unwedging
internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
new file mode 100644
index 00000000000..ef2fd1867a7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class MessagePollContent(
+ @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
new file mode 100644
index 00000000000..8f5ff53c85c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollAnswer(
+ @Json(name = "id") val id: String? = null,
+ @Json(name = "org.matrix.msc1767.text") val answer: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
new file mode 100644
index 00000000000..e652514b92c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollCreationInfo(
+ @Json(name = "question") val question: PollQuestion? = null,
+ @Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed",
+ @Json(name = "max_selections") val maxSelections: Int = 1,
+ @Json(name = "answers") val answers: List? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
new file mode 100644
index 00000000000..76025f745e6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollQuestion(
+ @Json(name = "org.matrix.msc1767.text") val question: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 6ae42de90cd..a2b38b66061 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.message.MessageType
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
@@ -84,10 +83,10 @@ interface SendService {
/**
* Send a poll to the room.
* @param question the question
- * @param options list of (label, value)
+ * @param options list of options
* @return a [Cancelable]
*/
- fun sendPoll(question: String, options: List): Cancelable
+ fun sendPoll(question: String, options: List): Cancelable
/**
* Method to send a poll response.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 177c98541c8..77aadef6bd3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.send.SendState
@@ -98,7 +97,7 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun sendPoll(question: String, options: List): Cancelable {
+ override fun sendPoll(question: String, options: List): Cancelable {
return localEchoEventFactory.createPollEvent(roomId, question, options)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 8dd0c593872..5cb96875186 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -38,13 +38,14 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
-import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
+import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
+import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
+import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo
import org.matrix.android.sdk.api.session.room.model.message.VideoInfo
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
@@ -138,24 +139,29 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createPollEvent(roomId: String,
question: String,
- options: List): Event {
- val compatLabel = buildString {
- append("[Poll] ")
- append(question)
- options.forEach {
- append("\n")
- append(it.value)
- }
- }
- return createMessageEvent(
- roomId,
- MessageOptionsContent(
- body = compatLabel,
- label = question,
- optionType = OPTION_TYPE_POLL,
- options = options.toList()
+ options: List): Event {
+ val content = MessagePollContent(
+ pollCreationInfo = PollCreationInfo(
+ question = PollQuestion(
+ question = question
+ ),
+ answers = options.mapIndexed { index, option ->
+ PollAnswer(
+ id = index.toString(),
+ answer = option
+ )
+ }
)
)
+ val localId = LocalEcho.createLocalEchoId()
+ return Event(
+ roomId = roomId,
+ originServerTs = dummyOriginServerTs(),
+ senderId = userId,
+ eventId = localId,
+ type = EventType.POLL_START,
+ content = content.toContent(),
+ unsignedData = UnsignedData(age = null, transactionId = localId))
}
fun createReplaceTextOfReply(roomId: String,
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 376e0e869a3..01d76306786 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -339,6 +339,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index bf72dcb076e..67cb9dd0f7e 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -94,6 +94,7 @@ import im.vector.app.features.login2.terms.LoginTermsFragment2
import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
import im.vector.app.features.matrixto.MatrixToUserFragment
import im.vector.app.features.pin.PinFragment
+import im.vector.app.features.poll.create.CreatePollFragment
import im.vector.app.features.qrcode.QrCodeScannerFragment
import im.vector.app.features.reactions.EmojiChooserFragment
import im.vector.app.features.reactions.EmojiSearchResultFragment
@@ -837,4 +838,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SpaceLeaveAdvancedFragment::class)
fun bindSpaceLeaveAdvancedFragment(fragment: SpaceLeaveAdvancedFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(CreatePollFragment::class)
+ fun bindCreatePollFragment(fragment: CreatePollFragment): Fragment
}
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 5637c97b514..da5e7392e11 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -54,6 +54,7 @@ import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
+import im.vector.app.features.poll.create.CreatePollViewModel
import im.vector.app.features.rageshake.BugReportViewModel
import im.vector.app.features.reactions.EmojiSearchResultViewModel
import im.vector.app.features.room.RequireActiveMembershipViewModel
@@ -540,4 +541,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(VerificationBottomSheetViewModel::class)
fun verificationBottomSheetViewModelFactory(factory: VerificationBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(CreatePollViewModel::class)
+ fun createPollViewModelFactory(factory: CreatePollViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt
index eb683631fde..fe59c82ce97 100644
--- a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt
@@ -15,6 +15,8 @@
*/
package im.vector.app.core.ui.list
+import android.graphics.Typeface
+import android.view.Gravity
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
@@ -47,6 +49,12 @@ abstract class GenericButtonItem : VectorEpoxyModel()
@DrawableRes
var iconRes: Int? = null
+ @EpoxyAttribute
+ var gravity: Int = Gravity.CENTER
+
+ @EpoxyAttribute
+ var bold: Boolean = false
+
override fun bind(holder: Holder) {
super.bind(holder)
holder.button.text = text
@@ -58,6 +66,10 @@ abstract class GenericButtonItem : VectorEpoxyModel()
holder.button.icon = null
}
+ holder.button.gravity = gravity or Gravity.CENTER_VERTICAL
+ val textStyle = if (bold) Typeface.BOLD else Typeface.NORMAL
+ holder.button.setTypeface(null, textStyle)
+
holder.button.onClick(buttonClickAction)
}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
index 35644e18430..6c349d18dce 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
@@ -75,6 +75,7 @@ class AttachmentTypeSelectorView(context: Context,
views.attachmentStickersButton.configure(Type.STICKER)
views.attachmentAudioButton.configure(Type.AUDIO)
views.attachmentContactButton.configure(Type.CONTACT)
+ views.attachmentPollButton.configure(Type.POLL)
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@@ -108,6 +109,7 @@ class AttachmentTypeSelectorView(context: Context,
animateButtonIn(views.attachmentAudioButton, 0)
animateButtonIn(views.attachmentContactButton, ANIMATION_DURATION / 4)
animateButtonIn(views.attachmentStickersButton, ANIMATION_DURATION / 2)
+ animateButtonIn(views.attachmentPollButton, ANIMATION_DURATION / 4)
}
override fun dismiss() {
@@ -212,6 +214,7 @@ class AttachmentTypeSelectorView(context: Context,
FILE(PERMISSIONS_EMPTY),
STICKER(PERMISSIONS_EMPTY),
AUDIO(PERMISSIONS_EMPTY),
- CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
+ CONTACT(PERMISSIONS_FOR_PICKING_CONTACT),
+ POLL(PERMISSIONS_EMPTY)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt
index ccabd25ff48..1950038691c 100644
--- a/vector/src/main/java/im/vector/app/features/command/Command.kt
+++ b/vector/src/main/java/im/vector/app/features/command/Command.kt
@@ -47,7 +47,6 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false),
SPOILER("/spoiler", "", R.string.command_description_spoiler, false),
- POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false),
SHRUG("/shrug", "", R.string.command_description_shrug, false),
LENNY("/lenny", "", R.string.command_description_lenny, false),
PLAIN("/plain", "", R.string.command_description_plain, false),
diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
index 3a6b005d6fc..4b2a4aa28ca 100644
--- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
@@ -345,15 +345,6 @@ object CommandParser {
ParsedCommand.SendLenny(message)
}
- Command.POLL.command -> {
- val rawCommand = textMessage.substring(Command.POLL.command.length).trim()
- val split = rawCommand.split("|").map { it.trim() }
- if (split.size > 2) {
- ParsedCommand.SendPoll(split[0], split.subList(1, split.size))
- } else {
- ParsedCommand.ErrorSyntax(Command.POLL)
- }
- }
Command.DISCARD_SESSION.command -> {
ParsedCommand.DiscardSession
}
diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt
index 89aa8d91889..4f8d19abb63 100644
--- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt
@@ -61,7 +61,6 @@ sealed class ParsedCommand {
class SendSpoiler(val message: String) : ParsedCommand()
class SendShrug(val message: CharSequence) : ParsedCommand()
class SendLenny(val message: CharSequence) : ParsedCommand()
- class SendPoll(val question: String, val options: List) : ParsedCommand()
object DiscardSession : ParsedCommand()
class ShowUser(val userId: String) : ParsedCommand()
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt
new file mode 100644
index 00000000000..abcd1429d47
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2021 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.app.features.form
+
+import android.text.Editable
+import android.view.inputmethod.EditorInfo
+import android.widget.ImageButton
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.TextListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.addTextChangedListenerOnce
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextIfDifferent
+import im.vector.app.core.platform.SimpleTextWatcher
+
+@EpoxyModelClass(layout = R.layout.item_form_text_input_with_delete)
+abstract class FormEditTextWithDeleteItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute
+ var hint: String? = null
+
+ @EpoxyAttribute
+ var value: String? = null
+
+ @EpoxyAttribute
+ var enabled: Boolean = true
+
+ @EpoxyAttribute
+ var singleLine: Boolean = true
+
+ @EpoxyAttribute
+ var imeOptions: Int? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var onTextChange: TextListener? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var onDeleteClicked: ClickListener? = null
+
+ private val onTextChangeListener = object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ onTextChange?.invoke(s.toString())
+ }
+ }
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.textInputLayout.isEnabled = enabled
+ holder.textInputLayout.hint = hint
+
+ holder.textInputEditText.setTextIfDifferent(value)
+
+ holder.textInputEditText.isEnabled = enabled
+ holder.textInputEditText.isSingleLine = singleLine
+
+ holder.textInputEditText.imeOptions =
+ imeOptions ?: when (singleLine) {
+ true -> EditorInfo.IME_ACTION_NEXT
+ false -> EditorInfo.IME_ACTION_NONE
+ }
+
+ holder.textInputEditText.addTextChangedListenerOnce(onTextChangeListener)
+
+ holder.textInputDeleteButton.onClick(onDeleteClicked)
+ }
+
+ override fun shouldSaveViewState(): Boolean {
+ return false
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ holder.textInputEditText.removeTextChangedListener(onTextChangeListener)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val textInputLayout by bind(R.id.formTextInputTextInputLayout)
+ val textInputEditText by bind(R.id.formTextInputTextInputEditText)
+ val textInputDeleteButton by bind(R.id.formTextInputDeleteButton)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index d20c9796d25..dab7fd9bd93 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -2144,6 +2144,7 @@ class RoomDetailFragment @Inject constructor(
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher)
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+ AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId)
}.exhaustive
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt
index e80f25de2f3..727eb2f6705 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt
@@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
@@ -259,11 +258,6 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
popDraft()
}
- is ParsedCommand.SendPoll -> {
- room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
- _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
- popDraft()
- }
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
}
diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
index debdf3739c9..89a05c88dab 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
@@ -64,6 +64,8 @@ import im.vector.app.features.media.VectorAttachmentViewerActivity
import im.vector.app.features.pin.PinActivity
import im.vector.app.features.pin.PinArgs
import im.vector.app.features.pin.PinMode
+import im.vector.app.features.poll.create.CreatePollActivity
+import im.vector.app.features.poll.create.CreatePollArgs
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity
@@ -498,6 +500,14 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent)
}
+ override fun openCreatePoll(context: Context, roomId: String) {
+ val intent = CreatePollActivity.getIntent(
+ context,
+ CreatePollArgs(roomId = roomId)
+ )
+ context.startActivity(intent)
+ }
+
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)
diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
index 612643c8041..264593fe180 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
@@ -140,4 +140,6 @@ interface Navigator {
fun openDevTools(context: Context, roomId: String)
fun openCallTransfer(context: Context, callId: String)
+
+ fun openCreatePoll(context: Context, roomId: String)
}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt
new file mode 100644
index 00000000000..182750fbd2b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed class CreatePollAction : VectorViewModelAction {
+ data class OnQuestionChanged(val question: String) : CreatePollAction()
+ data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction()
+ data class OnDeleteOption(val index: Int) : CreatePollAction()
+ object OnAddOption : CreatePollAction()
+ object OnCreatePoll : CreatePollAction()
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollActivity.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollActivity.kt
new file mode 100644
index 00000000000..fc830ddae9a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollActivity.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.platform.SimpleFragmentActivity
+
+@AndroidEntryPoint
+class CreatePollActivity : SimpleFragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ views.toolbar.visibility = View.GONE
+
+ val createPollArgs: CreatePollArgs? = intent?.extras?.getParcelable(EXTRA_CREATE_POLL_ARGS)
+
+ if (isFirstCreation()) {
+ addFragment(
+ R.id.container,
+ CreatePollFragment::class.java,
+ createPollArgs
+ )
+ }
+ }
+
+ companion object {
+
+ private const val EXTRA_CREATE_POLL_ARGS = "EXTRA_CREATE_POLL_ARGS"
+
+ fun getIntent(context: Context, createPollArgs: CreatePollArgs): Intent {
+ return Intent(context, CreatePollActivity::class.java).apply {
+ putExtra(EXTRA_CREATE_POLL_ARGS, createPollArgs)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt
new file mode 100644
index 00000000000..515f59a38ca
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import android.view.Gravity
+import android.view.inputmethod.EditorInfo
+import com.airbnb.epoxy.EpoxyController
+import im.vector.app.R
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.ItemStyle
+import im.vector.app.core.ui.list.genericButtonItem
+import im.vector.app.core.ui.list.genericItem
+import im.vector.app.features.form.formEditTextItem
+import im.vector.app.features.form.formEditTextWithDeleteItem
+import javax.inject.Inject
+
+class CreatePollController @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val colorProvider: ColorProvider
+) : EpoxyController() {
+
+ private var state: CreatePollViewState? = null
+ var callback: Callback? = null
+
+ fun setData(state: CreatePollViewState) {
+ this.state = state
+ requestModelBuild()
+ }
+
+ override fun buildModels() {
+ val currentState = state ?: return
+ val host = this
+
+ genericItem {
+ id("question_title")
+ style(ItemStyle.BIG_TEXT)
+ title(host.stringProvider.getString(R.string.create_poll_question_title))
+ }
+
+ val questionImeAction = if (currentState.options.isEmpty()) EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NEXT
+
+ formEditTextItem {
+ id("question")
+ value(currentState.question)
+ hint(host.stringProvider.getString(R.string.create_poll_question_hint))
+ singleLine(true)
+ imeOptions(questionImeAction)
+ maxLength(500)
+ onTextChange {
+ host.callback?.onQuestionChanged(it)
+ }
+ }
+
+ genericItem {
+ id("options_title")
+ style(ItemStyle.BIG_TEXT)
+ title(host.stringProvider.getString(R.string.create_poll_options_title))
+ }
+
+ currentState.options.forEachIndexed { index, option ->
+ val imeOptions = if (index == currentState.options.size - 1) EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NEXT
+ formEditTextWithDeleteItem {
+ id("option_$index")
+ value(option)
+ hint(host.stringProvider.getString(R.string.create_poll_options_hint, (index + 1)))
+ singleLine(true)
+ imeOptions(imeOptions)
+ onTextChange {
+ host.callback?.onOptionChanged(index, it)
+ }
+ onDeleteClicked {
+ host.callback?.onDeleteOption(index)
+ }
+ }
+ }
+
+ if (currentState.canAddMoreOptions) {
+ genericButtonItem {
+ id("add_option")
+ text(host.stringProvider.getString(R.string.create_poll_add_option))
+ textColor(host.colorProvider.getColor(R.color.palette_element_green))
+ gravity(Gravity.START)
+ bold(true)
+ buttonClickAction {
+ host.callback?.onAddOption()
+ }
+ }
+ }
+ }
+
+ interface Callback {
+ fun onQuestionChanged(question: String)
+ fun onOptionChanged(index: Int, option: String)
+ fun onDeleteOption(index: Int)
+ fun onAddOption()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt
new file mode 100644
index 00000000000..dc82579f15d
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import im.vector.app.R
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentCreatePollBinding
+import kotlinx.parcelize.Parcelize
+import javax.inject.Inject
+
+@Parcelize
+data class CreatePollArgs(
+ val roomId: String,
+) : Parcelable
+
+class CreatePollFragment @Inject constructor(
+ private val controller: CreatePollController
+) : VectorBaseFragment(), CreatePollController.Callback {
+
+ private val viewModel: CreatePollViewModel by activityViewModel()
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding {
+ return FragmentCreatePollBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ vectorBaseActivity.setSupportActionBar(views.createPollToolbar)
+
+ views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true)
+ controller.callback = this
+
+ views.createPollClose.debouncedClicks {
+ requireActivity().finish()
+ }
+
+ views.createPollButton.debouncedClicks {
+ viewModel.handle(CreatePollAction.OnCreatePoll)
+ }
+
+ viewModel.onEach(CreatePollViewState::canCreatePoll) { canCreatePoll ->
+ views.createPollButton.isEnabled = canCreatePoll
+ }
+
+ viewModel.observeViewEvents {
+ when (it) {
+ CreatePollViewEvents.Success -> handleSuccess()
+ CreatePollViewEvents.EmptyQuestionError -> handleEmptyQuestionError()
+ is CreatePollViewEvents.NotEnoughOptionsError -> handleNotEnoughOptionsError(it.requiredOptionsCount)
+ }
+ }
+ }
+
+ override fun invalidate() = withState(viewModel) {
+ controller.setData(it)
+ }
+
+ override fun onQuestionChanged(question: String) {
+ viewModel.handle(CreatePollAction.OnQuestionChanged(question))
+ }
+
+ override fun onOptionChanged(index: Int, option: String) {
+ viewModel.handle(CreatePollAction.OnOptionChanged(index, option))
+ }
+
+ override fun onDeleteOption(index: Int) {
+ viewModel.handle(CreatePollAction.OnDeleteOption(index))
+ }
+
+ override fun onAddOption() {
+ viewModel.handle(CreatePollAction.OnAddOption)
+ // Scroll to bottom to show "Add Option" button
+ views.createPollRecyclerView.apply {
+ postDelayed({
+ smoothScrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
+ }, 100)
+ }
+ }
+
+ private fun handleSuccess() {
+ requireActivity().finish()
+ }
+
+ private fun handleEmptyQuestionError() {
+ renderToast(getString(R.string.create_poll_empty_question_error))
+ }
+
+ private fun handleNotEnoughOptionsError(requiredOptionsCount: Int) {
+ renderToast(
+ resources.getQuantityString(
+ R.plurals.create_poll_not_enough_options_error,
+ requiredOptionsCount,
+ requiredOptionsCount
+ )
+ )
+ }
+
+ private fun renderToast(message: String) {
+ views.createPollToast.removeCallbacks(hideToastRunnable)
+ views.createPollToast.text = message
+ views.createPollToast.isVisible = true
+ views.createPollToast.postDelayed(hideToastRunnable, 2_000)
+ }
+
+ private val hideToastRunnable = Runnable {
+ views.createPollToast.isVisible = false
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewEvents.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewEvents.kt
new file mode 100644
index 00000000000..fa06deea6e5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewEvents.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed class CreatePollViewEvents : VectorViewEvents {
+ object Success : CreatePollViewEvents()
+ object EmptyQuestionError : CreatePollViewEvents()
+ data class NotEnoughOptionsError(val requiredOptionsCount: Int) : CreatePollViewEvents()
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
new file mode 100644
index 00000000000..eb4b441f18a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorViewModel
+import org.matrix.android.sdk.api.session.Session
+
+class CreatePollViewModel @AssistedInject constructor(
+ @Assisted private val initialState: CreatePollViewState,
+ session: Session
+) : VectorViewModel(initialState) {
+
+ private val room = session.getRoom(initialState.roomId)!!
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: CreatePollViewState): CreatePollViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
+
+ const val MIN_OPTIONS_COUNT = 2
+ private const val MAX_OPTIONS_COUNT = 20
+ }
+
+ init {
+ observeState()
+ }
+
+ private fun observeState() {
+ onEach(
+ CreatePollViewState::question,
+ CreatePollViewState::options
+ ) { question, options ->
+ setState {
+ copy(
+ canCreatePoll = canCreatePoll(question, options),
+ canAddMoreOptions = options.size < MAX_OPTIONS_COUNT
+ )
+ }
+ }
+ }
+
+ override fun handle(action: CreatePollAction) {
+ when (action) {
+ CreatePollAction.OnCreatePoll -> handleOnCreatePoll()
+ CreatePollAction.OnAddOption -> handleOnAddOption()
+ is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index)
+ is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option)
+ is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question)
+ }
+ }
+
+ private fun handleOnCreatePoll() = withState { state ->
+ val nonEmptyOptions = state.options.filter { it.isNotEmpty() }
+ when {
+ state.question.isEmpty() -> {
+ _viewEvents.post(CreatePollViewEvents.EmptyQuestionError)
+ }
+ nonEmptyOptions.size < MIN_OPTIONS_COUNT -> {
+ _viewEvents.post(CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = MIN_OPTIONS_COUNT))
+ }
+ else -> {
+ room.sendPoll(state.question, state.options)
+ _viewEvents.post(CreatePollViewEvents.Success)
+ }
+ }
+ }
+
+ private fun handleOnAddOption() {
+ setState {
+ val extendedOptions = options + ""
+ copy(
+ options = extendedOptions
+ )
+ }
+ }
+
+ private fun handleOnDeleteOption(index: Int) {
+ setState {
+ val filteredOptions = options.filterIndexed { ind, _ -> ind != index }
+ copy(
+ options = filteredOptions
+ )
+ }
+ }
+
+ private fun handleOnOptionChanged(index: Int, option: String) {
+ setState {
+ val changedOptions = options.mapIndexed { ind, s -> if (ind == index) option else s }
+ copy(
+ options = changedOptions
+ )
+ }
+ }
+
+ private fun handleOnQuestionChanged(question: String) {
+ setState {
+ copy(
+ question = question
+ )
+ }
+ }
+
+ private fun canCreatePoll(question: String, options: List): Boolean {
+ return question.isNotEmpty() &&
+ options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
new file mode 100644
index 00000000000..a9060cc89f7
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 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.app.features.poll.create
+
+import com.airbnb.mvrx.MavericksState
+
+data class CreatePollViewState(
+ val roomId: String,
+ val question: String = "",
+ val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
+ val canCreatePoll: Boolean = false,
+ val canAddMoreOptions: Boolean = true
+) : MavericksState {
+
+ constructor(args: CreatePollArgs) : this(
+ roomId = args.roomId
+ )
+}
diff --git a/vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml
new file mode 100644
index 00000000000..8cbcc6e47c1
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_delete_10dp.xml b/vector/src/main/res/drawable/ic_delete_10dp.xml
new file mode 100644
index 00000000000..f8229a71d08
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_delete_10dp.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_create_poll.xml b/vector/src/main/res/layout/fragment_create_poll.xml
new file mode 100644
index 00000000000..1911aaefc98
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_create_poll.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_form_text_input.xml b/vector/src/main/res/layout/item_form_text_input.xml
index adc599f3142..a9e76bd68d7 100644
--- a/vector/src/main/res/layout/item_form_text_input.xml
+++ b/vector/src/main/res/layout/item_form_text_input.xml
@@ -9,6 +9,7 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml
index 648ca91820d..bc747778a7c 100644
--- a/vector/src/main/res/layout/view_attachment_type_selector.xml
+++ b/vector/src/main/res/layout/view_attachment_type_selector.xml
@@ -163,5 +163,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 0e8197dbae9..85ce81eaeeb 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2431,6 +2431,7 @@
"Audio"
"Gallery"
"Sticker"
+ Poll
Rotate and crop
Couldn\'t handle share data
@@ -3626,4 +3627,18 @@
Link this email with your account
%s in Settings to receive invites directly in Element.
+
+
+ Create Poll
+ Poll question or topic
+ Question or topic
+ Create options
+ Option %1$d
+ ADD OPTION
+ CREATE POLL
+ Question cannot be empty
+
+ - At least %1$s option is required
+ - At least %1$s options are required
+