浏览代码

wip: view poll results

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 2 年之前
父节点
当前提交
5a070f5e1b

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

@@ -25,6 +25,7 @@ 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.PollResultsViewModel
 import com.nextcloud.talk.polls.viewmodels.PollViewModel
 import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
 import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
@@ -73,6 +74,11 @@ abstract class ViewModelModule {
     @ViewModelKey(PollVoteViewModel::class)
     abstract fun pollVoteViewModel(viewModel: PollVoteViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(PollResultsViewModel::class)
+    abstract fun pollResultsViewModel(viewModel: PollResultsViewModel): ViewModel
+
     @Binds
     @IntoMap
     @ViewModelKey(RemoteFileBrowserItemsViewModel::class)

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

@@ -0,0 +1,8 @@
+package com.nextcloud.talk.polls.adapters
+
+class PollResultItem(
+    val pollOption: String,
+    val pollPercent: Int
+
+    // val voters....
+)

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

@@ -0,0 +1,5 @@
+package com.nextcloud.talk.polls.adapters
+
+interface PollResultItemClickListener {
+    fun onClick(pollResultItem: PollResultItem)
+}

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

@@ -0,0 +1,18 @@
+package com.nextcloud.talk.polls.adapters
+
+import android.annotation.SuppressLint
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.PollResultItemBinding
+
+class PollResultViewHolder(
+    private val binding: PollResultItemBinding
+) : RecyclerView.ViewHolder(binding.root) {
+
+    @SuppressLint("SetTextI18n")
+    fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
+        binding.root.setOnClickListener { clickListener.onClick(pollResultItem) }
+        binding.pollOptionText.text = pollResultItem.pollOption
+        binding.pollOptionPercentText.text = pollResultItem.pollPercent.toString() + "%"
+        binding.pollOptionBar.progress = pollResultItem.pollPercent
+    }
+}

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

@@ -0,0 +1,25 @@
+package com.nextcloud.talk.polls.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.PollResultItemBinding
+
+class PollResultsAdapter(
+    private val clickListener: PollResultItemClickListener
+) : RecyclerView.Adapter<PollResultViewHolder>() {
+    internal var list: MutableList<PollResultItem> = ArrayList<PollResultItem>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder {
+        val itemBinding = PollResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return PollResultViewHolder(itemBinding)
+    }
+
+    override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) {
+        holder.bind(list[position], clickListener)
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+}

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

@@ -4,7 +4,7 @@ data class Poll(
     val id: String,
     val question: String?,
     val options: List<String>?,
-    val votes: List<Int>?,
+    val votes: Map<String, Int>?,
     val actorType: String?,
     val actorId: String?,
     val actorDisplayName: String?,

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

@@ -49,7 +49,6 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
             ),
         ).map { mapToPoll(it.ocs?.data!!) }
 
-        // // // TODO actual api call
         // return Observable.just(
         //     Poll(
         //         id = "aaa",

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

@@ -37,7 +37,7 @@ data class PollResponse(
     var options: ArrayList<String>? = null,
 
     @JsonField(name = ["votes"])
-    var votes: ArrayList<Int>? = null,
+    var votes: Map<String, Int>? = null,
 
     @JsonField(name = ["actorType"])
     var actorType: String? = null,

+ 38 - 10
app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt

@@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider
 import autodagger.AutoInjector
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.DialogPollMainBinding
+import com.nextcloud.talk.polls.model.Poll
 import com.nextcloud.talk.polls.viewmodels.PollViewModel
 import javax.inject.Inject
 
@@ -58,16 +59,21 @@ class PollMainDialogFragment(
         viewModel.viewState.observe(viewLifecycleOwner) { state ->
             when (state) {
                 PollViewModel.InitialState -> {}
-                is PollViewModel.PollClosedState -> TODO()
-                is PollViewModel.PollOpenState -> {
-                    val contentFragment = PollVoteFragment(
-                        viewModel,
-                        roomToken,
-                        pollId
-                    )
-                    val transaction = childFragmentManager.beginTransaction()
-                    transaction.replace(binding.messagePollContentFragment.id, contentFragment)
-                    transaction.commit()
+
+                is PollViewModel.PollVotedState -> {
+                    if (state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) {
+                        showVoteFragment()
+                    } else {
+                        showResultsFragment()
+                    }
+                }
+
+                is PollViewModel.PollUnvotedState -> {
+                    if (state.poll.status == Poll.STATUS_CLOSED) {
+                        showResultsFragment()
+                    } else {
+                        showVoteFragment()
+                    }
                 }
             }
         }
@@ -75,6 +81,28 @@ class PollMainDialogFragment(
         viewModel.initialize(roomToken, pollId)
     }
 
+    private fun showVoteFragment() {
+        val contentFragment = PollVoteFragment(
+            viewModel,
+            roomToken,
+            pollId
+        )
+        val transaction = childFragmentManager.beginTransaction()
+        transaction.replace(binding.messagePollContentFragment.id, contentFragment)
+        transaction.commit()
+    }
+
+    private fun showResultsFragment() {
+        val contentFragment = PollResultsFragment(
+            viewModel,
+            roomToken,
+            pollId
+        )
+        val transaction = childFragmentManager.beginTransaction()
+        transaction.replace(binding.messagePollContentFragment.id, contentFragment)
+        transaction.commit()
+    }
+
     /**
      * Fragment creator
      */

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

@@ -0,0 +1,159 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * 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.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+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.PollResultItem
+import com.nextcloud.talk.polls.adapters.PollResultItemClickListener
+import com.nextcloud.talk.polls.adapters.PollResultsAdapter
+import com.nextcloud.talk.polls.model.Poll
+import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
+import com.nextcloud.talk.polls.viewmodels.PollViewModel
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PollResultsFragment(
+    private val parentViewModel: PollViewModel,
+    private val roomToken: String,
+    private val pollId: String
+) : Fragment(), PollResultItemClickListener {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    lateinit var viewModel: PollResultsViewModel
+
+    var _binding: DialogPollResultsBinding? = null
+    val binding: DialogPollResultsBinding
+        get() = _binding!!
+
+    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]
+    }
+
+    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)
+
+        adapter = PollResultsAdapter(this)
+        _binding?.pollResultsList?.adapter = adapter
+        _binding?.pollResultsList?.layoutManager = LinearLayoutManager(context)
+
+        parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
+            if (state is PollViewModel.PollVotedState &&
+                state.poll.resultMode == Poll.RESULT_MODE_PUBLIC
+            ) {
+
+                initPollResults(state.poll)
+                initAmountVotersInfo(state)
+                initEditButton(state)
+            } else if (state is PollViewModel.PollUnvotedState &&
+                state.poll.status == Poll.STATUS_CLOSED
+            ) {
+                Log.d(TAG, "show results also if self never voted")
+            }
+
+        }
+    }
+
+    private fun initPollResults(poll: Poll) {
+        if (poll.details != null) {
+            val votersAmount = poll.details.size
+            val oneVoteInPercent = 100 / votersAmount  // TODO: poll.numVoters   when fixed on api
+
+            poll.options?.forEachIndexed { index, option ->
+                val votersForThisOption = poll.details.filter { it.optionId == index }.size
+                val optionsPercent = oneVoteInPercent * votersForThisOption
+
+                val pollResultItem = PollResultItem(option, optionsPercent) // TODO add participants to PollResultItem
+                adapter?.list?.add(pollResultItem)
+            }
+        } else if (poll.votes != null) {
+            val votersAmount = poll.numVoters
+            val oneVoteInPercent = 100 / votersAmount
+
+            poll.options?.forEachIndexed { index, option ->
+                val votersForThisOption = poll.votes?.filter { it.value == index }?.size!!
+                val optionsPercent = oneVoteInPercent * votersForThisOption
+
+                val pollResultItem = PollResultItem(option, optionsPercent)
+                adapter?.list?.add(pollResultItem)
+            }
+        } else {
+            Log.e(TAG, "failed to get data to show poll results")
+        }
+    }
+
+    private fun initAmountVotersInfo(state: PollViewModel.PollVotedState) {
+        _binding?.pollAmountVoters?.text = String.format(
+            resources.getString(R.string.polls_amount_voters),
+            state.poll.numVoters
+        )
+    }
+
+    private fun initEditButton(state: PollViewModel.PollVotedState) {
+        if (state.poll.status == Poll.STATUS_OPEN && state.poll.resultMode == Poll.RESULT_MODE_PUBLIC) {
+            _binding?.editVoteButton?.visibility = View.VISIBLE
+            _binding?.editVoteButton?.setOnClickListener {
+                parentViewModel.edit()
+            }
+        } else {
+            _binding?.editVoteButton?.visibility = View.GONE
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    companion object {
+        private val TAG = PollResultsFragment::class.java.simpleName
+    }
+
+    override fun onClick(pollResultItem: PollResultItem) {
+        Log.d(TAG, "click..")
+    }
+}

+ 5 - 1
app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt

@@ -32,6 +32,7 @@ import androidx.lifecycle.ViewModelProvider
 import autodagger.AutoInjector
 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.PollViewModel
 import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
 import javax.inject.Inject
@@ -70,7 +71,7 @@ class PollVoteFragment(
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
-            if (state is PollViewModel.PollOpenState) {
+            if (state is PollViewModel.PollUnvotedState) {
                 val poll = state.poll
                 binding.radioGroup.removeAllViews()
                 poll.options?.map { option ->
@@ -79,6 +80,9 @@ class PollVoteFragment(
                     radioButton.id = index
                     binding.radioGroup.addView(radioButton)
                 }
+            } else if (state is PollViewModel.PollVotedState && state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) {
+                Log.d(TAG, "show vote screen also for resultMode hidden poll when already voted")
+                // TODO: other text for submit button
             }
         }
 

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

@@ -0,0 +1,50 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * 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.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.polls.repositories.PollRepository
+import io.reactivex.disposables.Disposable
+import javax.inject.Inject
+
+class PollResultsViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
+
+    sealed interface ViewState
+    object InitialState : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    private var disposable: Disposable? = null
+
+    override fun onCleared() {
+        super.onCleared()
+        disposable?.dispose()
+    }
+
+    companion object {
+        private val TAG = PollResultsViewModel::class.java.simpleName
+    }
+}

+ 15 - 7
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt

@@ -28,11 +28,12 @@ class PollViewModel @Inject constructor(private val repository: PollRepository)
     private lateinit var roomToken: String
     private lateinit var pollId: String
 
+    private var editPoll: Boolean = false
+
     sealed interface ViewState
     object InitialState : ViewState
-    open class PollOpenState(val poll: Poll) : ViewState
+    open class PollUnvotedState(val poll: Poll) : ViewState
     open class PollVotedState(val poll: Poll) : ViewState
-    open class PollClosedState(val poll: Poll) : ViewState
 
     private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
     val viewState: LiveData<ViewState>
@@ -51,6 +52,11 @@ class PollViewModel @Inject constructor(private val repository: PollRepository)
         loadPoll() // TODO load other view
     }
 
+    fun edit() {
+        editPoll = true
+        loadPoll()
+    }
+
     private fun loadPoll() {
         repository.getPoll(roomToken, pollId)
             ?.doOnSubscribe { disposable = it }
@@ -79,11 +85,13 @@ class PollViewModel @Inject constructor(private val repository: PollRepository)
         }
 
         override fun onComplete() {
-            // TODO check attributes and decide if poll is open/closed/selfvoted...
-
-            when (poll.status) {
-                Poll.STATUS_OPEN -> _viewState.value = PollOpenState(poll)
-                Poll.STATUS_CLOSED -> _viewState.value = PollClosedState(poll)
+            if (editPoll) {
+                _viewState.value = PollUnvotedState(poll)
+                editPoll = false
+            } else if (poll.votedSelf.isNullOrEmpty()) {
+                _viewState.value = PollUnvotedState(poll)
+            } else {
+                _viewState.value = PollVotedState(poll)
             }
         }
     }

+ 2 - 2
app/src/main/res/layout/dialog_poll_main.xml

@@ -38,9 +38,8 @@
     <androidx.emoji.widget.EmojiTextView
         android:id="@+id/message_poll_title"
         android:layout_width="257dp"
-        android:layout_height="55dp"
+        android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
-        android:textAlignment="viewStart"
         android:textStyle="bold"
         app:layout_constraintStart_toEndOf="@id/message_poll_icon"
         app:layout_constraintTop_toTopOf="@+id/message_poll_icon"
@@ -50,6 +49,7 @@
         android:id="@+id/message_poll_content_fragment"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
         app:layout_constraintTop_toBottomOf="@id/message_poll_title"
         tools:layout_height="400dp" />
 

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

@@ -0,0 +1,62 @@
+<!--
+  Nextcloud Android client 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 version 2,
+  as published by the Free Software Foundation.
+
+  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"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:padding="@dimen/standard_padding"
+    tools:background="@color/white">
+
+    <LinearLayout
+        android:id="@+id/poll_results_list_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="288dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <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_item" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/poll_amount_voters"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="24dp"
+        android:textColor="@color/low_emphasis_text"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/poll_results_list_wrapper"
+        tools:text="Poll results - 93 votes" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/edit_vote_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:text="@string/edit"
+        android:theme="@style/Button.Primary"
+        app:cornerRadius="@dimen/button_corner_radius"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/poll_results_list_wrapper" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 3 - 3
app/src/main/res/layout/dialog_poll_vote.xml

@@ -28,9 +28,9 @@
         android:layout_height="wrap_content"
         app:layout_constraintStart_toStartOf="parent"
         android:layout_width="wrap_content"
-        app:layout_constraintTop_toTopOf="parent">
-
-    </RadioGroup>
+        app:layout_constraintTop_toTopOf="parent"
+        tools:layout_height="400dp"
+        tools:layout_width="match_parent"></RadioGroup>
 
     <com.google.android.material.button.MaterialButton
         android:id="@+id/submitVote"

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

@@ -0,0 +1,33 @@
+<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"
+    android:paddingBottom="@dimen/standard_padding"
+    tools:background="@color/white">
+
+    <TextView
+        android:id="@+id/poll_option_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        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="parent"
+        tools:text="25 %" />
+
+    <ProgressBar
+        android:id="@+id/poll_option_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="@+id/poll_option_text"
+        app:layout_constraintTop_toBottomOf="@+id/poll_option_text"
+        style="?android:attr/progressBarStyleHorizontal" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -534,6 +534,9 @@
     <string name="message_search_begin_typing">Start typing to search …</string>
     <string name="message_search_begin_empty">No search results</string>
 
+    <!-- Polls -->
+    <string name="polls_amount_voters">Poll results - %1$s votes</string>
+
     <string name="title_attachments">Attachments</string>
 
     <string name="reactions_tab_all">All</string>