mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-24 05:55:39 +03:00
wip: create poll. show outgoing polls
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
0388f3e3f6
commit
af427f8300
23 changed files with 727 additions and 33 deletions
|
@ -24,27 +24,35 @@ package com.nextcloud.talk.adapters.messages
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
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.repositories.model.PollOverall
|
||||
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 io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingPollMessageViewHolder(outcomingView: View) : MessageHolders
|
||||
.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView) {
|
||||
class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : MessageHolders
|
||||
.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomOutcomingPollMessageBinding =
|
||||
ItemCustomOutcomingPollMessageBinding.bind(itemView)
|
||||
|
@ -57,9 +65,11 @@ class OutcomingPollMessageViewHolder(outcomingView: View) : MessageHolders
|
|||
@Inject
|
||||
var appPreferences: AppPreferences? = null
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
@Inject
|
||||
@JvmField
|
||||
var ncApi: NcApi? = null
|
||||
|
||||
lateinit var handler: Handler
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var reactionsInterface: ReactionsInterface
|
||||
|
||||
|
@ -98,6 +108,8 @@ class OutcomingPollMessageViewHolder(outcomingView: View) : MessageHolders
|
|||
|
||||
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
|
||||
|
||||
setPollPreview(message)
|
||||
|
||||
Reaction().showReactions(message, binding.reactions, binding.messageTime.context, true)
|
||||
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
|
||||
reactionsInterface.onClickReactions(message)
|
||||
|
@ -108,6 +120,73 @@ class OutcomingPollMessageViewHolder(outcomingView: View) : MessageHolders
|
|||
}
|
||||
}
|
||||
|
||||
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)!!.roomToken
|
||||
|
||||
binding.bubble.setOnClickListener {
|
||||
val pollVoteDialog = PollMainDialogFragment.newInstance(
|
||||
roomToken,
|
||||
pollId,
|
||||
pollName
|
||||
)
|
||||
pollVoteDialog.show(
|
||||
(binding.messagePollIcon.context as MainActivity).supportFragmentManager,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
|
||||
val credentials = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)
|
||||
ncApi!!.getPoll(
|
||||
credentials,
|
||||
ApiUtils.getUrlForPoll(
|
||||
message.activeUser?.baseUrl,
|
||||
roomToken,
|
||||
pollId
|
||||
)
|
||||
).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<PollOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(pollOverall: PollOverall) {
|
||||
if (pollOverall.ocs!!.data!!.status == 0) {
|
||||
binding.messagePollSubtitle.text =
|
||||
context?.resources?.getString(R.string.message_poll_tap_to_vote)
|
||||
} else {
|
||||
binding.messagePollSubtitle.text =
|
||||
context?.resources?.getString(R.string.message_poll_tap_see_results)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error while fetching poll", e)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (!message.isDeleted && message.parentMessage != null) {
|
||||
val parentChatMessage = message.parentMessage
|
||||
|
|
|
@ -532,9 +532,14 @@ public interface NcApi {
|
|||
Observable<PollOverall> getPoll(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<PollOverall> createPoll(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
@Url String url,
|
||||
@Query("question") String question,
|
||||
@Field("options[]") List<String> options,
|
||||
@Query("resultMode") Integer resultMode,
|
||||
@Query("maxVotes") Integer maxVotes);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
|
|
|
@ -142,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
|
||||
|
@ -3138,6 +3139,16 @@ 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
|
||||
|
|
|
@ -25,8 +25,9 @@ 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.PollViewModel
|
||||
import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
|
||||
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
|
||||
import dagger.Binds
|
||||
|
@ -66,8 +67,8 @@ abstract class ViewModelModule {
|
|||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(PollViewModel::class)
|
||||
abstract fun pollViewModel(viewModel: PollViewModel): ViewModel
|
||||
@ViewModelKey(PollMainViewModel::class)
|
||||
abstract fun pollViewModel(viewModel: PollMainViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
@ -79,6 +80,11 @@ abstract class ViewModelModule {
|
|||
@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)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.nextcloud.talk.polls.adapters
|
||||
|
||||
class PollCreateOptionItem(
|
||||
var pollOption: String
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
package com.nextcloud.talk.polls.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding
|
||||
|
||||
class PollCreateOptionViewHolder(
|
||||
private val binding: PollCreateOptionsItemBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(
|
||||
pollCreateOptionItem: PollCreateOptionItem,
|
||||
clickListener: PollCreateOptionsItemClickListener,
|
||||
position: Int
|
||||
) {
|
||||
// binding.root.setOnClickListener { }
|
||||
|
||||
binding.pollOptionDelete.setOnClickListener {
|
||||
clickListener.onDeleteClick(pollCreateOptionItem, position)
|
||||
}
|
||||
|
||||
// binding.pollOptionText.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(option: CharSequence, start: Int, before: Int, count: Int) {
|
||||
// pollCreateOptionItem.pollOption = option.toString()
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
// fun onBind(item: SharedItem) {
|
||||
// Log.d("","bbbb")
|
||||
// }
|
||||
//
|
||||
// fun onLongClick(view: View?): Boolean {
|
||||
// // moviesList.remove(getAdapterPosition())
|
||||
// // notifyItemRemoved(getAdapterPosition())
|
||||
//
|
||||
// Log.d("", "dfdrg")
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onClick(v: View?) {
|
||||
// Log.d("", "dfdrg")
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
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: PollCreateOptionsItemClickListener
|
||||
) : RecyclerView.Adapter<PollCreateOptionViewHolder>() {
|
||||
|
||||
internal var list: MutableList<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) {
|
||||
holder.bind(list[position], clickListener, position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return list.size
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.nextcloud.talk.polls.adapters
|
||||
|
||||
interface PollCreateOptionsItemClickListener {
|
||||
fun onDeleteClick(pollCreateOptionItem: PollCreateOptionItem, position: Int)
|
||||
}
|
|
@ -8,4 +8,12 @@ interface PollRepository {
|
|||
fun getPoll(roomToken: String, pollId: String): Observable<Poll>?
|
||||
|
||||
fun vote(roomToken: String, pollId: String, option: Int): Observable<Poll>?
|
||||
|
||||
fun createPoll(
|
||||
roomToken: String,
|
||||
question: String,
|
||||
options: List<String>,
|
||||
resultMode: Int,
|
||||
maxVotes: Int
|
||||
): Observable<Poll>?
|
||||
}
|
||||
|
|
|
@ -38,6 +38,24 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
|
|||
currentUserProvider.currentUser?.token
|
||||
)
|
||||
|
||||
override fun createPoll(
|
||||
roomToken: String, question: String, options: List<String>, resultMode: Int, maxVotes:
|
||||
Int
|
||||
):
|
||||
Observable<Poll>? {
|
||||
return ncApi.createPoll(
|
||||
credentials,
|
||||
ApiUtils.getUrlForPoll(
|
||||
currentUserProvider.currentUser?.baseUrl,
|
||||
roomToken
|
||||
),
|
||||
question,
|
||||
options,
|
||||
resultMode,
|
||||
maxVotes
|
||||
).map { mapToPoll(it.ocs?.data!!) }
|
||||
}
|
||||
|
||||
override fun getPoll(roomToken: String, pollId: String): Observable<Poll> {
|
||||
|
||||
return ncApi.getPoll(
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.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.PollCreateOptionsItemClickListener
|
||||
import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class PollCreateDialogFragment(
|
||||
private val roomToken: String
|
||||
) : DialogFragment(), PollCreateOptionsItemClickListener {
|
||||
|
||||
@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]
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
binding = DialogPollCreateBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
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)
|
||||
|
||||
adapter = PollCreateOptionsAdapter(this)
|
||||
binding?.pollCreateOptionsList?.adapter = adapter
|
||||
binding?.pollCreateOptionsList?.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
viewModel.initialize(roomToken)
|
||||
|
||||
for (i in 1..3) {
|
||||
val item = PollCreateOptionItem("a")
|
||||
adapter?.list?.add(item)
|
||||
}
|
||||
|
||||
binding.pollAddOption.setOnClickListener {
|
||||
val item = PollCreateOptionItem("a")
|
||||
adapter?.list?.add(item)
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
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) {
|
||||
viewModel.question = question.toString()
|
||||
}
|
||||
})
|
||||
|
||||
// binding.option1.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(option: CharSequence, start: Int, before: Int, count: Int) {
|
||||
// viewModel.options = listOf(option.toString())
|
||||
// }
|
||||
// })
|
||||
|
||||
binding.pollPrivatePollCheckbox.setOnClickListener {
|
||||
viewModel.multipleAnswer = binding.pollMultipleAnswersCheckbox.isChecked
|
||||
}
|
||||
|
||||
binding.pollMultipleAnswersCheckbox.setOnClickListener {
|
||||
viewModel.multipleAnswer = binding.pollMultipleAnswersCheckbox.isChecked
|
||||
}
|
||||
|
||||
binding.pollCreateButton.setOnClickListener {
|
||||
viewModel.createPoll()
|
||||
}
|
||||
|
||||
viewModel.viewState.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
PollCreateViewModel.InitialState -> {}
|
||||
|
||||
is PollCreateViewModel.PollCreatedState -> {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.initialize(roomToken)
|
||||
}
|
||||
|
||||
override fun onDeleteClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) {
|
||||
adapter?.list?.remove(pollCreateOptionItem)
|
||||
adapter?.notifyItemRemoved(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragment creator
|
||||
*/
|
||||
companion object {
|
||||
private val TAG = PollCreateDialogFragment::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
roomTokenParam: String
|
||||
): PollCreateDialogFragment = PollCreateDialogFragment(roomTokenParam)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ 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 com.nextcloud.talk.polls.viewmodels.PollMainViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
|
@ -27,13 +27,13 @@ class PollMainDialogFragment(
|
|||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private lateinit var binding: DialogPollMainBinding
|
||||
private lateinit var viewModel: PollViewModel
|
||||
private lateinit var viewModel: PollMainViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
viewModel = ViewModelProvider(this, viewModelFactory)[PollViewModel::class.java]
|
||||
viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java]
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
|
@ -58,9 +58,9 @@ class PollMainDialogFragment(
|
|||
|
||||
viewModel.viewState.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
PollViewModel.InitialState -> {}
|
||||
PollMainViewModel.InitialState -> {}
|
||||
|
||||
is PollViewModel.PollVotedState -> {
|
||||
is PollMainViewModel.PollVotedState -> {
|
||||
if (state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) {
|
||||
showVoteFragment()
|
||||
} else {
|
||||
|
@ -68,7 +68,7 @@ class PollMainDialogFragment(
|
|||
}
|
||||
}
|
||||
|
||||
is PollViewModel.PollUnvotedState -> {
|
||||
is PollMainViewModel.PollUnvotedState -> {
|
||||
if (state.poll.status == Poll.STATUS_CLOSED) {
|
||||
showResultsFragment()
|
||||
} else {
|
||||
|
|
|
@ -37,13 +37,13 @@ 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.PollMainViewModel
|
||||
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 parentViewModel: PollMainViewModel,
|
||||
private val roomToken: String,
|
||||
private val pollId: String
|
||||
) : Fragment(), PollResultItemClickListener {
|
||||
|
@ -82,14 +82,14 @@ class PollResultsFragment(
|
|||
_binding?.pollResultsList?.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
|
||||
if (state is PollViewModel.PollVotedState &&
|
||||
if (state is PollMainViewModel.PollVotedState &&
|
||||
state.poll.resultMode == Poll.RESULT_MODE_PUBLIC
|
||||
) {
|
||||
|
||||
initPollResults(state.poll)
|
||||
initAmountVotersInfo(state)
|
||||
initEditButton(state)
|
||||
} else if (state is PollViewModel.PollUnvotedState &&
|
||||
} else if (state is PollMainViewModel.PollUnvotedState &&
|
||||
state.poll.status == Poll.STATUS_CLOSED
|
||||
) {
|
||||
Log.d(TAG, "show results also if self never voted")
|
||||
|
@ -129,14 +129,14 @@ class PollResultsFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun initAmountVotersInfo(state: PollViewModel.PollVotedState) {
|
||||
private fun initAmountVotersInfo(state: PollMainViewModel.PollVotedState) {
|
||||
_binding?.pollAmountVoters?.text = String.format(
|
||||
resources.getString(R.string.polls_amount_voters),
|
||||
state.poll.numVoters
|
||||
)
|
||||
}
|
||||
|
||||
private fun initEditButton(state: PollViewModel.PollVotedState) {
|
||||
private fun initEditButton(state: PollMainViewModel.PollVotedState) {
|
||||
if (state.poll.status == Poll.STATUS_OPEN && state.poll.resultMode == Poll.RESULT_MODE_PUBLIC) {
|
||||
_binding?.editVoteButton?.visibility = View.VISIBLE
|
||||
_binding?.editVoteButton?.setOnClickListener {
|
||||
|
@ -147,6 +147,10 @@ class PollResultsFragment(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onClick(pollResultItem: PollResultItem) {
|
||||
Log.d(TAG, "click..")
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
@ -155,8 +159,4 @@ class PollResultsFragment(
|
|||
companion object {
|
||||
private val TAG = PollResultsFragment::class.java.simpleName
|
||||
}
|
||||
|
||||
override fun onClick(pollResultItem: PollResultItem) {
|
||||
Log.d(TAG, "click..")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,13 +33,13 @@ 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.PollMainViewModel
|
||||
import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class PollVoteFragment(
|
||||
private val parentViewModel: PollViewModel,
|
||||
private val parentViewModel: PollMainViewModel,
|
||||
private val roomToken: String,
|
||||
private val pollId: String
|
||||
) : Fragment() {
|
||||
|
@ -71,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.PollUnvotedState) {
|
||||
if (state is PollMainViewModel.PollUnvotedState) {
|
||||
val poll = state.poll
|
||||
binding.radioGroup.removeAllViews()
|
||||
poll.options?.map { option ->
|
||||
|
@ -80,7 +80,7 @@ class PollVoteFragment(
|
|||
radioButton.id = index
|
||||
binding.radioGroup.addView(radioButton)
|
||||
}
|
||||
} else if (state is PollViewModel.PollVotedState && state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) {
|
||||
} else if (state is PollMainViewModel.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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package com.nextcloud.talk.polls.viewmodels
|
||||
|
||||
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 PollCreateViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
|
||||
|
||||
private lateinit var roomToken: String
|
||||
|
||||
lateinit var question: String
|
||||
lateinit var options: List<String>
|
||||
var privatePoll: Boolean = false
|
||||
var multipleAnswer: Boolean = false
|
||||
|
||||
sealed interface ViewState
|
||||
object InitialState : ViewState
|
||||
open class PollCreatingState() : ViewState
|
||||
open class PollCreatedState() : ViewState
|
||||
open class PollCreationFailedState() : ViewState
|
||||
|
||||
private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
|
||||
val viewState: LiveData<ViewState>
|
||||
get() = _viewState
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun initialize(roomToken: String) {
|
||||
this.roomToken = roomToken
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
fun createPoll() {
|
||||
var maxVotes = 1
|
||||
if (multipleAnswer) {
|
||||
maxVotes = 0
|
||||
}
|
||||
|
||||
var resultMode = 0
|
||||
if (privatePoll) {
|
||||
resultMode = 1
|
||||
}
|
||||
|
||||
repository.createPoll(roomToken, question, options, resultMode, maxVotes)
|
||||
?.doOnSubscribe { disposable = it }
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(PollObserver())
|
||||
}
|
||||
|
||||
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) {
|
||||
_viewState.value = PollCreationFailedState()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
_viewState.value = PollCreatedState()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = PollCreateViewModel::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
|||
* InitialState --> PollClosedState
|
||||
* @enduml
|
||||
*/
|
||||
class PollViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
|
||||
class PollMainViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
|
||||
|
||||
private lateinit var roomToken: String
|
||||
private lateinit var pollId: String
|
||||
|
@ -97,6 +97,6 @@ class PollViewModel @Inject constructor(private val repository: PollRepository)
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = PollViewModel::class.java.simpleName
|
||||
private val TAG = PollMainViewModel::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -74,6 +74,11 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
|
|||
dismiss()
|
||||
}
|
||||
|
||||
dialogAttachmentBinding.menuAttachPoll.setOnClickListener {
|
||||
chatController.createPoll()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener {
|
||||
chatController.showBrowserScreen()
|
||||
dismiss()
|
||||
|
|
10
app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
|
@ -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>
|
|
@ -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"
|
||||
|
|
133
app/src/main/res/layout/dialog_poll_create.xml
Normal file
133
app/src/main/res/layout/dialog_poll_create.xml
Normal file
|
@ -0,0 +1,133 @@
|
|||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<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"
|
||||
android:padding="@dimen/standard_padding"
|
||||
tools:background="@color/white">
|
||||
|
||||
<!-- <androidx.emoji.widget.EmojiTextView-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:text="@string/nc_create_poll"-->
|
||||
<!-- android:textStyle="bold"-->
|
||||
<!-- android:layout_marginBottom="@dimen/standard_half_margin"/>-->
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:text="Question" />
|
||||
|
||||
<com.nextcloud.talk.utils.EmojiTextInputEditText
|
||||
android:id="@+id/poll_create_question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true" />
|
||||
|
||||
<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="Options" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/poll_create_options_list_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/poll_add_option"
|
||||
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="Settings" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/poll_private_poll_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Private poll" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/poll_multiple_answers_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="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>
|
|
@ -47,11 +47,36 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
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_vote"
|
||||
android:textColor="@color/nc_outcoming_text_default" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/messageTime"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -59,6 +84,7 @@
|
|||
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
|
||||
|
@ -67,6 +93,7 @@
|
|||
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" />
|
||||
|
||||
|
|
27
app/src/main/res/layout/poll_create_options_item.xml
Normal file
27
app/src/main/res/layout/poll_create_options_item.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<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">
|
||||
|
||||
<com.nextcloud.talk.utils.EmojiTextInputEditText
|
||||
android:id="@+id/poll_option_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
<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>
|
|
@ -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>
|
||||
<string name="nc_common_submit">Submit</string>
|
||||
|
||||
|
@ -404,6 +405,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>
|
||||
|
@ -536,6 +538,7 @@
|
|||
|
||||
<!-- Polls -->
|
||||
<string name="polls_amount_voters">Poll results - %1$s votes</string>
|
||||
<string name="polls_add_option">Add Option</string>
|
||||
|
||||
<string name="title_attachments">Attachments</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue