Improved delivery bottom sheet

This commit is contained in:
Sylvain Berfini 2023-10-19 11:40:16 +02:00
parent 170e441744
commit c151ef1526
17 changed files with 266 additions and 66 deletions

View file

@ -77,7 +77,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.core:core-telecom:1.0.0-alpha02"
implementation "androidx.media:media:1.6.0"
implementation "androidx.recyclerview:recyclerview:1.3.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation "androidx.window:window:1.1.0"

View file

@ -137,6 +137,10 @@ class ConversationEventAdapter(
with(binding) {
model = message
setShowDeliveryInfoClickListener {
showDeliveryForChatMessageModelEvent.value = Event(message)
}
lifecycleOwner = viewLifecycleOwner
executePendingBindings()
}

View file

@ -43,13 +43,17 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.ChatMessage
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.ChatMessageDeliveryModel
import org.linphone.ui.main.chat.model.ChatMessageModel
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.fragment.GenericFragment
@ -177,13 +181,9 @@ class ConversationFragment : GenericFragment() {
emojisBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
emojisBottomSheetBehavior.isDraggable = false // To allow scrolling through the emojis
val imdnBottomSheetBehavior = BottomSheetBehavior.from(binding.deliveryBottomSheet.root)
imdnBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
adapter.showDeliveryForChatMessageModelEvent.observe(viewLifecycleOwner) {
it.consume { model ->
binding.deliveryBottomSheet.model = model
imdnBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
showDeliveryBottomSheetDialog(model)
}
}
@ -303,4 +303,62 @@ class ConversationFragment : GenericFragment() {
dialog.window?.setBackgroundDrawable(d)
dialog.show()
}
@UiThread
private fun showDeliveryBottomSheetDialog(chatMessageModel: ChatMessageModel) {
val deliveryBottomSheetBehavior = BottomSheetBehavior.from(binding.messageDelivery.root)
if (deliveryBottomSheetBehavior.state == BottomSheetBehavior.STATE_HALF_EXPANDED) {
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
binding.messageDelivery.setHandleClickedListener {
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
coreContext.postOnCoreThread {
val model = ChatMessageDeliveryModel(chatMessageModel.chatMessage)
coreContext.postOnMainThread {
binding.messageDelivery.tabs.removeAllTabs()
binding.messageDelivery.tabs.addTab(
binding.messageDelivery.tabs.newTab().setText(model.readLabel.value).setId(
ChatMessage.State.Displayed.toInt()
)
)
binding.messageDelivery.tabs.addTab(
binding.messageDelivery.tabs.newTab().setText(model.receivedLabel.value).setId(
ChatMessage.State.DeliveredToUser.toInt()
)
)
binding.messageDelivery.tabs.addTab(
binding.messageDelivery.tabs.newTab().setText(model.sentLabel.value).setId(
ChatMessage.State.Delivered.toInt()
)
)
binding.messageDelivery.tabs.addTab(
binding.messageDelivery.tabs.newTab().setText(model.errorLabel.value).setId(
ChatMessage.State.NotDelivered.toInt()
)
)
binding.messageDelivery.tabs.setOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val state = tab?.id ?: ChatMessage.State.Displayed.toInt()
model.computeListForState(ChatMessage.State.fromInt(state))
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
binding.messageDelivery.model = model
binding.messageDelivery.root.visibility = View.VISIBLE
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.ChatMessageDeliveryBottomSheetBinding
import org.linphone.ui.main.chat.model.ChatMessageDeliveryModel
import org.linphone.ui.main.chat.model.ChatMessageModel
@UiThread
class MessageDeliveryDialogFragment(
chatMessageModel: ChatMessageModel
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "MessageDeliveryDialogFragment"
}
val model = ChatMessageDeliveryModel(chatMessageModel.chatMessage)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = ChatMessageDeliveryBottomSheetBinding.inflate(layoutInflater)
view.model = model
return view.root
}
}

View file

@ -1,18 +1,102 @@
package org.linphone.ui.main.chat.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import org.linphone.LinphoneApplication
import org.linphone.core.ParticipantImdnState
import org.linphone.utils.TimestampUtils
import androidx.lifecycle.MutableLiveData
import org.linphone.R
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessage.State
import org.linphone.utils.AppUtils
class ChatMessageDeliveryModel @WorkerThread constructor(
imdnState: ParticipantImdnState
private val chatMessage: ChatMessage
) {
val address = imdnState.participant.address
companion object {
private const val TAG = "[Chat Message Delivery Model]"
}
val avatarModel = LinphoneApplication.coreContext.contactsManager.getContactAvatarModelForAddress(
address
)
val readLabel = MutableLiveData<String>()
val time = TimestampUtils.toString(imdnState.stateChangeTime)
val receivedLabel = MutableLiveData<String>()
val sentLabel = MutableLiveData<String>()
val errorLabel = MutableLiveData<String>()
val deliveryModels = MutableLiveData<ArrayList<ChatMessageParticipantDeliveryModel>>()
private val displayedModels = arrayListOf<ChatMessageParticipantDeliveryModel>()
private val deliveredModels = arrayListOf<ChatMessageParticipantDeliveryModel>()
private val sentModels = arrayListOf<ChatMessageParticipantDeliveryModel>()
private val errorModels = arrayListOf<ChatMessageParticipantDeliveryModel>()
init {
computeDeliveryStatus()
}
@UiThread
fun computeListForState(state: State) {
when (state) {
State.DeliveredToUser -> {
deliveryModels.value = deliveredModels
}
State.Delivered -> {
deliveryModels.value = sentModels
}
State.NotDelivered -> {
deliveryModels.value = errorModels
}
else -> {
deliveryModels.value = displayedModels
}
}
}
@WorkerThread
private fun computeDeliveryStatus() {
for (participant in chatMessage.getParticipantsByImdnState(State.Displayed)) {
displayedModels.add(ChatMessageParticipantDeliveryModel(participant))
}
readLabel.postValue(
AppUtils.getFormattedString(
R.string.message_delivery_info_read_title,
displayedModels.size.toString()
)
)
for (participant in chatMessage.getParticipantsByImdnState(State.DeliveredToUser)) {
deliveredModels.add(ChatMessageParticipantDeliveryModel(participant))
}
receivedLabel.postValue(
AppUtils.getFormattedString(
R.string.message_delivery_info_received_title,
deliveredModels.size.toString()
)
)
for (participant in chatMessage.getParticipantsByImdnState(State.Delivered)) {
sentModels.add(ChatMessageParticipantDeliveryModel(participant))
}
sentLabel.postValue(
AppUtils.getFormattedString(
R.string.message_delivery_info_sent_title,
sentModels.size.toString()
)
)
for (participant in chatMessage.getParticipantsByImdnState(State.NotDelivered)) {
errorModels.add(ChatMessageParticipantDeliveryModel(participant))
}
errorLabel.postValue(
AppUtils.getFormattedString(
R.string.message_delivery_info_error_title,
errorModels.size.toString()
)
)
deliveryModels.postValue(displayedModels)
}
}

View file

@ -59,8 +59,6 @@ class ChatMessageModel @WorkerThread constructor(
val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly
val deliveryModels = MutableLiveData<ArrayList<ChatMessageDeliveryModel>>()
val dismissLongPressMenuEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -87,8 +85,6 @@ class ChatMessageModel @WorkerThread constructor(
init {
chatMessage.addListener(chatMessageListener)
computeStatusIcon(chatMessage.state)
computeDeliveryStatus()
}
@WorkerThread
@ -127,15 +123,4 @@ class ChatMessageModel @WorkerThread constructor(
}
statusIcon.postValue(icon)
}
@WorkerThread
private fun computeDeliveryStatus() {
val list = arrayListOf<ChatMessageDeliveryModel>()
/*for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Displayed)) {
list.add(ChatMessageDeliveryModel(participant))
}*/
deliveryModels.postValue(list)
}
}

View file

@ -0,0 +1,18 @@
package org.linphone.ui.main.chat.model
import androidx.annotation.WorkerThread
import org.linphone.LinphoneApplication
import org.linphone.core.ParticipantImdnState
import org.linphone.utils.TimestampUtils
class ChatMessageParticipantDeliveryModel @WorkerThread constructor(
imdnState: ParticipantImdnState
) {
val address = imdnState.participant.address
val avatarModel = LinphoneApplication.coreContext.contactsManager.getContactAvatarModelForAddress(
address
)
val time = TimestampUtils.toString(imdnState.stateChangeTime)
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="20dp" android:topLeftRadius="20dp" />
<solid android:color="@color/white"/>
</shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="16dp" android:bottomRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/white"/>
<solid android:color="@color/gray_100"/>
</shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="@color/white"/>
<solid android:color="@color/gray_100"/>
</shape>

View file

@ -9,6 +9,9 @@
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="showDeliveryInfoClickListener"
type="View.OnClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
@ -75,6 +78,7 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/date_time"
android:onClick="@{showDeliveryInfoClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
@ -90,6 +94,7 @@
<ImageView
style="@style/default_text_style_300"
android:id="@+id/delivery_status"
android:onClick="@{showDeliveryInfoClickListener}"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="18dp"

View file

@ -192,7 +192,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingBottom="5dp"
android:background="@color/gray_100"
android:background="@color/white"
app:layout_constraintTop_toBottomOf="@id/top_bar_barrier"
app:layout_constraintBottom_toTopOf="@id/composing" />
@ -203,7 +203,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:paddingBottom="5dp"
android:background="@color/gray_100"
android:background="@color/white"
android:text="@{viewModel.composingLabel, default=`John Doe is composing...`}"
android:textSize="12sp"
android:textColor="@color/gray_main2_400"
@ -223,7 +223,8 @@
layout="@layout/chat_conversation_send_area"/>
<include
android:id="@+id/delivery_bottom_sheet"
android:id="@+id/message_delivery"
android:visibility="gone"
layout="@layout/chat_message_delivery_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -9,7 +9,7 @@
<import type="org.linphone.core.ChatRoom.SecurityLevel" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageDeliveryModel" />
type="org.linphone.ui.main.chat.model.ChatMessageParticipantDeliveryModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -9,15 +9,14 @@
type="View.OnClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
type="org.linphone.ui.main.chat.model.ChatMessageDeliveryModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_bottom_sheet_background"
android:clickable="true"
android:focusable="true"
android:background="@drawable/shape_bottom_sheet_white_background"
android:elevation="16dp"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
@ -39,40 +38,25 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent_color"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/handle"
app:layout_constraintBottom_toTopOf="@id/scrollview"
app:tabMode="fixed"
app:tabUnboundedRipple="true"
app:tabRippleColor="@color/orange_main_100"
app:tabGravity="fill"
app:tabPadding="0dp"
app:tabInlineLabel="true"
app:tabIndicatorColor="@color/orange_main_500"
app:tabTextColor="@color/gray_main2_400"
app:tabSelectedTextColor="@color/gray_main2_600">
<com.google.android.material.tabs.TabItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Lu" />
<com.google.android.material.tabs.TabItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reçu" />
<com.google.android.material.tabs.TabItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Envoyé" />
<com.google.android.material.tabs.TabItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Erreur" />
</com.google.android.material.tabs.TabLayout>
app:tabSelectedTextColor="@color/gray_main2_600" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tabs">
android:layout_height="250dp"
app:layout_constraintHeight_min="50dp"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="match_parent"

View file

@ -15,7 +15,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_bottom_sheet_background"
android:background="@drawable/shape_bottom_sheet_gray_100_background"
android:clickable="true"
android:focusable="true"
app:behavior_hideable="true"

View file

@ -361,6 +361,11 @@
<string name="conversation_info_admin_menu_set_participant_admin">Give admin rights</string>
<string name="conversation_info_admin_menu_unset_participant_admin">Remove admin rights</string>
<string name="message_delivery_info_read_title">Read (%s)</string>
<string name="message_delivery_info_received_title">Received (%s)</string>
<string name="message_delivery_info_sent_title">Sent (%s)</string>
<string name="message_delivery_info_error_title">Error (%s)</string>
<string name="meetings_list_empty">No meeting for the moment…</string>
<string name="meeting_schedule_title">New meeting</string>
<string name="meeting_schedule_meeting_label">Meeting</string>