Do not duplicate ConversationFragment.kt for in-call view anymore

This commit is contained in:
Sylvain Berfini 2024-07-15 13:53:44 +02:00
parent 2086fbad66
commit 681c0f22c3
2 changed files with 7 additions and 736 deletions

View file

@ -19,753 +19,24 @@
*/
package org.linphone.ui.call.fragment
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.ChatMessage
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatBubbleLongPressMenuBinding
import org.linphone.databinding.ChatConversationFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.ConversationScrollListener
import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter
import org.linphone.ui.main.chat.fragment.ConversationFragmentArgs
import org.linphone.ui.main.chat.fragment.EndToEndEncryptionDetailsDialogFragment
import org.linphone.ui.main.chat.model.MessageDeliveryModel
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.ui.main.chat.model.MessageReactionsModel
import org.linphone.ui.main.chat.view.RichEditText
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.RecyclerViewSwipeUtils
import org.linphone.utils.RecyclerViewSwipeUtilsCallback
import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
import org.linphone.ui.main.chat.fragment.ConversationFragment
class ConversationFragment : GenericCallFragment() {
class ConversationFragment : ConversationFragment() {
companion object {
private const val TAG = "[In-call Conversation Fragment]"
}
private lateinit var binding: ChatConversationFragmentBinding
private lateinit var viewModel: ConversationViewModel
private lateinit var sendMessageViewModel: SendMessageInConversationViewModel
private lateinit var adapter: ConversationEventAdapter
private lateinit var bottomSheetAdapter: MessageBottomSheetAdapter
private var messageLongPressDialog: Dialog? = null
private val args: ConversationFragmentArgs by navArgs()
private var bottomSheetDialog: BottomSheetDialogFragment? = null
private val dataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart > 0) {
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
}
if (viewModel.isUserScrollingUp.value == true) {
Log.i(
"$TAG [$itemCount] events have been loaded but user was scrolling up in conversation, do not scroll"
)
return
}
if (positionStart == 0 && adapter.itemCount == itemCount) {
// First time we fill the list with messages
Log.i(
"$TAG [$itemCount] events have been loaded"
)
} else {
Log.i(
"$TAG [$itemCount] new events have been loaded, scrolling to first unread message"
)
scrollToFirstUnreadMessageOrBottom()
}
}
}
private lateinit var scrollListener: ConversationScrollListener
private lateinit var headerItemDecoration: RecyclerViewHeaderDecoration
private val listItemTouchListener = object : RecyclerView.OnItemTouchListener {
override fun onInterceptTouchEvent(
rv: RecyclerView,
e: MotionEvent
): Boolean {
// Following code is only to detect click on header at position 0
if (::headerItemDecoration.isInitialized) {
if (e.action == MotionEvent.ACTION_UP) {
if ((rv.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) {
if (e.y >= 0 && e.y <= headerItemDecoration.getDecorationHeight(0)) {
showEndToEndEncryptionDetailsBottomSheet()
return true
}
}
}
}
return false
}
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { }
override fun onRequestDisallowInterceptTouchEvent(
disallowIntercept: Boolean
) { }
}
private var currentChatMessageModelForBottomSheet: MessageModel? = null
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
currentChatMessageModelForBottomSheet?.isSelected?.value = false
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
private var bottomSheetDeliveryModel: MessageDeliveryModel? = null
private var bottomSheetReactionsModel: MessageReactionsModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ConversationEventAdapter()
headerItemDecoration = RecyclerViewHeaderDecoration(
requireContext(),
adapter,
false
)
bottomSheetAdapter = MessageBottomSheetAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatConversationFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[ConversationViewModel::class.java]
sendMessageViewModel =
ViewModelProvider(this)[SendMessageInConversationViewModel::class.java]
viewModel.isInCallConversation.value = true
binding.viewModel = viewModel
observeToastEvents(viewModel)
sendMessageViewModel.isInCallConversation.value = true
binding.sendMessageViewModel = sendMessageViewModel
observeToastEvents(sendMessageViewModel)
viewModel.isInCallConversation.value = true
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.eventsList.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true
binding.eventsList.layoutManager = layoutManager
if (binding.eventsList.adapter != adapter) {
binding.eventsList.adapter = adapter
}
val callbacks = RecyclerViewSwipeUtilsCallback(
R.drawable.reply,
ConversationEventAdapter.EventViewHolder::class.java
) { viewHolder ->
val index = viewHolder.bindingAdapterPosition
if (index < 0 || index >= adapter.currentList.size) {
Log.e("$TAG Swipe viewHolder index [$index] is out of bounds!")
} else {
adapter.notifyItemChanged(index)
if (viewModel.isReadOnly.value == true || viewModel.isDisabledBecauseNotSecured.value == true) {
Log.w("$TAG Do not handle swipe action because conversation is read only")
return@RecyclerViewSwipeUtilsCallback
}
val chatMessageEventLog = adapter.currentList[index]
val chatMessageModel = (chatMessageEventLog.model as? MessageModel)
if (chatMessageModel != null) {
sendMessageViewModel.replyToMessage(chatMessageModel)
// Open keyboard & focus edit text
binding.sendArea.messageToSend.showKeyboard()
} else {
Log.e(
"$TAG Can't reply, failed to get a ChatMessageModel from adapter item #[$index]"
)
}
}
}
RecyclerViewSwipeUtils(callbacks).attachToRecyclerView(binding.eventsList)
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]"
)
viewModel.findChatRoom(null, localSipUri, remoteSipUri)
viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) {
it.consume { found ->
if (!found) {
(view.parent as? ViewGroup)?.doOnPreDraw {
Log.e("$TAG Failed to find conversation, going back")
findNavController().popBackStack()
val message = getString(R.string.conversation_to_display_no_found_toast)
(requireActivity() as GenericActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
} else {
sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
}
viewModel.updateEvents.observe(viewLifecycleOwner) {
it.consume {
val items = viewModel.eventsList
adapter.submitList(items)
Log.i("$TAG Events (messages) list updated, contains [${items.size}] items")
}
}
viewModel.isEndToEndEncrypted.observe(viewLifecycleOwner) { encrypted ->
if (encrypted) {
binding.eventsList.addItemDecoration(headerItemDecoration)
binding.eventsList.addOnItemTouchListener(listItemTouchListener)
}
}
binding.messageBottomSheet.bottomSheetList.setHasFixedSize(true)
val bottomSheetLayoutManager = LinearLayoutManager(requireContext())
binding.messageBottomSheet.bottomSheetList.layoutManager = bottomSheetLayoutManager
adapter.chatMessageLongPressEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showChatMessageLongPressMenu(model)
}
}
adapter.showDeliveryForChatMessageModelEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showBottomSheetDialog(model, showDelivery = true)
}
}
adapter.showReactionForChatMessageModelEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showBottomSheetDialog(model, showReactions = true)
}
}
adapter.scrollToRepliedMessageEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val repliedMessageId = model.replyToMessageId
if (repliedMessageId.isNullOrEmpty()) {
Log.w("$TAG Message [${model.id}] doesn't have a reply to ID!")
} else {
val originalMessage = adapter.currentList.find { eventLog ->
!eventLog.isEvent && (eventLog.model as MessageModel).id == repliedMessageId
}
if (originalMessage != null) {
val position = adapter.currentList.indexOf(originalMessage)
Log.i("$TAG Scrolling to position [$position]")
binding.eventsList.scrollToPosition(position)
} else {
Log.w("$TAG Failed to find matching message in adapter's items!")
}
}
}
}
binding.setScrollToBottomClickListener {
scrollToFirstUnreadMessageOrBottom()
}
binding.setEndToEndEncryptedEventClickListener {
showEndToEndEncryptionDetailsBottomSheet()
}
sendMessageViewModel.emojiToAddEvent.observe(viewLifecycleOwner) {
it.consume { emoji ->
binding.sendArea.messageToSend.addCharacterAtPosition(emoji)
}
}
sendMessageViewModel.participantUsernameToAddEvent.observe(viewLifecycleOwner) {
it.consume { username ->
Log.i("$TAG Adding username [$username] after '@'")
// Also add a space for convenience
binding.sendArea.messageToSend.addCharacterAtPosition("$username ")
}
}
sendMessageViewModel.requestKeyboardHidingEvent.observe(viewLifecycleOwner) {
it.consume {
binding.search.hideKeyboard()
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
viewModel.applyFilter(filter.trim())
}
viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.search.showKeyboard()
} else {
binding.search.hideKeyboard()
}
}
}
viewModel.openWebBrowserEvent.observe(viewLifecycleOwner) {
it.consume { url ->
if (messageLongPressDialog != null) return@consume
Log.i("$TAG Requesting to open web browser on page [$url]")
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(browserIntent)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
}
viewModel.messageDeletedEvent.observe(viewLifecycleOwner) {
it.consume {
val message = getString(R.string.conversation_message_deleted_toast)
val icon = R.drawable.trash_simple
(requireActivity() as GenericActivity).showGreenToast(message, icon)
}
}
binding.sendArea.messageToSend.setControlEnterListener(object :
RichEditText.RichEditTextSendListener {
override fun onControlEnterPressedAndReleased() {
Log.i("$TAG Detected left control + enter key presses, sending message")
sendMessageViewModel.sendMessage()
}
})
binding.root.setKeyboardInsetListener { keyboardVisible ->
sendMessageViewModel.isKeyboardOpen.value = keyboardVisible
if (keyboardVisible) {
sendMessageViewModel.isEmojiPickerOpen.value = false
}
}
scrollListener = object : ConversationScrollListener(layoutManager) {
@UiThread
override fun onLoadMore(totalItemsCount: Int) {
viewModel.loadMoreData(totalItemsCount)
}
@UiThread
override fun onScrolledUp() {
viewModel.isUserScrollingUp.value = true
}
@UiThread
override fun onScrolledToEnd() {
if (viewModel.isUserScrollingUp.value == true) {
viewModel.isUserScrollingUp.value = false
Log.i("$TAG Last message is visible, considering conversation as read")
viewModel.markAsRead()
}
}
}
binding.eventsList.addOnScrollListener(scrollListener)
}
override fun onResume() {
super.onResume()
viewModel.updateCurrentlyDisplayedConversation()
try {
adapter.registerAdapterDataObserver(dataObserver)
} catch (e: IllegalStateException) {
Log.e("$TAG Failed to register data observer to adapter: $e")
}
val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
}
override fun onPause() {
super.onPause()
bottomSheetDialog?.dismiss()
bottomSheetDialog = null
if (::scrollListener.isInitialized) {
binding.eventsList.removeOnScrollListener(scrollListener)
}
coreContext.postOnCoreThread {
bottomSheetReactionsModel?.destroy()
bottomSheetDeliveryModel?.destroy()
coreContext.notificationsManager.resetCurrentlyDisplayedChatRoomId()
}
try {
adapter.unregisterAdapterDataObserver(dataObserver)
} catch (e: IllegalStateException) {
Log.e("$TAG Failed to unregister data observer to adapter: $e")
}
val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)
currentChatMessageModelForBottomSheet = null
}
private fun scrollToFirstUnreadMessageOrBottom() {
if (adapter.itemCount == 0) return
val recyclerView = binding.eventsList
// Scroll to first unread message if any, unless we are already on it
val firstUnreadMessagePosition = adapter.getFirstUnreadMessagePosition()
val currentPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
val indexToScrollTo = if (firstUnreadMessagePosition != -1 && firstUnreadMessagePosition != currentPosition) {
firstUnreadMessagePosition
} else {
adapter.itemCount - 1
}
Log.i(
"$TAG Scrolling to position $indexToScrollTo, first unread message is at $firstUnreadMessagePosition"
)
recyclerView.scrollToPosition(indexToScrollTo)
if (indexToScrollTo == adapter.itemCount - 1) {
viewModel.isUserScrollingUp.postValue(false)
viewModel.markAsRead()
}
}
private fun dismissDialog() {
messageLongPressDialog?.dismiss()
messageLongPressDialog = null
}
private fun showChatMessageLongPressMenu(chatMessageModel: MessageModel) {
Compatibility.setBlurRenderEffect(binding.root)
val dialog = Dialog(requireContext(), R.style.Theme_LinphoneDialog)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val layout: ChatBubbleLongPressMenuBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.chat_bubble_long_press_menu,
null,
false
)
layout.hideForward = true
layout.root.setOnClickListener {
dismissDialog()
}
layout.setDeleteClickListener {
Log.i("$TAG Deleting message")
viewModel.deleteChatMessage(chatMessageModel)
dismissDialog()
}
layout.setCopyClickListener {
Log.i("$TAG Copying message text into clipboard")
val text = chatMessageModel.text.value?.toString()
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val label = "Message"
clipboard.setPrimaryClip(ClipData.newPlainText(label, text))
dismissDialog()
}
layout.setPickEmojiClickListener {
Log.i("$TAG Opening emoji-picker for reaction")
val emojiSheetBehavior = BottomSheetBehavior.from(layout.emojiPickerBottomSheet.root)
emojiSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
layout.setResendClickListener {
Log.i("$TAG Re-sending message in error state")
chatMessageModel.resend()
dismissDialog()
}
layout.setReplyClickListener {
Log.i("$TAG Updating sending area to reply to selected message")
sendMessageViewModel.replyToMessage(chatMessageModel)
dismissDialog()
// Open keyboard & focus edit text
binding.sendArea.messageToSend.showKeyboard()
}
layout.model = chatMessageModel
chatMessageModel.dismissLongPressMenuEvent.observe(viewLifecycleOwner) {
dismissDialog()
}
dialog.setContentView(layout.root)
dialog.setOnDismissListener {
Compatibility.removeBlurRenderEffect(binding.root)
}
dialog.window
?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
val d: Drawable = ColorDrawable(
requireContext().getColor(R.color.grey_300)
)
d.alpha = 102
dialog.window?.setBackgroundDrawable(d)
dialog.show()
messageLongPressDialog = dialog
}
@UiThread
private fun showBottomSheetDialog(
chatMessageModel: MessageModel,
showDelivery: Boolean = false,
showReactions: Boolean = false
) {
val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
binding.messageBottomSheet.setHandleClickedListener {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
if (binding.messageBottomSheet.bottomSheetList.adapter != bottomSheetAdapter) {
binding.messageBottomSheet.bottomSheetList.adapter = bottomSheetAdapter
}
currentChatMessageModelForBottomSheet?.isSelected?.value = false
currentChatMessageModelForBottomSheet = chatMessageModel
chatMessageModel.isSelected.value = true
lifecycleScope.launch {
withContext(Dispatchers.IO) {
// Wait for previous bottom sheet to go away
delay(200)
withContext(Dispatchers.Main) {
if (showDelivery) {
prepareBottomSheetForDeliveryStatus(chatMessageModel)
} else if (showReactions) {
prepareBottomSheetForReactions(chatMessageModel)
}
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
}
@UiThread
private fun prepareBottomSheetForDeliveryStatus(chatMessageModel: MessageModel) {
coreContext.postOnCoreThread {
bottomSheetDeliveryModel?.destroy()
val model = MessageDeliveryModel(chatMessageModel.chatMessage) { deliveryModel ->
coreContext.postOnMainThread {
displayDeliveryStatuses(deliveryModel)
}
}
bottomSheetDeliveryModel = model
}
}
@UiThread
private fun prepareBottomSheetForReactions(chatMessageModel: MessageModel) {
coreContext.postOnCoreThread {
bottomSheetReactionsModel?.destroy()
val model = MessageReactionsModel(chatMessageModel.chatMessage) { reactionsModel ->
coreContext.postOnMainThread {
if (reactionsModel.allReactions.isEmpty()) {
Log.i("$TAG No reaction to display, closing bottom sheet")
val bottomSheetBehavior = BottomSheetBehavior.from(
binding.messageBottomSheet.root
)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
} else {
displayReactions(reactionsModel)
}
}
}
bottomSheetReactionsModel = model
}
}
@UiThread
private fun displayDeliveryStatuses(model: MessageDeliveryModel) {
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(model.readLabel.value).setId(
ChatMessage.State.Displayed.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.receivedLabel.value
).setId(
ChatMessage.State.DeliveredToUser.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(model.sentLabel.value).setId(
ChatMessage.State.Delivered.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.errorLabel.value
).setId(
ChatMessage.State.NotDelivered.toInt()
)
)
tabs.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val state = tab?.id ?: ChatMessage.State.Displayed.toInt()
bottomSheetAdapter.submitList(
model.computeListForState(ChatMessage.State.fromInt(state))
)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.displayedModels
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default delivery status list")
}
@UiThread
private fun displayReactions(model: MessageReactionsModel) {
val totalCount = model.allReactions.size
val label = getString(R.string.message_reactions_info_all_title, totalCount.toString())
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(label).setId(0).setTag("")
)
var index = 1
for (reaction in model.differentReactions.value.orEmpty()) {
val count = model.reactionsMap[reaction]
val tabLabel = getString(
R.string.message_reactions_info_emoji_title,
reaction,
count.toString()
)
tabs.addTab(
tabs.newTab().setText(tabLabel).setId(index).setTag(reaction)
)
index += 1
}
tabs.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val filter = tab?.tag.toString()
if (filter.isEmpty()) {
bottomSheetAdapter.submitList(model.allReactions)
} else {
bottomSheetAdapter.submitList(model.filterReactions(filter))
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.allReactions
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default reactions list")
}
private fun showEndToEndEncryptionDetailsBottomSheet() {
val e2eEncryptionDetailsBottomSheet = EndToEndEncryptionDetailsDialogFragment()
e2eEncryptionDetailsBottomSheet.show(
requireActivity().supportFragmentManager,
EndToEndEncryptionDetailsDialogFragment.TAG
)
bottomSheetDialog = e2eEncryptionDetailsBottomSheet
}
}

View file

@ -106,18 +106,18 @@ import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
@UiThread
class ConversationFragment : SlidingPaneChildFragment() {
open class ConversationFragment : SlidingPaneChildFragment() {
companion object {
private const val TAG = "[Conversation Fragment]"
private const val EXPORT_FILE_AS_DOCUMENT = 10
}
private lateinit var binding: ChatConversationFragmentBinding
protected lateinit var binding: ChatConversationFragmentBinding
private lateinit var viewModel: ConversationViewModel
protected lateinit var viewModel: ConversationViewModel
private lateinit var sendMessageViewModel: SendMessageInConversationViewModel
protected lateinit var sendMessageViewModel: SendMessageInConversationViewModel
private lateinit var adapter: ConversationEventAdapter