Factorized code

This commit is contained in:
Sylvain Berfini 2024-06-17 14:47:26 +02:00
parent 217f116324
commit 60a3752fe8
30 changed files with 177 additions and 625 deletions

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.linphone.ui.main.chat.adapter package org.linphone.ui.adapter
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
@ -31,11 +31,11 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.linphone.R import org.linphone.R
import org.linphone.databinding.ChatMessageForwardContactListCellBinding import org.linphone.databinding.GenericAddressPickerContactListCellBinding
import org.linphone.databinding.ChatMessageForwardConversationListCellBinding import org.linphone.databinding.GenericAddressPickerConversationListCellBinding
import org.linphone.databinding.ChatMessageForwardSuggestionListCellBinding import org.linphone.databinding.GenericAddressPickerListDecorationBinding
import org.linphone.databinding.StartCallSuggestionListDecorationBinding import org.linphone.databinding.GenericAddressPickerSuggestionListCellBinding
import org.linphone.ui.main.chat.model.ConversationContactOrSuggestionModel import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter import org.linphone.utils.HeaderAdapter
@ -64,16 +64,18 @@ class ConversationsContactsAndSuggestionsListAdapter :
} }
override fun getHeaderViewForPosition(context: Context, position: Int): View { override fun getHeaderViewForPosition(context: Context, position: Int): View {
val binding = StartCallSuggestionListDecorationBinding.inflate(LayoutInflater.from(context)) val binding = GenericAddressPickerListDecorationBinding.inflate(
LayoutInflater.from(context)
)
binding.header.text = when (getItemViewType(position)) { binding.header.text = when (getItemViewType(position)) {
CONVERSATION_TYPE -> { CONVERSATION_TYPE -> {
AppUtils.getString(R.string.conversation_message_forward_conversations_list_title) AppUtils.getString(R.string.generic_address_picker_conversations_list_title)
} }
SUGGESTION_TYPE -> { SUGGESTION_TYPE -> {
AppUtils.getString(R.string.history_call_start_suggestions_list_title) AppUtils.getString(R.string.generic_address_picker_suggestions_list_title)
} }
else -> { else -> {
AppUtils.getString(R.string.history_call_start_contacts_list_title) AppUtils.getString(R.string.generic_address_picker_contacts_list_title)
} }
} }
return binding.root return binding.root
@ -93,33 +95,51 @@ class ConversationsContactsAndSuggestionsListAdapter :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) { return when (viewType) {
CONVERSATION_TYPE -> { CONVERSATION_TYPE -> {
val binding: ChatMessageForwardConversationListCellBinding = DataBindingUtil.inflate( val binding: GenericAddressPickerConversationListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
R.layout.chat_message_forward_conversation_list_cell, R.layout.generic_address_picker_conversation_list_cell,
parent, parent,
false false
) )
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner() binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
onClickedEvent.value = Event(model!!)
}
}
ConversationViewHolder(binding) ConversationViewHolder(binding)
} }
CONTACT_TYPE -> { CONTACT_TYPE -> {
val binding: ChatMessageForwardContactListCellBinding = DataBindingUtil.inflate( val binding: GenericAddressPickerContactListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
R.layout.chat_message_forward_contact_list_cell, R.layout.generic_address_picker_contact_list_cell,
parent, parent,
false false
) )
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner() binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
onClickedEvent.value = Event(model!!)
}
}
ContactViewHolder(binding) ContactViewHolder(binding)
} }
else -> { else -> {
val binding: ChatMessageForwardSuggestionListCellBinding = DataBindingUtil.inflate( val binding: GenericAddressPickerSuggestionListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
R.layout.chat_message_forward_suggestion_list_cell, R.layout.generic_address_picker_suggestion_list_cell,
parent, parent,
false false
) )
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner() binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
onClickedEvent.value = Event(model!!)
}
}
SuggestionViewHolder(binding) SuggestionViewHolder(binding)
} }
} }
@ -134,33 +154,25 @@ class ConversationsContactsAndSuggestionsListAdapter :
} }
inner class ConversationViewHolder( inner class ConversationViewHolder(
val binding: ChatMessageForwardConversationListCellBinding val binding: GenericAddressPickerConversationListCellBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@UiThread @UiThread
fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) { fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) {
with(binding) { with(binding) {
model = conversationContactOrSuggestionModel model = conversationContactOrSuggestionModel
setOnClickListener {
onClickedEvent.value = Event(conversationContactOrSuggestionModel)
}
executePendingBindings() executePendingBindings()
} }
} }
} }
inner class ContactViewHolder( inner class ContactViewHolder(
val binding: ChatMessageForwardContactListCellBinding val binding: GenericAddressPickerContactListCellBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@UiThread @UiThread
fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) { fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) {
with(binding) { with(binding) {
model = conversationContactOrSuggestionModel.avatarModel.value model = conversationContactOrSuggestionModel
setOnClickListener {
onClickedEvent.value = Event(conversationContactOrSuggestionModel)
}
val previousItem = bindingAdapterPosition - 1 val previousItem = bindingAdapterPosition - 1
val previousLetter = if (previousItem >= 0) { val previousLetter = if (previousItem >= 0) {
@ -179,17 +191,13 @@ class ConversationsContactsAndSuggestionsListAdapter :
} }
inner class SuggestionViewHolder( inner class SuggestionViewHolder(
val binding: ChatMessageForwardSuggestionListCellBinding val binding: GenericAddressPickerSuggestionListCellBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@UiThread @UiThread
fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) { fun bind(conversationContactOrSuggestionModel: ConversationContactOrSuggestionModel) {
with(binding) { with(binding) {
model = conversationContactOrSuggestionModel model = conversationContactOrSuggestionModel
setOnClickListener {
onClickedEvent.value = Event(conversationContactOrSuggestionModel)
}
executePendingBindings() executePendingBindings()
} }
} }

View file

@ -84,7 +84,7 @@ class ConferenceAddParticipantsFragment : GenericAddressPickerFragment() {
setupRecyclerView(binding.contactsList) setupRecyclerView(binding.contactsList)
viewModel.contactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")

View file

@ -37,12 +37,12 @@ import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
import org.linphone.utils.DialogUtils import org.linphone.utils.DialogUtils
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
@ -62,7 +62,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
R.id.call_nav_graph R.id.call_nav_graph
) )
private lateinit var adapter: ContactsAndSuggestionsListAdapter private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener { private val listener = object : ContactNumberOrAddressClickListener {
@UiThread @UiThread
@ -87,7 +87,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = ContactsAndSuggestionsListAdapter() adapter = ConversationsContactsAndSuggestionsListAdapter()
} }
override fun onCreateView( override fun onCreateView(
@ -125,7 +125,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
binding.contactsAndSuggestionsList.adapter = adapter binding.contactsAndSuggestionsList.adapter = adapter
} }
adapter.contactClickedEvent.observe(viewLifecycleOwner) { adapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model -> it.consume { model ->
startCall(model) startCall(model)
} }
@ -133,7 +133,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.contactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
@ -213,7 +213,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
@WorkerThread @WorkerThread
abstract fun action(address: Address) abstract fun action(address: Address)
private fun startCall(model: ContactOrSuggestionModel) { private fun startCall(model: ConversationContactOrSuggestionModel) {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
val friend = model.friend val friend = model.friend
if (friend == null) { if (friend == null) {
@ -225,7 +225,7 @@ abstract class AbstractNewTransferCallFragment : GenericCallFragment() {
val numbersCount = friend.phoneNumbers.size val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode // Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = isEndToEndEncryptionMandatory() != true val enablePhoneNumbers = !isEndToEndEncryptionMandatory()
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i( Log.i(

View file

@ -32,7 +32,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.R import org.linphone.R
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMessageForwardFragmentBinding import org.linphone.databinding.ChatMessageForwardFragmentBinding
import org.linphone.ui.main.chat.adapter.ConversationsContactsAndSuggestionsListAdapter import org.linphone.ui.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationForwardMessageViewModel import org.linphone.ui.main.chat.viewmodel.ConversationForwardMessageViewModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
@ -103,7 +103,7 @@ class ConversationForwardMessageFragment : SlidingPaneChildFragment() {
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
binding.contactsList.addItemDecoration(headerItemDecoration) binding.contactsList.addItemDecoration(headerItemDecoration)
viewModel.conversationsContactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i( Log.i(

View file

@ -79,7 +79,7 @@ class StartConversationFragment : GenericAddressPickerFragment() {
setupRecyclerView(binding.contactsList) setupRecyclerView(binding.contactsList)
viewModel.contactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")

View file

@ -101,7 +101,7 @@ class MessageModel @WorkerThread constructor(
val time = TimestampUtils.toString(timestamp) val time = TimestampUtils.toString(timestamp)
val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly || val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly ||
(!chatMessage.chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt()) && isEndToEndEncryptionMandatory() == true) (!chatMessage.chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt()) && isEndToEndEncryptionMandatory())
val groupedWithNextMessage = MutableLiveData<Boolean>() val groupedWithNextMessage = MutableLiveData<Boolean>()

View file

@ -22,49 +22,29 @@ package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.text.Collator
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.contacts.ContactsManager
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoomListenerStub import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.ChatRoomParams import org.linphone.core.ChatRoomParams
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.chat.model.ConversationContactOrSuggestionModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
import org.linphone.ui.main.viewmodel.AddressSelectionViewModel
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
class ConversationForwardMessageViewModel @UiThread constructor() : GenericViewModel() { class ConversationForwardMessageViewModel @UiThread constructor() : AddressSelectionViewModel() {
companion object { companion object {
private const val TAG = "[Conversation Forward Message ViewModel]" private const val TAG = "[Conversation Forward Message ViewModel]"
} }
protected var magicSearchSourceFlags = MagicSearch.Source.All.toInt()
private var currentFilter = ""
private var previousFilter = "NotSet"
val searchFilter = MutableLiveData<String>()
val conversationsContactsAndSuggestionsList = MutableLiveData<ArrayList<ConversationContactOrSuggestionModel>>()
private var limitSearchToLinphoneAccounts = true
private lateinit var magicSearch: MagicSearch
val operationInProgress = MutableLiveData<Boolean>() val operationInProgress = MutableLiveData<Boolean>()
val chatRoomCreatedEvent: MutableLiveData<Event<Pair<String, String>>> by lazy { val chatRoomCreatedEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
@ -79,26 +59,6 @@ class ConversationForwardMessageViewModel @UiThread constructor() : GenericViewM
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
private val magicSearchListener = object : MagicSearchListenerStub() {
@WorkerThread
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
Log.i("$TAG Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
Log.i("$TAG Contacts have been (re)loaded, updating list")
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
magicSearchSourceFlags
)
}
}
private val listener = object : ContactNumberOrAddressClickListener { private val listener = object : ContactNumberOrAddressClickListener {
@UiThread @UiThread
override fun onClicked(model: ContactNumberOrAddressModel) { override fun onClicked(model: ContactNumberOrAddressModel) {
@ -152,182 +112,7 @@ class ConversationForwardMessageViewModel @UiThread constructor() : GenericViewM
} }
init { init {
coreContext.postOnCoreThread { core -> skipConversation = false
limitSearchToLinphoneAccounts = isEndToEndEncryptionMandatory()
coreContext.contactsManager.addListener(contactsListener)
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter(currentFilter)
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread {
magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsListener)
}
super.onCleared()
}
@UiThread
fun clearFilter() {
if (searchFilter.value.orEmpty().isNotEmpty()) {
searchFilter.value = ""
}
}
@UiThread
fun applyFilter(filter: String) {
coreContext.postOnCoreThread {
applyFilter(
filter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
magicSearchSourceFlags
)
}
}
@WorkerThread
private fun applyFilter(
filter: String,
domain: String,
sources: Int
) {
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
(previousFilter.length == filter.length && previousFilter != filter)
)
) {
magicSearch.resetSearchCache()
}
currentFilter = filter
previousFilter = filter
Log.i(
"$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,
domain,
sources,
MagicSearch.Aggregation.Friend
)
}
@WorkerThread
private fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("$TAG Processing [${results.size}] results")
val conversationsList = arrayListOf<ConversationContactOrSuggestionModel>()
for (chatRoom in LinphoneUtils.getDefaultAccount()?.chatRooms.orEmpty()) {
// Only get group conversations
if (!chatRoom.currentParams.isGroupEnabled) {
continue
}
val found = if (currentFilter.isEmpty()) {
null
} else {
chatRoom.participants.find {
// Search in address but also in contact name if exists
val model =
coreContext.contactsManager.getContactAvatarModelForAddress(it.address)
model.contactName?.contains(
currentFilter,
ignoreCase = true
) == true || it.address.asStringUriOnly().contains(
currentFilter,
ignoreCase = true
)
}
}
if (
currentFilter.isEmpty() ||
found != null ||
chatRoom.peerAddress.asStringUriOnly().contains(currentFilter, ignoreCase = true) ||
chatRoom.subject.orEmpty().contains(currentFilter, ignoreCase = true)
) {
val localAddress = chatRoom.localAddress
val remoteAddress = chatRoom.peerAddress
val model = ConversationContactOrSuggestionModel(
remoteAddress,
localAddress,
chatRoom.subject
)
val fakeFriend = coreContext.core.createFriend()
fakeFriend.name = chatRoom.subject
val avatarModel = ContactAvatarModel(fakeFriend)
avatarModel.defaultToConversationIcon.postValue(true)
model.avatarModel.postValue(avatarModel)
conversationsList.add(model)
}
}
val contactsList = arrayListOf<ConversationContactOrSuggestionModel>()
val suggestionsList = arrayListOf<ConversationContactOrSuggestionModel>()
for (result in results) {
val address = result.address
if (address != null) {
val friend = coreContext.contactsManager.findContactByAddress(address)
if (friend != null) {
val model = ConversationContactOrSuggestionModel(address, friend = friend)
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
address
)
model.avatarModel.postValue(avatarModel)
contactsList.add(model)
} else {
// If user-input generated result (always last) already exists, don't show it again
if (result.sourceFlags == MagicSearch.Source.Request.toInt()) {
val found = suggestionsList.find {
it.address.weakEqual(address)
}
if (found != null) {
Log.i(
"$TAG Result generated from user input is a duplicate of an existing solution, preventing double"
)
continue
}
}
val defaultAccountAddress = coreContext.core.defaultAccount?.params?.identityAddress
if (defaultAccountAddress != null && address.weakEqual(defaultAccountAddress)) {
Log.i("$TAG Removing from suggestions current default account address")
continue
}
val model = ConversationContactOrSuggestionModel(address) {
coreContext.startAudioCall(address)
}
suggestionsList.add(model)
}
}
}
val collator = Collator.getInstance(Locale.getDefault())
contactsList.sortWith { model1, model2 ->
collator.compare(model1.name, model2.name)
}
suggestionsList.sortWith { model1, model2 ->
collator.compare(model1.name, model2.name)
}
val list = arrayListOf<ConversationContactOrSuggestionModel>()
list.addAll(conversationsList)
list.addAll(contactsList)
list.addAll(suggestionsList)
conversationsContactsAndSuggestionsList.postValue(list)
Log.i(
"$TAG Processed [${results.size}] results, including [${conversationsList.size}] conversations, [${contactsList.size}] contacts and [${suggestionsList.size}] suggestions"
)
} }
@WorkerThread @WorkerThread

View file

@ -498,7 +498,7 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
@WorkerThread @WorkerThread
fun checkIfConversationShouldBeDisabledForSecurityReasons() { fun checkIfConversationShouldBeDisabledForSecurityReasons() {
if (!chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())) { if (!chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())) {
if (isEndToEndEncryptionMandatory() == true) { if (isEndToEndEncryptionMandatory()) {
Log.w( Log.w(
"$TAG Conversation with subject [${chatRoom.subject}] has been disabled because it isn't encrypted and default account is in secure mode" "$TAG Conversation with subject [${chatRoom.subject}] has been disabled because it isn't encrypted and default account is in secure mode"
) )

View file

@ -250,7 +250,7 @@ class ContactViewModel @UiThread constructor() : GenericViewModel() {
) )
// Only expand contacts' devices & trust by default if in E2E encrypted mode // Only expand contacts' devices & trust by default if in E2E encrypted mode
expandDevicesTrust.postValue( expandDevicesTrust.postValue(
isEndToEndEncryptionMandatory() == true isEndToEndEncryptionMandatory()
) )
coreContext.contactsManager.addListener(contactsListener) coreContext.contactsManager.addListener(contactsListener)
} }
@ -427,7 +427,7 @@ class ContactViewModel @UiThread constructor() : GenericViewModel() {
val numbersCount = friend.phoneNumbers.size val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode // Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = isEndToEndEncryptionMandatory() != true val enablePhoneNumbers = !isEndToEndEncryptionMandatory()
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i( Log.i(
@ -464,7 +464,7 @@ class ContactViewModel @UiThread constructor() : GenericViewModel() {
val numbersCount = friend.phoneNumbers.size val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode // Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = isEndToEndEncryptionMandatory() != true val enablePhoneNumbers = !isEndToEndEncryptionMandatory()
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i( Log.i(
@ -501,7 +501,7 @@ class ContactViewModel @UiThread constructor() : GenericViewModel() {
val numbersCount = friend.phoneNumbers.size val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode // Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = isEndToEndEncryptionMandatory() != true val enablePhoneNumbers = !isEndToEndEncryptionMandatory()
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i( Log.i(

View file

@ -163,7 +163,7 @@ class ContactsListViewModel @UiThread constructor() : AbstractMainViewModel() {
val defaultAccount = coreContext.core.defaultAccount val defaultAccount = coreContext.core.defaultAccount
val defaultDomain = defaultAccount?.params?.domain == corePreferences.defaultDomain val defaultDomain = defaultAccount?.params?.domain == corePreferences.defaultDomain
isDefaultAccountLinphone.postValue(defaultDomain) isDefaultAccountLinphone.postValue(defaultDomain)
domainFilter = if (isEndToEndEncryptionMandatory() == true) { domainFilter = if (isEndToEndEncryptionMandatory()) {
corePreferences.defaultDomain corePreferences.defaultDomain
} else { } else {
"*" "*"

View file

@ -85,7 +85,7 @@ class AddParticipantsFragment : GenericAddressPickerFragment() {
viewModel.addSelectedParticipants(participants) viewModel.addSelectedParticipants(participants)
} }
viewModel.contactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")

View file

@ -31,11 +31,11 @@ import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.Friend import org.linphone.core.Friend
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.model.SelectedAddressModel import org.linphone.ui.main.model.SelectedAddressModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
import org.linphone.ui.main.viewmodel.AddressSelectionViewModel import org.linphone.ui.main.viewmodel.AddressSelectionViewModel
@ -51,7 +51,7 @@ abstract class GenericAddressPickerFragment : GenericMainFragment() {
private var numberOrAddressPickerDialog: Dialog? = null private var numberOrAddressPickerDialog: Dialog? = null
protected lateinit var adapter: ContactsAndSuggestionsListAdapter protected lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter
protected abstract val viewModel: AddressSelectionViewModel protected abstract val viewModel: AddressSelectionViewModel
@ -85,13 +85,13 @@ abstract class GenericAddressPickerFragment : GenericMainFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = ContactsAndSuggestionsListAdapter() adapter = ConversationsContactsAndSuggestionsListAdapter()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter.contactClickedEvent.observe(viewLifecycleOwner) { adapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model -> it.consume { model ->
handleClickOnContactModel(model) handleClickOnContactModel(model)
} }
@ -147,7 +147,7 @@ abstract class GenericAddressPickerFragment : GenericMainFragment() {
} }
} }
private fun handleClickOnContactModel(model: ContactOrSuggestionModel) { private fun handleClickOnContactModel(model: ConversationContactOrSuggestionModel) {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
val friend = model.friend val friend = model.friend
if (friend == null) { if (friend == null) {
@ -162,7 +162,7 @@ abstract class GenericAddressPickerFragment : GenericMainFragment() {
val numbersCount = friend.phoneNumbers.size val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode // Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = isEndToEndEncryptionMandatory() != true val enablePhoneNumbers = !isEndToEndEncryptionMandatory()
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) { if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
val address = friend.addresses.first() val address = friend.addresses.first()

View file

@ -1,179 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.history.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ContactListCellBinding
import org.linphone.databinding.StartCallSuggestionListCellBinding
import org.linphone.databinding.StartCallSuggestionListDecorationBinding
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter
class ContactsAndSuggestionsListAdapter :
ListAdapter<ContactOrSuggestionModel, RecyclerView.ViewHolder>(
ContactOrSuggestionDiffCallback()
),
HeaderAdapter {
companion object {
private const val CONTACT_TYPE = 0
private const val SUGGESTION_TYPE = 1
}
val contactClickedEvent: MutableLiveData<Event<ContactOrSuggestionModel>> by lazy {
MutableLiveData<Event<ContactOrSuggestionModel>>()
}
override fun displayHeaderForPosition(position: Int): Boolean {
val model = getItem(position)
if (position == 0) {
return true
} else if (model.friend == null) {
val previousModel = getItem(position - 1)
return previousModel.friend != null
}
return false
}
override fun getHeaderViewForPosition(context: Context, position: Int): View {
val binding = StartCallSuggestionListDecorationBinding.inflate(LayoutInflater.from(context))
binding.header.text = if (position == 0) {
if (getItemViewType(0) == SUGGESTION_TYPE) {
AppUtils.getString(R.string.history_call_start_suggestions_list_title)
} else {
AppUtils.getString(R.string.history_call_start_contacts_list_title)
}
} else {
AppUtils.getString(R.string.history_call_start_suggestions_list_title)
}
return binding.root
}
override fun getItemViewType(position: Int): Int {
val model = getItem(position)
return if (model.friend == null) SUGGESTION_TYPE else CONTACT_TYPE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
CONTACT_TYPE -> {
val binding: ContactListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.contact_list_cell,
parent,
false
)
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner()
ContactViewHolder(binding)
}
else -> {
val binding: StartCallSuggestionListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.start_call_suggestion_list_cell,
parent,
false
)
binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
contactClickedEvent.value = Event(model!!)
}
}
SuggestionViewHolder(binding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
CONTACT_TYPE -> (holder as ContactViewHolder).bind(getItem(position))
else -> (holder as SuggestionViewHolder).bind(getItem(position))
}
}
inner class ContactViewHolder(
val binding: ContactListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(contactOrSuggestionModel: ContactOrSuggestionModel) {
with(binding) {
model = contactOrSuggestionModel.avatarModel.value
setOnClickListener {
contactClickedEvent.value = Event(contactOrSuggestionModel)
}
val previousItem = bindingAdapterPosition - 1
val previousLetter = if (previousItem >= 0) {
getItem(previousItem).name[0].toString()
} else {
""
}
val currentLetter = contactOrSuggestionModel.name[0].toString()
val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter
firstContactStartingByThatLetter = displayLetter
executePendingBindings()
}
}
}
inner class SuggestionViewHolder(
val binding: StartCallSuggestionListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(contactOrSuggestionModel: ContactOrSuggestionModel) {
with(binding) {
model = contactOrSuggestionModel
executePendingBindings()
}
}
}
private class ContactOrSuggestionDiffCallback : DiffUtil.ItemCallback<ContactOrSuggestionModel>() {
override fun areItemsTheSame(
oldItem: ContactOrSuggestionModel,
newItem: ContactOrSuggestionModel
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: ContactOrSuggestionModel,
newItem: ContactOrSuggestionModel
): Boolean {
return false
}
}
}

View file

@ -91,7 +91,7 @@ class StartCallFragment : GenericAddressPickerFragment() {
setupRecyclerView(binding.contactsAndSuggestionsList) setupRecyclerView(binding.contactsAndSuggestionsList)
viewModel.contactsAndSuggestionsList.observe( viewModel.modelsList.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items") Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")

View file

@ -1,54 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.history.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.AppUtils
import org.linphone.utils.LinphoneUtils
class ContactOrSuggestionModel @WorkerThread constructor(
val address: Address,
val friend: Friend? = null,
private val onClicked: ((Address) -> Unit)? = null
) {
val id = friend?.refKey ?: address.asStringUriOnly().hashCode()
val name = if (friend != null) {
friend.name ?: LinphoneUtils.getDisplayName(address)
} else {
address.username.orEmpty()
}
val sipUri = address.asStringUriOnly()
val initials = AppUtils.getInitials(name)
val avatarModel = MutableLiveData<ContactAvatarModel>()
@UiThread
fun onClicked() {
onClicked?.invoke(address)
}
}

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.linphone.ui.main.chat.model package org.linphone.ui.main.model
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
@ -31,7 +31,7 @@ import org.linphone.utils.LinphoneUtils
class ConversationContactOrSuggestionModel @WorkerThread constructor( class ConversationContactOrSuggestionModel @WorkerThread constructor(
val address: Address, val address: Address,
val localAddress: Address? = null, val localAddress: Address? = null,
private val conversationSubject: String? = null, conversationSubject: String? = null,
val friend: Friend? = null, val friend: Friend? = null,
private val onClicked: ((Address) -> Unit)? = null private val onClicked: ((Address) -> Unit)? = null
) { ) {

View file

@ -25,9 +25,9 @@ import androidx.lifecycle.MutableLiveData
class CodecModel @WorkerThread constructor( class CodecModel @WorkerThread constructor(
val mimeType: String, val mimeType: String,
val clockRate: Int, clockRate: Int,
val recvFmtp: String?, recvFmtp: String?,
val isAudioCodec: Boolean, private val isAudioCodec: Boolean,
enabled: Boolean, enabled: Boolean,
val onEnabledChanged: ((enabled: Boolean) -> Unit) val onEnabledChanged: ((enabled: Boolean) -> Unit)
) { ) {

View file

@ -79,7 +79,6 @@ class SettingsViewModel @UiThread constructor() : GenericViewModel() {
val showConversationsSettings = MutableLiveData<Boolean>() val showConversationsSettings = MutableLiveData<Boolean>()
val autoDownloadEnabled = MutableLiveData<Boolean>() val autoDownloadEnabled = MutableLiveData<Boolean>()
val exportMediaEnabled = MutableLiveData<Boolean>()
// Contacts settings // Contacts settings
val showContactsSettings = MutableLiveData<Boolean>() val showContactsSettings = MutableLiveData<Boolean>()

View file

@ -32,10 +32,12 @@ import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult import org.linphone.core.SearchResult
import org.linphone.mediastream.Log import org.linphone.mediastream.Log
import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.ui.main.model.SelectedAddressModel import org.linphone.ui.main.model.SelectedAddressModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.LinphoneUtils
abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccountChangedViewModel() { abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccountChangedViewModel() {
companion object { companion object {
@ -48,15 +50,19 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
val selectionCount = MutableLiveData<String>() val selectionCount = MutableLiveData<String>()
val searchFilter = MutableLiveData<String>()
val modelsList = MutableLiveData<ArrayList<ConversationContactOrSuggestionModel>>()
val isEmpty = MutableLiveData<Boolean>()
protected var magicSearchSourceFlags = MagicSearch.Source.All.toInt() protected var magicSearchSourceFlags = MagicSearch.Source.All.toInt()
protected var skipConversation: Boolean = true
private var currentFilter = "" private var currentFilter = ""
private var previousFilter = "NotSet" private var previousFilter = "NotSet"
val searchFilter = MutableLiveData<String>()
val contactsAndSuggestionsList = MutableLiveData<ArrayList<ContactOrSuggestionModel>>()
private var limitSearchToLinphoneAccounts = true private var limitSearchToLinphoneAccounts = true
private lateinit var magicSearch: MagicSearch private lateinit var magicSearch: MagicSearch
@ -83,6 +89,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
init { init {
multipleSelectionMode.value = false multipleSelectionMode.value = false
isEmpty.value = true
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
limitSearchToLinphoneAccounts = isEndToEndEncryptionMandatory() limitSearchToLinphoneAccounts = isEndToEndEncryptionMandatory()
@ -221,15 +228,67 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
private fun processMagicSearchResults(results: Array<SearchResult>) { private fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("$TAG Processing [${results.size}] results") Log.i("$TAG Processing [${results.size}] results")
val contactsList = arrayListOf<ContactOrSuggestionModel>() val conversationsList = arrayListOf<ConversationContactOrSuggestionModel>()
val suggestionsList = arrayListOf<ContactOrSuggestionModel>() if (!skipConversation) {
for (chatRoom in LinphoneUtils.getDefaultAccount()?.chatRooms.orEmpty()) {
// Only get group conversations
if (!chatRoom.currentParams.isGroupEnabled) {
continue
}
val found = if (currentFilter.isEmpty()) {
null
} else {
chatRoom.participants.find {
// Search in address but also in contact name if exists
val model =
coreContext.contactsManager.getContactAvatarModelForAddress(it.address)
model.contactName?.contains(
currentFilter,
ignoreCase = true
) == true || it.address.asStringUriOnly().contains(
currentFilter,
ignoreCase = true
)
}
}
if (
currentFilter.isEmpty() ||
found != null ||
chatRoom.peerAddress.asStringUriOnly().contains(
currentFilter,
ignoreCase = true
) ||
chatRoom.subject.orEmpty().contains(currentFilter, ignoreCase = true)
) {
val localAddress = chatRoom.localAddress
val remoteAddress = chatRoom.peerAddress
val model = ConversationContactOrSuggestionModel(
remoteAddress,
localAddress,
chatRoom.subject
)
val fakeFriend = coreContext.core.createFriend()
fakeFriend.name = chatRoom.subject
val avatarModel = ContactAvatarModel(fakeFriend)
avatarModel.defaultToConversationIcon.postValue(true)
model.avatarModel.postValue(avatarModel)
conversationsList.add(model)
}
}
}
val contactsList = arrayListOf<ConversationContactOrSuggestionModel>()
val suggestionsList = arrayListOf<ConversationContactOrSuggestionModel>()
for (result in results) { for (result in results) {
val address = result.address val address = result.address
if (address != null) { if (address != null) {
val friend = coreContext.contactsManager.findContactByAddress(address) val friend = coreContext.contactsManager.findContactByAddress(address)
if (friend != null) { if (friend != null) {
val model = ContactOrSuggestionModel(address, friend) val model = ConversationContactOrSuggestionModel(address, friend = friend)
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
address address
) )
@ -255,7 +314,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
continue continue
} }
val model = ContactOrSuggestionModel(address) { val model = ConversationContactOrSuggestionModel(address) {
coreContext.startAudioCall(address) coreContext.startAudioCall(address)
} }
@ -272,12 +331,14 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
collator.compare(model1.name, model2.name) collator.compare(model1.name, model2.name)
} }
val list = arrayListOf<ContactOrSuggestionModel>() val list = arrayListOf<ConversationContactOrSuggestionModel>()
list.addAll(conversationsList)
list.addAll(contactsList) list.addAll(contactsList)
list.addAll(suggestionsList) list.addAll(suggestionsList)
contactsAndSuggestionsList.postValue(list) modelsList.postValue(list)
isEmpty.postValue(list.isEmpty())
Log.i( Log.i(
"$TAG Processed [${results.size}] results, extracted [${suggestionsList.size}] suggestions" "$TAG Processed [${results.size}] results: [${conversationsList.size}] conversations, [${contactsList.size}] contacts and [${suggestionsList.size}] suggestions"
) )
} }
} }

View file

@ -93,7 +93,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="@{viewModel.conversationsContactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}" android:visibility="@{viewModel.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/search_bar" app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -1,62 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="onClickListener"
type="View.OnClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ConversationContactOrSuggestionModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{onClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/primary_cell_background">
<ImageView
style="@style/avatar_imageview"
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:contentDescription="@null"
coilInitials="@{model.initials, default=`JD`}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.sipUri, default=`john.doe@sip.linphone.org`}"
android:textSize="14sp"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<View
android:id="@+id/separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginEnd="10dp"
android:background="?attr/color_main2_200"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -134,7 +134,7 @@
android:layout_margin="10dp" android:layout_margin="10dp"
android:src="@drawable/illu" android:src="@drawable/illu"
android:contentDescription="@null" android:contentDescription="@null"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp" app:layout_constraintHeight_max="200dp"
@ -150,7 +150,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{viewModel.searchFilter.length() > 0 ? @string/new_conversation_no_matching_contact : @string/new_conversation_no_contact, default=@string/new_conversation_no_contact}" android:text="@{viewModel.searchFilter.length() > 0 ? @string/new_conversation_no_matching_contact : @string/new_conversation_no_contact, default=@string/new_conversation_no_contact}"
android:gravity="center" android:gravity="center"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contact_image" /> app:layout_constraintTop_toBottomOf="@id/no_contact_image" />
@ -160,7 +160,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}" android:visibility="@{viewModel.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_bar" app:layout_constraintTop_toBottomOf="@id/search_bar"

View file

@ -6,15 +6,15 @@
<data> <data>
<import type="android.view.View" /> <import type="android.view.View" />
<import type="android.graphics.Typeface" /> <import type="android.graphics.Typeface" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
<variable <variable
name="onClickListener" name="onClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="firstContactStartingByThatLetter" name="firstContactStartingByThatLetter"
type="Boolean" /> type="Boolean" />
<variable
name="model"
type="org.linphone.ui.main.model.ConversationContactOrSuggestionModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -42,7 +42,7 @@
android:layout_width="25dp" android:layout_width="25dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:text="@{model.firstLetter, default=`A`}" android:text="@{model.avatarModel.firstLetter, default=`A`}"
android:visibility="@{firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}" android:visibility="@{firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}"
android:textColor="?attr/color_main2_400" android:textColor="?attr/color_main2_400"
android:textSize="20sp" android:textSize="20sp"
@ -59,7 +59,7 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
layout="@layout/contact_avatar" layout="@layout/contact_avatar"
bind:model="@{model}" bind:model="@{model.avatarModel}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/header" app:layout_constraintStart_toEndOf="@id/header"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -11,7 +11,7 @@
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="model" name="model"
type="org.linphone.ui.main.chat.model.ConversationContactOrSuggestionModel" /> type="org.linphone.ui.main.model.ConversationContactOrSuggestionModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View file

@ -16,7 +16,7 @@
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:text="@string/history_call_start_suggestions_list_title" android:text="@string/generic_address_picker_suggestions_list_title"
android:gravity="center_vertical"/> android:gravity="center_vertical"/>
</layout> </layout>

View file

@ -10,7 +10,7 @@
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="model" name="model"
type="org.linphone.ui.main.history.model.ContactOrSuggestionModel" /> type="org.linphone.ui.main.model.ConversationContactOrSuggestionModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View file

@ -216,7 +216,7 @@
android:layout_margin="10dp" android:layout_margin="10dp"
android:src="@drawable/illu" android:src="@drawable/illu"
android:contentDescription="@null" android:contentDescription="@null"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp" app:layout_constraintHeight_max="200dp"
@ -232,7 +232,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/history_call_start_no_suggestion_nor_contact" android:text="@string/history_call_start_no_suggestion_nor_contact"
android:gravity="center" android:gravity="center"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" /> app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" />
@ -242,7 +242,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}" android:visibility="@{viewModel.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_call_icon" app:layout_constraintTop_toBottomOf="@id/group_call_icon"

View file

@ -198,7 +198,7 @@
android:layout_margin="10dp" android:layout_margin="10dp"
android:src="@drawable/illu" android:src="@drawable/illu"
android:contentDescription="@null" android:contentDescription="@null"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp" app:layout_constraintHeight_max="200dp"
@ -214,7 +214,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{viewModel.searchFilter.length() > 0 ? @string/new_conversation_no_matching_contact : @string/new_conversation_no_contact, default=@string/new_conversation_no_contact}" android:text="@{viewModel.searchFilter.length() > 0 ? @string/new_conversation_no_matching_contact : @string/new_conversation_no_contact, default=@string/new_conversation_no_contact}"
android:gravity="center" android:gravity="center"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.isEmpty ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contact_image" /> app:layout_constraintTop_toBottomOf="@id/no_contact_image" />
@ -224,7 +224,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}" android:visibility="@{viewModel.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_chat_icon" app:layout_constraintTop_toBottomOf="@id/group_chat_icon"

View file

@ -36,7 +36,6 @@
<string name="dialog_ok">OK</string> <string name="dialog_ok">OK</string>
<string name="dialog_call">Appeler</string> <string name="dialog_call">Appeler</string>
<string name="dialog_delete">Supprimer</string> <string name="dialog_delete">Supprimer</string>
<string name="dialog_close">Fermer</string>
<string name="dialog_install">Installer</string> <string name="dialog_install">Installer</string>
<string name="dialog_do_not_show_anymore">Ne plus me montrer ce message</string> <string name="dialog_do_not_show_anymore">Ne plus me montrer ce message</string>
<string name="dialog_no">Non</string> <string name="dialog_no">Non</string>
@ -183,7 +182,6 @@
<string name="settings_calls_title">Appels</string> <string name="settings_calls_title">Appels</string>
<string name="settings_calls_echo_canceller_title">Utiliser l\'annulateur d\'écho</string> <string name="settings_calls_echo_canceller_title">Utiliser l\'annulateur d\'écho</string>
<string name="settings_calls_echo_canceller_subtitle">Évite que de l\'écho soit entendu par votre correspondant</string> <string name="settings_calls_echo_canceller_subtitle">Évite que de l\'écho soit entendu par votre correspondant</string>
<string name="settings_calls_route_audio_to_bluetooth_title">Utiliser le périphérique Bluetooth si possible</string>
<string name="settings_calls_enable_video_title">Activer la vidéo</string> <string name="settings_calls_enable_video_title">Activer la vidéo</string>
<string name="settings_calls_enable_fec_title">Activer la FEC vidéo</string> <string name="settings_calls_enable_fec_title">Activer la FEC vidéo</string>
<string name="settings_calls_vibrate_while_ringing_title">Vibrer lors de la réception d\'un appel</string> <string name="settings_calls_vibrate_while_ringing_title">Vibrer lors de la réception d\'un appel</string>
@ -306,8 +304,6 @@
<string name="history_call_start_search_bar_filter_hint">Cherchez un contact ou une suggestion</string> <string name="history_call_start_search_bar_filter_hint">Cherchez un contact ou une suggestion</string>
<string name="history_call_start_create_group_call">Démarrer un appel de groupe</string> <string name="history_call_start_create_group_call">Démarrer un appel de groupe</string>
<string name="history_call_start_no_suggestion_nor_contact">Aucun contact ni suggestion pour le moment…</string> <string name="history_call_start_no_suggestion_nor_contact">Aucun contact ni suggestion pour le moment…</string>
<string name="history_call_start_contacts_list_title">Contacts</string>
<string name="history_call_start_suggestions_list_title">Suggestions</string>
<string name="history_group_call_start_dialog_set_subject">Nommer l\'appel de groupe</string> <string name="history_group_call_start_dialog_set_subject">Nommer l\'appel de groupe</string>
<string name="history_group_call_start_dialog_subject_hint">Nom de l\'appel de groupe</string> <string name="history_group_call_start_dialog_subject_hint">Nom de l\'appel de groupe</string>
<string name="history_list_empty_history">Aucun appel dans votre historique…</string> <string name="history_list_empty_history">Aucun appel dans votre historique…</string>
@ -379,7 +375,6 @@
<string name="conversations_list_empty">Aucune conversation pour le moment…</string> <string name="conversations_list_empty">Aucune conversation pour le moment…</string>
<string name="conversations_list_is_being_removed_label">En cours de suppression…</string> <string name="conversations_list_is_being_removed_label">En cours de suppression…</string>
<string name="conversations_last_message_format">%s :</string> <string name="conversations_last_message_format">%s :</string>
<string name="conversations_message_waiting_to_be_forwarded_toast">Message en attente de transfert</string>
<plurals name="conversations_files_waiting_to_be_shared_toast" tools:ignore="MissingQuantity"> <plurals name="conversations_files_waiting_to_be_shared_toast" tools:ignore="MissingQuantity">
<item quantity="one">%s fichier en attente de partage</item> <item quantity="one">%s fichier en attente de partage</item>
<item quantity="other">%s fichiers en attente de partage</item> <item quantity="other">%s fichiers en attente de partage</item>
@ -495,7 +490,6 @@
<string name="conversation_forward_message_title">Transférer à…</string> <string name="conversation_forward_message_title">Transférer à…</string>
<string name="conversation_message_forwarded_toast">Le message a été transféré</string> <string name="conversation_message_forwarded_toast">Le message a été transféré</string>
<string name="conversation_message_forward_cancelled_toast">Transfert du message abandonné</string> <string name="conversation_message_forward_cancelled_toast">Transfert du message abandonné</string>
<string name="conversation_message_forward_conversations_list_title">Conversations</string>
<string name="message_delivery_info_read_title">Lu %s</string> <string name="message_delivery_info_read_title">Lu %s</string>
<string name="message_delivery_info_received_title">Reçu %s</string> <string name="message_delivery_info_received_title">Reçu %s</string>
@ -691,6 +685,9 @@
</plurals> </plurals>
<string name="network_not_reachable">Vous n\'êtes pas connecté à internet</string> <string name="network_not_reachable">Vous n\'êtes pas connecté à internet</string>
<string name="operation_in_progress_overlay">Opération en cours, merci de patienter…</string> <string name="operation_in_progress_overlay">Opération en cours, merci de patienter…</string>
<string name="generic_address_picker_conversations_list_title">Conversations</string>
<string name="generic_address_picker_contacts_list_title">Contacts</string>
<string name="generic_address_picker_suggestions_list_title">Suggestions</string>
<!-- Keep <u></u> in the following strings translations! --> <!-- Keep <u></u> in the following strings translations! -->
<string name="welcome_carousel_skip"><u>Passer</u></string> <string name="welcome_carousel_skip"><u>Passer</u></string>

View file

@ -72,7 +72,6 @@
<string name="dialog_ok">OK</string> <string name="dialog_ok">OK</string>
<string name="dialog_call">Call</string> <string name="dialog_call">Call</string>
<string name="dialog_delete">Delete</string> <string name="dialog_delete">Delete</string>
<string name="dialog_close">Close</string>
<string name="dialog_install">Install</string> <string name="dialog_install">Install</string>
<string name="dialog_do_not_show_anymore">Do not show this dialog anymore</string> <string name="dialog_do_not_show_anymore">Do not show this dialog anymore</string>
<string name="dialog_no">No</string> <string name="dialog_no">No</string>
@ -219,7 +218,6 @@
<string name="settings_calls_title">Calls</string> <string name="settings_calls_title">Calls</string>
<string name="settings_calls_echo_canceller_title">Use echo canceller</string> <string name="settings_calls_echo_canceller_title">Use echo canceller</string>
<string name="settings_calls_echo_canceller_subtitle">Prevents echo from being heard by remote end</string> <string name="settings_calls_echo_canceller_subtitle">Prevents echo from being heard by remote end</string>
<string name="settings_calls_route_audio_to_bluetooth_title">Route audio to bluetooth device, if any</string>
<string name="settings_calls_enable_video_title">Enable video</string> <string name="settings_calls_enable_video_title">Enable video</string>
<string name="settings_calls_enable_fec_title">Enable video FEC</string> <string name="settings_calls_enable_fec_title">Enable video FEC</string>
<string name="settings_calls_vibrate_while_ringing_title">Vibrate while incoming call is ringing</string> <string name="settings_calls_vibrate_while_ringing_title">Vibrate while incoming call is ringing</string>
@ -343,8 +341,6 @@
<string name="history_call_start_search_bar_filter_hint">Search contact or history call</string> <string name="history_call_start_search_bar_filter_hint">Search contact or history call</string>
<string name="history_call_start_create_group_call">Create a group call</string> <string name="history_call_start_create_group_call">Create a group call</string>
<string name="history_call_start_no_suggestion_nor_contact">No suggestion and no contact for the moment…</string> <string name="history_call_start_no_suggestion_nor_contact">No suggestion and no contact for the moment…</string>
<string name="history_call_start_contacts_list_title">Contacts</string>
<string name="history_call_start_suggestions_list_title">Suggestions</string>
<string name="history_group_call_start_dialog_set_subject">Set group call subject</string> <string name="history_group_call_start_dialog_set_subject">Set group call subject</string>
<string name="history_group_call_start_dialog_subject_hint">Group call subject</string> <string name="history_group_call_start_dialog_subject_hint">Group call subject</string>
<string name="history_list_empty_history">No call for the moment…</string> <string name="history_list_empty_history">No call for the moment…</string>
@ -416,7 +412,6 @@
<string name="conversations_list_empty">No conversation for the moment…</string> <string name="conversations_list_empty">No conversation for the moment…</string>
<string name="conversations_list_is_being_removed_label">Removal in progress…</string> <string name="conversations_list_is_being_removed_label">Removal in progress…</string>
<string name="conversations_last_message_format">%s:</string> <string name="conversations_last_message_format">%s:</string>
<string name="conversations_message_waiting_to_be_forwarded_toast">A message is waiting to be forwarded</string>
<plurals name="conversations_files_waiting_to_be_shared_toast"> <plurals name="conversations_files_waiting_to_be_shared_toast">
<item quantity="one">%s file waiting to be shared</item> <item quantity="one">%s file waiting to be shared</item>
<item quantity="other">%s files waiting to be shared</item> <item quantity="other">%s files waiting to be shared</item>
@ -532,7 +527,6 @@
<string name="conversation_forward_message_title">Forward message to…</string> <string name="conversation_forward_message_title">Forward message to…</string>
<string name="conversation_message_forwarded_toast">Message was forwarded</string> <string name="conversation_message_forwarded_toast">Message was forwarded</string>
<string name="conversation_message_forward_cancelled_toast">Message forward was cancelled</string> <string name="conversation_message_forward_cancelled_toast">Message forward was cancelled</string>
<string name="conversation_message_forward_conversations_list_title">Conversations</string>
<string name="message_delivery_info_read_title">Read %s</string> <string name="message_delivery_info_read_title">Read %s</string>
<string name="message_delivery_info_received_title">Received %s</string> <string name="message_delivery_info_received_title">Received %s</string>
@ -728,6 +722,9 @@
</plurals> </plurals>
<string name="network_not_reachable">You aren\'t connected to internet</string> <string name="network_not_reachable">You aren\'t connected to internet</string>
<string name="operation_in_progress_overlay">Operation in progress, please wait</string> <string name="operation_in_progress_overlay">Operation in progress, please wait</string>
<string name="generic_address_picker_conversations_list_title">Conversations</string>
<string name="generic_address_picker_contacts_list_title">Contacts</string>
<string name="generic_address_picker_suggestions_list_title">Suggestions</string>
<!-- Keep <u></u> in the following strings translations! --> <!-- Keep <u></u> in the following strings translations! -->
<string name="welcome_carousel_skip"><u>Skip</u></string> <string name="welcome_carousel_skip"><u>Skip</u></string>