mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Create poll UI implementation.
This commit is contained in:
parent
cb1d5e888d
commit
6cee266a95
@ -19,4 +19,9 @@ package im.vector.app.features.createpoll
|
|||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class CreatePollAction : VectorViewModelAction {
|
sealed class CreatePollAction : VectorViewModelAction {
|
||||||
|
data class OnQuestionChanged(val question: String) : CreatePollAction()
|
||||||
|
data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction()
|
||||||
|
data class OnDeleteOption(val index: Int) : CreatePollAction()
|
||||||
|
object OnAddOption : CreatePollAction()
|
||||||
|
object OnCreatePoll : CreatePollAction()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.createpoll
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.ui.list.ItemStyle
|
||||||
|
import im.vector.app.core.ui.list.genericButtonItem
|
||||||
|
import im.vector.app.core.ui.list.genericItem
|
||||||
|
import im.vector.app.features.form.formEditTextItem
|
||||||
|
import im.vector.app.features.form.formEditTextWithDeleteItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CreatePollController @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) : EpoxyController() {
|
||||||
|
|
||||||
|
private var state: CreatePollViewState? = null
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
fun setData(state: CreatePollViewState) {
|
||||||
|
this.state = state
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val currentState = state ?: return
|
||||||
|
val host = this
|
||||||
|
|
||||||
|
genericItem {
|
||||||
|
id("question_title")
|
||||||
|
style(ItemStyle.BIG_TEXT)
|
||||||
|
title(host.stringProvider.getString(R.string.create_poll_question_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
formEditTextItem {
|
||||||
|
id("question")
|
||||||
|
value(currentState.question)
|
||||||
|
hint(host.stringProvider.getString(R.string.create_poll_question_hint))
|
||||||
|
singleLine(false)
|
||||||
|
maxLength(500)
|
||||||
|
onTextChange {
|
||||||
|
host.callback?.onQuestionChanged(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genericItem {
|
||||||
|
id("options_title")
|
||||||
|
style(ItemStyle.BIG_TEXT)
|
||||||
|
title(host.stringProvider.getString(R.string.create_poll_options_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState.options.forEachIndexed { index, option ->
|
||||||
|
formEditTextWithDeleteItem {
|
||||||
|
id("option_$index")
|
||||||
|
value(option)
|
||||||
|
hint(host.stringProvider.getString(R.string.create_poll_options_hint, (index + 1)))
|
||||||
|
onTextChange {
|
||||||
|
host.callback?.onOptionChanged(index, it)
|
||||||
|
}
|
||||||
|
onDeleteClicked {
|
||||||
|
host.callback?.onDeleteOption(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genericButtonItem {
|
||||||
|
id("add_option")
|
||||||
|
text(host.stringProvider.getString(R.string.create_poll_add_option))
|
||||||
|
textColor(host.colorProvider.getColor(R.color.palette_element_green))
|
||||||
|
buttonClickAction {
|
||||||
|
host.callback?.onAddOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onQuestionChanged(question: String)
|
||||||
|
fun onOptionChanged(index: Int, option: String)
|
||||||
|
fun onDeleteOption(index: Int)
|
||||||
|
fun onAddOption()
|
||||||
|
}
|
||||||
|
}
|
@ -20,11 +20,18 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentCreatePollBinding
|
import im.vector.app.databinding.FragmentCreatePollBinding
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CreatePollFragment @Inject constructor() : VectorBaseFragment<FragmentCreatePollBinding>() {
|
class CreatePollFragment @Inject constructor(
|
||||||
|
private val controller: CreatePollController
|
||||||
|
) : VectorBaseFragment<FragmentCreatePollBinding>(), CreatePollController.Callback {
|
||||||
|
|
||||||
|
private val viewModel: CreatePollViewModel by activityViewModel()
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding {
|
||||||
return FragmentCreatePollBinding.inflate(inflater, container, false)
|
return FragmentCreatePollBinding.inflate(inflater, container, false)
|
||||||
@ -34,8 +41,35 @@ class CreatePollFragment @Inject constructor() : VectorBaseFragment<FragmentCrea
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
vectorBaseActivity.setSupportActionBar(views.createPollToolbar)
|
vectorBaseActivity.setSupportActionBar(views.createPollToolbar)
|
||||||
|
|
||||||
|
views.createPollRecyclerView.configureWith(controller)
|
||||||
|
controller.callback = this
|
||||||
|
|
||||||
views.createPollClose.debouncedClicks {
|
views.createPollClose.debouncedClicks {
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.createPollButton.debouncedClicks {
|
||||||
|
viewModel.handle(CreatePollAction.OnCreatePoll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
controller.setData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQuestionChanged(question: String) {
|
||||||
|
viewModel.handle(CreatePollAction.OnQuestionChanged(question))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionChanged(index: Int, option: String) {
|
||||||
|
viewModel.handle(CreatePollAction.OnOptionChanged(index, option))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteOption(index: Int) {
|
||||||
|
viewModel.handle(CreatePollAction.OnDeleteOption(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddOption() {
|
||||||
|
viewModel.handle(CreatePollAction.OnAddOption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class CreatePollViewModel @AssistedInject constructor(@Assisted
|
class CreatePollViewModel @AssistedInject constructor(@Assisted
|
||||||
initialState: CreatePollViewState) :
|
initialState: CreatePollViewState) :
|
||||||
@ -46,6 +47,62 @@ class CreatePollViewModel @AssistedInject constructor(@Assisted
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Initialize with 2 default empty options
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
question = "",
|
||||||
|
options = listOf("", "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handle(action: CreatePollAction) {
|
override fun handle(action: CreatePollAction) {
|
||||||
|
when (action) {
|
||||||
|
CreatePollAction.OnCreatePoll -> handleOnCreatePoll()
|
||||||
|
CreatePollAction.OnAddOption -> handleOnAddOption()
|
||||||
|
is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index)
|
||||||
|
is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option)
|
||||||
|
is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnCreatePoll() = withState { state ->
|
||||||
|
Timber.d(state.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnAddOption() {
|
||||||
|
setState {
|
||||||
|
val extendedOptions = options + ""
|
||||||
|
copy(
|
||||||
|
options = extendedOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnDeleteOption(index: Int) {
|
||||||
|
setState {
|
||||||
|
val filteredOptions = options.filterIndexed { ind, _ -> ind != index }
|
||||||
|
copy(
|
||||||
|
options = filteredOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnOptionChanged(index: Int, option: String) {
|
||||||
|
setState {
|
||||||
|
val changedOptions = options.mapIndexed { ind, s -> if(ind == index) option else s }
|
||||||
|
copy(
|
||||||
|
options = changedOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnQuestionChanged(question: String) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
question = question
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,6 @@ package im.vector.app.features.createpoll
|
|||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
|
||||||
data class CreatePollViewState(
|
data class CreatePollViewState(
|
||||||
val question: String = ""
|
val question: String = "",
|
||||||
|
val options: List<String> = emptyList()
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.form
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
|
import im.vector.app.core.epoxy.TextListener
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.core.epoxy.addTextChangedListenerOnce
|
||||||
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.core.extensions.setTextIfDifferent
|
||||||
|
import im.vector.app.core.platform.SimpleTextWatcher
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_form_text_input_with_delete)
|
||||||
|
abstract class FormEditTextWithDeleteItem : VectorEpoxyModel<FormEditTextWithDeleteItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var hint: String? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var value: String? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var enabled: Boolean = true
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
|
var onTextChange: TextListener? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
|
var onDeleteClicked: ClickListener? = null
|
||||||
|
|
||||||
|
private val onTextChangeListener = object : SimpleTextWatcher() {
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
onTextChange?.invoke(s.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.textInputLayout.isEnabled = enabled
|
||||||
|
holder.textInputLayout.hint = hint
|
||||||
|
|
||||||
|
holder.textInputEditText.setTextIfDifferent(value)
|
||||||
|
|
||||||
|
holder.textInputEditText.isEnabled = enabled
|
||||||
|
|
||||||
|
holder.textInputEditText.addTextChangedListenerOnce(onTextChangeListener)
|
||||||
|
|
||||||
|
holder.textInputDeleteButton.onClick(onDeleteClicked)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldSaveViewState(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unbind(holder: Holder) {
|
||||||
|
super.unbind(holder)
|
||||||
|
holder.textInputEditText.removeTextChangedListener(onTextChangeListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val textInputLayout by bind<TextInputLayout>(R.id.formTextInputTextInputLayout)
|
||||||
|
val textInputEditText by bind<TextInputEditText>(R.id.formTextInputTextInputEditText)
|
||||||
|
val textInputDeleteButton by bind<ImageButton>(R.id.formTextInputDeleteButton)
|
||||||
|
}
|
||||||
|
}
|
@ -60,4 +60,22 @@
|
|||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/createPollRecyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/createPollButton"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
||||||
|
tools:listitem="@layout/item_profile_action" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/createPollButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="@string/create_poll_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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:background="?android:colorBackground"
|
||||||
|
android:minHeight="@dimen/item_form_min_height">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/formTextInputTextInputLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/formTextInputDeleteButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/formTextInputTextInputEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:hint="@string/create_room_name_hint" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/formTextInputDeleteButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
android:background="@drawable/circle"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/formTextInputTextInputLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/formTextInputTextInputLayout"
|
||||||
|
app:tint="?vctr_content_secondary" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -3628,4 +3628,10 @@
|
|||||||
|
|
||||||
<!-- Poll -->
|
<!-- Poll -->
|
||||||
<string name="create_poll_title">Create Poll</string>
|
<string name="create_poll_title">Create Poll</string>
|
||||||
|
<string name="create_poll_question_title">Poll question or topic</string>
|
||||||
|
<string name="create_poll_question_hint">Question or topic</string>
|
||||||
|
<string name="create_poll_options_title">Create options</string>
|
||||||
|
<string name="create_poll_options_hint">Option %1$d</string>
|
||||||
|
<string name="create_poll_add_option">ADD OPTION</string>
|
||||||
|
<string name="create_poll_button">CREATE POLL</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user