Added conversation messages decorator in secured chat room to explain and show info on click

This commit is contained in:
Sylvain Berfini 2024-02-13 11:37:17 +01:00
parent d77f51a5e2
commit ab72e5eb62
12 changed files with 341 additions and 25 deletions

View file

@ -19,7 +19,9 @@
*/
package org.linphone.ui.main.chat.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
@ -33,15 +35,19 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.ChatBubbleIncomingBinding
import org.linphone.databinding.ChatBubbleOutgoingBinding
import org.linphone.databinding.ChatConversationEventBinding
import org.linphone.databinding.ChatConversationSecuredFirstEventBinding
import org.linphone.ui.main.chat.model.EventLogModel
import org.linphone.ui.main.chat.model.EventModel
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter
import org.linphone.utils.startAnimatedDrawable
class ConversationEventAdapter : ListAdapter<EventLogModel, RecyclerView.ViewHolder>(
EventLogDiffCallback()
) {
class ConversationEventAdapter :
ListAdapter<EventLogModel, RecyclerView.ViewHolder>(
EventLogDiffCallback()
),
HeaderAdapter {
companion object {
private const val TAG = "[Conversation Event Adapter]"
@ -55,13 +61,25 @@ class ConversationEventAdapter : ListAdapter<EventLogModel, RecyclerView.ViewHol
val showDeliveryForChatMessageModelEvent: MutableLiveData<Event<MessageModel>> by lazy {
MutableLiveData<Event<MessageModel>>()
}
val showReactionForChatMessageModelEvent: MutableLiveData<Event<MessageModel>> by lazy {
MutableLiveData<Event<MessageModel>>()
}
val scrollToRepliedMessageEvent: MutableLiveData<Event<MessageModel>> by lazy {
MutableLiveData<Event<MessageModel>>()
}
override fun displayHeaderForPosition(position: Int): Boolean {
// We only want to display it at top
return position == 0
}
override fun getHeaderViewForPosition(context: Context, position: Int): View {
val binding = ChatConversationSecuredFirstEventBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
INCOMING_CHAT_MESSAGE -> createIncomingChatBubble(parent)

View file

@ -35,6 +35,7 @@ import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
@ -52,9 +53,11 @@ 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 androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import java.io.File
@ -84,6 +87,7 @@ import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.RecyclerViewSwipeUtils
import org.linphone.utils.RecyclerViewSwipeUtilsCallback
import org.linphone.utils.TimestampUtils
@ -112,6 +116,8 @@ class ConversationFragment : SlidingPaneChildFragment() {
private val args: ConversationFragmentArgs by navArgs()
private var bottomSheetDialog: BottomSheetDialogFragment? = null
private val pickMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia()
) { list ->
@ -226,6 +232,32 @@ class ConversationFragment : SlidingPaneChildFragment() {
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 ((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 : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
@ -344,6 +376,16 @@ class ConversationFragment : SlidingPaneChildFragment() {
} else {
sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
if (viewModel.isEndToEndEncrypted.value == true) {
headerItemDecoration = RecyclerViewHeaderDecoration(
requireContext(),
adapter,
false
)
binding.eventsList.addItemDecoration(headerItemDecoration)
binding.eventsList.addOnItemTouchListener(listItemTouchListener)
}
// Wait for chat room to be ready before trying to forward a message in it
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) { event ->
event.consume { toForward ->
@ -682,6 +724,9 @@ class ConversationFragment : SlidingPaneChildFragment() {
override fun onPause() {
super.onPause()
bottomSheetDialog?.dismiss()
bottomSheetDialog = null
if (::scrollListener.isInitialized) {
binding.eventsList.removeOnScrollListener(scrollListener)
}
@ -1113,4 +1158,13 @@ class ConversationFragment : SlidingPaneChildFragment() {
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

@ -0,0 +1,72 @@
/*
* 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.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.R
import org.linphone.databinding.ChatConversationE2eDetailsBottomSheetBinding
@UiThread
class EndToEndEncryptionDetailsDialogFragment(
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "EndToEndEncryptionDetailsDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
// Makes sure all menu entries are visible,
// required for landscape mode (otherwise only first item is visible)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
// Force this navigation bar color
dialog.window?.navigationBarColor = requireContext().getColor(R.color.gray_600)
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = ChatConversationE2eDetailsBottomSheetBinding.inflate(layoutInflater)
return view.root
}
}

View file

@ -34,6 +34,10 @@ class RecyclerViewHeaderDecoration(
) : RecyclerView.ItemDecoration() {
private val headers: SparseArray<View> = SparseArray()
fun getDecorationHeight(position: Int): Int {
return headers.get(position, null)?.height ?: 0
}
override fun getItemOffsets(
outRect: Rect,
view: View,
@ -91,7 +95,9 @@ class RecyclerViewHeaderDecoration(
context,
position
)
canvas.translate(0f, child.y - headerView.height)
if (position != 0 || child.y < headerView.height) {
canvas.translate(0f, child.y - headerView.height)
}
headerView.draw(canvas)
canvas.restore()
}

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="M208,76H180V56A52,52 0,0 0,76 56V76H48A20,20 0,0 0,28 96V208a20,20 0,0 0,20 20H208a20,20 0,0 0,20 -20V96A20,20 0,0 0,208 76ZM100,56a28,28 0,0 1,56 0V76H100ZM204,204H52V100H204Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -148,7 +148,6 @@
bind:ignore="UseAppTint" />
<ImageView
style="@style/default_text_style"
android:id="@+id/media_encryption_icon"
android:onClick="@{() -> viewModel.showMediaEncryptionStatisticsIfPossible()}"
android:layout_width="@dimen/small_icon_size"

View file

@ -221,7 +221,6 @@
bind:ignore="UseAppTint" />
<ImageView
style="@style/default_text_style"
android:id="@+id/media_encryption_icon"
android:onClick="@{() -> viewModel.showMediaEncryptionStatisticsIfPossible()}"
android:layout_width="@dimen/small_icon_size"

View file

@ -61,7 +61,6 @@
app:layout_constraintBottom_toBottomOf="@id/call_direction_label"/>
<ImageView
style="@style/default_text_style"
android:id="@+id/media_encryption_icon"
android:onClick="@{() -> viewModel.showMediaEncryptionStatisticsIfPossible()}"
android:layout_width="@dimen/small_icon_size"

View file

@ -0,0 +1,79 @@
<?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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="@drawable/shape_bottom_sheet_gray_100_background"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:text="@string/conversation_end_to_end_encrypted_bottom_sheet_title"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/icon"/>
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:src="@drawable/profile_secure_logo"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/message" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:text="@string/conversation_end_to_end_encrypted_bottom_sheet_message"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/footer" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/footer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:text="@string/conversation_end_to_end_encrypted_bottom_sheet_link"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="@color/main1_500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -13,24 +13,24 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:padding="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/shape_squircle_main2_200_chat_event_border"
android:text="@{model.text, default=`You have left the group`}"
android:textColor="?attr/color_grey_400"
android:textSize="12sp"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center"
android:drawableStart="@{model.icon, default=@drawable/clock_countdown}"
android:drawablePadding="5dp"
android:drawableTint="?attr/color_grey_400"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:padding="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/shape_squircle_main2_200_chat_event_border"
android:text="@{model.text, default=`You have left the group`}"
android:textColor="?attr/color_grey_400"
android:textSize="12sp"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center"
android:drawableStart="@{model.icon, default=@drawable/clock_countdown}"
android:drawablePadding="5dp"
android:drawableTint="?attr/color_grey_400"/>
</RelativeLayout>

View file

@ -0,0 +1,76 @@
<?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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:background="@drawable/shape_squircle_main2_200_chat_event_border"
app:layout_constraintWidth_max="300dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/small_icon_size"
android:layout_height="0dp"
android:layout_marginEnd="5dp"
android:paddingTop="3dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:src="@drawable/lock_simple_bold"
app:tint="@color/blue_info_500"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/conversation_end_to_end_encrypted_event_title"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:textSize="12sp"
android:textColor="@color/blue_info_500"
app:layout_constraintWidth_max="280dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:gravity="center"
android:text="@string/conversation_end_to_end_encrypted_event_subtitle"
android:textColor="?attr/color_grey_400"
android:textSize="12sp"
app:layout_constraintWidth_max="300dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -431,6 +431,11 @@
<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_end_to_end_encrypted_event_title">End-to-end encrypted conversation</string>
<string name="conversation_end_to_end_encrypted_event_subtitle">Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them.</string>
<string name="conversation_end_to_end_encrypted_bottom_sheet_title">Guaranteed confidentiality</string>
<string name="conversation_end_to_end_encrypted_bottom_sheet_message">Thanks to end-to-end encryption technology in &appName;, messages, calls and meetings confidentiality are guaranteed. No-one can decrypt exchanged data, not even ourselves.</string>
<string name="conversation_end_to_end_encrypted_bottom_sheet_link"><u>https://linphone.org/security</u></string>
<string name="conversation_info_participants_list_title">Group members</string>
<string name="conversation_info_add_participants_label">Add participants</string>