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) {
@JvmField
@BindView(R.id.name_text)
@BindView(R.id.participantNameTextView)
var contactDisplayName: EmojiTextView? = null
@JvmField
@BindView(R.id.secondary_text)

View File

@ -268,7 +268,7 @@ class UserItem(
var checkedImageView: ImageView? = null
init {
contactDisplayName = view.findViewById(R.id.name_text)
contactDisplayName = view.findViewById(R.id.participantNameTextView)
avatarImageView = view.findViewById(R.id.avatarImageView)
contactMentionId = view.findViewById(R.id.secondary_text)
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.HeaderSource
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_title_header.view.*
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()
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 {
return when (elementType) {
ParticipantElementType.PARTICIPANT.ordinal -> {
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 -> {
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>) {
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 user = globalService.currentUserLiveData.value
holder.itemView.checkedImageView.isVisible = participant?.selected == true
holder.itemView.checkedImageView?.isVisible = participant?.selected == true
if (!payloads.contains(ElementPayload.SELECTION_TOGGLE)) {
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 {
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) {
"users" -> {
when (participant.type) {

View File

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

View File

@ -5,7 +5,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
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.RecyclerView
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.nextcloud.talk.R
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.Presenter
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 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
val factory: ContactsViewModelFactory by inject()
lateinit var adapter: Adapter
lateinit var participantsAdapter: Adapter
lateinit var selectedParticipantsAdapter: Adapter
override fun getLayoutId(): Int {
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)
// todo - change empty state magic
adapter = Adapter.builder(this)
.addSource(ContactsViewSource(viewModel.contactsLiveData, ParticipantElementType.PARTICIPANT.ordinal))
participantsAdapter = Adapter.builder(this)
.addSource(ContactsViewSource(data = viewModel.contactsLiveData, elementType = ParticipantElementType.PARTICIPANT.ordinal))
.addSource(ContactsHeaderSource(activity as Context, ParticipantElementType.PARTICIPANT_HEADER.ordinal))
.addSource(ContactsFooterSource(activity as Context, ParticipantElementType.PARTICIPANT_FOOTER.ordinal))
.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)
.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 {
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()
@ -71,8 +93,16 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
if (element.data is Participant?) {
val participant = element.data as Participant?
val isElementSelected = participant?.selected == true
participant?.selected = !isElementSelected
adapter.notifyItemChanged(holder.adapterPosition, ElementPayload.SELECTION_TOGGLE)
participant?.let {
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
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.models.json.participants.Participant
@ -17,7 +18,10 @@ class ContactsViewModel constructor(
private val getContactsUseCase: GetContactsUseCase,
val globalService: GlobalService
) : 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
var conversationToken: String? = null
@ -26,6 +30,16 @@ class ContactsViewModel constructor(
loadContacts()
}
fun selectParticipant(participant: Participant) {
selectedParticipants.add(participant)
selectedParticipantsLiveData.postValue(selectedParticipants)
}
fun unselectParticipant(participant: Participant) {
selectedParticipants.remove(participant)
selectedParticipantsLiveData.postValue(selectedParticipants)
}
fun loadContacts() {
getContactsUseCase.invoke(viewModelScope, parametersOf(globalService.currentUserLiveData.value, searchQuery, conversationToken), object :
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 {
return false
}

View File

@ -121,7 +121,7 @@ open class ConversationPresenter(context: Context, onElementClick: ((Page, Holde
)
} else {
authorDisplayName = if (!TextUtils.isEmpty(conversation.lastMessage?.actorDisplayName)) {
conversation.lastMessage?.actorDisplayName!!.substringBefore(" ")
conversation.lastMessage?.actorDisplayName!!.substringBefore(" ", conversation.lastMessage?.actorDisplayName!!)
} else if ("guests" == conversation.lastMessage!!.actorType)
context.getString(R.string.nc_guest)
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)
layers[0] = context.getDrawable(R.color.bg_message_list_incoming_bubble)
var scale = 0.25f
@ -76,8 +76,12 @@ class Images {
scale = 0.5f
}
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[1]?.level = 1
return LayerDrawable(layers).toBitmap()
}

View File

@ -2,12 +2,29 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
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
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/divider"
tools:listitem="@layout/rv_item_contact" />
</RelativeLayout>

View File

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

View File

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

View File

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