Added favourites contacts list

This commit is contained in:
Sylvain Berfini 2023-08-07 14:02:45 +02:00
parent f104d5c891
commit 437fa5c128
14 changed files with 275 additions and 65 deletions

View file

@ -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())

View file

@ -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<ContactModel, RecyclerView.ViewHolder>(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<ContactModel>() {
}
override fun areContentsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean {
return true
return oldItem.showFirstLetter.value == newItem.showFirstLetter.value
}
}

View file

@ -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 ?: "")
}
}
}
}

View file

@ -37,6 +37,12 @@ class ContactModel(val friend: Friend) {
val name = MutableLiveData<String>()
val firstLetter: String by lazy {
LinphoneUtils.getFirstLetter(friend.name.orEmpty())
}
val showFirstLetter = MutableLiveData<Boolean>()
private val friendListener = object : FriendListenerStub() {
override fun onPresenceReceived(fr: Friend) {
presenceStatus.postValue(fr.consolidatedPresence)

View file

@ -34,6 +34,10 @@ import org.linphone.ui.viewmodel.TopBarViewModel
class ContactsListViewModel : TopBarViewModel() {
val contactsList = MutableLiveData<ArrayList<ContactModel>>()
val favourites = MutableLiveData<ArrayList<ContactModel>>()
val showFavourites = MutableLiveData<Boolean>()
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<SearchResult>) {
// Core thread
Log.i("[Contacts List] Processing ${results.size} results")
contactsList.value.orEmpty().forEach(ContactModel::destroy)
val list = arrayListOf<ContactModel>()
val favouritesList = arrayListOf<ContactModel>()
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")

View file

@ -52,6 +52,7 @@ abstract class TopBarViewModel : ViewModel() {
fun closeSearchBar() {
// UI thread
clearFilter()
searchBarVisible.value = false
focusSearchBarEvent.value = Event(false)
}

View file

@ -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(

View file

@ -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 ""

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 7.41 16.41 L 12 11.83 L 16.59 16.41 L 18 15 L 12 9 L 6 15 L 7.41 16.41 Z"
android:fillColor="#4e6074"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,17 @@
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="180"
android:toDegrees="180">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 7.41 16.41 L 12 11.83 L 16.59 16.41 L 18 15 L 12 9 L 6 15 L 7.41 16.41 Z"
android:fillColor="#4e6074"
android:strokeWidth="1"/>
</vector>
</rotate>

View file

@ -94,6 +94,53 @@
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/no_contacts_image" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/favourites_label"
android:visibility="@{viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
onClickListener="@{() -> viewModel.toggleFavouritesVisibility()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="20dp"
android:text="Favourites"
android:drawableEnd="@drawable/collapse"
android:drawableTint="@color/gray_9"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/gray_9"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favourites_contacts_list"
android:visibility="@{viewModel.showFavourites &amp;&amp; viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/favourites_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/all_contacts_label"
android:visibility="@{viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="All contacts"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/gray_9"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/favourites_contacts_list"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactsList"
android:layout_width="0dp"
@ -103,7 +150,7 @@
android:layout_marginEnd="10dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton

View file

@ -21,16 +21,13 @@
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_marginStart="8dp"
android:background="@drawable/cell_background">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
contactAvatar="@{model}"
app:avatarViewPlaceholder="@drawable/contact_avatar"
@ -53,12 +50,15 @@
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@{model.name, default=`John Doe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:layout_marginStart="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/avatar"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,12 +9,6 @@
<variable
name="model"
type="org.linphone.ui.contacts.model.ContactModel" />
<variable
name="first_letter"
type="String" />
<variable
name="show_first_letter"
type="Boolean" />
<variable
name="onClickListener"
type="View.OnClickListener" />
@ -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"

View file

@ -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" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/favourites_label"
android:visibility="@{viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
onClickListener="@{() -> viewModel.toggleFavouritesVisibility()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="15dp"
android:text="Favourites"
android:drawableEnd="@{viewModel.showFavourites ? @drawable/collapse : @drawable/expand, default=@drawable/collapse}"
android:drawableTint="@color/gray_9"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/gray_9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favourites_contacts_list"
android:visibility="@{viewModel.showFavourites &amp;&amp; viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/favourites_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/all_contacts_label"
android:visibility="@{viewModel.searchFilter.length() == 0 ? View.VISIBLE : View.GONE}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="20dp"
android:text="All contacts"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/gray_9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/favourites_contacts_list"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactsList"
android:layout_width="0dp"
@ -85,8 +134,8 @@
android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
<include
bind:onConversationsClicked="@{onConversationsClicked}"