Better Call Started Indicator

- Pinned to MessageInputFragment
- Collapsable
- Looks cool

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
This commit is contained in:
rapterjet2004 2024-11-06 10:02:21 -06:00
parent 3e10813ae0
commit 990f6ec3fa
No known key found for this signature in database
GPG key ID: 3AA5FDFED7944099
8 changed files with 222 additions and 180 deletions

View file

@ -1,115 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@nextcloud.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import autodagger.AutoInjector
import coil.Coil.imageLoader
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.CallStartedMessageBinding
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.stfalcon.chatkit.messages.MessageHolders
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class CallStartedViewHolder(incomingView: View, payload: Any) :
MessageHolders.BaseIncomingMessageViewHolder<ChatMessage>(incomingView, payload) {
private val binding: CallStartedMessageBinding = CallStartedMessageBinding.bind(incomingView)
@Inject
lateinit var context: Context
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
private lateinit var messageInterface: CallStartedMessageInterface
override fun onBind(message: ChatMessage) {
super.onBind(message)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
themeBackground()
setUpAvatarProfile(message)
binding.callAuthorChip.text = message.actorDisplayName
binding.joinVideoCall.setOnClickListener { messageInterface.joinVideoCall() }
binding.joinAudioCall.setOnClickListener { messageInterface.joinAudioCall() }
}
private fun themeBackground() {
binding.callStartedBackground.apply {
viewThemeUtils.talk.themeOutgoingMessageBubble(this, grouped = true, false)
}
binding.callAuthorChip.apply {
viewThemeUtils.material.colorChipBackground(this)
}
}
private fun setUpAvatarProfile(message: ChatMessage) {
val user = userManager.currentUser.blockingGet()
val url: String = if (message.actorType == "guests" || message.actorType == "guest") {
ApiUtils.getUrlForGuestAvatar(
user!!.baseUrl!!,
message.actorDisplayName,
true
)
} else {
ApiUtils.getUrlForAvatar(user!!.baseUrl!!, message.actorDisplayName, false)
}
val imageRequest: ImageRequest = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.transformations(CircleCropTransformation())
.target(object : Target {
override fun onStart(placeholder: Drawable?) {
// unused atm
}
override fun onError(error: Drawable?) {
// unused atm
}
override fun onSuccess(result: Drawable) {
binding.callAuthorChip.chipIcon = result
}
})
.build()
imageLoader(context).enqueue(imageRequest)
}
fun assignCallStartedMessageInterface(inf: CallStartedMessageInterface) {
messageInterface = inf
}
override fun viewDetached() {
// unused atm
}
override fun viewAttached() {
// unused atm
}
override fun viewRecycled() {
// unused atm
}
companion object {
val TAG: String? = CallStartedViewHolder::class.simpleName
}
}

View file

@ -68,9 +68,6 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
} else if (holder instanceof SystemMessageViewHolder holderInstance) { } else if (holder instanceof SystemMessageViewHolder holderInstance) {
holderInstance.assignSystemMessageInterface(chatActivity); holderInstance.assignSystemMessageInterface(chatActivity);
} else if (holder instanceof CallStartedViewHolder holderInstance) {
holderInstance.assignCallStartedMessageInterface(chatActivity);
} else if (holder instanceof TemporaryMessageViewHolder holderInstance) { } else if (holder instanceof TemporaryMessageViewHolder holderInstance) {
holderInstance.assignTemporaryMessageInterface(chatActivity); holderInstance.assignTemporaryMessageInterface(chatActivity);

View file

@ -81,7 +81,6 @@ import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.activities.TakePhotoActivity import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.CallStartedMessageInterface import com.nextcloud.talk.adapters.messages.CallStartedMessageInterface
import com.nextcloud.talk.adapters.messages.CallStartedViewHolder
import com.nextcloud.talk.adapters.messages.CommonMessageInterface import com.nextcloud.talk.adapters.messages.CommonMessageInterface
import com.nextcloud.talk.adapters.messages.IncomingDeckCardViewHolder import com.nextcloud.talk.adapters.messages.IncomingDeckCardViewHolder
import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
@ -879,7 +878,7 @@ class ChatActivity :
} }
processExpiredMessages() processExpiredMessages()
processCallStartedMessages(chatMessageList) processCallStartedMessages()
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
} }
@ -1199,17 +1198,6 @@ class ChatActivity :
R.layout.item_custom_outcoming_preview_message R.layout.item_custom_outcoming_preview_message
) )
messageHolders.registerContentType(
CONTENT_TYPE_CALL_STARTED,
CallStartedViewHolder::class.java,
payload,
R.layout.call_started_message,
CallStartedViewHolder::class.java,
payload,
R.layout.call_started_message,
this
)
messageHolders.registerContentType( messageHolders.registerContentType(
CONTENT_TYPE_TEMP, CONTENT_TYPE_TEMP,
TemporaryMessageViewHolder::class.java, TemporaryMessageViewHolder::class.java,
@ -2559,7 +2547,7 @@ class ChatActivity :
webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener) webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
} }
private fun processCallStartedMessages(chatMessageList: List<ChatMessage>) { private fun processCallStartedMessages() {
try { try {
val mostRecentCallSystemMessage = adapter?.items?.first { val mostRecentCallSystemMessage = adapter?.items?.first {
it.item is ChatMessage && it.item is ChatMessage &&
@ -2577,8 +2565,7 @@ class ChatActivity :
if (mostRecentCallSystemMessage != null) { if (mostRecentCallSystemMessage != null) {
processMostRecentMessage( processMostRecentMessage(
mostRecentCallSystemMessage as ChatMessage, mostRecentCallSystemMessage as ChatMessage
chatMessageList
) )
} }
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
@ -3542,29 +3529,21 @@ class ChatActivity :
else -> false else -> false
} }
private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) { private fun processMostRecentMessage(recent: ChatMessage) {
when (recent.systemMessageType) { when (recent.systemMessageType) {
ChatMessage.SystemMessageType.CALL_STARTED -> { // add CallStartedMessage with id -2 ChatMessage.SystemMessageType.CALL_STARTED -> {
if (!callStarted) { if (!callStarted) {
val callStartedChatMessage = ChatMessage() messageInputViewModel.showCallStartedIndicator(recent, true)
callStartedChatMessage.jsonMessageId = CALL_STARTED_ID
callStartedChatMessage.actorId = "-2"
val name = if (recent.actorDisplayName.isNullOrEmpty()) "Guest" else recent.actorDisplayName
callStartedChatMessage.actorDisplayName = name
callStartedChatMessage.actorType = recent.actorType
callStartedChatMessage.timestamp = chatMessageList[0].timestamp
callStartedChatMessage.message = null
adapter?.addToStart(callStartedChatMessage, false)
callStarted = true callStarted = true
} }
} // remove CallStartedMessage with id -2 }
ChatMessage.SystemMessageType.CALL_ENDED, ChatMessage.SystemMessageType.CALL_ENDED,
ChatMessage.SystemMessageType.CALL_MISSED, ChatMessage.SystemMessageType.CALL_MISSED,
ChatMessage.SystemMessageType.CALL_TRIED, ChatMessage.SystemMessageType.CALL_TRIED,
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> { ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> {
adapter?.deleteById("-2")
callStarted = false callStarted = false
} // remove message of id -2 messageInputViewModel.showCallStartedIndicator(recent, false)
}
else -> {} else -> {}
} }
} }

View file

@ -9,6 +9,7 @@ package com.nextcloud.talk.chat
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
@ -36,6 +37,7 @@ import android.widget.PopupMenu
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@ -43,7 +45,11 @@ import androidx.emoji2.widget.EmojiTextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.Coil.imageLoader
import coil.load import coil.load
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -118,6 +124,7 @@ class MessageInputFragment : Fragment() {
private var mentionAutocomplete: Autocomplete<*>? = null private var mentionAutocomplete: Autocomplete<*>? = null
private var xcounter = 0f private var xcounter = 0f
private var ycounter = 0f private var ycounter = 0f
private var isCollapsed = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -196,6 +203,49 @@ class MessageInputFragment : Fragment() {
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued) binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued)
} }
} }
chatActivity.messageInputViewModel.callStartedFlow.observe(viewLifecycleOwner) {
val (message, show) = it
if (show) {
binding.fragmentCallStarted.callAuthorChip.text = message.actorDisplayName
binding.fragmentCallStarted.callAuthorChipSecondary.text = message.actorDisplayName
val user = userManager.currentUser.blockingGet()
val url: String = if (message.actorType == "guests" || message.actorType == "guest") {
ApiUtils.getUrlForGuestAvatar(
user!!.baseUrl!!,
message.actorDisplayName,
true
)
} else {
ApiUtils.getUrlForAvatar(user!!.baseUrl!!, message.actorId, false)
}
val imageRequest: ImageRequest = ImageRequest.Builder(requireContext())
.data(url)
.crossfade(true)
.transformations(CircleCropTransformation())
.target(object : Target {
override fun onStart(placeholder: Drawable?) {
// unused atm
}
override fun onError(error: Drawable?) {
// unused atm
}
override fun onSuccess(result: Drawable) {
binding.fragmentCallStarted.callAuthorChip.chipIcon = result
binding.fragmentCallStarted.callAuthorChipSecondary.chipIcon = result
}
})
.build()
imageLoader(requireContext()).enqueue(imageRequest)
binding.fragmentCallStarted.root.visibility = View.VISIBLE
} else {
binding.fragmentCallStarted.root.visibility = View.GONE
}
}
} }
private fun handleUI(isOnline: Boolean, connectionGained: Boolean) { private fun handleUI(isOnline: Boolean, connectionGained: Boolean) {
@ -390,6 +440,41 @@ class MessageInputFragment : Fragment() {
binding.fragmentMessageInputView.button?.contentDescription = binding.fragmentMessageInputView.button?.contentDescription =
resources.getString(R.string.nc_description_send_message_button) resources.getString(R.string.nc_description_send_message_button)
binding.fragmentCallStarted.joinAudioCall.setOnClickListener {
chatActivity.joinAudioCall()
}
binding.fragmentCallStarted.joinVideoCall.setOnClickListener {
chatActivity.joinVideoCall()
}
binding.fragmentCallStarted.callStartedCloseBtn.setOnClickListener {
isCollapsed = !isCollapsed
if (isCollapsed) {
binding.fragmentCallStarted.callAuthorLayout.visibility = View.GONE
binding.fragmentCallStarted.callBtnLayout.visibility = View.GONE
binding.fragmentCallStarted.callAuthorChipSecondary.visibility = View.VISIBLE
binding.fragmentCallStarted.callStartedSecondaryText.visibility = View.VISIBLE
} else {
binding.fragmentCallStarted.callAuthorLayout.visibility = View.VISIBLE
binding.fragmentCallStarted.callBtnLayout.visibility = View.VISIBLE
binding.fragmentCallStarted.callAuthorChipSecondary.visibility = View.GONE
binding.fragmentCallStarted.callStartedSecondaryText.visibility = View.GONE
}
setDropDown(isCollapsed)
}
}
private fun setDropDown(collapsed: Boolean) {
val drawable = if (collapsed) {
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_up)
} else {
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_down)
}
binding.fragmentCallStarted.callStartedCloseBtn.setImageDrawable(drawable)
} }
@Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod") @Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod")
@ -907,6 +992,22 @@ class MessageInputFragment : Fragment() {
binding.fragmentEditView.clearEdit.let { binding.fragmentEditView.clearEdit.let {
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
} }
binding.fragmentCallStarted.callStartedBackground.apply {
viewThemeUtils.talk.themeOutgoingMessageBubble(this, grouped = true, false)
}
binding.fragmentCallStarted.callAuthorChip.apply {
viewThemeUtils.material.colorChipBackground(this)
}
binding.fragmentCallStarted.callAuthorChipSecondary.apply {
viewThemeUtils.material.colorChipBackground(this)
}
binding.fragmentCallStarted.callStartedCloseBtn.apply {
viewThemeUtils.platform.colorImageView(this, ColorRole.PRIMARY)
}
} }
private fun cancelReply() { private fun cancelReply() {

View file

@ -18,6 +18,7 @@ import androidx.lifecycle.asLiveData
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.AudioRecorderManager import com.nextcloud.talk.chat.data.io.AudioRecorderManager
import com.nextcloud.talk.chat.data.io.MediaPlayerManager import com.nextcloud.talk.chat.data.io.MediaPlayerManager
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
@ -129,6 +130,10 @@ class MessageInputViewModel @Inject constructor(
val messageQueueFlow: LiveData<List<QueuedMessage>> val messageQueueFlow: LiveData<List<QueuedMessage>>
get() = _messageQueueFlow get() = _messageQueueFlow
private val _callStartedFlow: MutableLiveData<Pair<ChatMessage, Boolean>> = MutableLiveData()
val callStartedFlow: LiveData<Pair<ChatMessage, Boolean>>
get() = _callStartedFlow
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun sendChatMessage( fun sendChatMessage(
internalId: String, internalId: String,
@ -314,6 +319,10 @@ class MessageInputViewModel @Inject constructor(
dataStore.saveMessageQueue(internalId, queue) dataStore.saveMessageQueue(internalId, queue)
} }
fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) {
_callStartedFlow.postValue(Pair(recent, show))
}
companion object { companion object {
private val TAG = MessageInputViewModel::class.java.simpleName private val TAG = MessageInputViewModel::class.java.simpleName
private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 1000 private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 1000

View file

@ -0,0 +1,21 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<group
android:pivotX="12"
android:pivotY="12"
android:rotation="180"
>
<path
android:fillColor="#FF000000"
android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z" />
</group>
</vector>

View file

@ -20,7 +20,46 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/standard_margin" android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.chip.Chip
android:id="@+id/call_author_chip_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/account_circle_48dp"
app:chipCornerRadius="@dimen/dialogBorderRadius"
tools:text="Julius James Linus"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/call_started_secondary_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/standard_quarter_margin"
android:text="@string/started_a_call"
android:visibility="gone"
tools:visibility="visible"/>
<ImageView
android:id="@+id/call_started_close_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:contentDescription="@string/close_icon"
android:src="@drawable/ic_keyboard_arrow_down"
android:background="?attr/selectableItemBackground"
android:layout_gravity="center"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/call_author_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/standard_half_margin"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center"> android:gravity="center">
@ -42,6 +81,7 @@
<LinearLayout <LinearLayout
android:id="@+id/call_btn_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"

View file

@ -13,6 +13,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/fragment_connection_lost" android:id="@+id/fragment_connection_lost"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -24,6 +25,15 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<include
android:id="@+id/fragment_call_started"
layout="@layout/call_started_message"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginVertical="@dimen/standard_margin"
android:layout_marginHorizontal="@dimen/standard_quarter_margin"
android:visibility="gone"/>
<include <include
android:id="@+id/fragment_editView" android:id="@+id/fragment_editView"
layout="@layout/edit_message_view" layout="@layout/edit_message_view"