diff --git a/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt b/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt
new file mode 100644
index 000000000..36cca5069
--- /dev/null
+++ b/app/src/main/java/org/linphone/contacts/ContactSelectionData.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 .
+ */
+package org.linphone.contacts
+
+import androidx.lifecycle.MutableLiveData
+import org.linphone.core.*
+
+class ContactSelectionData(searchResult: SearchResult) {
+ val name = MutableLiveData()
+
+ init {
+ name.value = searchResult.friend?.name ?: searchResult.toString()
+ }
+}
diff --git a/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt b/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt
new file mode 100644
index 000000000..ed6055352
--- /dev/null
+++ b/app/src/main/java/org/linphone/contacts/ContactsSelectionAdapter.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 .
+ */
+package org.linphone.contacts
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.LifecycleOwner
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.linphone.R
+import org.linphone.core.SearchResult
+import org.linphone.databinding.ContactSelectionCellBinding
+
+class ContactsSelectionAdapter(
+ private val viewLifecycleOwner: LifecycleOwner
+) : ListAdapter(SearchResultDiffCallback()) {
+ init {
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val binding: ContactSelectionCellBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.contact_selection_cell,
+ parent,
+ false
+ )
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ (holder as ViewHolder).bind(getItem(position))
+ }
+
+ inner class ViewHolder(
+ private val binding: ContactSelectionCellBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(searchResult: SearchResult) {
+ with(binding) {
+ val searchResultViewModel = ContactSelectionData(searchResult)
+ data = searchResultViewModel
+
+ lifecycleOwner = viewLifecycleOwner
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class SearchResultDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: SearchResult,
+ newItem: SearchResult
+ ): Boolean {
+ val oldAddress = oldItem.address
+ val newAddress = newItem.address
+ return if (oldAddress != null && newAddress != null) oldAddress.weakEqual(newAddress) else false
+ }
+
+ override fun areContentsTheSame(
+ oldItem: SearchResult,
+ newItem: SearchResult
+ ): Boolean {
+ return newItem.friend != null
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/MainActivity.kt b/app/src/main/java/org/linphone/ui/MainActivity.kt
index 59967e4b7..aafa2c423 100644
--- a/app/src/main/java/org/linphone/ui/MainActivity.kt
+++ b/app/src/main/java/org/linphone/ui/MainActivity.kt
@@ -24,11 +24,11 @@ import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.loader.app.LoaderManager
-import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationBarView
@@ -45,15 +45,15 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
- private val onNavDestinationChangedListener =
- NavController.OnDestinationChangedListener { _, destination, _ ->
- binding.mainNavView?.visibility = View.VISIBLE
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true)
super.onCreate(savedInstanceState)
+ window.statusBarColor = ContextCompat.getColor(
+ this,
+ R.color.primary_color
+ )
+
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
val manager = LoaderManager.getInstance(this)
manager.restartLoader(0, null, ContactLoader())
@@ -84,9 +84,6 @@ class MainActivity : AppCompatActivity() {
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
- binding.mainNavHostFragment.findNavController()
- .addOnDestinationChangedListener(onNavDestinationChangedListener)
-
getNavBar()?.setupWithNavController(binding.mainNavHostFragment.findNavController())
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
@@ -113,4 +110,12 @@ class MainActivity : AppCompatActivity() {
private fun getNavBar(): NavigationBarView? {
return binding.mainNavView ?: binding.mainNavRail
}
+
+ fun hideNavBar() {
+ binding.mainNavView?.visibility = View.GONE
+ }
+
+ fun showNavBar() {
+ binding.mainNavView?.visibility = View.VISIBLE
+ }
}
diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt
index 7d407073b..80a6e5d10 100644
--- a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt
+++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt
@@ -23,13 +23,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.content.ContextCompat
+import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ConversationsFragmentBinding
+import org.linphone.ui.MainActivity
class ConversationsFragment : Fragment() {
private lateinit var binding: ConversationsFragmentBinding
@@ -42,11 +44,13 @@ class ConversationsFragment : Fragment() {
override fun onChanged() {
scrollToTop()
}
+
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && itemCount == 1) {
scrollToTop()
}
}
+
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
scrollToTop()
}
@@ -64,13 +68,6 @@ class ConversationsFragment : Fragment() {
savedInstanceState: Bundle?
): View {
binding = ConversationsFragmentBinding.inflate(layoutInflater)
-
- val window = requireActivity().window
- window.statusBarColor = ContextCompat.getColor(
- requireContext(),
- R.color.gray_1
- )
-
return binding.root
}
@@ -78,6 +75,9 @@ class ConversationsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
+ binding.viewModel = listViewModel
+
+ postponeEnterTransition()
adapter = ConversationsListAdapter(viewLifecycleOwner)
adapter.registerAdapterDataObserver(observer)
@@ -104,6 +104,11 @@ class ConversationsFragment : Fragment() {
viewLifecycleOwner
) {
adapter.submitList(it)
+
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ (requireActivity() as MainActivity).showNavBar()
+ }
}
listViewModel.notifyItemChangedEvent.observe(viewLifecycleOwner) {
@@ -111,6 +116,16 @@ class ConversationsFragment : Fragment() {
adapter.notifyItemChanged(index)
}
}
+
+ binding.setOnNewConversationClicked {
+ goToNewConversation()
+ }
+ }
+
+ private fun goToNewConversation() {
+ findNavController().navigate(
+ R.id.action_conversationsFragment_to_newConversationFragment
+ )
}
private fun scrollToTop() {
diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt
new file mode 100644
index 000000000..b608fee6c
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 .
+ */
+package org.linphone.ui.conversations
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.doOnPreDraw
+import androidx.fragment.app.Fragment
+import androidx.navigation.navGraphViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.R
+import org.linphone.contacts.ContactsSelectionAdapter
+import org.linphone.databinding.NewConversationFragmentBinding
+import org.linphone.ui.MainActivity
+
+class NewConversationFragment : Fragment() {
+ private lateinit var binding: NewConversationFragmentBinding
+ private lateinit var adapter: ContactsSelectionAdapter
+ private val viewModel: NewConversationViewModel by navGraphViewModels(
+ R.id.conversationsFragment
+ )
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = NewConversationFragmentBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+ binding.viewModel = viewModel
+
+ postponeEnterTransition()
+
+ adapter = ContactsSelectionAdapter(viewLifecycleOwner)
+ binding.contactsList.adapter = adapter
+ binding.contactsList.setHasFixedSize(true)
+
+ val layoutManager = LinearLayoutManager(requireContext())
+ binding.contactsList.layoutManager = layoutManager
+
+ viewModel.contactsList.observe(
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ (requireActivity() as MainActivity).hideNavBar()
+ }
+ }
+
+ viewModel.filter.observe(
+ viewLifecycleOwner
+ ) {
+ viewModel.applyFilter(it.orEmpty().trim())
+ }
+
+ binding.setCancelClickListener {
+ requireActivity().onBackPressedDispatcher.onBackPressed()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt
new file mode 100644
index 000000000..77b3ab612
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 .
+ */
+package org.linphone.ui.conversations
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.MagicSearch
+import org.linphone.core.MagicSearchListenerStub
+import org.linphone.core.SearchResult
+import org.linphone.core.tools.Log
+
+class NewConversationViewModel : ViewModel() {
+ val contactsList = MutableLiveData>()
+
+ val filter = MutableLiveData()
+ private var previousFilter = "NotSet"
+
+ private val magicSearch: MagicSearch by lazy {
+ val magicSearch = coreContext.core.createMagicSearch()
+ magicSearch.limitedSearch = false
+ magicSearch
+ }
+
+ private val magicSearchListener = object : MagicSearchListenerStub() {
+ override fun onSearchResultsReceived(magicSearch: MagicSearch) {
+ processMagicSearchResults(magicSearch.lastSearch)
+ }
+ }
+
+ init {
+ magicSearch.addListener(magicSearchListener)
+ applyFilter("")
+ }
+
+ override fun onCleared() {
+ magicSearch.removeListener(magicSearchListener)
+ super.onCleared()
+ }
+
+ fun applyFilter(filterValue: String) {
+ Log.i("[New Conversation ViewModel] Filtering contacts using [$filterValue]")
+ if (previousFilter.isNotEmpty() && (
+ previousFilter.length > filterValue.length ||
+ (previousFilter.length == filterValue.length && previousFilter != filterValue)
+ )
+ ) {
+ coreContext.postOnCoreThread { core ->
+ magicSearch.resetSearchCache()
+ }
+ }
+ previousFilter = filterValue
+
+ coreContext.postOnCoreThread { core ->
+ magicSearch.getContactsListAsync(
+ filterValue,
+ "",
+ MagicSearch.Source.Friends.toInt(),
+ MagicSearch.Aggregation.Friend
+ )
+ }
+ }
+
+ private fun processMagicSearchResults(results: Array) {
+ Log.i("[New Conversation ViewModel] [${results.size}] matching results")
+ val list = arrayListOf()
+ list.addAll(results)
+ contactsList.postValue(list)
+ }
+}
diff --git a/app/src/main/res/drawable/group_chat.xml b/app/src/main/res/drawable/group_chat.xml
new file mode 100644
index 000000000..fbfe18ea9
--- /dev/null
+++ b/app/src/main/res/drawable/group_chat.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/next.xml b/app/src/main/res/drawable/next.xml
new file mode 100644
index 000000000..6393d14b8
--- /dev/null
+++ b/app/src/main/res/drawable/next.xml
@@ -0,0 +1,16 @@
+
+
+
diff --git a/app/src/main/res/drawable/shape_gray_background.xml b/app/src/main/res/drawable/shape_gray_background.xml
new file mode 100644
index 000000000..2cb2df90e
--- /dev/null
+++ b/app/src/main/res/drawable/shape_gray_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_orange_round.xml b/app/src/main/res/drawable/shape_orange_round.xml
new file mode 100644
index 000000000..23a0d6ebc
--- /dev/null
+++ b/app/src/main/res/drawable/shape_orange_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_search_square_background.xml b/app/src/main/res/drawable/shape_search_square_background.xml
new file mode 100644
index 000000000..5b5623ce0
--- /dev/null
+++ b/app/src/main/res/drawable/shape_search_square_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 22d53a3d7..99c2ce2fd 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -14,6 +14,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversations_fragment.xml b/app/src/main/res/layout/conversations_fragment.xml
index 4f7994bd4..33d18c03b 100644
--- a/app/src/main/res/layout/conversations_fragment.xml
+++ b/app/src/main/res/layout/conversations_fragment.xml
@@ -9,10 +9,7 @@
name="viewModel"
type="org.linphone.ui.conversations.ConversationsListViewModel" />
-
@@ -22,8 +19,7 @@
+ android:layout_height="match_parent">
@@ -116,6 +113,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml
index 810928820..c619056d8 100644
--- a/app/src/main/res/navigation/main_nav_graph.xml
+++ b/app/src/main/res/navigation/main_nav_graph.xml
@@ -9,6 +9,16 @@
android:id="@+id/conversationsFragment"
android:name="org.linphone.ui.conversations.ConversationsFragment"
android:label="ConversationsFragment"
- tools:layout="@layout/conversations_fragment"/>
+ tools:layout="@layout/conversations_fragment">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 00d856972..8b71c3a1f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -11,5 +11,6 @@
#EEF6F8
#949494
#4E4E4E
+ #EDEDED
#E5E5EA
\ No newline at end of file