From 659d8419d059a652d56abdc3f55ba1e7ddd57c4f Mon Sep 17 00:00:00 2001 From: Julius Linus Date: Fri, 8 Sep 2023 10:30:55 -0500 Subject: [PATCH] Show Accept Call Button in Chat - added call_started_message.xml - added CallStartedViewHolder.kt - added CallStartedMessageInterface.kt - Both join audio and join video buttons - Gets destroyed if call ends, persists despite state changes Signed-off-by: Julius Linus --- .../messages/CallStartedMessageInterface.kt | 25 ++++ .../messages/CallStartedViewHolder.kt | 129 ++++++++++++++++++ .../messages/TalkMessagesListAdapter.java | 2 + .../com/nextcloud/talk/chat/ChatActivity.kt | 98 +++++++++++-- .../main/res/layout/call_started_message.xml | 90 ++++++++++++ app/src/main/res/values/strings.xml | 3 + 6 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt create mode 100644 app/src/main/res/layout/call_started_message.xml diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt new file mode 100644 index 00000000000..9d39d987c5b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.adapters.messages + +interface CallStartedMessageInterface { + fun joinAudioCall() + fun joinVideoCall() +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt new file mode 100644 index 00000000000..2f00d55a847 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt @@ -0,0 +1,129 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ + +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.models.json.chat.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(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, true) + } + + 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 + } + + companion object { + var TAG: String? = CallStartedViewHolder::class.simpleName + } + + override fun viewDetached() { + // unused atm + } + + override fun viewAttached() { + // unused atm + } + + override fun viewRecycled() { + // unused atm + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java index a245c60458a..16c8d9884e6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java @@ -77,6 +77,8 @@ public void onBindViewHolder(ViewHolder holder, int position) { } else if (holder instanceof SystemMessageViewHolder) { ((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity); + } else if (holder instanceof CallStartedViewHolder) { + ((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity); } } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 945ef3f497c..cc0c7bd9fab 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -121,6 +121,8 @@ import com.nextcloud.talk.R 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.IncomingLinkPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder @@ -268,7 +270,8 @@ class ChatActivity : VoiceMessageInterface, CommonMessageInterface, PreviewMessageInterface, - SystemMessageInterface { + SystemMessageInterface, + CallStartedMessageInterface { var active = false @@ -377,6 +380,8 @@ class ChatActivity : var typedWhileTypingTimerIsRunning: Boolean = false val typingParticipants = HashMap() + var callStarted = false + private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener { override fun onSwitchTo(token: String?) { if (token != null) { @@ -946,6 +951,17 @@ 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_SYSTEM_MESSAGE, SystemMessageViewHolder::class.java, @@ -1808,7 +1824,7 @@ class ChatActivity : } } } - mediaPlayerHandler.postDelayed(this, 15) + mediaPlayerHandler.postDelayed(this, MILISEC_15) } }) @@ -3189,6 +3205,31 @@ class ChatActivity : Integer.parseInt(it) } + try { + val mostRecentCallSystemMessage = adapter?.items?.first { + it.item is ChatMessage && + (it.item as ChatMessage).systemMessageType in + listOf( + ChatMessage.SystemMessageType.CALL_STARTED, + ChatMessage.SystemMessageType.CALL_JOINED, + ChatMessage.SystemMessageType.CALL_LEFT, + ChatMessage.SystemMessageType.CALL_ENDED, + ChatMessage.SystemMessageType.CALL_TRIED, + ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE, + ChatMessage.SystemMessageType.CALL_MISSED + ) + }?.item + + if (mostRecentCallSystemMessage != null) { + processMostRecentMessage( + mostRecentCallSystemMessage as ChatMessage, + chatMessageList + ) + } + } catch (e: java.util.NoSuchElementException) { + Log.d(TAG, "No System messages found $e") + } + updateReadStatusOfAllMessages(newXChatLastCommonRead) adapter?.notifyDataSetChanged() @@ -4230,10 +4271,39 @@ class ChatActivity : CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview() CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" + CONTENT_TYPE_CALL_STARTED -> message.id == "-2" + else -> false } } + private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List) { + when (recent.systemMessageType) { + ChatMessage.SystemMessageType.CALL_STARTED -> { // add CallStartedMessage with id -2 + 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) + 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 + else -> {} + } + } + @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { /* @@ -4367,6 +4437,14 @@ class ChatActivity : } } + override fun joinAudioCall() { + startACall(true, false) + } + + override fun joinVideoCall() { + startACall(false, false) + } + private fun logConversationInfos(methodName: String) { Log.d(TAG, " |-----------------------------------------------") Log.d(TAG, " | method: $methodName") @@ -4379,12 +4457,13 @@ class ChatActivity : companion object { private val TAG = ChatActivity::class.simpleName - private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 - private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 - private const val CONTENT_TYPE_LOCATION: Byte = 3 - private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4 - private const val CONTENT_TYPE_POLL: Byte = 5 - private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 6 + private const val CONTENT_TYPE_CALL_STARTED: Byte = 1 + private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 2 + private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 3 + private const val CONTENT_TYPE_LOCATION: Byte = 4 + private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 5 + private const val CONTENT_TYPE_POLL: Byte = 6 + private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7 private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200 private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000 private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000 @@ -4417,7 +4496,6 @@ class ChatActivity : private const val FULLY_OPAQUE_INT: Int = 255 private const val SEMI_TRANSPARENT_INT: Int = 99 private const val VOICE_MESSAGE_SEEKBAR_BASE = 1000 - private const val SECOND: Long = 1000 private const val NO_PREVIOUS_MESSAGE_ID: Int = -1 private const val GROUPED_MESSAGES_THRESHOLD = 4 private const val GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD = 5 @@ -4447,5 +4525,7 @@ class ChatActivity : private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping" private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" + private const val CALL_STARTED_ID = -2 + private const val MILISEC_15: Long = 15 } } diff --git a/app/src/main/res/layout/call_started_message.xml b/app/src/main/res/layout/call_started_message.xml new file mode 100644 index 00000000000..72ce73bfdc7 --- /dev/null +++ b/app/src/main/res/layout/call_started_message.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 50dc06158a3..8fa0ef657e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -706,6 +706,9 @@ How to translate with transifex: Custom Set Calendar + Video Call + Audio Call + started a call Error 429 Too Many Requests