Jelajahi Sumber

Merge pull request #2095 from nextcloud/feature/2024/simplePolls

Support "Simple polls"
Marcel Hibbe 3 tahun lalu
induk
melakukan
2896d0758f
65 mengubah file dengan 4233 tambahan dan 64 penghapusan
  1. 1 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  2. 248 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  3. 1 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  4. 1 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  5. 3 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  6. 29 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt
  7. 214 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  8. 25 1
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  9. 42 4
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  10. 8 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  11. 24 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  12. 34 2
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt
  13. 6 0
      app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt
  14. 25 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt
  15. 82 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt
  16. 59 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt
  17. 32 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt
  18. 39 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt
  19. 47 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt
  20. 25 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt
  21. 25 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt
  22. 30 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt
  23. 38 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt
  24. 87 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt
  25. 38 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt
  26. 141 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt
  27. 90 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt
  28. 44 0
      app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt
  29. 28 0
      app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt
  30. 43 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt
  31. 138 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt
  32. 44 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt
  33. 35 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt
  34. 35 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt
  35. 71 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt
  36. 187 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
  37. 74 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt
  38. 188 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
  39. 139 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
  40. 219 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
  41. 204 0
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
  42. 188 0
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
  43. 128 0
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt
  44. 133 0
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt
  45. 4 1
      app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt
  46. 19 0
      app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  47. 35 17
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  48. 10 0
      app/src/main/res/drawable/ic_baseline_bar_chart_24.xml
  49. 10 0
      app/src/main/res/drawable/ic_baseline_close_24.xml
  50. 0 29
      app/src/main/res/drawable/ic_comment_white.xml
  51. 33 0
      app/src/main/res/layout/dialog_attachment.xml
  52. 124 0
      app/src/main/res/layout/dialog_poll_create.xml
  53. 33 0
      app/src/main/res/layout/dialog_poll_loading.xml
  54. 95 0
      app/src/main/res/layout/dialog_poll_main.xml
  55. 65 0
      app/src/main/res/layout/dialog_poll_results.xml
  56. 89 0
      app/src/main/res/layout/dialog_poll_vote.xml
  57. 110 0
      app/src/main/res/layout/item_custom_incoming_poll_message.xml
  58. 105 0
      app/src/main/res/layout/item_custom_outcoming_poll_message.xml
  59. 49 0
      app/src/main/res/layout/poll_create_options_item.xml
  60. 60 0
      app/src/main/res/layout/poll_result_header_item.xml
  61. 44 0
      app/src/main/res/layout/poll_result_voter_item.xml
  62. 29 0
      app/src/main/res/layout/poll_result_voters_overview_item.xml
  63. 3 0
      app/src/main/res/values/colors.xml
  64. 22 0
      app/src/main/res/values/strings.xml
  65. 2 0
      app/src/main/res/values/styles.xml

+ 1 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -50,7 +50,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
-import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.UriUtils
@@ -117,7 +116,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
         if (!TextUtils.isEmpty(author)) {
             binding.messageAuthor.text = author
             binding.messageUserAvatar.setOnClickListener {
-                (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context)
+                (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
             }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)

+ 248 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt

@@ -0,0 +1,248 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Build
+import android.text.TextUtils
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.ViewCompat
+import autodagger.AutoInjector
+import coil.load
+import com.amulyakhare.textdrawable.TextDrawable
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.polls.ui.PollMainDialogFragment
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.stfalcon.chatkit.messages.MessageHolders
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageHolders
+.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
+
+    private val binding: ItemCustomIncomingPollMessageBinding =
+        ItemCustomIncomingPollMessageBinding.bind(itemView)
+
+    @Inject
+    lateinit var context: Context
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var message: ChatMessage
+
+    lateinit var reactionsInterface: ReactionsInterface
+
+    @SuppressLint("SetTextI18n")
+    override fun onBind(message: ChatMessage) {
+        super.onBind(message)
+        this.message = message
+        sharedApplication!!.componentApplication.inject(this)
+
+        setAvatarAndAuthorOnMessageItem(message)
+
+        colorizeMessageBubble(message)
+
+        itemView.isSelected = false
+        binding.messageTime.setTextColor(ResourcesCompat.getColor(context?.resources!!, R.color.warm_grey_four, null))
+
+        // parent message handling
+        setParentMessageDataOnMessageItem(message)
+
+        setPollPreview(message)
+
+        Reaction().showReactions(message, binding.reactions, binding.messageTime.context, false)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
+    }
+
+    private fun setPollPreview(message: ChatMessage) {
+        var pollId: String? = null
+        var pollName: String? = null
+
+        if (message.messageParameters != null && message.messageParameters!!.size > 0) {
+            for (key in message.messageParameters!!.keys) {
+                val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
+                if (individualHashMap["type"] == "talk-poll") {
+                    pollId = individualHashMap["id"]
+                    pollName = individualHashMap["name"].toString()
+                }
+            }
+        }
+
+        if (pollId != null && pollName != null) {
+            binding.messagePollTitle.text = pollName
+
+            val roomToken = (payload as? MessagePayload)!!.currentConversation.token!!
+            val isOwnerOrModerator = (payload as? MessagePayload)!!.currentConversation.isParticipantOwnerOrModerator
+
+            binding.bubble.setOnClickListener {
+                val pollVoteDialog = PollMainDialogFragment.newInstance(
+                    message.activeUser!!,
+                    roomToken,
+                    isOwnerOrModerator,
+                    pollId,
+                    pollName
+                )
+                pollVoteDialog.show(
+                    (binding.messagePollIcon.context as MainActivity).supportFragmentManager,
+                    TAG
+                )
+            }
+        }
+    }
+
+    private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
+        val author: String = message.actorDisplayName!!
+        if (!TextUtils.isEmpty(author)) {
+            binding.messageAuthor.text = author
+            binding.messageUserAvatar.setOnClickListener {
+                (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
+            }
+        } else {
+            binding.messageAuthor.setText(R.string.nc_nick_guest)
+        }
+
+        if (!message.isGrouped && !message.isOneToOneConversation) {
+            setAvatarOnMessage(message)
+        } else {
+            if (message.isOneToOneConversation) {
+                binding.messageUserAvatar.visibility = View.GONE
+            } else {
+                binding.messageUserAvatar.visibility = View.INVISIBLE
+            }
+            binding.messageAuthor.visibility = View.GONE
+        }
+    }
+
+    private fun setAvatarOnMessage(message: ChatMessage) {
+        binding.messageUserAvatar.visibility = View.VISIBLE
+        if (message.actorType == "guests") {
+            // do nothing, avatar is set
+        } else if (message.actorType == "bots" && message.actorId == "changelog") {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                val layers = arrayOfNulls<Drawable>(2)
+                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
+                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
+                val layerDrawable = LayerDrawable(layers)
+                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
+            } else {
+                binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
+            }
+        } else if (message.actorType == "bots") {
+            val drawable = TextDrawable.builder()
+                .beginConfig()
+                .bold()
+                .endConfig()
+                .buildRound(
+                    ">",
+                    ResourcesCompat.getColor(context.resources, R.color.black, null)
+                )
+            binding.messageUserAvatar.visibility = View.VISIBLE
+            binding.messageUserAvatar.setImageDrawable(drawable)
+        }
+    }
+
+    private fun colorizeMessageBubble(message: ChatMessage) {
+        val resources = itemView.resources
+
+        var bubbleResource = R.drawable.shape_incoming_message
+
+        if (message.isGrouped) {
+            bubbleResource = R.drawable.shape_grouped_incoming_message
+        }
+
+        val bgBubbleColor = if (message.isDeleted) {
+            ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble_deleted, null)
+        } else {
+            ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble, null)
+        }
+        val bubbleDrawable = DisplayUtils.getMessageSelector(
+            bgBubbleColor,
+            ResourcesCompat.getColor(resources, R.color.transparent, null),
+            bgBubbleColor, bubbleResource
+        )
+        ViewCompat.setBackground(bubble, bubbleDrawable)
+    }
+
+    private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
+        if (!message.isDeleted && message.parentMessage != null) {
+            val parentChatMessage = message.parentMessage
+            parentChatMessage!!.activeUser = message.activeUser
+            parentChatMessage.imageUrl?.let {
+                binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
+                binding.messageQuote.quotedMessageImage.load(it) {
+                    addHeader(
+                        "Authorization",
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    )
+                }
+            } ?: run {
+                binding.messageQuote.quotedMessageImage.visibility = View.GONE
+            }
+            binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
+                ?: context.getText(R.string.nc_nick_guest)
+            binding.messageQuote.quotedMessage.text = parentChatMessage.text
+
+            binding.messageQuote.quotedMessageAuthor
+                .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
+
+            if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
+                binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
+            } else {
+                binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
+            }
+
+            binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+        } else {
+            binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+        }
+    }
+
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
+    companion object {
+        private val TAG = NextcloudTalkApplication::class.java.simpleName
+    }
+}

+ 1 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -48,7 +48,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
-import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -210,7 +209,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
         if (!TextUtils.isEmpty(author)) {
             binding.messageAuthor.text = author
             binding.messageUserAvatar.setOnClickListener {
-                (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context)
+                (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
             }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)

+ 1 - 3
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

@@ -47,14 +47,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
-import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.TextMatchers
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
-import java.util.HashMap
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -136,7 +134,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
         if (!TextUtils.isEmpty(message.actorDisplayName)) {
             binding.messageAuthor.text = message.actorDisplayName
             binding.messageUserAvatar.setOnClickListener {
-                (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context)
+                (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
             }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)

+ 3 - 3
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -49,7 +49,6 @@ import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
-import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.DrawableUtils;
 import com.nextcloud.talk.utils.FileViewerUtils;
@@ -125,8 +124,9 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
             } else {
                 userAvatar.setVisibility(View.VISIBLE);
                 userAvatar.setOnClickListener(v -> {
-                    if (payload instanceof ProfileBottomSheet) {
-                        ((ProfileBottomSheet) payload).showFor(message.getActorId(), v.getContext());
+                    if (payload instanceof MessagePayload) {
+                        ((MessagePayload) payload).getProfileBottomSheet().showFor(message.getActorId(),
+                                                                                   v.getContext());
                     }
                 });
 

+ 29 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt

@@ -0,0 +1,29 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
+
+data class MessagePayload(
+    var currentConversation: Conversation,
+    val profileBottomSheet: ProfileBottomSheet
+)

+ 214 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

@@ -0,0 +1,214 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PorterDuff
+import android.view.View
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.view.ViewCompat
+import autodagger.AutoInjector
+import coil.load
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.chat.ReadStatus
+import com.nextcloud.talk.polls.ui.PollMainDialogFragment
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.stfalcon.chatkit.messages.MessageHolders
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : MessageHolders
+.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload) {
+
+    private val binding: ItemCustomOutcomingPollMessageBinding =
+        ItemCustomOutcomingPollMessageBinding.bind(itemView)
+
+    @Inject
+    lateinit var context: Context
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var message: ChatMessage
+
+    lateinit var reactionsInterface: ReactionsInterface
+
+    @SuppressLint("SetTextI18n")
+    override fun onBind(message: ChatMessage) {
+        super.onBind(message)
+        this.message = message
+        sharedApplication!!.componentApplication.inject(this)
+
+        colorizeMessageBubble(message)
+
+        itemView.isSelected = false
+        binding.messageTime.setTextColor(context.resources.getColor(R.color.white60))
+
+        // parent message handling
+        setParentMessageDataOnMessageItem(message)
+
+        val readStatusDrawableInt = when (message.readStatus) {
+            ReadStatus.READ -> R.drawable.ic_check_all
+            ReadStatus.SENT -> R.drawable.ic_check
+            else -> null
+        }
+
+        val readStatusContentDescriptionString = when (message.readStatus) {
+            ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
+            ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
+            else -> null
+        }
+
+        readStatusDrawableInt?.let { drawableInt ->
+            AppCompatResources.getDrawable(context, drawableInt)?.let {
+                it.setColorFilter(context.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
+                binding.checkMark.setImageDrawable(it)
+            }
+        }
+
+        binding.checkMark.contentDescription = readStatusContentDescriptionString
+
+        setPollPreview(message)
+
+        Reaction().showReactions(message, binding.reactions, binding.messageTime.context, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
+    }
+
+    private fun setPollPreview(message: ChatMessage) {
+        var pollId: String? = null
+        var pollName: String? = null
+
+        if (message.messageParameters != null && message.messageParameters!!.size > 0) {
+            for (key in message.messageParameters!!.keys) {
+                val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
+                if (individualHashMap["type"] == "talk-poll") {
+                    pollId = individualHashMap["id"]
+                    pollName = individualHashMap["name"].toString()
+                }
+            }
+        }
+
+        if (pollId != null && pollName != null) {
+            binding.messagePollTitle.text = pollName
+
+            val roomToken = (payload as? MessagePayload)!!.currentConversation.token!!
+            val isOwnerOrModerator = (payload as? MessagePayload)!!.currentConversation.isParticipantOwnerOrModerator
+
+            binding.bubble.setOnClickListener {
+                val pollVoteDialog = PollMainDialogFragment.newInstance(
+                    message.activeUser!!,
+                    roomToken,
+                    isOwnerOrModerator,
+                    pollId,
+                    pollName
+                )
+                pollVoteDialog.show(
+                    (binding.messagePollIcon.context as MainActivity).supportFragmentManager,
+                    TAG
+                )
+            }
+        }
+    }
+
+    private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
+        if (!message.isDeleted && message.parentMessage != null) {
+            val parentChatMessage = message.parentMessage
+            parentChatMessage!!.activeUser = message.activeUser
+            parentChatMessage.imageUrl?.let {
+                binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
+                binding.messageQuote.quotedMessageImage.load(it) {
+                    addHeader(
+                        "Authorization",
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    )
+                }
+            } ?: run {
+                binding.messageQuote.quotedMessageImage.visibility = View.GONE
+            }
+            binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
+                ?: context.getText(R.string.nc_nick_guest)
+            binding.messageQuote.quotedMessage.text = parentChatMessage.text
+            binding.messageQuote.quotedMessage.setTextColor(
+                context.resources.getColor(R.color.nc_outcoming_text_default)
+            )
+            binding.messageQuote.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.nc_grey))
+
+            binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+
+            binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+        } else {
+            binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+        }
+    }
+
+    private fun colorizeMessageBubble(message: ChatMessage) {
+        val resources = sharedApplication!!.resources
+        val bgBubbleColor = if (message.isDeleted) {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+        } else {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+        }
+        if (message.isGrouped) {
+            val bubbleDrawable = DisplayUtils.getMessageSelector(
+                bgBubbleColor,
+                resources.getColor(R.color.transparent),
+                bgBubbleColor,
+                R.drawable.shape_grouped_outcoming_message
+            )
+            ViewCompat.setBackground(bubble, bubbleDrawable)
+        } else {
+            val bubbleDrawable = DisplayUtils.getMessageSelector(
+                bgBubbleColor,
+                resources.getColor(R.color.transparent),
+                bgBubbleColor,
+                R.drawable.shape_outcoming_message
+            )
+            ViewCompat.setBackground(bubble, bubbleDrawable)
+        }
+    }
+
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
+    companion object {
+        private val TAG = NextcloudTalkApplication::class.java.simpleName
+    }
+}

+ 25 - 1
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -1,5 +1,5 @@
 /*
- * Nextcloud Talk application
+ *   Nextcloud Talk application
  *
  * @author Mario Danic
  * @author Marcel Hibbe
@@ -47,6 +47,7 @@ import com.nextcloud.talk.models.json.statuses.StatusesOverall;
 import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
+import com.nextcloud.talk.polls.repositories.model.PollOverall;
 
 import java.util.List;
 import java.util.Map;
@@ -526,4 +527,27 @@ public interface NcApi {
                                                           @Query("from") String fromUrl,
                                                           @Query("limit") Integer limit,
                                                           @Query("cursor") Integer cursor);
+
+    @GET
+    Observable<PollOverall> getPoll(@Header("Authorization") String authorization,
+                                    @Url String url);
+
+    @FormUrlEncoded
+    @POST
+    Observable<PollOverall> createPoll(@Header("Authorization") String authorization,
+                                       @Url String url,
+                                       @Query("question") String question,
+                                       @Field("options[]") List<String> options,
+                                       @Query("resultMode") Integer resultMode,
+                                       @Query("maxVotes") Integer maxVotes);
+
+    @FormUrlEncoded
+    @POST
+    Observable<PollOverall> votePoll(@Header("Authorization") String authorization,
+                                     @Url String url,
+                                     @Field("optionIds[]") List<Integer> optionIds);
+
+    @DELETE
+    Observable<PollOverall> closePoll(@Header("Authorization") String authorization,
+                                      @Url String url);
 }

+ 42 - 4
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -104,13 +104,16 @@ import com.nextcloud.talk.activities.CallActivity
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.activities.TakePhotoActivity
 import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
+import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
+import com.nextcloud.talk.adapters.messages.MessagePayload
 import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
+import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
 import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
@@ -139,6 +142,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomsOverall
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.mention.Mention
+import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
 import com.nextcloud.talk.presenters.MentionAutocompletePresenter
 import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
@@ -483,10 +487,12 @@ class ChatController(args: Bundle) :
             val messageHolders = MessageHolders()
             val profileBottomSheet = ProfileBottomSheet(ncApi!!, conversationUser!!, router)
 
+            val payload = MessagePayload(currentConversation!!, profileBottomSheet)
+
             messageHolders.setIncomingTextConfig(
                 MagicIncomingTextMessageViewHolder::class.java,
                 R.layout.item_custom_incoming_text_message,
-                profileBottomSheet
+                payload
             )
             messageHolders.setOutcomingTextConfig(
                 MagicOutcomingTextMessageViewHolder::class.java,
@@ -496,7 +502,7 @@ class ChatController(args: Bundle) :
             messageHolders.setIncomingImageConfig(
                 IncomingPreviewMessageViewHolder::class.java,
                 R.layout.item_custom_incoming_preview_message,
-                profileBottomSheet
+                payload
             )
 
             messageHolders.setOutcomingImageConfig(
@@ -525,7 +531,7 @@ class ChatController(args: Bundle) :
             messageHolders.registerContentType(
                 CONTENT_TYPE_LOCATION,
                 IncomingLocationMessageViewHolder::class.java,
-                profileBottomSheet,
+                payload,
                 R.layout.item_custom_incoming_location_message,
                 OutcomingLocationMessageViewHolder::class.java,
                 null,
@@ -536,7 +542,7 @@ class ChatController(args: Bundle) :
             messageHolders.registerContentType(
                 CONTENT_TYPE_VOICE_MESSAGE,
                 IncomingVoiceMessageViewHolder::class.java,
-                profileBottomSheet,
+                payload,
                 R.layout.item_custom_incoming_voice_message,
                 OutcomingVoiceMessageViewHolder::class.java,
                 null,
@@ -544,6 +550,17 @@ class ChatController(args: Bundle) :
                 this
             )
 
+            messageHolders.registerContentType(
+                CONTENT_TYPE_POLL,
+                IncomingPollMessageViewHolder::class.java,
+                payload,
+                R.layout.item_custom_incoming_poll_message,
+                OutcomingPollMessageViewHolder::class.java,
+                payload,
+                R.layout.item_custom_outcoming_poll_message,
+                this
+            )
+
             var senderId = ""
             if (!conversationUser?.userId.equals("?")) {
                 senderId = "users/" + conversationUser?.userId
@@ -2576,6 +2593,11 @@ class ChatController(args: Bundle) :
 
                 chatMessageIterator.remove()
             }
+
+            // delete poll system messages
+            else if (isPollVotedMessage(currentMessage)) {
+                chatMessageIterator.remove()
+            }
         }
         return chatMessageMap.values.toList()
     }
@@ -2591,6 +2613,10 @@ class ChatController(args: Bundle) :
             currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
     }
 
+    private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
+        return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
+    }
+
     private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
         if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
             Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
@@ -3012,6 +3038,7 @@ class ChatController(args: Bundle) :
         return when (type) {
             CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
             CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
+            CONTENT_TYPE_POLL -> message.isPoll()
             CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
             else -> false
@@ -3121,12 +3148,23 @@ class ChatController(args: Bundle) :
         }
     }
 
+    fun createPoll() {
+        val pollVoteDialog = PollCreateDialogFragment.newInstance(
+            roomToken!!
+        )
+        pollVoteDialog.show(
+            (activity as MainActivity?)!!.supportFragmentManager,
+            TAG
+        )
+    }
+
     companion object {
         private const val TAG = "ChatController"
         private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
         private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
         private const val CONTENT_TYPE_LOCATION: Byte = 3
         private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
+        private const val CONTENT_TYPE_POLL: Byte = 5
         private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
         private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
         private const val LOBBY_TIMER_DELAY: Long = 5000

+ 8 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt

@@ -29,6 +29,8 @@ import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
 import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl
 import com.nextcloud.talk.data.user.UsersRepository
 import com.nextcloud.talk.data.user.UsersRepositoryImpl
+import com.nextcloud.talk.polls.repositories.PollRepository
+import com.nextcloud.talk.polls.repositories.PollRepositoryImpl
 import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository
 import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl
 import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
@@ -36,6 +38,7 @@ import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl
 import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
 import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
 import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import dagger.Module
 import dagger.Provides
 import okhttp3.OkHttpClient
@@ -52,6 +55,11 @@ class RepositoryModule {
         return UnifiedSearchRepositoryImpl(ncApi, userProvider)
     }
 
+    @Provides
+    fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository {
+        return PollRepositoryImpl(ncApi, userProvider)
+    }
+
     @Provides
     fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider):
         RemoteFileBrowserItemsRepository {

+ 24 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

@@ -25,6 +25,10 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
 import com.nextcloud.talk.messagesearch.MessageSearchViewModel
+import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
+import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
+import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
+import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
 import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
 import dagger.Binds
 import dagger.MapKey
@@ -61,6 +65,26 @@ abstract class ViewModelModule {
     @ViewModelKey(MessageSearchViewModel::class)
     abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(PollMainViewModel::class)
+    abstract fun pollViewModel(viewModel: PollMainViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(PollVoteViewModel::class)
+    abstract fun pollVoteViewModel(viewModel: PollVoteViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(PollResultsViewModel::class)
+    abstract fun pollResultsViewModel(viewModel: PollResultsViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(PollCreateViewModel::class)
+    abstract fun pollCreateViewModel(viewModel: PollCreateViewModel): ViewModel
+
     @Binds
     @IntoMap
     @ViewModelKey(RemoteFileBrowserItemsViewModel::class)

+ 34 - 2
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt

@@ -124,6 +124,8 @@ data class ChatMessage(
 
     var voiceMessageDownloadProgress: Int = 0,
 ) : Parcelable, MessageContentType, MessageContentType.Image {
+
+    // messageTypesToIgnore is weird. must be deleted by refactoring!!!
     @JsonIgnore
     var messageTypesToIgnore = Arrays.asList(
         MessageType.REGULAR_TEXT_MESSAGE,
@@ -132,7 +134,8 @@ data class ChatMessage(
         MessageType.SINGLE_LINK_AUDIO_MESSAGE,
         MessageType.SINGLE_LINK_MESSAGE,
         MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
-        MessageType.VOICE_MESSAGE
+        MessageType.VOICE_MESSAGE,
+        MessageType.POLL_MESSAGE
     )
 
     fun hasFileAttachment(): Boolean {
@@ -165,6 +168,21 @@ data class ChatMessage(
         return false
     }
 
+    fun isPoll(): Boolean {
+        if (messageParameters != null && messageParameters!!.size > 0) {
+            for ((_, individualHashMap) in messageParameters!!) {
+                if (MessageDigest.isEqual(
+                        individualHashMap["type"]!!.toByteArray(),
+                        "talk-poll".toByteArray()
+                    )
+                ) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
     override fun getImageUrl(): String? {
         if (messageParameters != null && messageParameters!!.size > 0) {
             for ((_, individualHashMap) in messageParameters!!) {
@@ -207,6 +225,8 @@ data class ChatMessage(
             MessageType.SINGLE_NC_ATTACHMENT_MESSAGE
         } else if (hasGeoLocation()) {
             MessageType.SINGLE_NC_GEOLOCATION_MESSAGE
+        } else if (isPoll()) {
+            MessageType.POLL_MESSAGE
         } else {
             MessageType.REGULAR_TEXT_MESSAGE
         }
@@ -334,6 +354,15 @@ data class ChatMessage(
                             getNullsafeActorDisplayName()
                         )
                     }
+                } else if (MessageType.POLL_MESSAGE == getCalculateMessageType()) {
+                    return if (actorId == activeUser!!.userId) {
+                        sharedApplication!!.getString(R.string.nc_sent_poll_you)
+                    } else {
+                        String.format(
+                            sharedApplication!!.resources.getString(R.string.nc_sent_an_image),
+                            getNullsafeActorDisplayName()
+                        )
+                    }
                 }
             }
             return ""
@@ -410,6 +439,7 @@ data class ChatMessage(
         SINGLE_LINK_AUDIO_MESSAGE,
         SINGLE_NC_ATTACHMENT_MESSAGE,
         SINGLE_NC_GEOLOCATION_MESSAGE,
+        POLL_MESSAGE,
         VOICE_MESSAGE
     }
 
@@ -460,7 +490,9 @@ data class ChatMessage(
         CLEARED_CHAT,
         REACTION,
         REACTION_DELETED,
-        REACTION_REVOKED
+        REACTION_REVOKED,
+        POLL_VOTED,
+        POLL_CLOSED
     }
 
     companion object {

+ 6 - 0
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt

@@ -65,6 +65,8 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERAT
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET
+import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_CLOSED
+import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_VOTED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED
@@ -167,6 +169,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             "reaction" -> REACTION
             "reaction_deleted" -> REACTION_DELETED
             "reaction_revoked" -> REACTION_REVOKED
+            "poll_voted" -> POLL_VOTED
+            "poll_closed" -> POLL_CLOSED
             else -> DUMMY
         }
     }
@@ -224,6 +228,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             REACTION -> return "reaction"
             REACTION_DELETED -> return "reaction_deleted"
             REACTION_REVOKED -> return "reaction_revoked"
+            POLL_VOTED -> return "poll_voted"
+            POLL_CLOSED -> return "poll_closed"
             else -> return ""
         }
     }

+ 25 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt

@@ -0,0 +1,25 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+class PollCreateOptionItem(
+    var pollOption: String
+)

+ 82 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt

@@ -0,0 +1,82 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.annotation.SuppressLint
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding
+import com.nextcloud.talk.utils.EmojiTextInputEditText
+
+class PollCreateOptionViewHolder(
+    private val binding: PollCreateOptionsItemBinding
+) : RecyclerView.ViewHolder(binding.root) {
+
+    lateinit var optionText: EmojiTextInputEditText
+    private var textListener: TextWatcher? = null
+
+    @SuppressLint("SetTextI18n")
+    fun bind(
+        pollCreateOptionItem: PollCreateOptionItem,
+        itemsListener: PollCreateOptionsItemListener,
+        position: Int,
+        focus: Boolean
+    ) {
+
+        textListener?.let {
+            binding.pollOptionText.removeTextChangedListener(it)
+        }
+
+        binding.pollOptionText.setText(pollCreateOptionItem.pollOption)
+
+        if (focus) {
+            itemsListener.requestFocus(binding.pollOptionText)
+        }
+
+        binding.pollOptionDelete.setOnClickListener {
+            itemsListener.onRemoveOptionsItemClick(pollCreateOptionItem, position)
+        }
+
+        textListener = getTextWatcher(pollCreateOptionItem, itemsListener)
+        binding.pollOptionText.addTextChangedListener(textListener)
+    }
+
+    private fun getTextWatcher(
+        pollCreateOptionItem: PollCreateOptionItem,
+        itemsListener: PollCreateOptionsItemListener
+    ) =
+        object : TextWatcher {
+            override fun afterTextChanged(s: Editable) {
+                // unused atm
+            }
+
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // unused atm
+            }
+
+            override fun onTextChanged(option: CharSequence, start: Int, before: Int, count: Int) {
+                pollCreateOptionItem.pollOption = option.toString()
+
+                itemsListener.onOptionsItemTextChanged(pollCreateOptionItem)
+            }
+        }
+}

+ 59 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt

@@ -0,0 +1,59 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding
+
+class PollCreateOptionsAdapter(
+    private val clickListener: PollCreateOptionsItemListener
+) : RecyclerView.Adapter<PollCreateOptionViewHolder>() {
+
+    internal var list: ArrayList<PollCreateOptionItem> = ArrayList<PollCreateOptionItem>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollCreateOptionViewHolder {
+        val itemBinding = PollCreateOptionsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+
+        return PollCreateOptionViewHolder(itemBinding)
+    }
+
+    override fun onBindViewHolder(holder: PollCreateOptionViewHolder, position: Int) {
+        val currentItem = list[position]
+        var focus = false
+
+        if (list.size - 1 == position && currentItem.pollOption.isBlank()) {
+            focus = true
+        }
+
+        holder.bind(currentItem, clickListener, position, focus)
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+
+    fun updateOptionsList(optionsList: ArrayList<PollCreateOptionItem>) {
+        list = optionsList
+        notifyDataSetChanged()
+    }
+}

+ 32 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt

@@ -0,0 +1,32 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.widget.EditText
+
+interface PollCreateOptionsItemListener {
+
+    fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int)
+
+    fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem)
+
+    fun requestFocus(textField: EditText)
+}

+ 39 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt

@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import com.nextcloud.talk.R
+
+data class PollResultHeaderItem(
+    val name: String,
+    val percent: Int,
+    val selfVoted: Boolean
+) : PollResultItem {
+
+    override fun getViewType(): Int {
+        return VIEW_TYPE
+    }
+
+    companion object {
+        // layout is used as view type for uniqueness
+        const val VIEW_TYPE: Int = R.layout.poll_result_header_item
+    }
+}

+ 47 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt

@@ -0,0 +1,47 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.annotation.SuppressLint
+import android.graphics.Typeface
+import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
+
+class PollResultHeaderViewHolder(
+    override val binding: PollResultHeaderItemBinding
+) : PollResultViewHolder(binding) {
+
+    @SuppressLint("SetTextI18n")
+    override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
+        val item = pollResultItem as PollResultHeaderItem
+
+        binding.root.setOnClickListener { clickListener.onClick() }
+
+        binding.pollOptionText.text = item.name
+        binding.pollOptionPercentText.text = "${item.percent}%"
+
+        if (item.selfVoted) {
+            binding.pollOptionText.setTypeface(null, Typeface.BOLD)
+            binding.pollOptionPercentText.setTypeface(null, Typeface.BOLD)
+        }
+
+        binding.pollOptionBar.progress = item.percent
+    }
+}

+ 25 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt

@@ -0,0 +1,25 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+interface PollResultItem {
+    fun getViewType(): Int
+}

+ 25 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt

@@ -0,0 +1,25 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+interface PollResultItemClickListener {
+    fun onClick()
+}

+ 30 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt

@@ -0,0 +1,30 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+
+abstract class PollResultViewHolder(
+    open val binding: ViewBinding
+) : RecyclerView.ViewHolder(binding.root) {
+    abstract fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener)
+}

+ 38 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import com.nextcloud.talk.R
+import com.nextcloud.talk.polls.model.PollDetails
+
+data class PollResultVoterItem(
+    val details: PollDetails
+) : PollResultItem {
+
+    override fun getViewType(): Int {
+        return VIEW_TYPE
+    }
+
+    companion object {
+        // layout is used as view type for uniqueness
+        const val VIEW_TYPE: Int = R.layout.poll_result_voter_item
+    }
+}

+ 87 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt

@@ -0,0 +1,87 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.annotation.SuppressLint
+import android.text.TextUtils
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.PollResultVoterItemBinding
+import com.nextcloud.talk.polls.model.PollDetails
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+
+class PollResultVoterViewHolder(
+    private val user: User,
+    override val binding: PollResultVoterItemBinding
+) : PollResultViewHolder(binding) {
+
+    @SuppressLint("SetTextI18n")
+    override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
+        val item = pollResultItem as PollResultVoterItem
+
+        binding.root.setOnClickListener { clickListener.onClick() }
+
+        binding.pollVoterName.text = item.details.actorDisplayName
+        binding.pollVoterAvatar.controller = getAvatarDraweeController(item.details)
+    }
+
+    private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
+        var draweeController: DraweeController? = null
+        if (pollDetail.actorType == "guests") {
+            var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
+            if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
+                displayName = pollDetail.actorDisplayName!!
+            }
+            draweeController = Fresco.newDraweeControllerBuilder()
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForGuestAvatar(
+                            user.baseUrl,
+                            displayName,
+                            false
+                        ),
+                        user
+                    )
+                )
+                .build()
+        } else if (pollDetail.actorType == "users") {
+            draweeController = Fresco.newDraweeControllerBuilder()
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForAvatar(
+                            user.baseUrl,
+                            pollDetail.actorId,
+                            false
+                        ),
+                        user
+                    )
+                )
+                .build()
+        }
+        return draweeController
+    }
+}

+ 38 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import com.nextcloud.talk.R
+import com.nextcloud.talk.polls.model.PollDetails
+
+data class PollResultVotersOverviewItem(
+    val detailsList: List<PollDetails>
+) : PollResultItem {
+
+    override fun getViewType(): Int {
+        return VIEW_TYPE
+    }
+
+    companion object {
+        // layout is used as view type for uniqueness
+        const val VIEW_TYPE: Int = R.layout.poll_result_voters_overview_item
+    }
+}

+ 141 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt

@@ -0,0 +1,141 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.annotation.SuppressLint
+import android.text.TextUtils
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.res.ResourcesCompat
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.generic.RoundingParams
+import com.facebook.drawee.interfaces.DraweeController
+import com.facebook.drawee.view.SimpleDraweeView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding
+import com.nextcloud.talk.polls.model.PollDetails
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+
+class PollResultVotersOverviewViewHolder(
+    private val user: User,
+    override val binding: PollResultVotersOverviewItemBinding
+) : PollResultViewHolder(binding) {
+
+    @SuppressLint("SetTextI18n")
+    override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
+        val item = pollResultItem as PollResultVotersOverviewItem
+
+        binding.root.setOnClickListener { clickListener.onClick() }
+
+        val layoutParams = LinearLayout.LayoutParams(
+            AVATAR_SIZE,
+            AVATAR_SIZE
+        )
+
+        var avatarsToDisplay = MAX_AVATARS
+        if (item.detailsList.size < avatarsToDisplay) {
+            avatarsToDisplay = item.detailsList.size
+        }
+        val shotsDots = item.detailsList.size > avatarsToDisplay
+
+        for (i in 0 until avatarsToDisplay) {
+            val pollDetails = item.detailsList[i]
+            val avatar = SimpleDraweeView(binding.root.context)
+
+            layoutParams.marginStart = i * AVATAR_OFFSET
+            avatar.layoutParams = layoutParams
+
+            avatar.translationZ = i.toFloat() * -1
+
+            val roundingParams = RoundingParams.fromCornersRadius(AVATAR_RADIUS)
+            roundingParams.roundAsCircle = true
+            roundingParams.borderColor = ResourcesCompat.getColor(
+                itemView.context.resources!!,
+                R.color.colorPrimary,
+                null
+            )
+            roundingParams.borderWidth = 2.0f
+
+            avatar.hierarchy.roundingParams = roundingParams
+            avatar.controller = getAvatarDraweeController(pollDetails)
+
+            binding.votersAvatarsOverviewWrapper.addView(avatar)
+
+            if (i == avatarsToDisplay - 1 && shotsDots) {
+                val dotsView = TextView(itemView.context)
+                layoutParams.marginStart = i * AVATAR_OFFSET + DOTS_OFFSET
+                dotsView.layoutParams = layoutParams
+                dotsView.text = DOTS_TEXT
+                binding.votersAvatarsOverviewWrapper.addView(dotsView)
+            }
+        }
+    }
+
+    private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
+        var draweeController: DraweeController? = null
+        if (pollDetail.actorType == "guests") {
+            var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
+            if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
+                displayName = pollDetail.actorDisplayName!!
+            }
+            draweeController = Fresco.newDraweeControllerBuilder()
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForGuestAvatar(
+                            user.baseUrl,
+                            displayName,
+                            false
+                        ),
+                        user
+                    )
+                )
+                .build()
+        } else if (pollDetail.actorType == "users") {
+            draweeController = Fresco.newDraweeControllerBuilder()
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForAvatar(
+                            user.baseUrl,
+                            pollDetail.actorId,
+                            false
+                        ),
+                        user
+                    )
+                )
+                .build()
+        }
+        return draweeController
+    }
+
+    companion object {
+        const val AVATAR_SIZE = 60
+        const val AVATAR_RADIUS = 5f
+        const val MAX_AVATARS = 10
+        const val AVATAR_OFFSET = AVATAR_SIZE - 10
+        const val DOTS_OFFSET = 70
+        const val DOTS_TEXT = "…"
+    }
+}

+ 90 - 0
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt

@@ -0,0 +1,90 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
+import com.nextcloud.talk.databinding.PollResultVoterItemBinding
+import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding
+
+class PollResultsAdapter(
+    private val user: User,
+    private val clickListener: PollResultItemClickListener,
+) : RecyclerView.Adapter<PollResultViewHolder>() {
+    internal var list: MutableList<PollResultItem> = ArrayList()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder {
+        var viewHolder: PollResultViewHolder? = null
+
+        when (viewType) {
+            PollResultHeaderItem.VIEW_TYPE -> {
+                val itemBinding = PollResultHeaderItemBinding.inflate(
+                    LayoutInflater.from(parent.context), parent,
+                    false
+                )
+                viewHolder = PollResultHeaderViewHolder(itemBinding)
+            }
+            PollResultVoterItem.VIEW_TYPE -> {
+                val itemBinding = PollResultVoterItemBinding.inflate(
+                    LayoutInflater.from(parent.context), parent,
+                    false
+                )
+                viewHolder = PollResultVoterViewHolder(user, itemBinding)
+            }
+            PollResultVotersOverviewItem.VIEW_TYPE -> {
+                val itemBinding = PollResultVotersOverviewItemBinding.inflate(
+                    LayoutInflater.from(parent.context), parent,
+                    false
+                )
+                viewHolder = PollResultVotersOverviewViewHolder(user, itemBinding)
+            }
+        }
+        return viewHolder!!
+    }
+
+    override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) {
+        when (holder.itemViewType) {
+            PollResultHeaderItem.VIEW_TYPE -> {
+                val pollResultItem = list[position]
+                holder.bind(pollResultItem as PollResultHeaderItem, clickListener)
+            }
+            PollResultVoterItem.VIEW_TYPE -> {
+                val pollResultItem = list[position]
+                holder.bind(pollResultItem as PollResultVoterItem, clickListener)
+            }
+            PollResultVotersOverviewItem.VIEW_TYPE -> {
+                val pollResultItem = list[position]
+                holder.bind(pollResultItem as PollResultVotersOverviewItem, clickListener)
+            }
+        }
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return list[position].getViewType()
+    }
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.model
+
+data class Poll(
+    val id: String,
+    val question: String?,
+    val options: List<String>?,
+    val votes: Map<String, Int>?,
+    val actorType: String?,
+    val actorId: String?,
+    val actorDisplayName: String?,
+    val status: Int,
+    val resultMode: Int,
+    val maxVotes: Int,
+    val votedSelf: List<Int>?,
+    val numVoters: Int,
+    val details: List<PollDetails>?
+) {
+    companion object {
+        const val STATUS_OPEN: Int = 0
+        const val STATUS_CLOSED: Int = 1
+        const val RESULT_MODE_PUBLIC: Int = 0
+        const val RESULT_MODE_HIDDEN: Int = 1
+    }
+}

+ 28 - 0
app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt

@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.model
+
+data class PollDetails(
+    val actorType: String?,
+    val actorId: String?,
+    val actorDisplayName: String?,
+    val optionId: Int
+)

+ 43 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt

@@ -0,0 +1,43 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.repositories
+
+import com.nextcloud.talk.polls.model.Poll
+import io.reactivex.Observable
+
+interface PollRepository {
+
+    fun createPoll(
+        roomToken: String,
+        question: String,
+        options: List<String>,
+        resultMode: Int,
+        maxVotes: Int
+    ): Observable<Poll>
+
+    fun getPoll(roomToken: String, pollId: String): Observable<Poll>
+
+    fun vote(roomToken: String, pollId: String, options: List<Int>): Observable<Poll>
+
+    fun closePoll(roomToken: String, pollId: String): Observable<Poll>
+}

+ 138 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt

@@ -0,0 +1,138 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.repositories
+
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.model.PollDetails
+import com.nextcloud.talk.polls.repositories.model.PollDetailsResponse
+import com.nextcloud.talk.polls.repositories.model.PollResponse
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import io.reactivex.Observable
+
+class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvider: CurrentUserProviderNew) :
+    PollRepository {
+
+    val currentUser: User = currentUserProvider.currentUser.blockingGet()
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+
+    override fun createPoll(
+        roomToken: String,
+        question: String,
+        options: List<String>,
+        resultMode: Int,
+        maxVotes:
+            Int
+    ): Observable<Poll> {
+        return ncApi.createPoll(
+            credentials,
+            ApiUtils.getUrlForPoll(
+                currentUser.baseUrl,
+                roomToken
+            ),
+            question,
+            options,
+            resultMode,
+            maxVotes
+        ).map { mapToPoll(it.ocs?.data!!) }
+    }
+
+    override fun getPoll(roomToken: String, pollId: String): Observable<Poll> {
+
+        return ncApi.getPoll(
+            credentials,
+            ApiUtils.getUrlForPoll(
+                currentUser.baseUrl,
+                roomToken,
+                pollId
+            ),
+        ).map { mapToPoll(it.ocs?.data!!) }
+    }
+
+    override fun vote(roomToken: String, pollId: String, options: List<Int>): Observable<Poll> {
+
+        return ncApi.votePoll(
+            credentials,
+            ApiUtils.getUrlForPoll(
+                currentUser.baseUrl,
+                roomToken,
+                pollId
+            ),
+            options
+        ).map { mapToPoll(it.ocs?.data!!) }
+    }
+
+    override fun closePoll(roomToken: String, pollId: String): Observable<Poll> {
+
+        return ncApi.closePoll(
+            credentials,
+            ApiUtils.getUrlForPoll(
+                currentUser.baseUrl,
+                roomToken,
+                pollId
+            ),
+        ).map { mapToPoll(it.ocs?.data!!) }
+    }
+
+    companion object {
+
+        private fun mapToPoll(pollResponse: PollResponse): Poll {
+            val pollDetails = pollResponse.details?.map { it -> mapToPollDetails(it) }
+
+            return Poll(
+                pollResponse.id,
+                pollResponse.question,
+                pollResponse.options,
+                convertVotes(pollResponse.votes),
+                pollResponse.actorType,
+                pollResponse.actorId,
+                pollResponse.actorDisplayName,
+                pollResponse.status,
+                pollResponse.resultMode,
+                pollResponse.maxVotes,
+                pollResponse.votedSelf,
+                pollResponse.numVoters,
+                pollDetails
+            )
+        }
+
+        private fun convertVotes(votes: Map<String, Int>?): Map<String, Int> {
+            val resultMap: MutableMap<String, Int> = HashMap()
+            votes?.forEach {
+                resultMap[it.key.replace("option-", "")] = it.value
+            }
+            return resultMap
+        }
+
+        private fun mapToPollDetails(pollDetailsResponse: PollDetailsResponse): PollDetails {
+            return PollDetails(
+                pollDetailsResponse.actorType,
+                pollDetailsResponse.actorId,
+                pollDetailsResponse.actorDisplayName,
+                pollDetailsResponse.optionId,
+            )
+        }
+    }
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.polls.repositories.model
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PollDetailsResponse(
+    @JsonField(name = ["actorType"])
+    var actorType: String? = null,
+
+    @JsonField(name = ["actorId"])
+    var actorId: String,
+
+    @JsonField(name = ["actorDisplayName"])
+    var actorDisplayName: String,
+
+    @JsonField(name = ["optionId"])
+    var optionId: Int,
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, "", "", 0)
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt

@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.polls.repositories.model
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PollOCS(
+    @JsonField(name = ["data"])
+    var data: PollResponse?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt

@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.polls.repositories.model
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PollOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: PollOCS? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 71 - 0
app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt

@@ -0,0 +1,71 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.polls.repositories.model
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PollResponse(
+    @JsonField(name = ["id"])
+    var id: String,
+
+    @JsonField(name = ["question"])
+    var question: String? = null,
+
+    @JsonField(name = ["options"])
+    var options: ArrayList<String>? = null,
+
+    @JsonField(name = ["votes"])
+    var votes: Map<String, Int>? = null,
+
+    @JsonField(name = ["actorType"])
+    var actorType: String? = null,
+
+    @JsonField(name = ["actorId"])
+    var actorId: String? = null,
+
+    @JsonField(name = ["actorDisplayName"])
+    var actorDisplayName: String? = null,
+
+    @JsonField(name = ["status"])
+    var status: Int = 0,
+
+    @JsonField(name = ["resultMode"])
+    var resultMode: Int = 0,
+
+    @JsonField(name = ["maxVotes"])
+    var maxVotes: Int = 0,
+
+    @JsonField(name = ["votedSelf"])
+    var votedSelf: ArrayList<Int>? = null,
+
+    @JsonField(name = ["numVoters"])
+    var numVoters: Int = 0,
+
+    @JsonField(name = ["details"])
+    var details: ArrayList<PollDetailsResponse>? = null,
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("id", null, null, null, null, null, null, 0, 0, 0, null, 0, null)
+}

+ 187 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt

@@ -0,0 +1,187 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogPollCreateBinding
+import com.nextcloud.talk.polls.adapters.PollCreateOptionItem
+import com.nextcloud.talk.polls.adapters.PollCreateOptionsAdapter
+import com.nextcloud.talk.polls.adapters.PollCreateOptionsItemListener
+import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollCreateDialogFragment : DialogFragment(), PollCreateOptionsItemListener {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private lateinit var binding: DialogPollCreateBinding
+    private lateinit var viewModel: PollCreateViewModel
+
+    private var adapter: PollCreateOptionsAdapter? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+        viewModel = ViewModelProvider(this, viewModelFactory)[PollCreateViewModel::class.java]
+        val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
+        viewModel.setData(roomToken)
+    }
+
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = DialogPollCreateBinding.inflate(LayoutInflater.from(context))
+
+        return AlertDialog.Builder(requireContext())
+            .setView(binding.root)
+            .create()
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewModel.options.observe(viewLifecycleOwner) { options -> adapter?.updateOptionsList(options) }
+
+        binding.pollCreateOptionsList.layoutManager = LinearLayoutManager(context)
+
+        adapter = PollCreateOptionsAdapter(this)
+        binding.pollCreateOptionsList.adapter = adapter
+
+        setupListeners()
+        setupStateObserver()
+    }
+
+    private fun setupListeners() {
+        binding.pollAddOptionsItem.setOnClickListener {
+            viewModel.addOption()
+            adapter?.itemCount?.minus(1)?.let { binding.pollCreateOptionsList.scrollToPosition(it) }
+        }
+
+        binding.pollDismiss.setOnClickListener {
+            dismiss()
+        }
+
+        binding.pollCreateQuestion.addTextChangedListener(object : TextWatcher {
+            override fun afterTextChanged(s: Editable) {
+                // unused atm
+            }
+
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // unused atm
+            }
+
+            override fun onTextChanged(question: CharSequence, start: Int, before: Int, count: Int) {
+                if (question.toString() != viewModel.question) {
+                    viewModel.setQuestion(question.toString())
+                }
+            }
+        })
+
+        binding.pollPrivatePollCheckbox.setOnClickListener {
+            viewModel.setPrivatePoll(binding.pollPrivatePollCheckbox.isChecked)
+        }
+
+        binding.pollMultipleAnswersCheckbox.setOnClickListener {
+            viewModel.setMultipleAnswer(binding.pollMultipleAnswersCheckbox.isChecked)
+        }
+
+        binding.pollCreateButton.setOnClickListener {
+            viewModel.createPoll()
+        }
+    }
+
+    private fun setupStateObserver() {
+        viewModel.viewState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is PollCreateViewModel.PollCreatedState -> dismiss()
+                is PollCreateViewModel.PollCreationFailedState -> showError()
+                is PollCreateViewModel.PollCreationState -> updateButtons(state)
+            }
+        }
+    }
+
+    private fun updateButtons(state: PollCreateViewModel.PollCreationState) {
+        binding.pollAddOptionsItem.isEnabled = state.enableAddOptionButton
+        binding.pollCreateButton.isEnabled = state.enableCreatePollButton
+    }
+
+    private fun showError() {
+        dismiss()
+        Log.e(TAG, "Failed to create poll")
+        Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+    }
+
+    override fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) {
+        viewModel.removeOption(pollCreateOptionItem)
+    }
+
+    override fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem) {
+        viewModel.optionsItemTextChanged()
+    }
+
+    override fun requestFocus(textField: EditText) {
+        if (binding.pollCreateQuestion.text.isBlank()) {
+            binding.pollCreateQuestion.requestFocus()
+        } else {
+            textField.requestFocus()
+        }
+    }
+
+    /**
+     * Fragment creator
+     */
+    companion object {
+        private val TAG = PollCreateDialogFragment::class.java.simpleName
+        private const val KEY_ROOM_TOKEN = "keyRoomToken"
+
+        @JvmStatic
+        fun newInstance(roomTokenParam: String): PollCreateDialogFragment {
+            val args = Bundle()
+            args.putString(KEY_ROOM_TOKEN, roomTokenParam)
+            val fragment = PollCreateDialogFragment()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 74 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt

@@ -0,0 +1,74 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import autodagger.AutoInjector
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogPollLoadingBinding
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollLoadingFragment : Fragment() {
+
+    private lateinit var binding: DialogPollLoadingBinding
+
+    var fragmentHeight = 0
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+        fragmentHeight = arguments?.getInt(KEY_FRAGMENT_HEIGHT)!!
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogPollLoadingBinding.inflate(inflater, container, false)
+        binding.root.layoutParams.height = fragmentHeight
+        return binding.root
+    }
+
+    companion object {
+        private val TAG = PollLoadingFragment::class.java.simpleName
+        private const val KEY_FRAGMENT_HEIGHT = "keyFragmentHeight"
+
+        @JvmStatic
+        fun newInstance(
+            fragmentHeight: Int
+        ): PollLoadingFragment {
+
+            val args = bundleOf(
+                KEY_FRAGMENT_HEIGHT to fragmentHeight,
+            )
+
+            val fragment = PollLoadingFragment()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 188 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt

@@ -0,0 +1,188 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.DialogPollMainBinding
+import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollMainDialogFragment : DialogFragment() {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private lateinit var binding: DialogPollMainBinding
+    private lateinit var viewModel: PollMainViewModel
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+        viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java]
+
+        val user: User = arguments?.getParcelable(KEY_USER_ENTITY)!!
+        val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
+        val isOwnerOrModerator = arguments?.getBoolean(KEY_OWNER_OR_MODERATOR)!!
+        val pollId = arguments?.getString(KEY_POLL_ID)!!
+        val pollTitle = arguments?.getString(KEY_POLL_TITLE)!!
+
+        viewModel.setData(user, roomToken, isOwnerOrModerator, pollId, pollTitle)
+    }
+
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = DialogPollMainBinding.inflate(LayoutInflater.from(context))
+
+        val dialog = AlertDialog.Builder(requireContext())
+            .setView(binding.root)
+            .create()
+
+        binding.messagePollTitle.text = viewModel.pollTitle
+
+        return dialog
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewModel.viewState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                PollMainViewModel.InitialState -> {}
+                is PollMainViewModel.PollVoteState -> {
+                    initVotersAmount(state.showVotersAmount, state.poll.numVoters, false)
+                    showVoteScreen()
+                }
+                is PollMainViewModel.PollResultState -> {
+                    initVotersAmount(state.showVotersAmount, state.poll.numVoters, true)
+                    showResultsScreen()
+                }
+                is PollMainViewModel.LoadingState -> {
+                    showLoadingScreen()
+                }
+                is PollMainViewModel.DismissDialogState -> {
+                    dismiss()
+                }
+                else -> {}
+            }
+        }
+    }
+
+    private fun showLoadingScreen() {
+        binding.root.post {
+            run() {
+                val fragmentHeight = binding.messagePollContentFragment.measuredHeight
+
+                val contentFragment = PollLoadingFragment.newInstance(fragmentHeight)
+                val transaction = childFragmentManager.beginTransaction()
+                transaction.replace(binding.messagePollContentFragment.id, contentFragment)
+                transaction.commit()
+            }
+        }
+    }
+
+    private fun showVoteScreen() {
+        val contentFragment = PollVoteFragment.newInstance()
+
+        val transaction = childFragmentManager.beginTransaction()
+        transaction.replace(binding.messagePollContentFragment.id, contentFragment)
+        transaction.commit()
+    }
+
+    private fun showResultsScreen() {
+        val contentFragment = PollResultsFragment.newInstance()
+
+        val transaction = childFragmentManager.beginTransaction()
+        transaction.replace(binding.messagePollContentFragment.id, contentFragment)
+        transaction.commit()
+    }
+
+    private fun initVotersAmount(showVotersAmount: Boolean, numVoters: Int, showResultSubtitle: Boolean) {
+        if (showVotersAmount) {
+            binding.pollVotesAmount.visibility = View.VISIBLE
+            binding.pollVotesAmount.text = String.format(
+                resources.getString(R.string.polls_amount_voters),
+                numVoters
+            )
+        } else {
+            binding.pollVotesAmount.visibility = View.GONE
+        }
+
+        if (showResultSubtitle) {
+            binding.pollResultsSubtitle.visibility = View.VISIBLE
+            binding.pollResultsSubtitleSeperator.visibility = View.VISIBLE
+        } else {
+            binding.pollResultsSubtitle.visibility = View.GONE
+            binding.pollResultsSubtitleSeperator.visibility = View.GONE
+        }
+    }
+
+    /**
+     * Fragment creator
+     */
+    companion object {
+        private const val KEY_USER_ENTITY = "keyUserEntity"
+        private const val KEY_ROOM_TOKEN = "keyRoomToken"
+        private const val KEY_OWNER_OR_MODERATOR = "keyIsOwnerOrModerator"
+        private const val KEY_POLL_ID = "keyPollId"
+        private const val KEY_POLL_TITLE = "keyPollTitle"
+
+        @JvmStatic
+        fun newInstance(
+            user: User,
+            roomTokenParam: String,
+            isOwnerOrModerator: Boolean,
+            pollId: String,
+            name: String
+        ): PollMainDialogFragment {
+
+            val args = bundleOf(
+                KEY_USER_ENTITY to user,
+                KEY_ROOM_TOKEN to roomTokenParam,
+                KEY_OWNER_OR_MODERATOR to isOwnerOrModerator,
+                KEY_POLL_ID to pollId,
+                KEY_POLL_TITLE to name
+            )
+
+            val fragment = PollMainDialogFragment()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 139 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt

@@ -0,0 +1,139 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogPollResultsBinding
+import com.nextcloud.talk.polls.adapters.PollResultItemClickListener
+import com.nextcloud.talk.polls.adapters.PollResultsAdapter
+import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
+import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollResultsFragment : Fragment(), PollResultItemClickListener {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private lateinit var parentViewModel: PollMainViewModel
+    lateinit var viewModel: PollResultsViewModel
+
+    lateinit var binding: DialogPollResultsBinding
+
+    private var adapter: PollResultsAdapter? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+        viewModel = ViewModelProvider(this, viewModelFactory)[PollResultsViewModel::class.java]
+        parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java]
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogPollResultsBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
+            if (state is PollMainViewModel.PollResultState) {
+                initAdapter()
+                viewModel.setPoll(state.poll)
+                initEditButton(state.showEditButton)
+                initEndPollButton(state.showEndPollButton)
+            }
+        }
+
+        viewModel.items.observe(viewLifecycleOwner) {
+            val adapter = PollResultsAdapter(parentViewModel.user, this).apply {
+                if (it != null) {
+                    list = it
+                }
+            }
+            binding.pollResultsList.adapter = adapter
+        }
+    }
+
+    private fun initAdapter() {
+        adapter = PollResultsAdapter(parentViewModel.user, this)
+        binding.pollResultsList.adapter = adapter
+        binding.pollResultsList.layoutManager = LinearLayoutManager(context)
+    }
+
+    private fun initEditButton(showEditButton: Boolean) {
+        if (showEditButton) {
+            binding.editVoteButton.visibility = View.VISIBLE
+            binding.editVoteButton.setOnClickListener {
+                parentViewModel.editVotes()
+            }
+        } else {
+            binding.editVoteButton.visibility = View.GONE
+        }
+    }
+
+    private fun initEndPollButton(showEndPollButton: Boolean) {
+        if (showEndPollButton) {
+            binding.pollResultsEndPollButton.visibility = View.VISIBLE
+            binding.pollResultsEndPollButton.setOnClickListener {
+                AlertDialog.Builder(requireContext())
+                    .setTitle(R.string.polls_end_poll)
+                    .setMessage(R.string.polls_end_poll_confirm)
+                    .setPositiveButton(R.string.polls_end_poll) { _, _ ->
+                        parentViewModel.endPoll()
+                    }
+                    .setNegativeButton(R.string.nc_cancel, null)
+                    .show()
+            }
+        } else {
+            binding.pollResultsEndPollButton.visibility = View.GONE
+        }
+    }
+
+    override fun onClick() {
+        viewModel.toggleDetails()
+    }
+
+    companion object {
+        @JvmStatic
+        fun newInstance(): PollResultsFragment {
+            return PollResultsFragment()
+        }
+    }
+}

+ 219 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt

@@ -0,0 +1,219 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.ui
+
+import android.graphics.Typeface
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.LinearLayout
+import android.widget.RadioButton
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogPollVoteBinding
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
+import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollVoteFragment : Fragment() {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private lateinit var parentViewModel: PollMainViewModel
+    lateinit var viewModel: PollVoteViewModel
+
+    private lateinit var binding: DialogPollVoteBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+        viewModel = ViewModelProvider(this, viewModelFactory)[PollVoteViewModel::class.java]
+
+        parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java]
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogPollVoteBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
+            if (state is PollMainViewModel.PollVoteState) {
+                initPollOptions(state.poll)
+                initEndPollButton(state.showEndPollButton)
+                updateSubmitButton()
+                updateDismissEditButton(state.showDismissEditButton)
+            }
+        }
+
+        viewModel.viewState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                PollVoteViewModel.InitialState -> {}
+                is PollVoteViewModel.PollVoteFailedState -> {
+                    Log.e(TAG, "Failed to vote on poll.")
+                    Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+                }
+                is PollVoteViewModel.PollVoteHiddenSuccessState -> {
+                    Toast.makeText(context, R.string.polls_voted_hidden_success, Toast.LENGTH_LONG).show()
+                    parentViewModel.dismissDialog()
+                }
+                is PollVoteViewModel.PollVoteSuccessState -> {
+                    parentViewModel.voted()
+                }
+            }
+        }
+
+        viewModel.submitButtonEnabled.observe(viewLifecycleOwner) { enabled ->
+            binding.pollVoteSubmitButton.isEnabled = enabled
+        }
+
+        binding.pollVoteRadioGroup.setOnCheckedChangeListener { _, checkedId ->
+            viewModel.selectOption(checkedId, true)
+            updateSubmitButton()
+        }
+
+        binding.pollVoteSubmitButton.setOnClickListener {
+            viewModel.vote(parentViewModel.roomToken, parentViewModel.pollId)
+        }
+
+        binding.pollVoteEditDismiss.setOnClickListener {
+            parentViewModel.dismissEditVotes()
+        }
+    }
+
+    private fun updateDismissEditButton(showDismissEditButton: Boolean) {
+        if (showDismissEditButton) {
+            binding.pollVoteEditDismiss.visibility = View.VISIBLE
+        } else {
+            binding.pollVoteEditDismiss.visibility = View.GONE
+        }
+    }
+
+    private fun initPollOptions(poll: Poll) {
+        poll.votedSelf?.let { viewModel.initVotedOptions(it as ArrayList<Int>) }
+
+        if (poll.maxVotes == 1) {
+            binding.pollVoteRadioGroup.removeAllViews()
+            poll.options?.map { option ->
+                RadioButton(context).apply { text = option }
+            }?.forEachIndexed { index, radioButton ->
+                radioButton.id = index
+                makeOptionBoldIfSelfVoted(radioButton, poll, index)
+                binding.pollVoteRadioGroup.addView(radioButton)
+
+                radioButton.isChecked = viewModel.selectedOptions.contains(index) == true
+            }
+        } else {
+            binding.voteOptionsCheckboxesWrapper.removeAllViews()
+
+            val layoutParams = LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+            layoutParams.marginStart = CHECKBOX_MARGIN_LEFT
+
+            poll.options?.map { option ->
+                CheckBox(context).apply {
+                    text = option
+                    setLayoutParams(layoutParams)
+                }
+            }?.forEachIndexed { index, checkBox ->
+                checkBox.id = index
+                makeOptionBoldIfSelfVoted(checkBox, poll, index)
+                binding.voteOptionsCheckboxesWrapper.addView(checkBox)
+
+                checkBox.isChecked = viewModel.selectedOptions.contains(index) == true
+                checkBox.setOnCheckedChangeListener { _, isChecked ->
+                    if (isChecked) {
+                        if (poll.maxVotes == UNLIMITED_VOTES || viewModel.selectedOptions.size < poll.maxVotes) {
+                            viewModel.selectOption(index, false)
+                        } else {
+                            checkBox.isChecked = false
+                            Toast.makeText(context, R.string.polls_max_votes_reached, Toast.LENGTH_LONG).show()
+                        }
+                    } else {
+                        viewModel.deSelectOption(index)
+                    }
+                    updateSubmitButton()
+                }
+            }
+        }
+    }
+
+    private fun updateSubmitButton() {
+        viewModel.updateSubmitButton()
+    }
+
+    private fun makeOptionBoldIfSelfVoted(button: CompoundButton, poll: Poll, index: Int) {
+        if (poll.votedSelf?.contains(index) == true) {
+            button.setTypeface(null, Typeface.BOLD)
+        }
+    }
+
+    private fun initEndPollButton(showEndPollButton: Boolean) {
+        if (showEndPollButton) {
+            binding.pollVoteEndPollButton.visibility = View.VISIBLE
+            binding.pollVoteEndPollButton.setOnClickListener {
+                AlertDialog.Builder(requireContext())
+                    .setTitle(R.string.polls_end_poll)
+                    .setMessage(R.string.polls_end_poll_confirm)
+                    .setPositiveButton(R.string.polls_end_poll) { _, _ ->
+                        parentViewModel.endPoll()
+                    }
+                    .setNegativeButton(R.string.nc_cancel, null)
+                    .show()
+            }
+        } else {
+            binding.pollVoteEndPollButton.visibility = View.GONE
+        }
+    }
+
+    companion object {
+        private val TAG = PollVoteFragment::class.java.simpleName
+        private const val UNLIMITED_VOTES = 0
+        private const val CHECKBOX_MARGIN_LEFT = -18
+
+        @JvmStatic
+        fun newInstance(): PollVoteFragment {
+            return PollVoteFragment()
+        }
+    }
+}

+ 204 - 0
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt

@@ -0,0 +1,204 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.viewmodels
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.polls.adapters.PollCreateOptionItem
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.repositories.PollRepository
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class PollCreateViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
+
+    private lateinit var roomToken: String
+
+    sealed interface ViewState
+    open class PollCreationState(val enableAddOptionButton: Boolean, val enableCreatePollButton: Boolean) : ViewState
+    object PollCreatedState : ViewState
+    object PollCreationFailedState : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(
+        PollCreationState(
+            enableAddOptionButton = true,
+            enableCreatePollButton = false
+        )
+    )
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    private var _options: MutableLiveData<ArrayList<PollCreateOptionItem>> =
+        MutableLiveData<ArrayList<PollCreateOptionItem>>()
+    val options: LiveData<ArrayList<PollCreateOptionItem>>
+        get() = _options
+
+    private var _question: String = ""
+    val question: String
+        get() = _question
+
+    private var _privatePoll: Boolean = false
+    val privatePoll: Boolean
+        get() = _privatePoll
+
+    private var _multipleAnswer: Boolean = false
+    val multipleAnswer: Boolean
+        get() = _multipleAnswer
+
+    private var disposable: Disposable? = null
+
+    init {
+        addOption()
+        addOption()
+    }
+
+    fun setData(roomToken: String) {
+        this.roomToken = roomToken
+        updateCreationState()
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        disposable?.dispose()
+    }
+
+    fun addOption() {
+        val item = PollCreateOptionItem("")
+        val currentOptions: ArrayList<PollCreateOptionItem> = _options.value ?: ArrayList()
+        currentOptions.add(item)
+        _options.value = currentOptions
+        updateCreationState()
+    }
+
+    fun removeOption(item: PollCreateOptionItem) {
+        val currentOptions: ArrayList<PollCreateOptionItem> = _options.value ?: ArrayList()
+        currentOptions.remove(item)
+        _options.value = currentOptions
+        updateCreationState()
+    }
+
+    fun createPoll() {
+        var maxVotes = 1
+        if (multipleAnswer) {
+            maxVotes = 0
+        }
+
+        var resultMode = 0
+        if (privatePoll) {
+            resultMode = 1
+        }
+
+        _options.value = _options.value?.filter { it.pollOption.isNotEmpty() } as ArrayList<PollCreateOptionItem>
+
+        if (_question.isNotEmpty() && _options.value?.isNotEmpty() == true) {
+            _viewState.value = PollCreationState(enableAddOptionButton = false, enableCreatePollButton = false)
+
+            repository.createPoll(
+                roomToken, _question, _options.value!!.map { it.pollOption }, resultMode,
+                maxVotes
+            )
+                .doOnSubscribe { disposable = it }
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(PollObserver())
+        }
+    }
+
+    fun setQuestion(question: String) {
+        _question = question
+        updateCreationState()
+    }
+
+    fun setPrivatePoll(checked: Boolean) {
+        _privatePoll = checked
+    }
+
+    fun setMultipleAnswer(checked: Boolean) {
+        _multipleAnswer = checked
+    }
+
+    fun optionsItemTextChanged() {
+        updateCreationState()
+    }
+
+    private fun updateCreationState() {
+        _viewState.value = PollCreationState(enableAddOptionButton(), enableCreatePollButton())
+    }
+
+    private fun enableCreatePollButton(): Boolean {
+        return _question.isNotEmpty() && atLeastTwoOptionsAreFilled()
+    }
+
+    private fun atLeastTwoOptionsAreFilled(): Boolean {
+        if (_options.value != null) {
+            var filledOptions = 0
+            _options.value?.forEach {
+                if (it.pollOption.isNotEmpty()) {
+                    filledOptions++
+                }
+                if (filledOptions >= 2) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    private fun enableAddOptionButton(): Boolean {
+        if (_options.value != null && _options.value?.size != 0) {
+            _options.value?.forEach {
+                if (it.pollOption.isBlank()) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+
+    inner class PollObserver : Observer<Poll> {
+
+        lateinit var poll: Poll
+
+        override fun onSubscribe(d: Disposable) = Unit
+
+        override fun onNext(response: Poll) {
+            poll = response
+        }
+
+        override fun onError(e: Throwable) {
+            Log.e(TAG, "Failed to create poll", e)
+            _viewState.value = PollCreationFailedState
+        }
+
+        override fun onComplete() {
+            _viewState.value = PollCreatedState
+        }
+    }
+
+    companion object {
+        private val TAG = PollCreateViewModel::class.java.simpleName
+    }
+}

+ 188 - 0
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt

@@ -0,0 +1,188 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.viewmodels
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.repositories.PollRepository
+import com.nextcloud.talk.utils.database.user.UserUtils
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class PollMainViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    lateinit var user: User
+    lateinit var roomToken: String
+    private var isOwnerOrModerator: Boolean = false
+    lateinit var pollId: String
+    lateinit var pollTitle: String
+
+    private var editVotes: Boolean = false
+
+    sealed interface ViewState
+    object InitialState : ViewState
+    object DismissDialogState : ViewState
+    object LoadingState : ViewState
+
+    open class PollVoteState(
+        val poll: Poll,
+        val showVotersAmount: Boolean,
+        val showEndPollButton: Boolean,
+        val showDismissEditButton: Boolean
+    ) : ViewState
+
+    open class PollResultState(
+        val poll: Poll,
+        val showVotersAmount: Boolean,
+        val showEndPollButton: Boolean,
+        val showEditButton: Boolean
+    ) : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    private var disposable: Disposable? = null
+
+    fun setData(user: User, roomToken: String, isOwnerOrModerator: Boolean, pollId: String, pollTitle: String) {
+        this.user = user
+        this.roomToken = roomToken
+        this.isOwnerOrModerator = isOwnerOrModerator
+        this.pollId = pollId
+        this.pollTitle = pollTitle
+
+        loadPoll()
+    }
+
+    fun voted() {
+        loadPoll()
+    }
+
+    fun editVotes() {
+        editVotes = true
+        loadPoll()
+    }
+
+    fun dismissEditVotes() {
+        loadPoll()
+    }
+
+    private fun loadPoll() {
+        _viewState.value = LoadingState
+        repository.getPoll(roomToken, pollId)
+            .doOnSubscribe { disposable = it }
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(PollObserver())
+    }
+
+    fun endPoll() {
+        _viewState.value = LoadingState
+        repository.closePoll(roomToken, pollId)
+            .doOnSubscribe { disposable = it }
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(PollObserver())
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        disposable?.dispose()
+    }
+
+    inner class PollObserver : Observer<Poll> {
+
+        lateinit var poll: Poll
+
+        override fun onSubscribe(d: Disposable) = Unit
+
+        override fun onNext(response: Poll) {
+            poll = response
+        }
+
+        override fun onError(e: Throwable) {
+            Log.e(TAG, "An error occurred: $e")
+        }
+
+        override fun onComplete() {
+            val showEndPollButton = showEndPollButton(poll)
+            val showVotersAmount = showVotersAmount(poll)
+
+            if (votedForOpenHiddenPoll(poll)) {
+                _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false)
+            } else if (editVotes && poll.status == Poll.STATUS_OPEN) {
+                _viewState.value = PollVoteState(poll, false, showEndPollButton, true)
+                editVotes = false
+            } else if (poll.status == Poll.STATUS_CLOSED || poll.votedSelf?.isNotEmpty() == true) {
+                val showEditButton = poll.status == Poll.STATUS_OPEN && poll.resultMode == Poll.RESULT_MODE_PUBLIC
+                _viewState.value = PollResultState(poll, showVotersAmount, showEndPollButton, showEditButton)
+            } else if (poll.votedSelf.isNullOrEmpty()) {
+                _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false)
+            } else {
+                Log.w(TAG, "unknown poll state")
+            }
+        }
+    }
+
+    private fun showEndPollButton(poll: Poll): Boolean {
+        return poll.status == Poll.STATUS_OPEN && (isPollCreatedByCurrentUser(poll) || isOwnerOrModerator)
+    }
+
+    private fun showVotersAmount(poll: Poll): Boolean {
+        return votedForPublicPoll(poll) ||
+            poll.status == Poll.STATUS_CLOSED ||
+            isOwnerOrModerator ||
+            isPollCreatedByCurrentUser(poll)
+    }
+
+    private fun votedForOpenHiddenPoll(poll: Poll): Boolean {
+        return poll.status == Poll.STATUS_OPEN &&
+            poll.resultMode == Poll.RESULT_MODE_HIDDEN &&
+            poll.votedSelf?.isNotEmpty() == true
+    }
+
+    private fun votedForPublicPoll(poll: Poll): Boolean {
+        return poll.resultMode == Poll.RESULT_MODE_PUBLIC &&
+            poll.votedSelf?.isNotEmpty() == true
+    }
+
+    private fun isPollCreatedByCurrentUser(poll: Poll): Boolean {
+        return userUtils.currentUser?.userId == poll.actorId
+    }
+
+    fun dismissDialog() {
+        _viewState.value = DismissDialogState
+    }
+
+    companion object {
+        private val TAG = PollMainViewModel::class.java.simpleName
+    }
+}

+ 128 - 0
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt

@@ -0,0 +1,128 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.polls.adapters.PollResultHeaderItem
+import com.nextcloud.talk.polls.adapters.PollResultItem
+import com.nextcloud.talk.polls.adapters.PollResultVoterItem
+import com.nextcloud.talk.polls.adapters.PollResultVotersOverviewItem
+import com.nextcloud.talk.polls.model.Poll
+import io.reactivex.disposables.Disposable
+import javax.inject.Inject
+
+class PollResultsViewModel @Inject constructor() : ViewModel() {
+
+    sealed interface ViewState
+    object InitialState : ViewState
+
+    private var _poll: Poll? = null
+    val poll: Poll?
+        get() = _poll
+
+    private var _itemsOverviewList: ArrayList<PollResultItem> = ArrayList()
+    private var _itemsDetailsList: ArrayList<PollResultItem> = ArrayList()
+
+    private var _items: MutableLiveData<ArrayList<PollResultItem>?> = MutableLiveData<ArrayList<PollResultItem>?>()
+    val items: MutableLiveData<ArrayList<PollResultItem>?>
+        get() = _items
+
+    private var disposable: Disposable? = null
+
+    override fun onCleared() {
+        super.onCleared()
+        disposable?.dispose()
+    }
+
+    fun setPoll(poll: Poll) {
+        _poll = poll
+        initPollResults(_poll!!)
+    }
+
+    private fun initPollResults(poll: Poll) {
+        _items.value = ArrayList()
+
+        var oneVoteInPercent = 0
+        if (poll.numVoters != 0) {
+            oneVoteInPercent = HUNDRED / poll.numVoters
+        }
+
+        poll.options?.forEachIndexed { index, option ->
+            val votersAmountForThisOption = getVotersAmountForOption(poll, index)
+            val optionsPercent = oneVoteInPercent * votersAmountForThisOption
+
+            val pollResultHeaderItem = PollResultHeaderItem(
+                option,
+                optionsPercent,
+                isOptionSelfVoted(poll, index)
+            )
+            _itemsOverviewList.add(pollResultHeaderItem)
+            _itemsDetailsList.add(pollResultHeaderItem)
+
+            val voters = poll.details?.filter { it.optionId == index }
+
+            if (!voters.isNullOrEmpty()) {
+                _itemsOverviewList.add(PollResultVotersOverviewItem(voters))
+            }
+
+            if (!voters.isNullOrEmpty()) {
+                voters.forEach {
+                    _itemsDetailsList.add(PollResultVoterItem(it))
+                }
+            }
+        }
+
+        _items.value = _itemsOverviewList
+    }
+
+    private fun getVotersAmountForOption(poll: Poll, index: Int): Int {
+        var votersAmountForThisOption: Int? = 0
+        if (poll.details != null) {
+            votersAmountForThisOption = poll.details.filter { it.optionId == index }.size
+        } else if (poll.votes != null) {
+            votersAmountForThisOption = poll.votes.filter { it.key.toInt() == index }[index.toString()]
+            if (votersAmountForThisOption == null) {
+                votersAmountForThisOption = 0
+            }
+        }
+        return votersAmountForThisOption!!
+    }
+
+    private fun isOptionSelfVoted(poll: Poll, index: Int): Boolean {
+        return poll.votedSelf?.contains(index) == true
+    }
+
+    fun toggleDetails() {
+        if (_items.value?.containsAll(_itemsDetailsList) == true) {
+            _items.value = _itemsOverviewList
+        } else {
+            _items.value = _itemsDetailsList
+        }
+    }
+
+    companion object {
+        private val TAG = PollResultsViewModel::class.java.simpleName
+        private const val HUNDRED = 100
+    }
+}

+ 133 - 0
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt

@@ -0,0 +1,133 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.polls.viewmodels
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.repositories.PollRepository
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class PollVoteViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
+
+    sealed interface ViewState
+    object InitialState : ViewState
+    open class PollVoteSuccessState : ViewState
+    open class PollVoteHiddenSuccessState : ViewState
+    open class PollVoteFailedState : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    private val _submitButtonEnabled: MutableLiveData<Boolean> = MutableLiveData()
+    val submitButtonEnabled: LiveData<Boolean>
+        get() = _submitButtonEnabled
+
+    private var disposable: Disposable? = null
+
+    private var _votedOptions: List<Int> = emptyList()
+    val votedOptions: List<Int>
+        get() = _votedOptions
+
+    private var _selectedOptions: List<Int> = emptyList()
+    val selectedOptions: List<Int>
+        get() = _selectedOptions
+
+    fun initVotedOptions(selectedOptions: List<Int>) {
+        _votedOptions = selectedOptions
+        _selectedOptions = selectedOptions
+    }
+
+    fun selectOption(option: Int, isRadioBox: Boolean) {
+        _selectedOptions = if (isRadioBox) {
+            listOf(option)
+        } else {
+            _selectedOptions.plus(option)
+        }
+    }
+
+    fun deSelectOption(option: Int) {
+        _selectedOptions = _selectedOptions.minus(option)
+    }
+
+    fun vote(roomToken: String, pollId: String) {
+        if (_selectedOptions.isNotEmpty()) {
+            _submitButtonEnabled.value = false
+
+            repository.vote(roomToken, pollId, _selectedOptions)
+                .doOnSubscribe { disposable = it }
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(PollObserver())
+        }
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        disposable?.dispose()
+    }
+
+    fun updateSubmitButton() {
+        val areSelectedOptionsDifferentToVotedOptions = !(
+            votedOptions.containsAll(selectedOptions) &&
+                selectedOptions.containsAll(votedOptions)
+            )
+
+        _submitButtonEnabled.value = areSelectedOptionsDifferentToVotedOptions && selectedOptions.isNotEmpty()
+    }
+
+    inner class PollObserver : Observer<Poll> {
+
+        lateinit var poll: Poll
+
+        override fun onSubscribe(d: Disposable) = Unit
+
+        override fun onNext(response: Poll) {
+            poll = response
+        }
+
+        override fun onError(e: Throwable) {
+            Log.e(TAG, "An error occurred: $e")
+            _viewState.value = PollVoteFailedState()
+        }
+
+        override fun onComplete() {
+            if (poll.resultMode == 1) {
+                _viewState.value = PollVoteHiddenSuccessState()
+            } else {
+                _viewState.value = PollVoteSuccessState()
+            }
+        }
+    }
+
+    companion object {
+        private val TAG = PollVoteViewModel::class.java.simpleName
+    }
+}

+ 4 - 1
app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt

@@ -28,7 +28,10 @@ import io.reactivex.Observable
 
 interface SharedItemsRepository {
 
-    fun media(parameters: Parameters, type: SharedItemType): Observable<SharedMediaItems>?
+    fun media(
+        parameters: Parameters,
+        type: SharedItemType
+    ): Observable<SharedMediaItems>?
 
     fun media(
         parameters: Parameters,

+ 19 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt

@@ -43,6 +43,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
         setContentView(dialogAttachmentBinding.root)
         window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
 
+        initItemsStrings()
+        initItemsVisibility()
+        initItemsClickListeners()
+    }
+
+    private fun initItemsStrings() {
         var serverName = CapabilitiesUtilNew.getServerName(chatController.conversationUser)
         dialogAttachmentBinding.txtAttachFileFromCloud.text = chatController.resources?.let {
             if (serverName.isNullOrEmpty()) {
@@ -50,7 +56,9 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
             }
             String.format(it.getString(R.string.nc_upload_from_cloud), serverName)
         }
+    }
 
+    private fun initItemsVisibility() {
         if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(
                 chatController.conversationUser,
                 "geo-location-sharing"
@@ -59,6 +67,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
             dialogAttachmentBinding.menuShareLocation.visibility = View.GONE
         }
 
+        if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(chatController.conversationUser, "talk-polls")) {
+            dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE
+        }
+    }
+
+    private fun initItemsClickListeners() {
         dialogAttachmentBinding.menuShareLocation.setOnClickListener {
             chatController.showShareLocationScreen()
             dismiss()
@@ -74,6 +88,11 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
             dismiss()
         }
 
+        dialogAttachmentBinding.menuAttachPoll.setOnClickListener {
+            chatController.createPoll()
+            dismiss()
+        }
+
         dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener {
             chatController.showBrowserScreen()
             dismiss()

+ 35 - 17
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -2,8 +2,10 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Marcel Hibbe
  * @author Tim Krüger
  * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -61,8 +63,8 @@ public class ApiUtils {
     }
 
     /**
-     * @deprecated This is only supported on API v1-3, in API v4+ please use
-     * {@link ApiUtils#getUrlForAttendees(int, String, String)} instead.
+     * @deprecated This is only supported on API v1-3, in API v4+ please use {@link ApiUtils#getUrlForAttendees(int,
+     * String, String)} instead.
      */
     @Deprecated
     public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) {
@@ -95,13 +97,13 @@ public class ApiUtils {
 
     public static String getUrlForFilePreviewWithRemotePath(String baseUrl, String remotePath, int px) {
         return baseUrl + "/index.php/core/preview.png?file="
-                + Uri.encode(remotePath, "UTF-8")
-                + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
+            + Uri.encode(remotePath, "UTF-8")
+            + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
     }
 
     public static String getUrlForFilePreviewWithFileId(String baseUrl, String fileId, int px) {
         return baseUrl + "/index.php/core/preview?fileId="
-                + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
+            + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
     }
 
     public static String getSharingUrl(String baseUrl) {
@@ -151,8 +153,8 @@ public class ApiUtils {
                 if (user.hasSpreedFeatureCapability("conversation-v2")) {
                     return version;
                 }
-                if (version == APIv1  &&
-                    user.hasSpreedFeatureCapability("mention-flag") &&
+                if (version == APIv1 &&
+                    user.hasSpreedFeatureCapability( "mention-flag") &&
                     !user.hasSpreedFeatureCapability("conversation-v4")) {
                     return version;
                 }
@@ -238,7 +240,7 @@ public class ApiUtils {
     }
 
     public static String getUrlForParticipants(int version, String baseUrl, String token) {
-        if (token == null || token.isEmpty()){
+        if (token == null || token.isEmpty()) {
             Log.e(TAG, "token was null or empty");
         }
         return getUrlForRoom(version, baseUrl, token) + "/participants";
@@ -287,6 +289,7 @@ public class ApiUtils {
     public static String getUrlForCall(int version, String baseUrl, String token) {
         return getUrlForApi(version, baseUrl) + "/call/" + token;
     }
+
     public static String getUrlForChat(int version, String baseUrl, String token) {
         return getUrlForApi(version, baseUrl) + "/chat/" + token;
     }
@@ -294,10 +297,11 @@ public class ApiUtils {
     public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) {
         return getUrlForChat(version, baseUrl, token) + "/mentions";
     }
+
     public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) {
         return getUrlForChat(version, baseUrl, token) + "/" + messageId;
     }
-    
+
     public static String getUrlForChatSharedItems(int version, String baseUrl, String token) {
         return getUrlForChat(version, baseUrl, token) + "/share";
     }
@@ -366,11 +370,11 @@ public class ApiUtils {
     }
 
     public static RetrofitBucket getRetrofitBucketForAddParticipantWithSource(
-            int version,
-            String baseUrl,
-            String token,
-            String source,
-            String id
+        int version,
+        String baseUrl,
+        String token,
+        String source,
+        String id
                                                                              ) {
         RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, id);
         retrofitBucket.getQueryMap().put("source", source);
@@ -417,7 +421,7 @@ public class ApiUtils {
 
     public static String getUrlPushProxy() {
         return NextcloudTalkApplication.Companion.getSharedApplication().
-                getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
+            getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
     }
 
     public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
@@ -448,8 +452,10 @@ public class ApiUtils {
         return getUrlForChat(version, baseUrl, roomToken) + "/share";
     }
 
-    public static String getUrlForHoverCard(String baseUrl, String userId) { return baseUrl + ocsApiVersion +
-        "/hovercard/v1/" + userId; }
+    public static String getUrlForHoverCard(String baseUrl, String userId) {
+        return baseUrl + ocsApiVersion +
+            "/hovercard/v1/" + userId;
+    }
 
     public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) {
         return getUrlForChat(version, baseUrl, roomToken) + "/read";
@@ -497,4 +503,16 @@ public class ApiUtils {
     public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) {
         return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search";
     }
+
+    public static String getUrlForPoll(String baseUrl,
+                                       String roomToken,
+                                       String pollId) {
+        return getUrlForPoll(baseUrl, roomToken) + "/" + pollId;
+    }
+
+    public static String getUrlForPoll(String baseUrl,
+                                       String roomToken) {
+        return baseUrl + ocsApiVersion + spreedApiVersion + "/poll/" + roomToken;
+    }
+
 }

+ 10 - 0
app/src/main/res/drawable/ic_baseline_bar_chart_24.xml

@@ -0,0 +1,10 @@
+<vector android:height="24dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M5,9.2h3L8,19L5,19zM10.6,5h2.8v14h-2.8zM16.2,13L19,13v6h-2.8z" />
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_baseline_close_24.xml

@@ -0,0 +1,10 @@
+<vector android:height="24dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+</vector>

+ 0 - 29
app/src/main/res/drawable/ic_comment_white.xml

@@ -1,29 +0,0 @@
-<!--
-  ~ Nextcloud Talk application
-  ~  
-  ~ @author Mario Danic
-  ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
-  ~  
-  ~ This program is free software: you can redistribute it and/or modify
-  ~ it under the terms of the GNU General Public License as published by
-  ~ the Free Software Foundation, either version 3 of the License, or
-  ~ at your option) any later version.
-  ~  
-  ~ This program is distributed in the hope that it will be useful,
-  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
-  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  ~ GNU General Public License for more details.
-  ~  
-  ~ You should have received a copy of the GNU General Public License
-  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="32"
-    android:viewportHeight="32">
-  <path
-      android:fillColor="#FFFFFF"
-      android:pathData="M6.667,4C4.089,4 2,6.105 2,8.7v11.282c0,2.597 2.09,4.701 4.667,4.701 1.716,0.01 12.083,0.003 17.057,0 1.115,0.842 1.807,1.748 3.057,3.206a0.93,0.93 0,0 0,0.561 0.103,0.969 0.969,0 0,0 0.445,-0.187c0.302,-0.223 0.466,-0.603 0.427,-0.988l-0.314,-2.912a4.699,4.699 0,0 0,2.1 -3.923L30,8.701C30,6.105 27.91,4 25.333,4zM10.4,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21zM16,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21zM21.6,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21z"/>
-</vector>

+ 33 - 0
app/src/main/res/layout/dialog_attachment.xml

@@ -39,6 +39,39 @@
         android:textColor="@color/medium_emphasis_text"
         android:textSize="@dimen/bottom_sheet_text_size" />
 
+    <LinearLayout
+        android:id="@+id/menu_attach_poll"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingStart="@dimen/standard_padding"
+        android:paddingEnd="@dimen/standard_padding"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/menu_icon_attach_poll"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_baseline_bar_chart_24"
+            app:tint="@color/high_emphasis_menu_icon" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/txt_attach_poll"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
+            android:text="@string/nc_create_poll"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
     <LinearLayout
         android:id="@+id/menu_attach_contact"
         android:layout_width="match_parent"

+ 124 - 0
app/src/main/res/layout/dialog_poll_create.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="@dimen/standard_padding"
+    tools:background="@color/white">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@color/colorPrimary"
+            android:textStyle="bold"
+            android:text="@string/polls_question" />
+
+        <EditText
+            android:id="@+id/poll_create_question"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="textMultiLine"
+            tools:ignore="Autofill,LabelFor"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@color/colorPrimary"
+            android:textStyle="bold"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:text="@string/polls_options" />
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/poll_create_options_list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:listitem="@layout/poll_create_options_item" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/poll_add_options_item"
+            style="@style/OutlinedButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_half_margin"
+            app:icon="@drawable/ic_add_grey600_24px"
+            app:cornerRadius="@dimen/button_corner_radius"
+            app:layout_constraintEnd_toEndOf="parent"
+            android:text="@string/polls_add_option" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/colorPrimary"
+            android:textStyle="bold"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:layout_marginBottom="@dimen/standard_half_margin"
+            android:text="@string/polls_settings" />
+
+        <CheckBox
+            android:id="@+id/poll_private_poll_checkbox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_private_poll" />
+
+        <CheckBox
+            android:id="@+id/poll_multiple_answers_checkbox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_multiple_answers" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:gravity="end">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/poll_dismiss"
+                style="@style/OutlinedButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/standard_half_margin"
+                app:cornerRadius="@dimen/button_corner_radius"
+                app:layout_constraintEnd_toEndOf="parent"
+                android:text="@string/nc_common_dismiss" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/poll_create_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/standard_half_margin"
+                app:cornerRadius="@dimen/button_corner_radius"
+                app:layout_constraintEnd_toEndOf="parent"
+                android:text="@string/nc_create_poll"
+                android:theme="@style/Button.Primary" />
+
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>

+ 33 - 0
app/src/main/res/layout/dialog_poll_loading.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+<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="match_parent"
+    android:orientation="vertical"
+    android:gravity="center"
+    tools:background="@color/white">
+
+    <ProgressBar
+        android:layout_width="25dp"
+        android:layout_height="25dp">
+    </ProgressBar>
+
+</LinearLayout>

+ 95 - 0
app/src/main/res/layout/dialog_poll_main.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="@dimen/standard_padding"
+    android:orientation="vertical"
+    tools:background="@color/white">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/message_poll_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_baseline_bar_chart_24"
+            app:tint="@color/high_emphasis_menu_icon" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@+id/message_poll_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:textStyle="bold"
+            tools:text="This is the poll title?" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/poll_results_subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:textColor="@color/low_emphasis_text"
+            android:text="@string/polls_results_subtitle"
+            android:visibility="gone"
+            tools:visibility="visible"/>
+
+        <TextView
+            android:id="@+id/poll_results_subtitle_seperator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:textColor="@color/low_emphasis_text"
+            android:text=" - "
+            android:visibility="gone"
+            tools:visibility="visible"
+            tools:ignore="HardcodedText" />
+
+        <TextView
+            android:id="@+id/poll_votes_amount"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:textColor="@color/low_emphasis_text"
+            tools:text="93 votes" />
+
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/message_poll_content_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginTop="16dp"
+        android:layout_weight="1" />
+
+</LinearLayout>

+ 65 - 0
app/src/main/res/layout/dialog_poll_results.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:background="@color/white"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/poll_results_list_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/poll_results_list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:listitem="@layout/poll_result_header_item" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:gravity="end">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/poll_results_end_poll_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_end_poll"
+            style="@style/OutlinedButton"
+            android:layout_marginEnd="@dimen/standard_margin"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/edit_vote_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_edit_vote"
+            android:theme="@style/Button.Primary"
+            app:cornerRadius="@dimen/button_corner_radius" />
+    </LinearLayout>
+
+</LinearLayout>

+ 89 - 0
app/src/main/res/layout/dialog_poll_vote.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:background="@color/white">
+
+    <ScrollView
+        android:id="@+id/vote_options_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:id="@+id/vote_options_checkboxes_wrapper"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical" />
+
+            <RadioGroup
+                android:id="@+id/poll_vote_radio_group"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="-4dp"
+                tools:layout_height="400dp" />
+        </LinearLayout>
+    </ScrollView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:gravity="end">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/poll_vote_end_poll_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_end_poll"
+            style="@style/OutlinedButton"
+            android:layout_marginEnd="@dimen/standard_margin"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/poll_vote_edit_dismiss"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/nc_common_dismiss"
+            style="@style/OutlinedButton"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:visibility="gone"
+            app:cornerRadius="@dimen/button_corner_radius"
+            tools:visibility="visible"/>
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/poll_vote_submit_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/polls_submit_vote"
+            android:theme="@style/Button.Primary"
+            app:cornerRadius="@dimen/button_corner_radius" />
+    </LinearLayout>
+
+</LinearLayout>

+ 110 - 0
app/src/main/res/layout/item_custom_incoming_poll_message.xml

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@id/messageUserAvatar"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_alignParentTop="true"
+        android:layout_marginEnd="8dp"
+        app:roundAsCircle="true" />
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
+        android:layout_toEndOf="@id/messageUserAvatar"
+        android:orientation="vertical"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@+id/messageAuthor"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:textAlignment="viewStart"
+            android:textColor="@color/textColorMaxContrast"
+            android:textSize="12sp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@+id/message_poll_icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_baseline_bar_chart_24"
+                app:tint="@color/high_emphasis_menu_icon" />
+
+            <androidx.emoji.widget.EmojiTextView
+                android:id="@+id/message_poll_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textAlignment="viewStart"
+                android:textStyle="bold"
+                tools:text="This is the poll title?" />
+
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/message_poll_subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/double_margin_between_elements"
+            android:text="@string/message_poll_tap_to_open" />
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            tools:text="12:38" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 105 - 0
app/src/main/res/layout/item_custom_outcoming_poll_message.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentEnd="true"
+        android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@+id/message_poll_icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_baseline_bar_chart_24"
+                app:tint="@color/nc_outcoming_text_default" />
+
+            <androidx.emoji.widget.EmojiTextView
+                android:id="@+id/message_poll_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textAlignment="viewStart"
+                android:textStyle="bold"
+                android:textColor="@color/nc_outcoming_text_default"
+                tools:text="This is the poll title?" />
+
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/message_poll_subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/double_margin_between_elements"
+            android:text="@string/message_poll_tap_to_open"
+            android:textColor="@color/nc_outcoming_text_default" />
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            android:textColor="@color/nc_outcoming_text_default"
+            tools:text="10:35" />
+
+        <ImageView
+            android:id="@+id/checkMark"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageTime"
+            android:layout_marginStart="8dp"
+            android:textColor="@color/nc_outcoming_text_default"
+            app:layout_alignSelf="center"
+            android:contentDescription="@null" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 49 - 0
app/src/main/res/layout/poll_create_options_item.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    tools:background="@color/white">
+
+    <EditText
+        android:id="@+id/poll_option_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:singleLine="true"
+        android:inputType="text"
+        tools:ignore="Autofill,LabelFor" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/poll_option_delete"
+        style="@style/Widget.AppTheme.Button.IconButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="5dp"
+        android:contentDescription="@string/nc_action_open_main_menu"
+        app:cornerRadius="@dimen/button_corner_radius"
+        app:icon="@drawable/ic_baseline_close_24"
+        app:iconTint="@color/fontAppbar" />
+
+</LinearLayout>

+ 60 - 0
app/src/main/res/layout/poll_result_header_item.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:background="@color/white">
+
+    <TextView
+        android:id="@+id/poll_option_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Option Number One" />
+
+    <TextView
+        android:id="@+id/poll_option_percent_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/poll_option_text"
+        tools:text="50%" />
+
+    <com.google.android.material.progressindicator.LinearProgressIndicator
+        android:id="@+id/poll_option_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:indeterminate="false"
+        app:indicatorColor="@color/poll_bar_color"
+        app:layout_constraintStart_toStartOf="@+id/poll_option_text"
+        app:layout_constraintTop_toBottomOf="@+id/poll_option_text"
+        app:trackColor="@color/dialog_background"
+        app:trackCornerRadius="5dp"
+        app:trackThickness="5dp"
+        android:paddingBottom="@dimen/standard_half_padding"
+        tools:progress="50" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 44 - 0
app/src/main/res/layout/poll_result_voter_item.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="4dp"
+    tools:background="@color/white">
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@+id/poll_voter_avatar"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginEnd="8dp"
+        android:layout_gravity="center"
+        app:roundAsCircle="true" />
+
+    <TextView
+        android:id="@+id/poll_voter_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        tools:text="Bill Murray" />
+
+</LinearLayout>

+ 29 - 0
app/src/main/res/layout/poll_result_voters_overview_item.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/voters_avatars_overview_wrapper"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="4dp"
+    android:orientation="horizontal"
+    tools:background="@color/white">
+</RelativeLayout>

+ 3 - 0
app/src/main/res/values/colors.xml

@@ -106,6 +106,9 @@
     <color name="list_divider_background">#eeeeee</color>
     <color name="grey_200">#EEEEEE</color>
 
+    <!-- poll -->
+    <color name="poll_bar_color">#8dd4f6</color>
+
     <!-- this is just a helper for status icon background because getting the background color of a dialog is not
     possible?! don't use this to set the background of dialogs -->
     <color name="dialog_background">#FFFFFF</color>

+ 22 - 0
app/src/main/res/values/strings.xml

@@ -26,6 +26,7 @@
     <string name="nc_no">No</string>
     <string name="nc_common_skip">Skip</string>
     <string name="nc_common_set">Set</string>
+    <string name="nc_common_dismiss">Dismiss</string>
     <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
 
     <!-- Bottom Navigation -->
@@ -309,6 +310,7 @@
     <string name="nc_sent_an_audio" formatted="true">%1$s sent an audio.</string>
     <string name="nc_sent_a_video" formatted="true">%1$s sent a video.</string>
     <string name="nc_sent_an_image" formatted="true">%1$s sent an image.</string>
+    <string name="nc_sent_poll" formatted="true">%1$s sent a poll.</string>
     <string name="nc_sent_location" formatted="true">%1$s sent a location.</string>
     <string name="nc_sent_voice" formatted="true">%1$s sent a voice message.</string>
     <string name="nc_sent_a_link_you">You sent a link.</string>
@@ -317,6 +319,7 @@
     <string name="nc_sent_an_audio_you">You sent an audio.</string>
     <string name="nc_sent_a_video_you">You sent a video.</string>
     <string name="nc_sent_an_image_you">You sent an image.</string>
+    <string name="nc_sent_poll_you">You sent a poll.</string>
     <string name="nc_sent_location_you">You sent a location.</string>
     <string name="nc_sent_voice_you">You sent a voice message.</string>
     <string name="nc_formatted_message" translatable="false">%1$s: %2$s</string>
@@ -401,6 +404,7 @@
     <!-- Upload -->
     <string name="nc_add_file">Add to conversation</string>
     <string name="nc_upload_picture_from_cam">Take photo</string>
+    <string name="nc_create_poll">Create poll</string>
     <string name="nc_upload_from_cloud">Share from %1$s</string>
     <string name="nc_upload_failed">Sorry, upload failed</string>
     <string name="nc_upload_choose_local_files">Choose files</string>
@@ -527,6 +531,23 @@
     <string name="message_search_begin_typing">Start typing to search …</string>
     <string name="message_search_begin_empty">No search results</string>
 
+    <!-- Polls -->
+    <string name="message_poll_tap_to_open">Tap to open poll</string>
+    <string name="polls_amount_voters">%1$s votes</string>
+    <string name="polls_add_option">Add option</string>
+    <string name="polls_edit_vote">Edit vote</string>
+    <string name="polls_submit_vote">Vote</string>
+    <string name="polls_voted_hidden_success">Successfully voted</string>
+    <string name="polls_end_poll">End poll</string>
+    <string name="polls_end_poll_confirm">Do you really want to end this poll? This can\'t be undone.</string>
+    <string name="polls_max_votes_reached">You can\'t vote with more options for this poll.</string>
+    <string name="polls_results_subtitle">Results</string>
+    <string name="polls_question">Question</string>
+    <string name="polls_options">Options</string>
+    <string name="polls_settings">Settings</string>
+    <string name="polls_private_poll">Private poll</string>
+    <string name="polls_multiple_answers">Multiple answers</string>
+
     <string name="title_attachments">Attachments</string>
 
     <string name="reactions_tab_all">All</string>
@@ -534,4 +555,5 @@
     <string name="call_without_notification">Call without notification</string>
     <string name="set_avatar_from_camera">Set avatar from camera</string>
 
+
 </resources>

+ 2 - 0
app/src/main/res/values/styles.xml

@@ -147,6 +147,7 @@
         <item name="android:textColor">@color/white</item>
         <item name="android:typeface">sans</item>
         <item name="android:textStyle">bold</item>
+        <item name="android:layout_gravity">center_vertical</item>
     </style>
 
     <style name="Widget.AppTheme.Button.IconButton" parent="Widget.MaterialComponents.Button.TextButton">
@@ -268,6 +269,7 @@
         <item name="android:textAllCaps">false</item>
         <item name="android:typeface">sans</item>
         <item name="android:textStyle">bold</item>
+        <item name="android:layout_gravity">center_vertical</item>
     </style>
 
     <style name="TextAppearanceTab" parent="TextAppearance.Design.Tab">