浏览代码

wip: create poll. show outgoing polls

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 2 年之前
父节点
当前提交
af427f8300
共有 23 个文件被更改,包括 727 次插入33 次删除
  1. 84 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  2. 6 1
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  3. 11 0
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  4. 9 3
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  5. 5 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt
  6. 53 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt
  7. 27 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt
  8. 5 0
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemClickListener.kt
  9. 8 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt
  10. 18 0
      app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt
  11. 155 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
  12. 6 6
      app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
  13. 10 10
      app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
  14. 4 4
      app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
  15. 84 0
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
  16. 2 2
      app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
  17. 5 0
      app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  18. 10 0
      app/src/main/res/drawable/ic_baseline_close_24.xml
  19. 33 0
      app/src/main/res/layout/dialog_attachment.xml
  20. 133 0
      app/src/main/res/layout/dialog_poll_create.xml
  21. 29 2
      app/src/main/res/layout/item_custom_outcoming_poll_message.xml
  22. 27 0
      app/src/main/res/layout/poll_create_options_item.xml
  23. 3 0
      app/src/main/res/values/strings.xml

+ 84 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

@@ -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

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

@@ -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

+ 11 - 0
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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

+ 9 - 3
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

@@ -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)

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

@@ -0,0 +1,5 @@
+package com.nextcloud.talk.polls.adapters
+
+class PollCreateOptionItem(
+    var pollOption: String
+)

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

@@ -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")
+    // }
+}

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

@@ -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
+    }
+}

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

@@ -0,0 +1,5 @@
+package com.nextcloud.talk.polls.adapters
+
+interface PollCreateOptionsItemClickListener {
+    fun onDeleteClick(pollCreateOptionItem: PollCreateOptionItem, position: Int)
+}

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

@@ -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>?
 }

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

@@ -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(

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

@@ -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)
+    }
+}

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

@@ -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 {

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

@@ -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..")
-    }
 }

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

@@ -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
             }

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

@@ -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
+    }
+}

+ 2 - 2
app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt → app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt

@@ -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
     }
 }

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

@@ -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 - 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>

+ 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"

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

@@ -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>

+ 29 - 2
app/src/main/res/layout/item_custom_outcoming_poll_message.xml

@@ -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 - 0
app/src/main/res/layout/poll_create_options_item.xml

@@ -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>

+ 3 - 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>
     <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>