Lots of progress on selecting contacts

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-01-23 13:51:41 +01:00
parent df1e65e238
commit 6427e0bafd
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
15 changed files with 174 additions and 25 deletions

View File

@ -137,7 +137,7 @@ class AdvancedUserItem(
) : FlexibleViewHolder(view, adapter) { ) : FlexibleViewHolder(view, adapter) {
@JvmField @JvmField
@BindView(R.id.name_text) @BindView(R.id.participantNameTextView)
var contactDisplayName: EmojiTextView? = null var contactDisplayName: EmojiTextView? = null
@JvmField @JvmField
@BindView(R.id.secondary_text) @BindView(R.id.secondary_text)

View File

@ -268,7 +268,7 @@ class UserItem(
var checkedImageView: ImageView? = null var checkedImageView: ImageView? = null
init { init {
contactDisplayName = view.findViewById(R.id.name_text) contactDisplayName = view.findViewById(R.id.participantNameTextView)
avatarImageView = view.findViewById(R.id.avatarImageView) avatarImageView = view.findViewById(R.id.avatarImageView)
contactMentionId = view.findViewById(R.id.secondary_text) contactMentionId = view.findViewById(R.id.secondary_text)
voiceOrSimpleCallImageView = view.findViewById(R.id.voiceOrSimpleCallImageView) voiceOrSimpleCallImageView = view.findViewById(R.id.voiceOrSimpleCallImageView)

View File

@ -17,6 +17,9 @@ import com.otaliastudios.elements.Presenter
import com.otaliastudios.elements.extensions.FooterSource import com.otaliastudios.elements.extensions.FooterSource
import com.otaliastudios.elements.extensions.HeaderSource import com.otaliastudios.elements.extensions.HeaderSource
import kotlinx.android.synthetic.main.rv_item_contact.view.* import kotlinx.android.synthetic.main.rv_item_contact.view.*
import kotlinx.android.synthetic.main.rv_item_contact.view.avatarImageView
import kotlinx.android.synthetic.main.rv_item_contact.view.participantNameTextView
import kotlinx.android.synthetic.main.rv_item_contact_selected.view.*
import kotlinx.android.synthetic.main.rv_item_participant_rv_footer.view.* import kotlinx.android.synthetic.main.rv_item_participant_rv_footer.view.*
import kotlinx.android.synthetic.main.rv_item_title_header.view.* import kotlinx.android.synthetic.main.rv_item_title_header.view.*
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@ -26,13 +29,16 @@ open class ContactPresenter<T : Any>(context: Context, onElementClick: ((Page, H
private val globalService: GlobalService by inject() private val globalService: GlobalService by inject()
override val elementTypes: Collection<Int> override val elementTypes: Collection<Int>
get() = listOf(ParticipantElementType.PARTICIPANT.ordinal, ParticipantElementType.PARTICIPANT_HEADER.ordinal, ParticipantElementType.PARTICIPANT_FOOTER.ordinal) get() = listOf(ParticipantElementType.PARTICIPANT.ordinal, ParticipantElementType.PARTICIPANT_SELECTED.ordinal, ParticipantElementType.PARTICIPANT_HEADER.ordinal, ParticipantElementType.PARTICIPANT_FOOTER.ordinal)
override fun onCreate(parent: ViewGroup, elementType: Int): Holder { override fun onCreate(parent: ViewGroup, elementType: Int): Holder {
return when (elementType) { return when (elementType) {
ParticipantElementType.PARTICIPANT.ordinal -> { ParticipantElementType.PARTICIPANT.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_item_contact, parent, false)) Holder(getLayoutInflater().inflate(R.layout.rv_item_contact, parent, false))
} }
ParticipantElementType.PARTICIPANT_SELECTED.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_item_contact_selected, parent, false))
}
ParticipantElementType.PARTICIPANT_HEADER.ordinal -> { ParticipantElementType.PARTICIPANT_HEADER.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_item_title_header, parent, false)) Holder(getLayoutInflater().inflate(R.layout.rv_item_title_header, parent, false))
} }
@ -45,19 +51,26 @@ open class ContactPresenter<T : Any>(context: Context, onElementClick: ((Page, H
override fun onBind(page: Page, holder: Holder, element: Element<T>, payloads: List<Any>) { override fun onBind(page: Page, holder: Holder, element: Element<T>, payloads: List<Any>) {
super.onBind(page, holder, element, payloads) super.onBind(page, holder, element, payloads)
if (element.type == ParticipantElementType.PARTICIPANT.ordinal) { if (element.type == ParticipantElementType.PARTICIPANT.ordinal || element.type == ParticipantElementType.PARTICIPANT_SELECTED.ordinal) {
val participant = element.data as Participant? val participant = element.data as Participant?
val user = globalService.currentUserLiveData.value val user = globalService.currentUserLiveData.value
holder.itemView.checkedImageView.isVisible = participant?.selected == true holder.itemView.checkedImageView?.isVisible = participant?.selected == true
if (!payloads.contains(ElementPayload.SELECTION_TOGGLE)) { if (!payloads.contains(ElementPayload.SELECTION_TOGGLE)) {
participant?.displayName?.let { participant?.displayName?.let {
holder.itemView.name_text.text = it if (element.type == ParticipantElementType.PARTICIPANT_SELECTED.ordinal) {
holder.itemView.participantNameTextView.text = it.substringBefore(" ", it)
} else {
holder.itemView.participantNameTextView.text = it
}
} ?: run { } ?: run {
holder.itemView.name_text.text = context.getString(R.string.nc_guest) holder.itemView.participantNameTextView.text = context.getString(R.string.nc_guest)
} }
holder.itemView.clearImageView?.load(Images().getImageWithBackground(context, R.drawable.ic_baseline_clear_24, R.color.white))
when (participant?.source) { when (participant?.source) {
"users" -> { "users" -> {
when (participant.type) { when (participant.type) {

View File

@ -2,6 +2,7 @@ package com.nextcloud.talk.newarch.features.contactsflow
enum class ParticipantElementType { enum class ParticipantElementType {
PARTICIPANT, PARTICIPANT,
PARTICIPANT_SELECTED,
PARTICIPANT_HEADER, PARTICIPANT_HEADER,
PARTICIPANT_FOOTER PARTICIPANT_FOOTER
} }

View File

@ -5,7 +5,11 @@ 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 androidx.core.view.isVisible
import androidx.lifecycle.LiveData
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
@ -18,7 +22,8 @@ import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Presenter import com.otaliastudios.elements.Presenter
import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.conversations_list_view.view.* import kotlinx.android.synthetic.main.contacts_list_view.view.*
import kotlinx.android.synthetic.main.conversations_list_view.view.recyclerView
import kotlinx.android.synthetic.main.message_state.view.* import kotlinx.android.synthetic.main.message_state.view.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -27,7 +32,8 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
private lateinit var viewModel: ContactsViewModel private lateinit var viewModel: ContactsViewModel
val factory: ContactsViewModelFactory by inject() val factory: ContactsViewModelFactory by inject()
lateinit var adapter: Adapter lateinit var participantsAdapter: Adapter
lateinit var selectedParticipantsAdapter: Adapter
override fun getLayoutId(): Int { override fun getLayoutId(): Int {
return R.layout.contacts_list_view return R.layout.contacts_list_view
} }
@ -44,8 +50,8 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
val view = super.onCreateView(inflater, container) val view = super.onCreateView(inflater, container)
// todo - change empty state magic // todo - change empty state magic
adapter = Adapter.builder(this) participantsAdapter = Adapter.builder(this)
.addSource(ContactsViewSource(viewModel.contactsLiveData, ParticipantElementType.PARTICIPANT.ordinal)) .addSource(ContactsViewSource(data = viewModel.contactsLiveData, elementType = ParticipantElementType.PARTICIPANT.ordinal))
.addSource(ContactsHeaderSource(activity as Context, ParticipantElementType.PARTICIPANT_HEADER.ordinal)) .addSource(ContactsHeaderSource(activity as Context, ParticipantElementType.PARTICIPANT_HEADER.ordinal))
.addSource(ContactsFooterSource(activity as Context, ParticipantElementType.PARTICIPANT_FOOTER.ordinal)) .addSource(ContactsFooterSource(activity as Context, ParticipantElementType.PARTICIPANT_FOOTER.ordinal))
.addPresenter(ContactPresenter(activity as Context, ::onElementClick)) .addPresenter(ContactPresenter(activity as Context, ::onElementClick))
@ -58,8 +64,24 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
.setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true) .setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true)
.into(view.recyclerView) .into(view.recyclerView)
selectedParticipantsAdapter = Adapter.builder(this)
.addSource(ContactsViewSource(data = viewModel.selectedParticipantsLiveData, elementType = ParticipantElementType.PARTICIPANT_SELECTED.ordinal, loadingIndicatorsEnabled = false, errorIndicatorEnabled = false, emptyIndicatorEnabled = false))
.addPresenter(ContactPresenter(activity as Context, ::onElementClick))
.setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_ANY, true)
.into(view.selectedParticipantsRecyclerView)
view.apply { view.apply {
recyclerView.initRecyclerView(LinearLayoutManager(activity), adapter, true) recyclerView.initRecyclerView(LinearLayoutManager(activity), participantsAdapter, true)
selectedParticipantsRecyclerView.initRecyclerView(LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false), selectedParticipantsAdapter, true)
}
viewModel.apply {
selectedParticipantsLiveData.observe(this@ContactsView) { participants ->
view.selectedParticipantsRecyclerView.isVisible = participants.isNotEmpty()
view.divider.isVisible = participants.isNotEmpty()
}
} }
viewModel.loadContacts() viewModel.loadContacts()
@ -71,8 +93,16 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
if (element.data is Participant?) { if (element.data is Participant?) {
val participant = element.data as Participant? val participant = element.data as Participant?
val isElementSelected = participant?.selected == true val isElementSelected = participant?.selected == true
participant?.selected = !isElementSelected participant?.let {
adapter.notifyItemChanged(holder.adapterPosition, ElementPayload.SELECTION_TOGGLE) if (isElementSelected) {
viewModel.unselectParticipant(it)
} else {
viewModel.selectParticipant(it)
}
it.selected = !isElementSelected
participantsAdapter.notifyItemChanged(holder.adapterPosition, ElementPayload.SELECTION_TOGGLE)
}
} }
} }

View File

@ -1,6 +1,7 @@
package com.nextcloud.talk.newarch.features.contactsflow package com.nextcloud.talk.newarch.features.contactsflow
import android.app.Application import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
@ -17,7 +18,10 @@ class ContactsViewModel constructor(
private val getContactsUseCase: GetContactsUseCase, private val getContactsUseCase: GetContactsUseCase,
val globalService: GlobalService val globalService: GlobalService
) : BaseViewModel<ConversationsListView>(application) { ) : BaseViewModel<ConversationsListView>(application) {
val contactsLiveData = MutableLiveData<List<Participant>>() private val selectedParticipants = mutableListOf<Participant>()
val selectedParticipantsLiveData: MutableLiveData<List<Participant>> = MutableLiveData()
val contactsLiveData: MutableLiveData<List<Participant>> = MutableLiveData()
private var searchQuery: String? = null private var searchQuery: String? = null
var conversationToken: String? = null var conversationToken: String? = null
@ -26,6 +30,16 @@ class ContactsViewModel constructor(
loadContacts() loadContacts()
} }
fun selectParticipant(participant: Participant) {
selectedParticipants.add(participant)
selectedParticipantsLiveData.postValue(selectedParticipants)
}
fun unselectParticipant(participant: Participant) {
selectedParticipants.remove(participant)
selectedParticipantsLiveData.postValue(selectedParticipants)
}
fun loadContacts() { fun loadContacts() {
getContactsUseCase.invoke(viewModelScope, parametersOf(globalService.currentUserLiveData.value, searchQuery, conversationToken), object : getContactsUseCase.invoke(viewModelScope, parametersOf(globalService.currentUserLiveData.value, searchQuery, conversationToken), object :
UseCaseResponse<List<Participant>> { UseCaseResponse<List<Participant>> {

View File

@ -38,6 +38,10 @@ class ContactsViewSource<T : Participant>(private val data: LiveData<List<T>>, p
} }
} }
override fun getElementType(data: T): Int {
return elementType
}
override fun dependsOn(source: Source<*>): Boolean { override fun dependsOn(source: Source<*>): Boolean {
return false return false
} }

View File

@ -121,7 +121,7 @@ open class ConversationPresenter(context: Context, onElementClick: ((Page, Holde
) )
} else { } else {
authorDisplayName = if (!TextUtils.isEmpty(conversation.lastMessage?.actorDisplayName)) { authorDisplayName = if (!TextUtils.isEmpty(conversation.lastMessage?.actorDisplayName)) {
conversation.lastMessage?.actorDisplayName!!.substringBefore(" ") conversation.lastMessage?.actorDisplayName!!.substringBefore(" ", conversation.lastMessage?.actorDisplayName!!)
} else if ("guests" == conversation.lastMessage!!.actorType) } else if ("guests" == conversation.lastMessage!!.actorType)
context.getString(R.string.nc_guest) context.getString(R.string.nc_guest)
else else

View File

@ -68,7 +68,7 @@ class Images {
} }
} }
fun getImageWithBackground(context: Context, drawableId: Int): Bitmap { fun getImageWithBackground(context: Context, drawableId: Int, foregroundColorTint: Int? = null): Bitmap {
val layers = arrayOfNulls<Drawable>(2) val layers = arrayOfNulls<Drawable>(2)
layers[0] = context.getDrawable(R.color.bg_message_list_incoming_bubble) layers[0] = context.getDrawable(R.color.bg_message_list_incoming_bubble)
var scale = 0.25f var scale = 0.25f
@ -76,8 +76,12 @@ class Images {
scale = 0.5f scale = 0.5f
} }
layers[1] = ScaleDrawable(context.getDrawable(drawableId), Gravity.CENTER, scale, scale) layers[1] = ScaleDrawable(context.getDrawable(drawableId), Gravity.CENTER, scale, scale)
if (foregroundColorTint != null) {
layers[1]?.setTint(context.resources.getColor(foregroundColorTint))
}
layers[0]?.level = 0 layers[0]?.level = 0
layers[1]?.level = 1 layers[1]?.level = 1
return LayerDrawable(layers).toBitmap() return LayerDrawable(layers).toBitmap()
} }

View File

@ -2,12 +2,29 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:animateLayoutChanges="true">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selectedParticipantsRecyclerView"
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_alignParentTop="true"
android:visibility="gone"
tools:listitem="@layout/rv_item_contact_selected" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_below="@id/selectedParticipantsRecyclerView"
android:background="?android:attr/listDivider" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@id/divider"
tools:listitem="@layout/rv_item_contact" /> tools:listitem="@layout/rv_item_contact" />
</RelativeLayout> </RelativeLayout>

View File

@ -26,20 +26,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/rv_item_view_height" android:layout_height="@dimen/rv_item_view_height"
android:layout_margin="@dimen/margin_between_elements" android:layout_margin="@dimen/margin_between_elements"
android:orientation="vertical"> android:orientation="vertical"
android:layout_centerInParent="true">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatarImageView" android:id="@+id/avatarImageView"
android:layout_width="@dimen/small_item_height" android:layout_width="@dimen/small_item_height"
android:layout_height="@dimen/small_item_height" android:layout_height="@dimen/small_item_height"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="@dimen/double_margin_between_elements" android:layout_marginHorizontal="8dp"
android:layout_marginEnd="@dimen/margin_between_elements"
app:shapeAppearanceOverlay="@style/circleImageView" app:shapeAppearanceOverlay="@style/circleImageView"
tools:srcCompat="@tools:sample/avatars"/> tools:srcCompat="@tools:sample/avatars"/>
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@+id/name_text" android:id="@+id/participantNameTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Andy Scherzinger
~ Copyright (C) 2017 Mario Danic
~ Copyright (C) 2017 Andy Scherzinger
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="8dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dp"
android:id="@+id/avatarFrameLayout">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatarImageView"
android:layout_width="@dimen/small_item_height"
android:layout_height="@dimen/small_item_height"
app:shapeAppearanceOverlay="@style/circleImageView"
tools:srcCompat="@tools:sample/avatars"/>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/clearImageView"
android:layout_width="16dp"
android:layout_height="16dp"
app:shapeAppearanceOverlay="@style/circleImageView"
android:layout_gravity="bottom|end"/>
</FrameLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/participantNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_centerHorizontal="true"
android:layout_below="@id/avatarFrameLayout"
android:ellipsize="end"
android:maxLines="2"
tools:text="Contact item text" />
</RelativeLayout>

View File

@ -74,7 +74,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@+id/name_text" android:id="@+id/participantNameTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="middle" android:ellipsize="middle"

View File

@ -69,7 +69,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@+id/name_text" android:id="@+id/participantNameTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="middle" android:ellipsize="middle"

View File

@ -53,7 +53,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@+id/name_text" android:id="@+id/participantNameTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="middle" android:ellipsize="middle"