Update UI for contacts

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-01-24 13:56:16 +01:00
parent 3c15393ba4
commit c80b611ae9
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
12 changed files with 125 additions and 40 deletions

View File

@ -92,8 +92,8 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou
} }
} }
override suspend fun getContactsForUser(user: UserNgEntity, searchQuery: String?, conversationToken: String?): List<Participant> { override suspend fun getContactsForUser(user: UserNgEntity, groupConversation: Boolean, searchQuery: String?, conversationToken: String?): List<Participant> {
return apiService.getContacts(authorization = user.getCredentials(), url = ApiUtils.getUrlForContactsSearch(user.baseUrl), shareTypes = ApiUtils.getShareTypesForContactsSearch(), options = ApiUtils.getQueryMapForContactsSearch(searchQuery, conversationToken)).ocs.data.map { return apiService.getContacts(authorization = user.getCredentials(), url = ApiUtils.getUrlForContactsSearch(user.baseUrl), shareTypes = ApiUtils.getShareTypesForContactsSearch(groupConversation), options = ApiUtils.getQueryMapForContactsSearch(searchQuery, conversationToken)).ocs.data.map {
val participant = Participant() val participant = Participant()
participant.userId = it.id participant.userId = it.id
participant.displayName = it.label participant.displayName = it.label

View File

@ -33,7 +33,7 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
interface NextcloudTalkRepository { interface NextcloudTalkRepository {
suspend fun getContactsForUser(user: UserNgEntity, searchQuery: String?, conversationToken: String?): List<Participant> suspend fun getContactsForUser(user: UserNgEntity, groupConversation: Boolean, searchQuery: String?, conversationToken: String?): List<Participant>
suspend fun registerPushWithServerForUser(user: UserNgEntity, options: Map<String, String>): PushRegistrationOverall suspend fun registerPushWithServerForUser(user: UserNgEntity, options: Map<String, String>): PushRegistrationOverall
suspend fun unregisterPushWithServerForUser(user: UserNgEntity): GenericOverall suspend fun unregisterPushWithServerForUser(user: UserNgEntity): GenericOverall
suspend fun registerPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any suspend fun registerPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any

View File

@ -34,6 +34,6 @@ class GetContactsUseCase constructor(
) : UseCase<List<Participant>, Any?>(apiErrorHandler) { ) : UseCase<List<Participant>, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): List<Participant> { override suspend fun run(params: Any?): List<Participant> {
val definitionParameters = params as DefinitionParameters val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.getContactsForUser(definitionParameters[0], definitionParameters[1], definitionParameters[2]) return nextcloudTalkRepository.getContactsForUser(definitionParameters[0], definitionParameters[1], definitionParameters[2], definitionParameters[3])
} }
} }

View File

@ -51,7 +51,7 @@ 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_SELECTED.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, ParticipantElementType.PARTICIPANT_NEW_GROUP.ordinal, ParticipantElementType.PARTICIPANT_JOIN_VIA_LINK.ordinal)
override fun onCreate(parent: ViewGroup, elementType: Int): Holder { override fun onCreate(parent: ViewGroup, elementType: Int): Holder {
return when (elementType) { return when (elementType) {
@ -64,9 +64,13 @@ open class ContactPresenter<T : Any>(context: Context, onElementClick: ((Page, H
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))
} }
else -> { ParticipantElementType.PARTICIPANT_FOOTER.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_item_participant_rv_footer, parent, false)) Holder(getLayoutInflater().inflate(R.layout.rv_item_participant_rv_footer, parent, false))
} }
else -> {
// for join via link and new group
Holder(getLayoutInflater().inflate(R.layout.rv_item_contact, parent, false))
}
} }
} }
@ -120,8 +124,16 @@ open class ContactPresenter<T : Any>(context: Context, onElementClick: ((Page, H
} }
} else if (element.type == ParticipantElementType.PARTICIPANT_HEADER.ordinal) { } else if (element.type == ParticipantElementType.PARTICIPANT_HEADER.ordinal) {
holder.itemView.titleTextView.text = (element.data as HeaderSource.Data<*, *>).header.toString() holder.itemView.titleTextView.text = (element.data as HeaderSource.Data<*, *>).header.toString()
} else { } else if (element.type == ParticipantElementType.PARTICIPANT_FOOTER.ordinal){
holder.itemView.messageTextView.text = (element.data as FooterSource.Data<*, *>).footer.toString() holder.itemView.messageTextView.text = (element.data as FooterSource.Data<*, *>).footer.toString()
} else if (element.type == ParticipantElementType.PARTICIPANT_NEW_GROUP.ordinal) {
val pairData = element.data as Pair<*, *>
holder.itemView.participantNameTextView.text = pairData.first as CharSequence
holder.itemView.avatarImageView.load(Images().getImageWithBackground(context, pairData.second as Int))
} else {
val pairData = element.data as Pair<*, *>
holder.itemView.participantNameTextView.text = pairData.first as CharSequence
holder.itemView.avatarImageView.load(Images().getImageWithBackground(context, pairData.second as Int))
} }
} }
} }

View File

@ -26,5 +26,7 @@ enum class ParticipantElementType {
PARTICIPANT, PARTICIPANT,
PARTICIPANT_SELECTED, PARTICIPANT_SELECTED,
PARTICIPANT_HEADER, PARTICIPANT_HEADER,
PARTICIPANT_FOOTER PARTICIPANT_FOOTER,
PARTICIPANT_NEW_GROUP,
PARTICIPANT_JOIN_VIA_LINK
} }

View File

@ -34,14 +34,12 @@ 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
import com.nextcloud.talk.newarch.features.contactsflow.source.FixedListSource
import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.newarch.utils.ElementPayload import com.nextcloud.talk.newarch.utils.ElementPayload
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.otaliastudios.elements.Adapter import com.otaliastudios.elements.*
import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Presenter
import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.contacts_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.conversations_list_view.view.recyclerView
@ -59,6 +57,9 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
return R.layout.contacts_list_view return R.layout.contacts_list_view
} }
private val isGroupConversation = bundle?.containsKey(BundleKeys.KEY_CONVERSATION_NAME) == true
private val hasToken = bundle?.containsKey(BundleKeys.KEY_CONVERSATION_TOKEN) == true
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup container: ViewGroup
@ -71,9 +72,11 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
// todo - change empty state magic // todo - change empty state magic
participantsAdapter = Adapter.builder(this) participantsAdapter = Adapter.builder(this)
.addSource(FixedListSource(listOf(Pair(context.getString(R.string.nc_new_group), R.drawable.ic_people_group_white_24px)), ParticipantElementType.PARTICIPANT_NEW_GROUP.ordinal))
//.addSource(FixedListSource(listOf(Pair(context.getString(R.string.nc_join_via_link), R.drawable.ic_link_white_24px)), ParticipantElementType.PARTICIPANT_JOIN_VIA_LINK.ordinal))
.addSource(ContactsViewSource(data = viewModel.contactsLiveData, elementType = 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(ContactsViewFooterSource(activity as Context, ParticipantElementType.PARTICIPANT_FOOTER.ordinal))
.addPresenter(ContactPresenter(activity as Context, ::onElementClick)) .addPresenter(ContactPresenter(activity as Context, ::onElementClick))
.addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state)) .addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state))
.addPresenter(Presenter.forEmptyIndicator(activity as Context, R.layout.message_state)) .addPresenter(Presenter.forEmptyIndicator(activity as Context, R.layout.message_state))
@ -136,7 +139,7 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
} }
viewModel.initialize(bundle?.getString(BundleKeys.KEY_CONVERSATION_TOKEN)) viewModel.initialize(bundle?.getString(BundleKeys.KEY_CONVERSATION_TOKEN), bundle?.containsKey(BundleKeys.KEY_CONVERSATION_NAME) == true)
return view return view
} }
@ -149,26 +152,46 @@ class ContactsView<T : Any>(private val bundle: Bundle? = null) : BaseView() {
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<T>) { private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<T>) {
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
participant?.let {
if (isElementSelected) {
viewModel.unselectParticipant(it)
} else {
viewModel.selectParticipant(it)
}
it.selected = !isElementSelected
if (element.type == ParticipantElementType.PARTICIPANT_SELECTED.ordinal) {
participantsAdapter.notifyItemRangeChanged(0, participantsAdapter.itemCount, ElementPayload.SELECTION_TOGGLE)
} else {
participantsAdapter.notifyItemChanged(holder.adapterPosition, ElementPayload.SELECTION_TOGGLE)
}
if (isGroupConversation || hasToken) {
val isElementSelected = participant?.selected == true
participant?.let {
if (isElementSelected) {
viewModel.unselectParticipant(it)
} else {
viewModel.selectParticipant(it)
}
it.selected = !isElementSelected
if (element.type == ParticipantElementType.PARTICIPANT_SELECTED.ordinal) {
participantsAdapter.notifyItemRangeChanged(0, participantsAdapter.itemCount, ElementPayload.SELECTION_TOGGLE)
} else {
participantsAdapter.notifyItemChanged(holder.adapterPosition, ElementPayload.SELECTION_TOGGLE)
}
}
} else {
participant?.let {
// create room etc etc
}
} }
} else if (element.type == ParticipantElementType.PARTICIPANT_NEW_GROUP.ordinal) {
} else if (element.type == ParticipantElementType.PARTICIPANT_JOIN_VIA_LINK.ordinal) {
} }
} }
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.nc_select_contacts) return when {
isGroupConversation -> {
resources?.getString(R.string.nc_select_contacts)
}
hasToken -> {
resources?.getString(R.string.nc_select_new_contacts)
}
else -> {
resources?.getString(R.string.nc_select_contact)
}
}
} }
} }

View File

@ -29,7 +29,7 @@ import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Source import com.otaliastudios.elements.Source
import com.otaliastudios.elements.extensions.FooterSource import com.otaliastudios.elements.extensions.FooterSource
class ContactsFooterSource(private val context: Context, private val elementType: Int) : FooterSource<Participant, String>() { class ContactsViewFooterSource(private val context: Context, private val elementType: Int) : FooterSource<Participant, String>() {
private var lastAnchor: Participant? = null private var lastAnchor: Participant? = null
override fun dependsOn(source: Source<*>): Boolean { override fun dependsOn(source: Source<*>): Boolean {

View File

@ -45,11 +45,13 @@ class ContactsViewModel constructor(
private var searchQuery: String? = null private var searchQuery: String? = null
private var conversationToken: String? = null private var conversationToken: String? = null
private var groupConversation: Boolean = false
private var initialized = false private var initialized = false
fun initialize(conversationToken: String?) { fun initialize(conversationToken: String?, groupConversation: Boolean) {
if (!initialized || conversationToken != this.conversationToken) { if (!initialized || conversationToken != this.conversationToken || groupConversation != this.groupConversation) {
this.conversationToken = conversationToken this.conversationToken = conversationToken
this.groupConversation = groupConversation
loadContacts() loadContacts()
} }
} }
@ -70,7 +72,7 @@ class ContactsViewModel constructor(
} }
fun loadContacts() { fun loadContacts() {
getContactsUseCase.invoke(viewModelScope, parametersOf(globalService.currentUserLiveData.value, searchQuery, conversationToken), object : getContactsUseCase.invoke(viewModelScope, parametersOf(globalService.currentUserLiveData.value, groupConversation, searchQuery, conversationToken), object :
UseCaseResponse<List<Participant>> { UseCaseResponse<List<Participant>> {
override suspend fun onSuccess(result: List<Participant>) { override suspend fun onSuccess(result: List<Participant>) {
val sortPriority = mapOf("users" to 0, "groups" to 1, "emails" to 2, "circles" to 0) val sortPriority = mapOf("users" to 0, "groups" to 1, "emails" to 2, "circles" to 0)

View File

@ -0,0 +1,41 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
* *
* * 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/>.
*
*/
package com.nextcloud.talk.newarch.features.contactsflow.source
import com.nextcloud.talk.newarch.features.contactsflow.ContactsViewSource
import com.otaliastudios.elements.Source
import com.otaliastudios.elements.extensions.ListSource
class FixedListSource(list: List<Any>, elementType: Int) : ListSource<Any>(list, elementType) {
override fun areContentsTheSame(first: Any, second: Any): Boolean {
return true
}
override fun <E : Any> areItemsTheSame(own: Any, dependency: Source<E>, other: E?): Boolean {
return true
}
override fun dependsOn(source: Source<*>): Boolean {
return source is ContactsViewSource
}
}

View File

@ -67,16 +67,18 @@ public class ApiUtils {
return baseUrl + ocsApiVersion + "/core/autocomplete/get"; return baseUrl + ocsApiVersion + "/core/autocomplete/get";
} }
public static List<String> getShareTypesForContactsSearch() { public static List<String> getShareTypesForContactsSearch(boolean groupConversation) {
List<String> shareTypesList = new ArrayList<>(); List<String> shareTypesList = new ArrayList<>();
// user // user
shareTypesList.add("0"); shareTypesList.add("0");
// group if (groupConversation) {
shareTypesList.add("1"); // group
// group shareTypesList.add("1");
shareTypesList.add("4"); // email
// remote/circles shareTypesList.add("4");
shareTypesList.add("7"); // remote/circles
shareTypesList.add("7");
}
return shareTypesList; return shareTypesList;
} }

View File

@ -27,7 +27,7 @@
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_search_white_24dp"
android:title="@string/nc_search" android:title="@string/nc_search"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|always" /> app:showAsAction="collapseActionView|ifRoom" />
<item <item
android:id="@+id/contacts_selection_done" android:id="@+id/contacts_selection_done"

View File

@ -180,7 +180,9 @@
conversations list</string> conversations list</string>
<!-- Contacts --> <!-- Contacts -->
<string name="nc_select_contact">Find participant</string>
<string name="nc_select_contacts">Find participants</string> <string name="nc_select_contacts">Find participants</string>
<string name="nc_select_new_contacts">Find new participants</string>
<string name="nc_contacts_done">Done</string> <string name="nc_contacts_done">Done</string>
<!-- Permissions --> <!-- Permissions -->
@ -339,4 +341,5 @@
<string name="path_password_strike_through" translatable="false" <string name="path_password_strike_through" translatable="false"
tools:override="true">M3.27,4.27L19.74,20.74</string> tools:override="true">M3.27,4.27L19.74,20.74</string>
<string name="nc_search_for_more">Search for more participants</string> <string name="nc_search_for_more">Search for more participants</string>
<string name="nc_new_group">New group</string>
</resources> </resources>