From 437fa5c1288fb73791886eded9db7bca86c59168 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 7 Aug 2023 14:02:45 +0200 Subject: [PATCH] Added favourites contacts list --- .../java/org/linphone/LinphoneApplication.kt | 9 +++ .../contacts/adapter/ContactsListAdapter.kt | 80 ++++++++++++------- .../contacts/fragment/ContactsListFragment.kt | 51 ++++++++---- .../ui/contacts/model/ContactModel.kt | 6 ++ .../viewmodel/ContactsListViewModel.kt | 29 ++++++- .../linphone/ui/viewmodel/TopBarViewModel.kt | 1 + .../org/linphone/utils/DataBindingUtils.kt | 2 +- .../java/org/linphone/utils/LinphoneUtils.kt | 4 + app/src/main/res/drawable/collapse.xml | 13 +++ app/src/main/res/drawable/expand.xml | 17 ++++ .../layout-land/contacts_list_fragment.xml | 49 +++++++++++- .../layout/contact_favourite_list_cell.xml | 14 ++-- app/src/main/res/layout/contact_list_cell.xml | 10 +-- .../res/layout/contacts_list_fragment.xml | 55 ++++++++++++- 14 files changed, 275 insertions(+), 65 deletions(-) create mode 100644 app/src/main/res/drawable/collapse.xml create mode 100644 app/src/main/res/drawable/expand.xml diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt index 11813143a..4bca42e9f 100644 --- a/app/src/main/java/org/linphone/LinphoneApplication.kt +++ b/app/src/main/java/org/linphone/LinphoneApplication.kt @@ -30,6 +30,8 @@ import coil.decode.VideoFrameDecoder import coil.disk.DiskCache import coil.memory.MemoryCache import com.google.android.material.color.DynamicColors +import io.getstream.avatarview.coil.AvatarCoil +import io.getstream.avatarview.coil.AvatarImageLoaderFactory import org.linphone.core.CoreContext import org.linphone.core.CorePreferences import org.linphone.core.Factory @@ -72,10 +74,17 @@ class LinphoneApplication : Application(), ImageLoaderFactory { coreContext.start() DynamicColors.applyToActivitiesIfAvailable(this) + AvatarCoil.setImageLoader( + AvatarImageLoaderFactory(context) { + newImageLoader() + } + ) } override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this) + .crossfade(true) + .crossfade(400) .components { add(VideoFrameDecoder.Factory()) add(SvgDecoder.Factory()) diff --git a/app/src/main/java/org/linphone/ui/contacts/adapter/ContactsListAdapter.kt b/app/src/main/java/org/linphone/ui/contacts/adapter/ContactsListAdapter.kt index df3705eb0..fda58cb80 100644 --- a/app/src/main/java/org/linphone/ui/contacts/adapter/ContactsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/contacts/adapter/ContactsListAdapter.kt @@ -9,12 +9,14 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.R +import org.linphone.databinding.ContactFavouriteListCellBinding import org.linphone.databinding.ContactListCellBinding import org.linphone.ui.contacts.model.ContactModel import org.linphone.utils.Event class ContactsListAdapter( - private val viewLifecycleOwner: LifecycleOwner + private val viewLifecycleOwner: LifecycleOwner, + private val favourites: Boolean ) : ListAdapter(ContactDiffCallback()) { var selectedAdapterPosition = -1 @@ -27,17 +29,31 @@ class ContactsListAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: ContactListCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.contact_list_cell, - parent, - false - ) - return ViewHolder(binding) + if (favourites) { + val binding: ContactFavouriteListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.contact_favourite_list_cell, + parent, + false + ) + return FavouriteViewHolder(binding) + } else { + val binding: ContactListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.contact_list_cell, + parent, + false + ) + return ViewHolder(binding) + } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as ViewHolder).bind(getItem(position)) + if (favourites) { + (holder as FavouriteViewHolder).bind(getItem(position)) + } else { + (holder as ViewHolder).bind(getItem(position)) + } } fun resetSelection() { @@ -45,29 +61,39 @@ class ContactsListAdapter( selectedAdapterPosition = -1 } - fun showHeaderForPosition(position: Int): Boolean { - if (position >= itemCount) return false - - val contact = getItem(position) - val firstLetter = contact.name.value?.get(0).toString() - val previousPosition = position - 1 - - return if (previousPosition >= 0) { - val previousItemFirstLetter = getItem(previousPosition).name.value?.get(0).toString() - !firstLetter.equals(previousItemFirstLetter, ignoreCase = true) - } else { - true - } - } - inner class ViewHolder( val binding: ContactListCellBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(contactModel: ContactModel) { with(binding) { model = contactModel - firstLetter = contactModel.name.value?.get(0).toString() - showFirstLetter = showHeaderForPosition(bindingAdapterPosition) + + lifecycleOwner = viewLifecycleOwner + + binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition + + binding.setOnClickListener { + contactClickedEvent.value = Event(contactModel) + } + + binding.setOnLongClickListener { + selectedAdapterPosition = bindingAdapterPosition + binding.root.isSelected = true + contactLongClickedEvent.value = Event(contactModel) + true + } + + executePendingBindings() + } + } + } + + inner class FavouriteViewHolder( + val binding: ContactFavouriteListCellBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(contactModel: ContactModel) { + with(binding) { + model = contactModel lifecycleOwner = viewLifecycleOwner @@ -96,6 +122,6 @@ private class ContactDiffCallback : DiffUtil.ItemCallback() { } override fun areContentsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean { - return true + return oldItem.showFirstLetter.value == newItem.showFirstLetter.value } } diff --git a/app/src/main/java/org/linphone/ui/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/contacts/fragment/ContactsListFragment.kt index 48c7a55e2..533d1d4cc 100644 --- a/app/src/main/java/org/linphone/ui/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/contacts/fragment/ContactsListFragment.kt @@ -53,6 +53,7 @@ class ContactsListFragment : Fragment() { ) private lateinit var adapter: ContactsListAdapter + private lateinit var favouritesAdapter: ContactsListAdapter override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? { if (findNavController().currentDestination?.id == R.id.newContactFragment) { @@ -89,28 +90,23 @@ class ContactsListFragment : Fragment() { listViewModel.bottomNavBarVisible.value = !portraitOrientation || !keyboardVisible } - adapter = ContactsListAdapter(viewLifecycleOwner) + adapter = ContactsListAdapter(viewLifecycleOwner, false) binding.contactsList.setHasFixedSize(true) binding.contactsList.adapter = adapter - - adapter.contactLongClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - val modalBottomSheet = ContactsListMenuDialogFragment(model.friend) { - adapter.resetSelection() - } - modalBottomSheet.show(parentFragmentManager, ContactsListMenuDialogFragment.TAG) - } - } - - adapter.contactClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - sharedViewModel.showContactEvent.value = Event(model.id ?: "") - } - } + configureAdapter(adapter) val layoutManager = LinearLayoutManager(requireContext()) binding.contactsList.layoutManager = layoutManager + favouritesAdapter = ContactsListAdapter(viewLifecycleOwner, true) + binding.favouritesContactsList.setHasFixedSize(true) + binding.favouritesContactsList.adapter = favouritesAdapter + configureAdapter(favouritesAdapter) + + val favouritesLayoutManager = LinearLayoutManager(requireContext()) + favouritesLayoutManager.orientation = LinearLayoutManager.HORIZONTAL + binding.favouritesContactsList.layoutManager = favouritesLayoutManager + listViewModel.contactsList.observe( viewLifecycleOwner ) { @@ -121,6 +117,12 @@ class ContactsListFragment : Fragment() { } } + listViewModel.favourites.observe( + viewLifecycleOwner + ) { + favouritesAdapter.submitList(it) + } + listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) { it.consume { show -> if (show) { @@ -154,4 +156,21 @@ class ContactsListFragment : Fragment() { (requireActivity() as MainActivity).toggleDrawerMenu() } } + + private fun configureAdapter(adapter: ContactsListAdapter) { + adapter.contactLongClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + val modalBottomSheet = ContactsListMenuDialogFragment(model.friend) { + adapter.resetSelection() + } + modalBottomSheet.show(parentFragmentManager, ContactsListMenuDialogFragment.TAG) + } + } + + adapter.contactClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + sharedViewModel.showContactEvent.value = Event(model.id ?: "") + } + } + } } diff --git a/app/src/main/java/org/linphone/ui/contacts/model/ContactModel.kt b/app/src/main/java/org/linphone/ui/contacts/model/ContactModel.kt index edec25aac..836882c16 100644 --- a/app/src/main/java/org/linphone/ui/contacts/model/ContactModel.kt +++ b/app/src/main/java/org/linphone/ui/contacts/model/ContactModel.kt @@ -37,6 +37,12 @@ class ContactModel(val friend: Friend) { val name = MutableLiveData() + val firstLetter: String by lazy { + LinphoneUtils.getFirstLetter(friend.name.orEmpty()) + } + + val showFirstLetter = MutableLiveData() + private val friendListener = object : FriendListenerStub() { override fun onPresenceReceived(fr: Friend) { presenceStatus.postValue(fr.consolidatedPresence) diff --git a/app/src/main/java/org/linphone/ui/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/contacts/viewmodel/ContactsListViewModel.kt index 72e05fba2..987068daa 100644 --- a/app/src/main/java/org/linphone/ui/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/contacts/viewmodel/ContactsListViewModel.kt @@ -34,6 +34,10 @@ import org.linphone.ui.viewmodel.TopBarViewModel class ContactsListViewModel : TopBarViewModel() { val contactsList = MutableLiveData>() + val favourites = MutableLiveData>() + + val showFavourites = MutableLiveData() + private var currentFilter = "" private var previousFilter = "NotSet" @@ -57,6 +61,7 @@ class ContactsListViewModel : TopBarViewModel() { init { title.value = "Contacts" bottomNavBarVisible.value = true + showFavourites.value = true coreContext.postOnCoreThread { core -> coreContext.contactsManager.addListener(contactsListener) @@ -75,28 +80,48 @@ class ContactsListViewModel : TopBarViewModel() { super.onCleared() } + fun toggleFavouritesVisibility() { + // UI thread + showFavourites.value = showFavourites.value == false + } + fun processMagicSearchResults(results: Array) { // Core thread Log.i("[Contacts List] Processing ${results.size} results") contactsList.value.orEmpty().forEach(ContactModel::destroy) val list = arrayListOf() + val favouritesList = arrayListOf() + var previousLetter = "" for (result in results) { val friend = result.friend - val viewModel = if (friend != null) { + var currentLetter = "" + val model = if (friend != null) { + currentLetter = friend.name?.get(0).toString() ContactModel(friend) } else { Log.w("[Contacts] SearchResult [$result] has no Friend!") val fakeFriend = createFriendFromSearchResult(result) + currentLetter = fakeFriend.name?.get(0).toString() ContactModel(fakeFriend) } - list.add(viewModel) + val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter + if (currentLetter != previousLetter) { + previousLetter = currentLetter + } + model.showFirstLetter.postValue(displayLetter) + + list.add(model) + if (friend?.starred == true) { + favouritesList.add(model) + } } + favourites.postValue(favouritesList) contactsList.postValue(list) Log.i("[Contacts] Processed ${results.size} results") diff --git a/app/src/main/java/org/linphone/ui/viewmodel/TopBarViewModel.kt b/app/src/main/java/org/linphone/ui/viewmodel/TopBarViewModel.kt index 24188cdc9..a81323763 100644 --- a/app/src/main/java/org/linphone/ui/viewmodel/TopBarViewModel.kt +++ b/app/src/main/java/org/linphone/ui/viewmodel/TopBarViewModel.kt @@ -52,6 +52,7 @@ abstract class TopBarViewModel : ViewModel() { fun closeSearchBar() { // UI thread + clearFilter() searchBarVisible.value = false focusSearchBarEvent.value = Event(false) } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index a88fa9cfa..98cbc6b6d 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -116,7 +116,7 @@ fun AvatarView.loadContactPicture(contact: ContactModel?) { ConsolidatedPresence.Online -> R.color.green_online else -> R.color.blue_outgoing_message } - indicatorEnabled = true + indicatorEnabled = contact.presenceStatus.value != ConsolidatedPresence.Offline val uri = contact.getAvatarUri() loadImage( diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 80d1a2800..8c077a6b7 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -47,6 +47,10 @@ class LinphoneUtils { } } + fun getFirstLetter(displayName: String): String { + return getInitials(displayName, 1) + } + fun getInitials(displayName: String, limit: Int = 2): String { if (displayName.isEmpty()) return "" diff --git a/app/src/main/res/drawable/collapse.xml b/app/src/main/res/drawable/collapse.xml new file mode 100644 index 000000000..b1fdf9889 --- /dev/null +++ b/app/src/main/res/drawable/collapse.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/expand.xml b/app/src/main/res/drawable/expand.xml new file mode 100644 index 000000000..b4ff0f75c --- /dev/null +++ b/app/src/main/res/drawable/expand.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index 7ad4f6c95..00d6f4466 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -94,6 +94,53 @@ app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintTop_toBottomOf="@id/no_contacts_image" /> + + + + + + diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml index 0ff1c3961..71e37a24f 100644 --- a/app/src/main/res/layout/contact_list_cell.xml +++ b/app/src/main/res/layout/contact_list_cell.xml @@ -9,12 +9,6 @@ - - @@ -36,8 +30,8 @@ android:id="@+id/header" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@{first_letter, default=`A`}" - android:visibility="@{show_first_letter ? View.VISIBLE : View.INVISIBLE, default=invisible}" + android:text="@{model.firstLetter, default=`A`}" + android:visibility="@{model.showFirstLetter ? View.VISIBLE : View.INVISIBLE, default=invisible}" android:textColor="@color/gray_10" android:textSize="20sp" android:textStyle="bold" diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 91abcfb6c..547495eb7 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -45,7 +45,7 @@ android:layout_height="0dp" android:layout_marginTop="7dp" android:src="@drawable/shape_white_background" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/top_bar" /> @@ -76,6 +76,55 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/no_contacts_image" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/all_contacts_label" + app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />