PoC for blurring conversation except for long pressed chat bubble

This commit is contained in:
Sylvain Berfini 2023-10-12 12:12:38 +02:00
parent b8d8e877d7
commit d9b6f0482a
12 changed files with 329 additions and 29 deletions

View file

@ -23,10 +23,10 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import java.lang.Math.abs
import org.linphone.R
import org.linphone.core.ChatMessage
import org.linphone.databinding.ChatBubbleIncomingBinding
@ -35,6 +35,7 @@ import org.linphone.databinding.ChatEventBinding
import org.linphone.ui.main.chat.model.ChatMessageModel
import org.linphone.ui.main.chat.model.EventLogModel
import org.linphone.ui.main.chat.model.EventModel
import org.linphone.utils.Event
class ConversationEventAdapter(
private val viewLifecycleOwner: LifecycleOwner
@ -47,7 +48,7 @@ class ConversationEventAdapter(
const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute
}
var selectedAdapterPosition = -1
val chatMessageLongPressEvent = MutableLiveData<Event<Pair<ChatMessageModel, Int>>>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
@ -59,13 +60,12 @@ class ConversationEventAdapter(
override fun getItemViewType(position: Int): Int {
val data = getItem(position)
if (data.data is ChatMessageModel) {
if (data.data.isOutgoing) {
return OUTGOING_CHAT_MESSAGE
}
return INCOMING_CHAT_MESSAGE
if (data.isEvent) return EVENT
if ((data.model as ChatMessageModel).isOutgoing) {
return OUTGOING_CHAT_MESSAGE
}
return EVENT
return INCOMING_CHAT_MESSAGE
}
private fun createIncomingChatBubble(parent: ViewGroup): IncomingBubbleViewHolder {
@ -101,24 +101,19 @@ class ConversationEventAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val eventLog = getItem(position)
when (holder) {
is IncomingBubbleViewHolder -> holder.bind(eventLog.data as ChatMessageModel)
is OutgoingBubbleViewHolder -> holder.bind(eventLog.data as ChatMessageModel)
is EventViewHolder -> holder.bind(eventLog.data as EventModel)
is IncomingBubbleViewHolder -> holder.bind(eventLog.model as ChatMessageModel)
is OutgoingBubbleViewHolder -> holder.bind(eventLog.model as ChatMessageModel)
is EventViewHolder -> holder.bind(eventLog.model as EventModel)
}
}
fun resetSelection() {
notifyItemChanged(selectedAdapterPosition)
selectedAdapterPosition = -1
}
fun groupPreviousItem(item: ChatMessageModel, position: Int): Boolean {
return if (position == 0) {
false
} else {
val previous = position - 1
if (getItemViewType(position) == getItemViewType(previous)) {
val previousItem = getItem(previous).data as ChatMessageModel
val previousItem = getItem(previous).model as ChatMessageModel
if (kotlin.math.abs(item.timestamp - previousItem.timestamp) < MAX_TIME_TO_GROUP_MESSAGES) {
previousItem.fromSipUri == item.fromSipUri
} else {
@ -136,7 +131,7 @@ class ConversationEventAdapter(
} else {
val next = position + 1
if (getItemViewType(next) == getItemViewType(position)) {
val nextItem = getItem(next).data as ChatMessageModel
val nextItem = getItem(next).model as ChatMessageModel
if (kotlin.math.abs(item.timestamp - nextItem.timestamp) < MAX_TIME_TO_GROUP_MESSAGES) {
nextItem.fromSipUri != item.fromSipUri
} else {
@ -159,6 +154,14 @@ class ConversationEventAdapter(
isGroupedWithPreviousOne = groupPreviousItem(message, position)
isLastOneOfGroup = isLastItemOfGroup(message, position)
setOnLongClickListener {
val screen = IntArray(2)
root.getLocationOnScreen(screen)
chatMessageLongPressEvent.value = Event(Pair(message, screen[1]))
true
}
lifecycleOwner = viewLifecycleOwner
executePendingBindings()
}
@ -199,8 +202,8 @@ class ConversationEventAdapter(
return if (oldItem.isEvent && newItem.isEvent) {
oldItem.notifyId == newItem.notifyId
} else if (!oldItem.isEvent && !newItem.isEvent) {
val oldData = (oldItem.data as ChatMessageModel)
val newData = (newItem.data as ChatMessageModel)
val oldData = (oldItem.model as ChatMessageModel)
val newData = (newItem.model as ChatMessageModel)
oldData.id.isNotEmpty() && oldData.id == newData.id
} else {
false
@ -211,7 +214,7 @@ class ConversationEventAdapter(
return if (oldItem.isEvent && newItem.isEvent) {
true
} else {
val newData = (newItem.data as ChatMessageModel)
val newData = (newItem.model as ChatMessageModel)
newData.state.value == ChatMessage.State.Displayed
}
}

View file

@ -19,22 +19,38 @@
*/
package org.linphone.ui.main.chat.fragment
import android.app.Dialog
import android.graphics.Rect
import android.graphics.RenderEffect
import android.graphics.Shader
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.annotation.UiThread
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlin.math.max
import kotlin.math.min
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatConversationFragmentBinding
import org.linphone.databinding.ChatConversationLongPressMenuBinding
import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.model.ChatMessageModel
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
@UiThread
@ -115,6 +131,12 @@ class ConversationFragment : GenericFragment() {
val layoutManager = LinearLayoutManager(requireContext())
binding.eventsList.layoutManager = layoutManager
adapter.chatMessageLongPressEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
showChatMessageLongPressMenu(pair.first, pair.second)
}
}
viewModel.events.observe(viewLifecycleOwner) { items ->
val currentCount = adapter.itemCount
adapter.submitList(items)
@ -146,4 +168,56 @@ class ConversationFragment : GenericFragment() {
}*/
}
}
private fun showChatMessageLongPressMenu(chatMessageModel: ChatMessageModel, yPosition: Int) {
// TODO: handle backward compat for blurring
val blurEffect = RenderEffect.createBlurEffect(16F, 16F, Shader.TileMode.MIRROR)
binding.root.setRenderEffect(blurEffect)
val dialog = Dialog(requireContext(), R.style.Theme_LinphoneDialog)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val layout: ChatConversationLongPressMenuBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.chat_conversation_long_press_menu,
null,
false
)
layout.root.setOnClickListener {
dialog.dismiss()
binding.root.setRenderEffect(null)
}
layout.model = chatMessageModel
val screenY = yPosition - AppUtils.getDimension(
R.dimen.chat_bubble_long_press_menu_bubble_offset
)
val rect = Rect()
binding.root.getGlobalVisibleRect(rect)
val height = rect.height()
val percent = ((screenY * 100) / height)
// To prevent bubble from being behind the bottom actions or the emojis to be out of the screen
val guideline = min(max(0.1f, (percent / 100)), 0.4f) // value must be between 0 and 1
val constraintLayout = layout.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)
set.setGuidelinePercent(R.id.guideline, guideline)
set.applyTo(constraintLayout)
dialog.setContentView(layout.root)
dialog.window
?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
val d: Drawable = ColorDrawable(
AppUtils.getColor(R.color.gray_300)
)
d.alpha = 102
dialog.window?.setBackgroundDrawable(d)
dialog.show()
}
}

View file

@ -28,7 +28,7 @@ class EventLogModel @WorkerThread constructor(eventLog: EventLog, avatarModel: C
val isEvent = type != EventLog.Type.ConferenceChatMessage
val data = if (isEvent) {
val model = if (isEvent) {
EventModel(eventLog)
} else {
ChatMessageModel(eventLog.chatMessage!!, avatarModel)

View file

@ -305,15 +305,15 @@ class DialogUtils {
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(binding.root)
val d: Drawable = ColorDrawable(
AppUtils.getColor(R.color.gray_main2_800_alpha_65)
)
// d.alpha = 166
dialog.window
?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
val d: Drawable = ColorDrawable(
AppUtils.getColor(R.color.gray_300)
)
d.alpha = 102
dialog.window?.setBackgroundDrawable(d)
return dialog
}

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="M236,200a12,12 0,0 1,-24 0,84.09 84.09,0 0,0 -84,-84H61l27.52,27.51a12,12 0,0 1,-17 17l-48,-48a12,12 0,0 1,0 -17l48,-48a12,12 0,0 1,17 17L61,92h67A108.12,108.12 0,0 1,236 200Z"
android:fillColor="#4e6074"/>
</vector>

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="M232.49,112.49l-48,48a12,12 0,0 1,-17 -17L195,116H128a84.09,84.09 0,0 0,-84 84,12 12,0 0,1 -24,0A108.12,108.12 0,0 1,128 92h67L167.51,64.48a12,12 0,0 1,17 -17l48,48A12,12 0,0 1,232.49 112.49Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -7,6 +7,9 @@
<import type="android.view.View" />
<import type="org.linphone.core.ConsolidatedPresence" />
<import type="org.linphone.core.ChatMessage.State" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
@ -19,7 +22,7 @@
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onLongClick="@{() -> model.onLongClick()}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@{isGroupedWithPreviousOne ? @dimen/chat_bubble_grouped_top_margin : @dimen/chat_bubble_top_margin, default=@dimen/chat_bubble_top_margin}"

View file

@ -0,0 +1,191 @@
<?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.ChatMessageModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.4" />
<ImageView
android:id="@+id/emojis_background"
android:layout_width="0dp"
android:layout_height="@dimen/chat_bubble_long_press_menu_emojis_height"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/shape_squircle_white_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="@id/thumbs_up"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/thumbs_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/emoji_thumbs_up"
android:textSize="37sp"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toTopOf="@id/emojis_background"
app:layout_constraintStart_toStartOf="@id/emojis_background"
app:layout_constraintEnd_toStartOf="@id/love"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/love"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/emoji_love"
android:textSize="37sp"
app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintStart_toEndOf="@id/thumbs_up"
app:layout_constraintEnd_toStartOf="@id/laughing"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/laughing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/emoji_laughing"
android:textSize="37sp"
app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintStart_toEndOf="@id/love"
app:layout_constraintEnd_toStartOf="@id/surprised"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/surprised"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/emoji_surprised"
android:textSize="37sp"
app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintStart_toEndOf="@id/laughing"
app:layout_constraintEnd_toStartOf="@id/laughing"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/emoji_tear"
android:textSize="37sp"
app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintStart_toEndOf="@id/surprised"
app:layout_constraintEnd_toStartOf="@id/plus"/>
<ImageView
android:id="@+id/plus"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="16dp"
android:src="@drawable/plus_circle"
app:layout_constraintStart_toEndOf="@id/tear"
app:layout_constraintEnd_toEndOf="@id/emojis_background"
app:layout_constraintTop_toTopOf="@id/emojis_background"
app:layout_constraintBottom_toBottomOf="@id/emojis_background" />
<include
android:id="@+id/bubble"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="12dp"
app:layout_constrainedWidth="true"
app:layout_constrainedHeight="true"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/emojis_background"
app:layout_constraintBottom_toTopOf="@id/reply"
model="@{model}"
isGroupedWithPreviousOne="@{false}"
isLastOneOfGroup="@{true}"
layout="@layout/chat_bubble_incoming"/>
<View
android:id="@+id/actions_background"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/gray_main2_200"
app:layout_constraintTop_toTopOf="@id/reply"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/reply"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/menu_reply_to_chat_message"
style="@style/context_menu_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/arrow_bend_up_left_bold"
app:layout_constraintBottom_toTopOf="@id/copy"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/copy"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/menu_copy_chat_message"
style="@style/context_menu_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/copy"
app:layout_constraintBottom_toTopOf="@id/forward"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/forward"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/menu_forward_chat_message"
style="@style/context_menu_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/arrow_bend_up_right_bold"
app:layout_constraintBottom_toTopOf="@id/delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/menu_delete_selected_item"
style="@style/context_menu_danger_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/trash_simple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -12,7 +12,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_main2_800_alpha_65"
android:background="@color/gray_300"
android:alpha="102"
android:visibility="@{visibility ? View.VISIBLE : View.GONE, default=gone}">
<ImageView

View file

@ -11,7 +11,6 @@
<color name="orange_main_100_alpha_50">#80FFEACB</color>
<color name="gray_main2_800">#22334D</color>
<color name="gray_main2_800_alpha_65">#A622334D</color>
<color name="gray_main2_700">#364860</color>
<color name="gray_main2_600">#4E6074</color>
<color name="gray_main2_500">#6C7A87</color>

View file

@ -55,4 +55,6 @@
<dimen name="chat_bubble_grouped_top_margin">4dp</dimen>
<dimen name="chat_bubble_top_margin">16dp</dimen>
<dimen name="chat_bubble_long_press_menu_emojis_height">60dp</dimen>
<dimen name="chat_bubble_long_press_menu_bubble_offset">105dp</dimen>
</resources>

View file

@ -15,6 +15,12 @@
<string name="notification_channel_service_id" translatable="false">linphone_notification_service_id</string>
<string name="notification_channel_chat_id" translatable="false">linphone_notification_chat_id</string>
<string name="emoji_love" translatable="false">❤️</string>
<string name="emoji_thumbs_up" translatable="false">👍</string>
<string name="emoji_laughing" translatable="false">😂</string>
<string name="emoji_surprised" translatable="false">😮</string>
<string name="emoji_tear" translatable="false">😢</string>
<string name="help_about_open_source_licenses_title" translatable="false">GNU General Public License v3.0</string>
<string name="help_about_open_source_licenses_subtitle" translatable="false">© Belledonne Communications 2010-2023</string>
<string name="help_advanced_send_debug_logs_email_address" translatable="false">linphone-android@belledonne-communications.com</string>
@ -272,6 +278,9 @@
<string name="menu_delete_history">Delete history</string>
<string name="menu_delete_selected_item">Delete</string>
<string name="menu_invite">Invite</string>
<string name="menu_reply_to_chat_message">Reply</string>
<string name="menu_forward_chat_message">Forward</string>
<string name="menu_copy_chat_message">Copy</string>
<string name="history_title">Call history</string>
<string name="history_call_start_title">New call</string>