Added medias list in conversation

This commit is contained in:
Sylvain Berfini 2024-02-05 16:12:42 +01:00
parent db486360cc
commit 3810ab4ae9
20 changed files with 1320 additions and 108 deletions

View file

@ -42,9 +42,11 @@ import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
@ -265,33 +267,60 @@ class MainActivity : GenericActivity() {
}
fun showGreenToast(message: String, @DrawableRes icon: Int) {
val greenToast = ToastUtils.getGreenToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(greenToast.root)
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val greenToast = ToastUtils.getGreenToast(
this@MainActivity,
binding.toastsArea,
message,
icon
)
binding.toastsArea.addView(greenToast.root)
greenToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
greenToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
}
}
}
fun showBlueToast(message: String, @DrawableRes icon: Int) {
val blueToast = ToastUtils.getBlueToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(blueToast.root)
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val blueToast = ToastUtils.getBlueToast(
this@MainActivity,
binding.toastsArea,
message,
icon
)
binding.toastsArea.addView(blueToast.root)
blueToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
blueToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
}
}
}
fun showRedToast(message: String, @DrawableRes icon: Int) {
val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(redToast.root)
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val redToast = ToastUtils.getRedToast(
this@MainActivity,
binding.toastsArea,
message,
icon
)
binding.toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
redToast.root.slideInToastFromTopForDuration(
binding.toastsArea as ViewGroup,
lifecycleScope
)
}
}
}
private fun showPersistentRedToast(
@ -300,20 +329,35 @@ class MainActivity : GenericActivity() {
tag: String,
doNotTint: Boolean = false
) {
val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint)
redToast.root.tag = tag
binding.toastsArea.addView(redToast.root)
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val redToast =
ToastUtils.getRedToast(
this@MainActivity,
binding.toastsArea,
message,
icon,
doNotTint
)
redToast.root.tag = tag
binding.toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTop(
binding.toastsArea as ViewGroup,
true
)
redToast.root.slideInToastFromTop(
binding.toastsArea as ViewGroup,
true
)
}
}
}
private fun removePersistentRedToast(tag: String) {
for (child in binding.toastsArea.children) {
if (child.tag == tag) {
binding.toastsArea.removeView(child)
lifecycleScope.launch {
withContext(Dispatchers.Main) {
for (child in binding.toastsArea.children) {
if (child.tag == tag) {
binding.toastsArea.removeView(child)
}
}
}
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.chat.adapter
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.fragment.MediaViewerFragment
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
class MediaListAdapter(fragment: Fragment, private val viewModel: ConversationMediaListViewModel) : FragmentStateAdapter(
fragment
) {
companion object {
private const val TAG = "[Media List Adapter]"
}
override fun getItemCount(): Int {
return viewModel.mediaList.value.orEmpty().size
}
override fun createFragment(position: Int): Fragment {
val fragment = MediaViewerFragment()
fragment.arguments = Bundle().apply {
val path = viewModel.mediaList.value.orEmpty().getOrNull(position)?.file
Log.i("$TAG Path is [$path] for position [$position]")
putString("path", path)
}
return fragment
}
}

View file

@ -21,6 +21,7 @@ package org.linphone.ui.main.chat.fragment
import android.Manifest
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -38,6 +39,8 @@ import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.PopupWindow
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@ -239,6 +242,17 @@ class ConversationFragment : SlidingPaneChildFragment() {
private var bottomSheetReactionsModel: MessageReactionsModel? = null
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (
findNavController().currentDestination?.id == R.id.fileViewerFragment ||
findNavController().currentDestination?.id == R.id.mediaListViewerFragment
) {
// Holds fragment in place while new fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -505,7 +519,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
viewModel.fileToDisplayEvent.observe(viewLifecycleOwner) {
it.consume { file ->
Log.i("$TAG User clicked on file [$file], let's display it in file viewer")
sharedViewModel.displayFileEvent.value = Event(file)
goToFileViewer(file)
}
}
@ -729,6 +743,49 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
}
private fun goToFileViewer(path: String) {
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video -> {
val action =
ConversationFragmentDirections.actionConversationFragmentToMediaListViewerFragment(
localSipUri = viewModel.localSipUri,
remoteSipUri = viewModel.remoteSipUri,
path = path
)
findNavController().navigate(action)
}
FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> {
val action =
ConversationFragmentDirections.actionConversationFragmentToFileViewerFragment(
path
)
findNavController().navigate(action)
}
else -> {
val intent = Intent(Intent.ACTION_VIEW)
val contentUri: Uri =
FileUtils.getPublicFilePath(requireContext(), path)
intent.setDataAndType(contentUri, "file/$mime")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
requireContext().startActivity(intent)
} catch (anfe: ActivityNotFoundException) {
Log.e("$TAG Can't open file [$path] in third party app: $anfe")
val message = getString(
R.string.toast_no_app_registered_to_handle_content_type_error
)
val icon = R.drawable.file
(requireActivity() as MainActivity).showRedToast(message, icon)
}
}
}
}
}
private fun showPopupMenu(view: View) {
val popupView: ChatConversationPopupMenuBinding = DataBindingUtil.inflate(
LayoutInflater.from(requireContext()),
@ -776,6 +833,18 @@ class ConversationFragment : SlidingPaneChildFragment() {
popupWindow.dismiss()
}
popupView.setMediasClickListener {
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
val action =
ConversationFragmentDirections.actionConversationFragmentToConversationMediaListFragment(
localSipUri = viewModel.localSipUri,
remoteSipUri = viewModel.remoteSipUri
)
findNavController().navigate(action)
}
popupWindow.dismiss()
}
// Elevation is for showing a shadow around the popup
popupWindow.elevation = 20f
popupWindow.showAsDropDown(view, 0, 0, Gravity.BOTTOM)

View file

@ -0,0 +1,160 @@
/*
* 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.chat.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
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.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMediaFragmentBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.FileUtils
@UiThread
class ConversationMediaListFragment : SlidingPaneChildFragment() {
companion object {
private const val TAG = "[Conversation Media List Fragment]"
}
private lateinit var binding: ChatMediaFragmentBinding
private lateinit var viewModel: ConversationMediaListViewModel
private val args: ConversationMediaListFragmentArgs by navArgs()
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (
findNavController().currentDestination?.id == R.id.mediaListViewerFragment
) {
// Holds fragment in place while new fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun goBack(): Boolean {
return findNavController().popBackStack()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatMediaFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[ConversationMediaListViewModel::class.java]
binding.viewModel = viewModel
val localSipUri = args.localSipUri
val remoteSipUri = args.remoteSipUri
Log.i(
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
)
val chatRoom = sharedViewModel.displayedChatRoom
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
binding.setBackClickListener {
goBack()
}
viewModel.mediaList.observe(viewLifecycleOwner) {
val count = it.size
Log.i(
"$TAG Found [$count] media for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
viewModel.openMediaEvent.observe(viewLifecycleOwner) {
it.consume { model ->
Log.i("$TAG User clicked on file [${model.file}], let's display it in file viewer")
goToFileViewer(model.file)
}
}
}
private fun goToFileViewer(path: String) {
if (findNavController().currentDestination?.id == R.id.conversationMediaListFragment) {
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video -> {
val action =
ConversationMediaListFragmentDirections.actionConversationMediaListFragmentToMediaListViewerFragment(
localSipUri = viewModel.localSipUri,
remoteSipUri = viewModel.remoteSipUri,
path = path
)
findNavController().navigate(action)
}
FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> {
val action =
ConversationMediaListFragmentDirections.actionConversationMediaListFragmentToFileViewerFragment(
path
)
findNavController().navigate(action)
}
else -> {
val intent = Intent(Intent.ACTION_VIEW)
val contentUri: Uri =
FileUtils.getPublicFilePath(requireContext(), path)
intent.setDataAndType(contentUri, "file/$mime")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
requireContext().startActivity(intent)
} catch (anfe: ActivityNotFoundException) {
Log.e("$TAG Can't open file [$path] in third party app: $anfe")
val message = getString(
R.string.toast_no_app_registered_to_handle_content_type_error
)
val icon = R.drawable.file
(requireActivity() as MainActivity).showRedToast(message, icon)
}
}
}
}
}
}

View file

@ -19,9 +19,6 @@
*/
package org.linphone.ui.main.chat.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -44,10 +41,8 @@ import org.linphone.ui.main.chat.adapter.ConversationsListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel
import org.linphone.ui.main.fragment.AbstractTopBarFragment
import org.linphone.ui.main.history.fragment.HistoryMenuDialogFragment
import org.linphone.ui.main.viewer.fragment.FileViewerFragmentDirections
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
@UiThread
class ConversationsListFragment : AbstractTopBarFragment() {
@ -91,7 +86,8 @@ class ConversationsListFragment : AbstractTopBarFragment() {
if (
findNavController().currentDestination?.id == R.id.startConversationFragment ||
findNavController().currentDestination?.id == R.id.meetingWaitingRoomFragment ||
findNavController().currentDestination?.id == R.id.fileViewerFragment
findNavController().currentDestination?.id == R.id.fileViewerFragment ||
findNavController().currentDestination?.id == R.id.mediaListViewerFragment
) {
// Holds fragment in place while new fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
@ -222,40 +218,6 @@ class ConversationsListFragment : AbstractTopBarFragment() {
}
}
sharedViewModel.displayFileEvent.observe(viewLifecycleOwner) {
it.consume { path ->
if (findNavController().currentDestination?.id == R.id.conversationsListFragment) {
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> {
val action =
FileViewerFragmentDirections.actionGlobalFileViewerFragment(path)
findNavController().navigate(action)
}
else -> {
val intent = Intent(Intent.ACTION_VIEW)
val contentUri: Uri =
FileUtils.getPublicFilePath(requireContext(), path)
intent.setDataAndType(contentUri, "file/$mime")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
requireContext().startActivity(intent)
} catch (anfe: ActivityNotFoundException) {
Log.e("$TAG Can't open file [$path] in third party app: $anfe")
val message = getString(
R.string.toast_no_app_registered_to_handle_content_type_error
)
val icon = R.drawable.file
(requireActivity() as MainActivity).showRedToast(message, icon)
}
}
}
}
}
}
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) { event ->
if (!event.consumed()) {
// Do not consume it yet

View file

@ -0,0 +1,225 @@
/*
* 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.chat.fragment
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.viewpager2.widget.ViewPager2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMediaViewerFragmentBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.adapter.MediaListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
class MediaListViewerFragment : SlidingPaneChildFragment() {
companion object {
private const val TAG = "[Media List Viewer]"
}
private lateinit var binding: ChatMediaViewerFragmentBinding
private lateinit var adapter: MediaListAdapter
private lateinit var viewModel: ConversationMediaListViewModel
private lateinit var viewPager: ViewPager2
private val args: MediaListViewerFragmentArgs by navArgs()
private var navBarDefaultColor: Int = -1
private val pageListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val list = viewModel.mediaList.value.orEmpty()
if (position >= 0 && position < list.size) {
val model = list[position]
viewModel.currentlyDisplayedFileName.value = model.fileName
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatMediaViewerFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack(): Boolean {
return findNavController().popBackStack()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
navBarDefaultColor = requireActivity().window.navigationBarColor
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[ConversationMediaListViewModel::class.java]
binding.viewModel = viewModel
// Consider full screen mode the default
sharedViewModel.mediaViewerFullScreenMode.value = true
val localSipUri = args.localSipUri
val remoteSipUri = args.remoteSipUri
val path = args.path
Log.i(
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri] trying to display file [$path]"
)
val chatRoom = sharedViewModel.displayedChatRoom
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
viewModel.mediaList.observe(viewLifecycleOwner) {
val count = it.size
Log.i(
"$TAG Found [$count] media for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
)
adapter = MediaListAdapter(this, viewModel)
viewPager = binding.mediaViewPager
viewPager.adapter = adapter
viewPager.registerOnPageChangeCallback(pageListener)
val index = it.indexOfFirst { model ->
model.file == path
}
Log.i("$TAG Path [$path] is at index [$index]")
val position = if (index == -1) {
count - 1
} else {
index
}
viewPager.setCurrentItem(position, false)
viewPager.offscreenPageLimit = 2
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
binding.setBackClickListener {
goBack()
}
binding.setShareClickListener {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
lifecycleScope.launch {
val filePath = FileUtils.getProperFilePath(model.file)
val copy = FileUtils.getFilePath(requireContext(), Uri.parse(filePath), false)
if (!copy.isNullOrEmpty()) {
sharedViewModel.filesToShareFromIntent.value = arrayListOf(copy)
Log.i("$TAG Sharing file [$copy], going back to conversations list")
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
Log.e("$TAG Failed to copy file [$filePath] to share!")
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
binding.setExportClickListener {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
val filePath = FileUtils.getProperFilePath(model.file)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i(
"$TAG File [$filePath] has been successfully exported to MediaStore"
)
val message = AppUtils.getString(
R.string.toast_file_successfully_exported_to_media_store
)
(requireActivity() as MainActivity).showGreenToast(
message,
R.drawable.check
)
} else {
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
val message = AppUtils.getString(
R.string.toast_export_file_to_media_store_error
)
(requireActivity() as MainActivity).showRedToast(message, R.drawable.x)
}
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
sharedViewModel.mediaViewerFullScreenMode.observe(viewLifecycleOwner) {
if (it != viewModel.fullScreenMode.value) {
viewModel.fullScreenMode.value = it
}
}
}
override fun onResume() {
// Force this navigation bar color
requireActivity().window.navigationBarColor = requireContext().getColor(R.color.gray_900)
super.onResume()
}
override fun onDestroy() {
// Reset default navigation bar color
requireActivity().window.navigationBarColor = navBarDefaultColor
viewPager.unregisterOnPageChangeCallback(pageListener)
super.onDestroy()
}
}

View file

@ -0,0 +1,118 @@
package org.linphone.ui.main.chat.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMediaViewerChildFragmentBinding
import org.linphone.ui.main.chat.viewmodel.MediaViewModel
import org.linphone.ui.main.viewmodel.SharedMainViewModel
@UiThread
class MediaViewerFragment : Fragment() {
companion object {
private const val TAG = "[Media Viewer Fragment]"
}
private lateinit var binding: ChatMediaViewerChildFragmentBinding
private lateinit var sharedViewModel: SharedMainViewModel
private lateinit var viewModel: MediaViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatMediaViewerChildFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = requireActivity().run {
ViewModelProvider(this)[SharedMainViewModel::class.java]
}
viewModel = ViewModelProvider(this)[MediaViewModel::class.java]
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
val path = if (arguments?.containsKey("path") == true) {
requireArguments().getString("path")
} else {
""
}
if (path.isNullOrEmpty()) {
Log.e("$TAG Path argument not found!")
return
}
Log.i("$TAG Path argument is [$path]")
viewModel.loadFile(path)
viewModel.isVideo.observe(viewLifecycleOwner) { isVideo ->
if (isVideo) {
Log.i("$TAG Creating video player for file [$path]")
binding.videoPlayer.setVideoPath(path)
binding.videoPlayer.setOnCompletionListener {
Log.i("$TAG End of file reached")
viewModel.isVideoPlaying.value = false
}
}
}
viewModel.toggleVideoPlayPauseEvent.observe(viewLifecycleOwner) {
it.consume { play ->
if (play) {
Log.i("$TAG Starting video player")
binding.videoPlayer.start()
} else {
Log.i("$TAG Pausing video player")
binding.videoPlayer.pause()
}
}
}
viewModel.fullScreenMode.observe(viewLifecycleOwner) {
if (it != sharedViewModel.mediaViewerFullScreenMode.value) {
sharedViewModel.mediaViewerFullScreenMode.value = it
}
}
}
override fun onResume() {
super.onResume()
viewModel.fullScreenMode.value = sharedViewModel.mediaViewerFullScreenMode.value
if (viewModel.isVideo.value == true) {
Log.i("$TAG Resumed, starting video player")
binding.videoPlayer.start()
viewModel.isVideoPlaying.value = true
}
}
override fun onPause() {
if (binding.videoPlayer.isPlaying) {
Log.i("$TAG Paused, stopping video player")
binding.videoPlayer.pause()
viewModel.isVideoPlaying.value = false
}
super.onPause()
}
override fun onDestroyView() {
binding.videoPlayer.stopPlayback()
super.onDestroyView()
}
}

View file

@ -0,0 +1,121 @@
/*
* 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.chat.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ChatRoom
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.utils.Event
class ConversationMediaListViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Conversation Media List ViewModel]"
}
val mediaList = MutableLiveData<ArrayList<FileModel>>()
val fullScreenMode = MutableLiveData<Boolean>()
val currentlyDisplayedFileName = MutableLiveData<String>()
val openMediaEvent: MutableLiveData<Event<FileModel>> by lazy {
MutableLiveData<Event<FileModel>>()
}
private lateinit var chatRoom: ChatRoom
lateinit var localSipUri: String
lateinit var remoteSipUri: String
@UiThread
fun findChatRoom(room: ChatRoom?, localSipUri: String, remoteSipUri: String) {
this.localSipUri = localSipUri
this.remoteSipUri = remoteSipUri
coreContext.postOnCoreThread { core ->
Log.i(
"$TAG Looking for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
)
if (room != null && ::chatRoom.isInitialized && chatRoom == room) {
Log.i("$TAG Conversation object already in memory, skipping")
loadMediaList()
return@postOnCoreThread
}
val localAddress = Factory.instance().createAddress(localSipUri)
val remoteAddress = Factory.instance().createAddress(remoteSipUri)
if (room != null && (!::chatRoom.isInitialized || chatRoom != room)) {
if (localAddress?.weakEqual(room.localAddress) == true && remoteAddress?.weakEqual(
room.peerAddress
) == true
) {
Log.i("$TAG Conversation object available in sharedViewModel, using it")
chatRoom = room
loadMediaList()
return@postOnCoreThread
}
}
if (localAddress != null && remoteAddress != null) {
Log.i("$TAG Searching for conversation in Core using local & peer SIP addresses")
val found = core.searchChatRoom(
null,
localAddress,
remoteAddress,
arrayOfNulls(
0
)
)
if (found != null) {
chatRoom = found
loadMediaList()
}
}
}
}
@WorkerThread
private fun loadMediaList() {
val list = arrayListOf<FileModel>()
if (::chatRoom.isInitialized) {
val media = chatRoom.mediaContents
for (mediaContent in media) {
val path = mediaContent.filePath.orEmpty()
val name = mediaContent.name.orEmpty()
val size = mediaContent.size.toLong()
if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size) {
openMediaEvent.postValue(Event(it))
}
list.add(model)
}
}
}
mediaList.postValue(list)
}
}

View file

@ -0,0 +1,67 @@
package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
class MediaViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Media ViewModel]"
}
val path = MutableLiveData<String>()
val fileName = MutableLiveData<String>()
val fullScreenMode = MutableLiveData<Boolean>()
val isImage = MutableLiveData<Boolean>()
val isVideo = MutableLiveData<Boolean>()
val isVideoPlaying = MutableLiveData<Boolean>()
val toggleVideoPlayPauseEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private lateinit var filePath: String
@UiThread
fun loadFile(file: String) {
filePath = file
val name = FileUtils.getNameFromFilePath(file)
fileName.value = name
val extension = FileUtils.getExtensionFromFileName(name)
val mime = FileUtils.getMimeTypeFromExtension(extension)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image -> {
Log.i("$TAG File [$file] seems to be an image")
isImage.value = true
path.value = file
}
FileUtils.MimeType.Video -> {
Log.i("$TAG File [$file] seems to be a video")
isVideo.value = true
isVideoPlaying.value = false
}
else -> { }
}
}
@UiThread
fun toggleFullScreen() {
fullScreenMode.value = fullScreenMode.value != true
}
@UiThread
fun playPauseVideo() {
val playVideo = isVideoPlaying.value == false
isVideoPlaying.value = playVideo
toggleVideoPlayPauseEvent.value = Event(playVideo)
}
}

View file

@ -20,13 +20,14 @@ import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerFragmentBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.ui.main.viewer.adapter.PdfPagesListAdapter
import org.linphone.ui.main.viewer.viewmodel.FileViewModel
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
@UiThread
class FileViewerFragment : GenericFragment() {
class FileViewerFragment : SlidingPaneChildFragment() {
companion object {
private const val TAG = "[File Viewer Fragment]"
@ -73,7 +74,11 @@ class FileViewerFragment : GenericFragment() {
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
val path = args.path
val path = if (arguments?.containsKey("path") == true) {
requireArguments().getString("path", args.path)
} else {
args.path
}
Log.i("$TAG Path argument is [$path]")
viewModel.loadFile(path)
@ -103,9 +108,7 @@ class FileViewerFragment : GenericFragment() {
if (!copy.isNullOrEmpty()) {
sharedViewModel.filesToShareFromIntent.value = arrayListOf(copy)
Log.i("$TAG Sharing file [$copy], going back to conversations list")
val action =
FileViewerFragmentDirections.actionFileViewerFragmentToConversationsListFragment()
findNavController().navigate(action)
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
Log.e("$TAG Failed to copy file [$filePath] to share!")
}

View file

@ -106,6 +106,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
// When using keyboard to share gif or other, see RichContentReceiver & RichEditText classes
val richContentUri = MutableLiveData<Event<Uri>>()
val mediaViewerFullScreenMode = MutableLiveData<Boolean>()
val forceRefreshConversationInfo: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -137,8 +139,4 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
val listOfSelectedSipUrisEvent: MutableLiveData<Event<ArrayList<String>>> by lazy {
MutableLiveData<Event<ArrayList<String>>>()
}
val displayFileEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M216,40L40,40A16,16 0,0 0,24 56L24,200a16,16 0,0 0,16 16L216,216a16,16 0,0 0,16 -16L232,56A16,16 0,0 0,216 40ZM216,56L216,158.75l-26.07,-26.06a16,16 0,0 0,-22.63 0l-20,20 -44,-44a16,16 0,0 0,-22.62 0L40,149.37L40,56ZM40,172l52,-52 80,80L40,200ZM216,200L194.63,200l-36,-36 20,-20L216,181.38L216,200ZM144,100a12,12 0,1 1,12 12A12,12 0,0 1,144 100Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -21,6 +21,9 @@
<variable
name="configureEphemeralMessagesClickListener"
type="View.OnClickListener" />
<variable
name="mediasClickListener"
type="View.OnClickListener" />
<variable
name="conversationMuted"
type="Boolean" />
@ -44,7 +47,7 @@
style="@style/default_text_style"
android:id="@+id/info"
android:onClick="@{goToInfoClickListener}"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/conversation_menu_go_to_info"
@ -56,6 +59,7 @@
android:drawablePadding="5dp"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/search"/>
@ -63,7 +67,7 @@
style="@style/default_text_style"
android:id="@+id/search"
android:onClick="@{searchClickListener}"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/conversation_menu_search_in_messages"
@ -75,6 +79,7 @@
android:drawablePadding="5dp"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/info"
app:layout_constraintBottom_toTopOf="@id/mute"/>
@ -82,7 +87,7 @@
style="@style/default_text_style"
android:id="@+id/mute"
android:onClick="@{muteClickListener}"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/conversation_action_mute"
@ -95,6 +100,7 @@
android:visibility="@{conversationMuted || readOnlyConversation ? View.GONE : View.VISIBLE}"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/search"
app:layout_constraintBottom_toTopOf="@id/unmute"/>
@ -122,7 +128,7 @@
style="@style/default_text_style"
android:id="@+id/ephemeral"
android:onClick="@{configureEphemeralMessagesClickListener}"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/conversation_menu_configure_ephemeral_messages"
@ -135,7 +141,28 @@
android:visibility="@{ephemeralMessagesAvailable &amp;&amp; !readOnlyConversation ? View.VISIBLE : View.GONE}"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/unmute"
app:layout_constraintBottom_toTopOf="@id/medias"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/medias"
android:onClick="@{mediasClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/conversation_menu_media_files"
android:textSize="14sp"
android:textColor="?attr/color_main2_500"
android:maxLines="1"
android:ellipsize="end"
android:drawableStart="@drawable/image"
android:drawablePadding="5dp"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/ephemeral"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,58 @@
<?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="model"
type="org.linphone.ui.main.chat.model.FileModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_flexBasisPercent="25%">
<ImageView
android:id="@+id/image"
android:onClick="@{() -> model.onClick()}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="1dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
coilBubbleGrid="@{model.file}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/video_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{model.videoDuration, default=`00:42`}"
android:textColor="@color/white"
android:textSize="12sp"
android:visibility="@{model.isVideoPreview &amp;&amp; model.videoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"/>
<ImageView
android:id="@+id/video_preview"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:src="@drawable/play_fill"
android:visibility="@{model.isVideoPreview ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintTop_toTopOf="@id/image"
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"
app:layout_constraintEnd_toEndOf="@id/image"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,85 @@
<?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="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:padding="15dp"
android:onClick="@{backClickListener}"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/conversation_media_list_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/color_grey_100"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/files_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:alignItems="stretch"
app:alignContent="flex_start"
entries="@{viewModel.mediaList}"
layout="@{@layout/chat_media_content_grid_cell}"/>
</ScrollView>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/conversation_no_media_found"
android:textColor="?attr/color_main2_600"
android:textSize="16sp"
android:visibility="@{viewModel.mediaList.empty ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,56 @@
<?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="viewModel"
type="org.linphone.ui.main.chat.viewmodel.MediaViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<VideoView
android:id="@+id/video_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="@{viewModel.isVideo ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/play_pause_video_playback"
android:onClick="@{() -> viewModel.playPauseVideo()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:src="@{viewModel.isVideoPlaying ? @drawable/pause_fill : @drawable/play_fill, default=@drawable/play_fill}"
android:visibility="@{viewModel.isVideo &amp;&amp; !viewModel.fullScreenMode ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white"/>
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/illu"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
coilFile="@{viewModel.path}"
android:visibility="@{viewModel.isImage ? View.VISIBLE : View.GONE, default=gone}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,111 @@
<?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="backClickListener"
type="View.OnClickListener" />
<variable
name="shareClickListener"
type="View.OnClickListener" />
<variable
name="exportClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/media_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" />
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="?attr/color_main2_000"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:background="?attr/color_main2_000"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text=""
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/share"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/share"
android:onClick="@{shareClickListener}"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="?attr/color_main2_000"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/share_network"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/save"
app:layout_constraintTop_toTopOf="@id/title" />
<ImageView
android:id="@+id/save"
android:onClick="@{exportClickListener}"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="?attr/color_main2_000"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/download_simple"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/color_main2_000"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@{viewModel.currentlyDisplayedFileName, default=`nomdufichier.jpg\nenvoyé le 02/05/2023 à 11h05`}"
android:textSize="12sp"
android:textColor="?attr/color_main2_400"
android:textAlignment="center"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -35,6 +35,26 @@
app:destination="@id/emptyFragment"
app:popUpTo="@id/conversationFragment"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_conversationFragment_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" />
<action
android:id="@+id/action_conversationFragment_to_fileViewerFragment"
app:destination="@id/fileViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<action
android:id="@+id/action_conversationFragment_to_mediaListViewerFragment"
app:destination="@id/mediaListViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
</fragment>
<action
@ -87,4 +107,54 @@
app:argType="long" />
</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="localSipUri"
app:argType="string" />
<argument
android:name="remoteSipUri"
app:argType="string" />
<action
android:id="@+id/action_conversationMediaListFragment_to_fileViewerFragment"
app:destination="@id/fileViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<action
android:id="@+id/action_conversationMediaListFragment_to_mediaListViewerFragment"
app:destination="@id/mediaListViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
</fragment>
<fragment
android:id="@+id/fileViewerFragment"
android:name="org.linphone.ui.main.viewer.fragment.FileViewerFragment"
android:label="FileViewerFragment"
tools:layout="@layout/file_viewer_fragment">
<argument
android:name="path"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/mediaListViewerFragment"
android:name="org.linphone.ui.main.chat.fragment.MediaListViewerFragment"
android:label="MediaListViewerFragment"
tools:layout="@layout/chat_media_viewer_fragment">
<argument
android:name="localSipUri"
app:argType="string" />
<argument
android:name="remoteSipUri"
app:argType="string" />
<argument
android:name="path"
app:argType="string" />
</fragment>
</navigation>

View file

@ -321,28 +321,4 @@
android:name="conferenceUri"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/fileViewerFragment"
android:name="org.linphone.ui.main.viewer.fragment.FileViewerFragment"
android:label="FileViewerFragment"
tools:layout="@layout/file_viewer_fragment">
<argument
android:name="path"
app:argType="string" />
<action
android:id="@+id/action_fileViewerFragment_to_conversationsListFragment"
app:destination="@id/conversationsListFragment"
app:launchSingleTop="true"
app:popUpTo="@id/conversationsListFragment"
app:popUpToInclusive="true"/>
</fragment>
<action
android:id="@+id/action_global_fileViewerFragment"
app:destination="@id/fileViewerFragment"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true"/>
</navigation>

View file

@ -429,6 +429,8 @@
<string name="conversation_menu_search_in_messages">Search</string>
<string name="conversation_menu_go_to_info">Conversation info</string>
<string name="conversation_menu_configure_ephemeral_messages">Ephemeral messages</string>
<string name="conversation_menu_media_files">Medias</string>
<string name="conversation_no_media_found">No media found</string>
<string name="conversation_filter_no_matching_result">No matching result</string>
<string name="conversation_info_participants_list_title">Group members</string>
@ -455,6 +457,8 @@
<string name="conversation_event_ephemeral_messages_disabled">Ephemeral messages have been disabled</string>
<string name="conversation_event_ephemeral_messages_lifetime_changed">Ephemeral lifetime is now %s</string>
<string name="conversation_media_list_title">Shared media</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_sent_title">Sent %s</string>
@ -565,7 +569,7 @@
<!-- Keep <i></i> in following strings translations! -->
<string name="message_meeting_invitation_content_description"><i>meeting invite: </i>%s</string>
<string name="message_voice_message_content_description"><i>voice message</i></string>
<!-- Content description for accessibility -->
<string name="content_description_trusted_contact_icon">Contact is trusted</string>
<string name="content_description_not_trusted_contact_icon">Contact is not trusted!</string>