From c3796306100470893779c95119bfc22b6c670c46 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 28 Apr 2023 16:04:30 +0200 Subject: [PATCH 1/3] add animated emoji reactions to calls (no signaling yet) Signed-off-by: Marcel Hibbe --- .../talk/activities/CallActivity.java | 11 +- .../nextcloud/talk/call/ReactionAnimator.kt | 184 ++++++++++++++++++ .../talk/ui/dialog/MoreCallActionsDialog.kt | 51 +++++ .../database/user/CapabilitiesUtilNew.kt | 10 + app/src/main/res/layout/call_activity.xml | 47 +++-- .../res/layout/dialog_more_call_actions.xml | 14 ++ 6 files changed, 300 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index b7a7b59d9..6e49c26ed 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -66,6 +66,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.call.CallParticipant; import com.nextcloud.talk.call.CallParticipantList; import com.nextcloud.talk.call.CallParticipantModel; +import com.nextcloud.talk.call.ReactionAnimator; import com.nextcloud.talk.chat.ChatActivity; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.databinding.CallActivityBinding; @@ -251,7 +252,7 @@ public class CallActivity extends CallBaseActivity { private List iceServers; private CameraEnumerator cameraEnumerator; private String roomToken; - private User conversationUser; + public User conversationUser; private String conversationName; private String callSession; private MediaStream localStream; @@ -370,6 +371,8 @@ public class CallActivity extends CallBaseActivity { private boolean isModerator; + private ReactionAnimator reactionAnimator; + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { @@ -506,6 +509,8 @@ public class CallActivity extends CallBaseActivity { initiateCall(); } updateSelfVideoViewPosition(); + + reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils); } @Override @@ -2725,6 +2730,10 @@ public class CallActivity extends CallBaseActivity { } } + public void addCallReaction(String emoji, String displayName) { + reactionAnimator.addReaction(emoji, displayName); + } + /** * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from * CallActivity. diff --git a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt new file mode 100644 index 000000000..80b2a2e90 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt @@ -0,0 +1,184 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * 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.call + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.nextcloud.talk.R +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.vanniktech.emoji.EmojiTextView + +class ReactionAnimator( + val context: Context, + private val startPointView: RelativeLayout, + val viewThemeUtils: ViewThemeUtils? +) { + private val reactionsList: MutableList = ArrayList() + + fun addReaction( + emoji: String, + displayName: String + ) { + val callReaction = CallReaction(emoji, displayName) + reactionsList.add(callReaction) + + if (reactionsList.size == 1) { + animateReaction(reactionsList[0]) + } + } + + private fun animateReaction( + callReaction: CallReaction + ) { + val reactionWrapper = getReactionWrapperView(callReaction) + + val params = RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + leftMargin = 0 + bottomMargin = 0 + } + + params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 1) + startPointView.addView(reactionWrapper, params) + + val moveWithFullAlpha = ObjectAnimator.ofFloat( + reactionWrapper, + TRANSLATION_Y_PROPERTY, + POSITION_Y_WITH_FULL_ALPHA + ) + moveWithFullAlpha.duration = DURATION_FULL_ALPHA + moveWithFullAlpha.interpolator = LinearInterpolator() + + val moveWithDecreasingAlpha = ObjectAnimator.ofFloat( + reactionWrapper, + TRANSLATION_Y_PROPERTY, + POSITION_Y_WITH_DECREASING_ALPHA + ) + moveWithDecreasingAlpha.duration = DURATION_DECREASING_ALPHA + moveWithDecreasingAlpha.interpolator = LinearInterpolator() + + val decreasingAlpha: ObjectAnimator = ObjectAnimator.ofFloat( + reactionWrapper, + ALPHA_PROPERTY, + ZERO_ALPHA + ) + decreasingAlpha.duration = DURATION_DECREASING_ALPHA + + val animatorWithFullAlpha = AnimatorSet() + animatorWithFullAlpha.play(moveWithFullAlpha) + + animatorWithFullAlpha.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + reactionsList.remove(callReaction) + if (reactionsList.isNotEmpty()) { + animateReaction(reactionsList[0]) + } + } + }) + + val animatorWithDecreasingAlpha = AnimatorSet() + animatorWithDecreasingAlpha.playTogether(moveWithDecreasingAlpha, decreasingAlpha) + + val finalAnimator = AnimatorSet() + finalAnimator.play(animatorWithFullAlpha).before(animatorWithDecreasingAlpha) + + finalAnimator.start() + } + + private fun getReactionWrapperView(callReaction: CallReaction): LinearLayout { + val reactionWrapper = LinearLayout(context) + reactionWrapper.orientation = LinearLayout.HORIZONTAL + + val emojiView = EmojiTextView(context) + emojiView.text = callReaction.emoji + emojiView.textSize = 20f + + val nameView = getNameView(callReaction) + reactionWrapper.addView(emojiView) + reactionWrapper.addView(nameView) + return reactionWrapper + } + + @SuppressLint("SetTextI18n") + private fun getNameView(callReaction: CallReaction): TextView { + val nameView = TextView(context) + + val nameViewParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + nameViewParams.setMargins(20, 0, 20, 5) + nameView.layoutParams = nameViewParams + + nameView.text = " " + callReaction.userName + " " + nameView.setTextColor(context.resources.getColor(R.color.white)) + + val backgroundColor = ContextCompat.getColor( + context, + R.color.colorPrimary + ) + + val drawable = AppCompatResources + .getDrawable(context, R.drawable.reaction_self_background)!! + .mutate() + DrawableCompat.setTintList( + drawable, + ColorStateList.valueOf(backgroundColor) + ) + nameView.background = drawable + return nameView + } + + companion object { + private const val TRANSLATION_Y_PROPERTY = "translationY" + + // 1333ms to move emoji up 400px with full alpha + private const val DURATION_FULL_ALPHA = 1333L + private const val POSITION_Y_WITH_FULL_ALPHA = -400f + + // 666ms to move emoji up 200px while decreasing alpha + private const val DURATION_DECREASING_ALPHA = 666L + private const val POSITION_Y_WITH_DECREASING_ALPHA = -600f + + private const val ZERO_ALPHA = 0f + private const val ALPHA_PROPERTY = "alpha" + } +} +data class CallReaction( + var emoji: String, + var userName: String +) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index 53bec49d2..4e8fa20a0 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -24,6 +24,7 @@ import android.os.Bundle import android.util.Log import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import androidx.core.content.ContextCompat import autodagger.AutoInjector import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -34,7 +35,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.viewmodels.CallRecordingViewModel +import com.vanniktech.emoji.EmojiTextView import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -56,6 +59,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee viewThemeUtils.platform.themeDialogDark(binding.root) initItemsVisibility() + initEmojiBar() initClickListeners() initObservers() } @@ -68,6 +72,12 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee } private fun initItemsVisibility() { + if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) { + binding.callEmojiBar.visibility = View.VISIBLE + } else { + binding.callEmojiBar.visibility = View.GONE + } + if (callActivity.isAllowedToStartOrStopRecording) { binding.recordCall.visibility = View.VISIBLE } else { @@ -91,6 +101,40 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee } } + private fun initEmojiBar() { + if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) { + binding.advancedCallOptionsTitle.visibility = View.GONE + + val capabilities = callActivity.conversationUser.capabilities + val availableReactions: ArrayList<*> = + capabilities?.spreedCapability?.config!!["call"]!!["supported-reactions"] as ArrayList<*> + + val param = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + 1.0f + ) + + availableReactions.forEach { + val emojiView = EmojiTextView(context) + emojiView.text = it.toString() + emojiView.textSize = 20f + emojiView.layoutParams = param + + emojiView.setOnClickListener { view -> + callActivity.addCallReaction( + (view as EmojiTextView).text.toString(), + callActivity.conversationUser.displayName + ) + dismiss() + } + binding.callEmojiBar.addView(emojiView) + } + } else { + binding.callEmojiBar.visibility = View.GONE + } + } + private fun initObservers() { callActivity.callRecordingViewModel.viewState.observe(this) { state -> when (state) { @@ -102,12 +146,14 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee ) dismiss() } + is CallRecordingViewModel.RecordingStartingState -> { binding.recordCallText.text = context.getText(R.string.record_cancel_start) binding.recordCallIcon.setImageDrawable( ContextCompat.getDrawable(context, R.drawable.record_stop) ) } + is CallRecordingViewModel.RecordingStartedState -> { binding.recordCallText.text = context.getText(R.string.record_stop_description) binding.recordCallIcon.setImageDrawable( @@ -115,12 +161,15 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee ) dismiss() } + is CallRecordingViewModel.RecordingStoppingState -> { binding.recordCallText.text = context.getText(R.string.record_stopping) } + is CallRecordingViewModel.RecordingConfirmStopState -> { binding.recordCallText.text = context.getText(R.string.record_stop_description) } + else -> { Log.e(TAG, "unknown viewState for callRecordingViewModel") } @@ -136,6 +185,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee ) dismiss() } + is RaiseHandViewModel.LoweredHandState -> { binding.raiseHandText.text = context.getText(R.string.raise_hand) binding.raiseHandIcon.setImageDrawable( @@ -143,6 +193,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee ) dismiss() } + else -> {} } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt index cc4658db7..2c1802faf 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt @@ -174,6 +174,16 @@ object CapabilitiesUtilNew { return false } + fun isCallReactionsSupported(user: User?): Boolean { + if (user?.capabilities != null) { + val capabilities = user.capabilities + return capabilities?.spreedCapability?.config?.containsKey("call") == true && + capabilities.spreedCapability!!.config!!["call"] != null && + capabilities.spreedCapability!!.config!!["call"]!!.containsKey("supported-reactions") + } + return false + } + @JvmStatic fun isUnifiedSearchAvailable(user: User): Boolean { return hasSpreedFeatureCapability(user, "unified-search") diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index 9c89324a9..ca26d5484 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -175,22 +175,37 @@ android:gravity="center_vertical" android:orientation="vertical"> - + + + + + + + + + + + Date: Thu, 4 May 2023 15:28:42 +0200 Subject: [PATCH 2/3] show call reactions from other participants Signed-off-by: Marcel Hibbe --- .../main/java/com/nextcloud/talk/activities/CallActivity.java | 1 + .../java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index 6e49c26ed..4b7eff3a1 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -2910,6 +2910,7 @@ public class CallActivity extends CallBaseActivity { @Override public void onReaction(String reaction) { + addCallReaction(reaction, callParticipantModel.getNick()); } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index 4e8fa20a0..36d6b8d37 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -122,6 +122,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee emojiView.layoutParams = param emojiView.setOnClickListener { view -> + // TODO: send signaling messages instead to directly show reaction on own device callActivity.addCallReaction( (view as EmojiTextView).text.toString(), callActivity.conversationUser.displayName From cafb8b649a5c11244fc1bab2399c72eaeef653db Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 4 May 2023 16:20:44 +0200 Subject: [PATCH 3/3] send call reactions Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.java | 14 ++++++++++++-- .../talk/ui/dialog/MoreCallActionsDialog.kt | 6 +----- .../talk/webrtc/PeerConnectionWrapper.java | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index 4b7eff3a1..dc70807e4 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -513,6 +513,16 @@ public class CallActivity extends CallBaseActivity { reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils); } + public void sendReaction(String emoji) { + addReactionForAnimation(emoji, conversationUser.getDisplayName()); + + if (isConnectionEstablished() && peerConnectionWrapperList != null) { + for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) { + peerConnectionWrapper.sendReaction(emoji); + } + } + } + @Override public void onStart() { super.onStart(); @@ -2730,7 +2740,7 @@ public class CallActivity extends CallBaseActivity { } } - public void addCallReaction(String emoji, String displayName) { + public void addReactionForAnimation(String emoji, String displayName) { reactionAnimator.addReaction(emoji, displayName); } @@ -2910,7 +2920,7 @@ public class CallActivity extends CallBaseActivity { @Override public void onReaction(String reaction) { - addCallReaction(reaction, callParticipantModel.getNick()); + addReactionForAnimation(reaction, callParticipantModel.getNick()); } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index 36d6b8d37..f21543272 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -122,11 +122,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee emojiView.layoutParams = param emojiView.setOnClickListener { view -> - // TODO: send signaling messages instead to directly show reaction on own device - callActivity.addCallReaction( - (view as EmojiTextView).text.toString(), - callActivity.conversationUser.displayName - ) + callActivity.sendReaction((view as EmojiTextView).text.toString()) dismiss() } binding.callEmojiBar.addView(emojiView) diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java index 6b93ffc03..43ddf4a87 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java @@ -204,6 +204,20 @@ public class PeerConnectionWrapper { signalingMessageSender.send(ncSignalingMessage); } + public void sendReaction(String emoji) { + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setReaction(emoji); + ncMessagePayload.setTimestamp(System.currentTimeMillis()); + + NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); + ncSignalingMessage.setTo(sessionId); + ncSignalingMessage.setType("reaction"); + ncSignalingMessage.setPayload(ncMessagePayload); + ncSignalingMessage.setRoomType(videoStreamType); + + signalingMessageSender.send(ncSignalingMessage); + } + /** * Adds a listener for data channel messages. *