diff --git a/app/src/main/java/org/linphone/ui/MainActivity.kt b/app/src/main/java/org/linphone/ui/MainActivity.kt index c384b9571..5b4c7f4e9 100644 --- a/app/src/main/java/org/linphone/ui/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/MainActivity.kt @@ -22,6 +22,7 @@ package org.linphone.ui import android.Manifest 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 @@ -105,4 +106,12 @@ class MainActivity : AppCompatActivity() { private fun getNavBar(): NavigationBarView? { return binding.mainNavView ?: binding.mainNavRail } + + fun hideNavBar() { + getNavBar()?.visibility = View.GONE + } + + fun showNavBar() { + getNavBar()?.visibility = View.VISIBLE + } } diff --git a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt index d1cc9cf85..28306a868 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt @@ -32,6 +32,8 @@ import org.linphone.utils.TimestampUtils class ChatRoomData(val chatRoom: ChatRoom) { val id = LinphoneUtils.getChatRoomId(chatRoom) + val localSipUri = chatRoom.localAddress.asString() + val remoteSipUri = chatRoom.peerAddress.asString() val contactName = MutableLiveData() diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt index 7846d4c78..bd2e41ff9 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt @@ -24,10 +24,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.navigation.navGraphViewModels +import org.linphone.R import org.linphone.databinding.ConversationFragmentBinding class ConversationFragment : Fragment() { private lateinit var binding: ConversationFragmentBinding + private val viewModel: ConversationViewModel by navGraphViewModels( + R.id.conversationFragment + ) override fun onCreateView( inflater: LayoutInflater, @@ -42,5 +47,23 @@ class ConversationFragment : Fragment() { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + + val localSipUri = arguments?.getString("localSipUri") + ?: savedInstanceState?.getString("localSipUri") + val remoteSipUri = arguments?.getString("remoteSipUri") + ?: savedInstanceState?.getString("remoteSipUri") + if (localSipUri != null && remoteSipUri != null) { + viewModel.loadChatRoom(localSipUri, remoteSipUri) + } else { + // Chat room not found, going back + + // TODO FIXME : show error + } + arguments?.clear() + + binding.setBackClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } } } diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt index 55e292792..e49111241 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt @@ -54,6 +54,7 @@ class ConversationMenuDialogFragment( container: ViewGroup?, savedInstanceState: Bundle? ): View { + // TODO FIXME: use a viewmodel and use core thread val view = ChatRoomMenuBinding.inflate(layoutInflater) val id = LinphoneUtils.getChatRoomId(chatRoom) diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt new file mode 100644 index 000000000..cb56b502d --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt @@ -0,0 +1,106 @@ +/* + * 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.contacts.ContactData +import org.linphone.contacts.ContactsListener +import org.linphone.core.ChatRoom +import org.linphone.core.Factory +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils + +class ConversationViewModel : ViewModel() { + private lateinit var chatRoom: ChatRoom + + val contactName = MutableLiveData() + + val contactData = MutableLiveData() + + val subject = MutableLiveData() + + val isOneToOne = MutableLiveData() + + private val contactsListener = object : ContactsListener { + override fun onContactsLoaded() { + contactLookup() + } + } + + init { + coreContext.contactsManager.addListener(contactsListener) + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsListener) + } + + fun loadChatRoom(localSipUri: String, remoteSipUri: String) { + coreContext.postOnCoreThread { core -> + val localAddress = Factory.instance().createAddress(localSipUri) + val remoteSipAddress = Factory.instance().createAddress(remoteSipUri) + + val found = core.searchChatRoom( + null, + localAddress, + remoteSipAddress, + arrayOfNulls( + 0 + ) + ) + if (found != null) { + chatRoom = found + + isOneToOne.postValue(chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) + subject.postValue(chatRoom.subject) + contactLookup() + } + } + } + + private fun contactLookup() { + if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { + val remoteAddress = chatRoom.peerAddress + val friend = chatRoom.core.findFriend(remoteAddress) + if (friend != null) { + contactData.postValue(ContactData(friend)) + } + contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) + } else { + if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { + val first = chatRoom.participants.firstOrNull() + if (first != null) { + val remoteAddress = first.address + val friend = chatRoom.core.findFriend(remoteAddress) + if (friend != null) { + contactData.postValue(ContactData(friend)) + } + contactName.postValue( + friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress) + ) + } else { + Log.e("[Conversation View Model] No participant in the chat room!") + } + } + } + } +} 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 91fde1188..e2275a408 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt @@ -25,6 +25,7 @@ import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.core.os.bundleOf import androidx.core.view.doOnPreDraw import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController @@ -33,6 +34,7 @@ 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 @@ -58,6 +60,7 @@ class ConversationsFragment : Fragment() { } override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? { + // Holds fragment in place while new fragment slides over it return AnimationUtils.loadAnimation(activity, R.anim.hold) } @@ -91,9 +94,17 @@ class ConversationsFragment : Fragment() { adapter.chatRoomClickedEvent.observe(viewLifecycleOwner) { it.consume { data -> - findNavController().navigate( - R.id.action_conversationsFragment_to_conversationFragment - ) + val bundle = bundleOf() + bundle.putString("localSipUri", data.localSipUri) + bundle.putString("remoteSipUri", data.remoteSipUri) + + if (findNavController().currentDestination?.id == R.id.conversationsFragment) { + (requireActivity() as MainActivity).hideNavBar() + findNavController().navigate( + R.id.action_conversationsFragment_to_conversationFragment, + bundle + ) + } } } @@ -126,12 +137,20 @@ class ConversationsFragment : Fragment() { } binding.setOnNewConversationClicked { - findNavController().navigate( - R.id.action_conversationsFragment_to_newConversationFragment - ) + if (findNavController().currentDestination?.id == R.id.conversationsFragment) { + (requireActivity() as MainActivity).hideNavBar() + findNavController().navigate( + R.id.action_conversationsFragment_to_newConversationFragment + ) + } } } + override fun onResume() { + super.onResume() + (requireActivity() as MainActivity).showNavBar() + } + private fun scrollToTop() { binding.conversationsList.scrollToPosition(0) } diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt index 0d9c111da..a900e3cba 100644 --- a/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt @@ -23,8 +23,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils 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 org.linphone.LinphoneApplication.Companion.coreContext @@ -39,6 +42,14 @@ class NewConversationFragment : Fragment() { R.id.conversationsFragment ) + override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? { + if (findNavController().currentDestination?.id == R.id.conversationFragment) { + // Holds fragment in place while created conversation fragment slides over it + return AnimationUtils.loadAnimation(activity, R.anim.hold) + } + return super.onCreateAnimation(transit, enter, nextAnim) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -85,5 +96,15 @@ class NewConversationFragment : Fragment() { binding.setCancelClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } + + viewModel.goToChatRoom.observe(viewLifecycleOwner) { + it.consume { + if (findNavController().currentDestination?.id == R.id.newConversationFragment) { + findNavController().navigate( + R.id.action_newConversationFragment_to_conversationFragment + ) + } + } + } } } diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt index bfb1354ca..969655c2d 100644 --- a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt @@ -28,10 +28,17 @@ import org.linphone.core.MagicSearch import org.linphone.core.MagicSearchListenerStub import org.linphone.core.SearchResult import org.linphone.core.tools.Log +import org.linphone.utils.Event class NewConversationViewModel : ViewModel() { val contactsList = MutableLiveData>() + val groupEnabled = MutableLiveData() + + val goToChatRoom: MutableLiveData>> by lazy { + MutableLiveData>>() + } + val filter = MutableLiveData() private var previousFilter = "NotSet" @@ -56,16 +63,16 @@ class NewConversationViewModel : ViewModel() { init { coreContext.postOnCoreThread { magicSearch.addListener(magicSearchListener) - coreContext.contactsManager.addListener(contactsListener) applyFilter("") } + coreContext.contactsManager.addListener(contactsListener) } override fun onCleared() { coreContext.postOnCoreThread { - coreContext.contactsManager.removeListener(contactsListener) magicSearch.removeListener(magicSearchListener) } + coreContext.contactsManager.removeListener(contactsListener) super.onCleared() } @@ -88,6 +95,14 @@ class NewConversationViewModel : ViewModel() { ) } + fun createGroup() { + goToChatRoom.value = Event(Pair("", "")) + } + + fun enableGroupSelection() { + groupEnabled.value = true + } + private fun processMagicSearchResults(results: Array) { Log.i("[New Conversation ViewModel] [${results.size}] matching results") contactsList.value.orEmpty().forEach(ContactData::onDestroy) diff --git a/app/src/main/res/drawable/add_file.xml b/app/src/main/res/drawable/add_file.xml new file mode 100644 index 000000000..81697472b --- /dev/null +++ b/app/src/main/res/drawable/add_file.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/back.xml b/app/src/main/res/drawable/back.xml new file mode 100644 index 000000000..f50faa58b --- /dev/null +++ b/app/src/main/res/drawable/back.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/drawable/imdn_delivered.xml b/app/src/main/res/drawable/imdn_delivered.xml index 9deccd49e..5bbffa5a5 100644 --- a/app/src/main/res/drawable/imdn_delivered.xml +++ b/app/src/main/res/drawable/imdn_delivered.xml @@ -7,7 +7,7 @@ android:viewportHeight="12"> diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml new file mode 100644 index 000000000..4880f8a5a --- /dev/null +++ b/app/src/main/res/drawable/info.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/phone_call.xml b/app/src/main/res/drawable/phone_call.xml new file mode 100644 index 000000000..efc96e7a5 --- /dev/null +++ b/app/src/main/res/drawable/phone_call.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/shape_edittext_white_background.xml b/app/src/main/res/drawable/shape_edittext_white_background.xml new file mode 100644 index 000000000..e54b22b94 --- /dev/null +++ b/app/src/main/res/drawable/shape_edittext_white_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/video_call.xml b/app/src/main/res/drawable/video_call.xml new file mode 100644 index 000000000..c67c63d2f --- /dev/null +++ b/app/src/main/res/drawable/video_call.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/voice_message.xml b/app/src/main/res/drawable/voice_message.xml new file mode 100644 index 000000000..17ba14020 --- /dev/null +++ b/app/src/main/res/drawable/voice_message.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/layout/chat_room_list_cell.xml b/app/src/main/res/layout/chat_room_list_cell.xml index 6f4b81b4f..f281eb60e 100644 --- a/app/src/main/res/layout/chat_room_list_cell.xml +++ b/app/src/main/res/layout/chat_room_list_cell.xml @@ -56,7 +56,7 @@ android:layout_height="wrap_content" android:layout_marginStart="12dp" android:text="@{data.isComposing ? `... est en train d'écrire` : data.lastMessage, default=`Lorem Ipsum`}" - android:textColor="@{data.unreadChatCount > 0 ? @color/black : @color/gray_4, default=@color/gray_4}" + android:textColor="@{data.isComposing ? @color/primary_color : data.unreadChatCount > 0 ? @color/black : @color/gray_4, default=@color/gray_4}" android:textSize="14sp" android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}" android:maxLines="1" diff --git a/app/src/main/res/layout/conversation_fragment.xml b/app/src/main/res/layout/conversation_fragment.xml index 09b036f0e..8fc260346 100644 --- a/app/src/main/res/layout/conversation_fragment.xml +++ b/app/src/main/res/layout/conversation_fragment.xml @@ -8,23 +8,176 @@ + + android:background="@color/white"> + + + + + + + + + + + + + + + + + + + + + + + + \ 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 33d18c03b..586a144b9 100644 --- a/app/src/main/res/layout/conversations_fragment.xml +++ b/app/src/main/res/layout/conversations_fragment.xml @@ -91,7 +91,7 @@ android:layout_marginTop="10dp" android:text="Récents" android:textSize="14sp" - android:textColor="@color/gray_1" + android:textColor="@color/blue_filter" android:drawablePadding="5dp" app:layout_constraintStart_toEndOf="@id/sort_by_label" app:layout_constraintTop_toBottomOf="@id/search_bar" diff --git a/app/src/main/res/layout/new_conversation_fragment.xml b/app/src/main/res/layout/new_conversation_fragment.xml index 738ea6138..e88e6250c 100644 --- a/app/src/main/res/layout/new_conversation_fragment.xml +++ b/app/src/main/res/layout/new_conversation_fragment.xml @@ -42,6 +42,21 @@ app:layout_constraintTop_toTopOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/subtitle"/> + + + app:popExitAnim="@anim/slide_out"/> + app:destination="@id/conversationFragment" + app:enterAnim="@anim/slide_in_right" + app:popExitAnim="@anim/slide_out_right" + app:popUpTo="@id/conversationsFragment" /> + tools:layout="@layout/conversation_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 8b71c3a1f..cb50e78b8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,8 @@ #000000 #FFFFFF #DD5F5F + #4FAE80 + #09C5F4 #6C7A87 #F9F9F9 @@ -12,5 +14,6 @@ #949494 #4E4E4E #EDEDED + #F9F9F9 #E5E5EA \ No newline at end of file