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) {
holderInstance.assignSystemMessageInterface(chatActivity);
} else if (holder instanceof CallStartedViewHolder holderInstance) {
holderInstance.assignCallStartedMessageInterface(chatActivity);
} else if (holder instanceof TemporaryMessageViewHolder holderInstance) {
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.TakePhotoActivity
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.IncomingDeckCardViewHolder
import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
@ -879,7 +878,7 @@ class ChatActivity :
}
processExpiredMessages()
processCallStartedMessages(chatMessageList)
processCallStartedMessages()
adapter?.notifyDataSetChanged()
}
@ -1199,17 +1198,6 @@ class ChatActivity :
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(
CONTENT_TYPE_TEMP,
TemporaryMessageViewHolder::class.java,
@ -2559,7 +2547,7 @@ class ChatActivity :
webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
}
private fun processCallStartedMessages(chatMessageList: List<ChatMessage>) {
private fun processCallStartedMessages() {
try {
val mostRecentCallSystemMessage = adapter?.items?.first {
it.item is ChatMessage &&
@ -2577,8 +2565,7 @@ class ChatActivity :
if (mostRecentCallSystemMessage != null) {
processMostRecentMessage(
mostRecentCallSystemMessage as ChatMessage,
chatMessageList
mostRecentCallSystemMessage as ChatMessage
)
}
} catch (e: NoSuchElementException) {
@ -3542,29 +3529,21 @@ class ChatActivity :
else -> false
}
private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) {
private fun processMostRecentMessage(recent: ChatMessage) {
when (recent.systemMessageType) {
ChatMessage.SystemMessageType.CALL_STARTED -> { // add CallStartedMessage with id -2
ChatMessage.SystemMessageType.CALL_STARTED -> {
if (!callStarted) {
val callStartedChatMessage = ChatMessage()
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)
messageInputViewModel.showCallStartedIndicator(recent, true)
callStarted = true
}
} // remove CallStartedMessage with id -2
}
ChatMessage.SystemMessageType.CALL_ENDED,
ChatMessage.SystemMessageType.CALL_MISSED,
ChatMessage.SystemMessageType.CALL_TRIED,
ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> {
adapter?.deleteById("-2")
callStarted = false
} // remove message of id -2
messageInputViewModel.showCallStartedIndicator(recent, false)
}
else -> {}
}
}

View file

@ -9,6 +9,7 @@ package com.nextcloud.talk.chat
import android.content.res.Resources
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@ -36,6 +37,7 @@ import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
@ -43,7 +45,11 @@ import androidx.emoji2.widget.EmojiTextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.Coil.imageLoader
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.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
@ -118,6 +124,7 @@ class MessageInputFragment : Fragment() {
private var mentionAutocomplete: Autocomplete<*>? = null
private var xcounter = 0f
private var ycounter = 0f
private var isCollapsed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -196,6 +203,49 @@ class MessageInputFragment : Fragment() {
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) {
@ -390,6 +440,41 @@ class MessageInputFragment : Fragment() {
binding.fragmentMessageInputView.button?.contentDescription =
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")
@ -907,6 +992,22 @@ class MessageInputFragment : Fragment() {
binding.fragmentEditView.clearEdit.let {
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() {

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.AudioRecorderManager
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.models.json.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.generic.GenericOverall
@ -129,6 +130,10 @@ class MessageInputViewModel @Inject constructor(
val messageQueueFlow: LiveData<List<QueuedMessage>>
get() = _messageQueueFlow
private val _callStartedFlow: MutableLiveData<Pair<ChatMessage, Boolean>> = MutableLiveData()
val callStartedFlow: LiveData<Pair<ChatMessage, Boolean>>
get() = _callStartedFlow
@Suppress("LongParameterList")
fun sendChatMessage(
internalId: String,
@ -314,6 +319,10 @@ class MessageInputViewModel @Inject constructor(
dataStore.saveMessageQueue(internalId, queue)
}
fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) {
_callStartedFlow.postValue(Pair(recent, show))
}
companion object {
private val TAG = MessageInputViewModel::class.java.simpleName
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
android:layout_width="match_parent"
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:gravity="center">
@ -42,6 +81,7 @@
<LinearLayout
android:id="@+id/call_btn_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"

View file

@ -13,6 +13,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/fragment_connection_lost"
android:layout_width="match_parent"
@ -24,36 +25,45 @@
android:visibility="gone"
tools:visibility="visible" />
<include
android:id="@+id/fragment_editView"
layout="@layout/edit_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp">
</include>
<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"/>
<com.nextcloud.talk.ui.MessageInput
android:id="@+id/fragment_message_input_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:inputType="textLongMessage|textAutoComplete"
android:maxLength="1000"
app:attachmentButtonBackground="@color/transparent"
app:attachmentButtonHeight="48dp"
app:attachmentButtonIcon="@drawable/ic_baseline_attach_file_24"
app:attachmentButtonMargin="0dp"
app:attachmentButtonWidth="48dp"
app:delayTypingStatus="200"
app:inputButtonDefaultBgColor="@color/transparent"
app:inputButtonDefaultBgDisabledColor="@color/transparent"
app:inputButtonDefaultBgPressedColor="@color/transparent"
app:inputButtonDefaultIconColor="@color/colorPrimary"
app:inputButtonHeight="48dp"
app:inputButtonMargin="0dp"
app:inputButtonWidth="48dp"
app:inputHint="@string/nc_hint_enter_a_message"
app:inputTextColor="@color/nc_incoming_text_default"
app:inputTextSize="16sp"
app:showAttachmentButton="true" />
<include
android:id="@+id/fragment_editView"
layout="@layout/edit_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp">
</include>
<com.nextcloud.talk.ui.MessageInput
android:id="@+id/fragment_message_input_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:inputType="textLongMessage|textAutoComplete"
android:maxLength="1000"
app:attachmentButtonBackground="@color/transparent"
app:attachmentButtonHeight="48dp"
app:attachmentButtonIcon="@drawable/ic_baseline_attach_file_24"
app:attachmentButtonMargin="0dp"
app:attachmentButtonWidth="48dp"
app:delayTypingStatus="200"
app:inputButtonDefaultBgColor="@color/transparent"
app:inputButtonDefaultBgDisabledColor="@color/transparent"
app:inputButtonDefaultBgPressedColor="@color/transparent"
app:inputButtonDefaultIconColor="@color/colorPrimary"
app:inputButtonHeight="48dp"
app:inputButtonMargin="0dp"
app:inputButtonWidth="48dp"
app:inputHint="@string/nc_hint_enter_a_message"
app:inputTextColor="@color/nc_incoming_text_default"
app:inputTextSize="16sp"
app:showAttachmentButton="true" />
</LinearLayout>