Access shared media & documents from contact page if a 1-1 chat room is found

This commit is contained in:
Sylvain Berfini 2026-02-25 09:56:59 +01:00
parent 3c9d08e14d
commit 3ad6c6c8ed
7 changed files with 232 additions and 14 deletions

View file

@ -17,6 +17,7 @@ Group changes to describe their impact on the project, as follows:
- Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it
- Added seeking feature to recordings & media player within app
- Added PDF preview in conversation (message bubble & documents list)
- Added media/documents access from contact page if a 1-1 conversation with any of the contact SIP addresses is found
- Added hover effect when using a mouse (useful for tablets or devices with desktop mode)
- Support right click on some items to open bottom sheet/menu
- Added toggle speaker action in active call notification

View file

@ -115,6 +115,24 @@ class ContactFragment : SlidingPaneChildFragment() {
showDeleteConfirmationDialog()
}
binding.setGoToSharedMediaClickListener {
if (findNavController().currentDestination?.id == R.id.contactFragment) {
val conversationId = viewModel.existingConversationId.value.orEmpty()
Log.i("$TAG Going to shared media fragment for conversation [$conversationId]")
val action = ContactFragmentDirections.actionContactFragmentToConversationMediaListFragment(conversationId)
findNavController().navigate(action)
}
}
binding.setGoToSharedDocumentsClickListener {
if (findNavController().currentDestination?.id == R.id.contactFragment) {
val conversationId = viewModel.existingConversationId.value.orEmpty()
Log.i("$TAG Going to shared documents fragment for conversation [$conversationId]")
val action = ContactFragmentDirections.actionContactFragmentToConversationDocumentsListFragment(conversationId)
findNavController().navigate(action)
}
}
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->
viewModel.showBackButton.value = slideable
}

View file

@ -50,6 +50,8 @@ import org.linphone.core.FriendList
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactsListFilterPopupMenuBinding
import org.linphone.databinding.ContactsListFragmentBinding
import org.linphone.ui.fileviewer.FileViewerActivity
import org.linphone.ui.fileviewer.MediaViewerActivity
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.contacts.adapter.ContactsListAdapter
import org.linphone.ui.main.contacts.model.ContactAvatarModel
@ -234,6 +236,32 @@ class ContactsListFragment : AbstractMainFragment() {
}
}
sharedViewModel.displayFileEvent.observe(viewLifecycleOwner) {
it.consume { bundle ->
if (findNavController().currentDestination?.id == R.id.contactsListFragment) {
val path = bundle.getString("path", "")
val isMedia = bundle.getBoolean("isMedia", false)
if (path.isEmpty()) {
Log.e("$TAG Can't navigate to file viewer for empty path!")
return@consume
}
Log.i(
"$TAG Navigating to [${if (isMedia) "media" else "file"}] viewer fragment with path [$path]"
)
if (isMedia) {
val intent = Intent(requireActivity(), MediaViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
} else {
val intent = Intent(requireActivity(), FileViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
}
}
}
// AbstractMainFragment related
listViewModel.title.value = getString(R.string.bottom_navigation_contacts_label)

View file

@ -99,6 +99,8 @@ class ContactViewModel
val videoCallDisabled = MutableLiveData<Boolean>()
val existingConversationId = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
val showLongPressMenuForNumberOrAddressEvent: MutableLiveData<Event<ContactNumberOrAddressModel>> by lazy {
@ -207,7 +209,12 @@ class ContactViewModel
Log.i("$TAG Conversation [$id] successfully created")
chatRoom.removeListener(this)
operationInProgress.postValue(false)
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
val conversationId = LinphoneUtils.getConversationId(chatRoom)
if (existingConversationId.value.orEmpty().isEmpty()) {
existingConversationId.postValue(conversationId)
}
goToConversationEvent.postValue(Event(conversationId))
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("$TAG Conversation [$id] creation has failed!")
chatRoom.removeListener(this)
@ -330,6 +337,7 @@ class ContactViewModel
sipAddressesAndPhoneNumbers.postValue(addressesAndNumbers)
fetchDevicesAndTrust()
lookUpExistingChatRoom()
}
@UiThread
@ -545,7 +553,12 @@ class ContactViewModel
existingChatRoom
)}], going to it"
)
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(existingChatRoom)))
val conversationId = LinphoneUtils.getConversationId(existingChatRoom)
if (existingConversationId.value.orEmpty().isEmpty()) {
existingConversationId.postValue(conversationId)
}
goToConversationEvent.postValue(Event(conversationId))
} else {
Log.i(
"$TAG No existing conversation between [$localSipUri] and [$remoteSipUri] was found, let's create it"
@ -558,7 +571,12 @@ class ContactViewModel
val id = LinphoneUtils.getConversationId(chatRoom)
Log.i("$TAG 1-1 conversation [$id] has been created")
operationInProgress.postValue(false)
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
val conversationId = LinphoneUtils.getConversationId(chatRoom)
if (existingConversationId.value.orEmpty().isEmpty()) {
existingConversationId.postValue(conversationId)
}
goToConversationEvent.postValue(Event(conversationId))
} else {
Log.i("$TAG Conversation isn't in Created state yet, wait for it")
chatRoom.addListener(chatRoomListener)
@ -567,7 +585,12 @@ class ContactViewModel
val id = LinphoneUtils.getConversationId(chatRoom)
Log.i("$TAG Conversation successfully created [$id]")
operationInProgress.postValue(false)
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
val conversationId = LinphoneUtils.getConversationId(chatRoom)
if (existingConversationId.value.orEmpty().isEmpty()) {
existingConversationId.postValue(conversationId)
}
goToConversationEvent.postValue(Event(conversationId))
}
} else {
Log.e(
@ -627,4 +650,44 @@ class ContactViewModel
devices.postValue(devicesList)
}
@WorkerThread
private fun lookUpExistingChatRoom() {
val account = LinphoneUtils.getDefaultAccount()
if (account != null) {
val params = coreContext.core.createConferenceParams(null)
params.isChatEnabled = true
params.isGroupEnabled = false
params.account = account
val localAddress = account.params.identityAddress
val addresses = friend.addresses
for (address in addresses) {
val sameDomain = address.domain == corePreferences.defaultDomain && address.domain == account.params.domain
if (account.params.instantMessagingEncryptionMandatory && sameDomain) {
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else if (!account.params.instantMessagingEncryptionMandatory) {
if (LinphoneUtils.isEndToEndEncryptedChatAvailable(coreContext.core)) {
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else {
params.securityLevel = Conference.SecurityLevel.None
}
}
val participants = arrayOf(address)
val existingChatRoom = coreContext.core.searchChatRoom(params, localAddress, null, participants)
if (existingChatRoom != null) {
val conversationId = LinphoneUtils.getConversationId(existingChatRoom)
Log.i("$TAG Found existing conversation with ID [$conversationId]")
existingConversationId.postValue(conversationId)
return
}
}
Log.w("$TAG No existing conversation was found for this contact with any of it's SIP addresses")
existingConversationId.postValue("")
} else {
Log.e("$TAG No default account found!")
}
}
}

View file

@ -416,10 +416,10 @@
style="@style/section_header_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:padding="5dp"
android:layout_marginEnd="16dp"
android:padding="10dp"
android:text="@string/conversation_details_media_documents_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintBottom_toTopOf="@id/action_media"
@ -476,10 +476,10 @@
style="@style/section_header_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:padding="5dp"
android:layout_marginEnd="16dp"
android:padding="10dp"
android:text="@string/contact_details_actions_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -15,6 +15,12 @@
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="goToSharedMediaClickListener"
type="View.OnClickListener" />
<variable
name="goToSharedDocumentsClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.contacts.viewmodel.ContactViewModel" />
@ -55,7 +61,6 @@
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
style="@style/icon_top_bar_button_style"
android:onClick="@{() -> viewModel.editContact()}"
@ -86,6 +91,12 @@
android:layout_height="wrap_content"
android:paddingBottom="@dimen/screen_bottom_margin">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.existingConversationId.empty ? View.GONE : View.VISIBLE, default=gone}"
app:constraint_referenced_ids="action_media, action_documents, media_documents_actions, media_documents_separator" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatar_content"
android:layout_width="0dp"
@ -520,20 +531,81 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/media_documents_actions"
style="@style/section_header_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:padding="10dp"
android:text="@string/conversation_details_media_documents_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintBottom_toTopOf="@id/action_media"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/devices_trust" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/action_media"
style="@style/context_menu_action_label_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/action_background_top"
android:drawableStart="@drawable/image"
android:onClick="@{goToSharedMediaClickListener}"
android:text="@string/conversation_media_list_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/media_documents_actions" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/action_documents"
style="@style/context_menu_action_label_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/screen_bottom_margin"
android:background="@drawable/action_background_bottom"
android:drawableStart="@drawable/file_pdf"
android:onClick="@{goToSharedDocumentsClickListener}"
android:text="@string/conversation_document_list_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/action_media" />
<View
android:id="@+id/media_documents_separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/color_separator"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="@id/action_media"
app:layout_constraintStart_toStartOf="@id/action_media"
app:layout_constraintTop_toBottomOf="@id/action_media" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/actions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/contact_details_actions_title"
app:layout_constraintWidth_max="@dimen/section_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/devices_trust"/>
app:layout_constraintTop_toBottomOf="@id/action_documents"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/context_menu_action_label_style"

View file

@ -33,6 +33,22 @@
app:destination="@id/emptyFragment"
app:popUpTo="@id/contactFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_contactFragment_to_conversationDocumentsListFragment"
app:destination="@id/conversationDocumentsListFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_contactFragment_to_conversationMediaListFragment"
app:destination="@id/conversationMediaListFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<action
@ -50,4 +66,24 @@
app:argType="string" />
</fragment>
<fragment
android:id="@+id/conversationMediaListFragment"
android:name="org.linphone.ui.main.chat.fragment.ConversationMediaListFragment"
android:label="ConversationMediaListFragment"
tools:layout="@layout/chat_media_fragment">
<argument
android:name="conversationId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/conversationDocumentsListFragment"
android:name="org.linphone.ui.main.chat.fragment.ConversationDocumentsListFragment"
android:label="ConversationDocumentsListFragment"
tools:layout="@layout/chat_documents_fragment">
<argument
android:name="conversationId"
app:argType="string" />
</fragment>
</navigation>