diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt
index e5cde22c9..53701b5c7 100644
--- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt
+++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt
@@ -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)
+ }
+ }
}
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/MediaListAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/MediaListAdapter.kt
new file mode 100644
index 000000000..3c8ae7ff2
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/MediaListAdapter.kt
@@ -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 .
+ */
+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
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
index 16b693c54..c7bb26971 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
@@ -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)
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt
new file mode 100644
index 000000000..d52cc09a3
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt
@@ -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 .
+ */
+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)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
index 8f05d54fe..1c88b2fda 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt
@@ -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
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaListViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaListViewerFragment.kt
new file mode 100644
index 000000000..091ca5809
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaListViewerFragment.kt
@@ -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 .
+ */
+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()
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaViewerFragment.kt
new file mode 100644
index 000000000..cb11c406a
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/MediaViewerFragment.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt
new file mode 100644
index 000000000..042724e0a
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt
@@ -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 .
+ */
+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>()
+
+ val fullScreenMode = MutableLiveData()
+
+ val currentlyDisplayedFileName = MutableLiveData()
+
+ val openMediaEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ 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()
+ 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)
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/MediaViewModel.kt
new file mode 100644
index 000000000..61e9512ee
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/MediaViewModel.kt
@@ -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()
+
+ val fileName = MutableLiveData()
+
+ val fullScreenMode = MutableLiveData()
+
+ val isImage = MutableLiveData()
+
+ val isVideo = MutableLiveData()
+
+ val isVideoPlaying = MutableLiveData()
+
+ val toggleVideoPlayPauseEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ 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)
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt
index 6f3a6d4d7..ecd80629a 100644
--- a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt
@@ -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!")
}
diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
index ac59ede5e..5c4fd7138 100644
--- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
@@ -106,6 +106,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
// When using keyboard to share gif or other, see RichContentReceiver & RichEditText classes
val richContentUri = MutableLiveData>()
+ val mediaViewerFullScreenMode = MutableLiveData()
+
val forceRefreshConversationInfo: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -137,8 +139,4 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
val listOfSelectedSipUrisEvent: MutableLiveData>> by lazy {
MutableLiveData>>()
}
-
- val displayFileEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
}
diff --git a/app/src/main/res/drawable/image.xml b/app/src/main/res/drawable/image.xml
new file mode 100644
index 000000000..dca0644d7
--- /dev/null
+++ b/app/src/main/res/drawable/image.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/chat_conversation_popup_menu.xml b/app/src/main/res/layout/chat_conversation_popup_menu.xml
index c98ed4355..a4b198c1c 100644
--- a/app/src/main/res/layout/chat_conversation_popup_menu.xml
+++ b/app/src/main/res/layout/chat_conversation_popup_menu.xml
@@ -21,6 +21,9 @@
+
@@ -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 && !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"/>
+
+
diff --git a/app/src/main/res/layout/chat_media_content_grid_cell.xml b/app/src/main/res/layout/chat_media_content_grid_cell.xml
new file mode 100644
index 000000000..593743674
--- /dev/null
+++ b/app/src/main/res/layout/chat_media_content_grid_cell.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_media_fragment.xml b/app/src/main/res/layout/chat_media_fragment.xml
new file mode 100644
index 000000000..2503fc5f7
--- /dev/null
+++ b/app/src/main/res/layout/chat_media_fragment.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_media_viewer_child_fragment.xml b/app/src/main/res/layout/chat_media_viewer_child_fragment.xml
new file mode 100644
index 000000000..3289c2358
--- /dev/null
+++ b/app/src/main/res/layout/chat_media_viewer_child_fragment.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_media_viewer_fragment.xml b/app/src/main/res/layout/chat_media_viewer_fragment.xml
new file mode 100644
index 000000000..c45ce9855
--- /dev/null
+++ b/app/src/main/res/layout/chat_media_viewer_fragment.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/chat_nav_graph.xml b/app/src/main/res/navigation/chat_nav_graph.xml
index d579686d1..eb1023b30 100644
--- a/app/src/main/res/navigation/chat_nav_graph.xml
+++ b/app/src/main/res/navigation/chat_nav_graph.xml
@@ -35,6 +35,26 @@
app:destination="@id/emptyFragment"
app:popUpTo="@id/conversationFragment"
app:popUpToInclusive="true"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml
index 793eb26dc..612f1d015 100644
--- a/app/src/main/res/navigation/main_nav_graph.xml
+++ b/app/src/main/res/navigation/main_nav_graph.xml
@@ -321,28 +321,4 @@
android:name="conferenceUri"
app:argType="string" />
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 997333093..934b250aa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -429,6 +429,8 @@
Search
Conversation info
Ephemeral messages
+ Medias
+ No media found
No matching result
Group members
@@ -455,6 +457,8 @@
Ephemeral messages have been disabled
Ephemeral lifetime is now %s
+ Shared media
+
Read %s
Received %s
Sent %s
@@ -565,7 +569,7 @@
meeting invite: %s
voice message
-
+
Contact is trusted
Contact is not trusted!