Эх сурвалжийг харах

add animated emoji reactions to calls (no signaling yet)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 жил өмнө
parent
commit
c379630610

+ 10 - 1
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<PeerConnection.IceServer> 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.

+ 184 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 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<CallReaction> = 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
+)

+ 51 - 0
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 -> {}
             }
         }

+ 10 - 0
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")

+ 31 - 16
app/src/main/res/layout/call_activity.xml

@@ -175,22 +175,37 @@
         android:gravity="center_vertical"
         android:orientation="vertical">
 
-        <com.google.android.material.floatingactionbutton.FloatingActionButton
-            android:id="@+id/lower_hand_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="end"
-            android:layout_marginEnd="@dimen/standard_margin"
-            android:layout_marginBottom="@dimen/standard_half_margin"
-            android:contentDescription="@string/lower_hand"
-            android:visibility="gone"
-            app:backgroundTint="@color/call_buttons_background"
-            app:borderWidth="0dp"
-            app:fabCustomSize="40dp"
-            app:shapeAppearance="@style/fab_3_rounded"
-            app:srcCompat="@drawable/ic_baseline_do_not_touch_24"
-            app:tint="@color/white"
-            tools:visibility="visible" />
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="300dp">
+
+            <RelativeLayout
+                android:id="@+id/reaction_animation_wrapper"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_marginStart="50dp"
+                android:layout_marginBottom="50dp">
+            </RelativeLayout>
+
+            <com.google.android.material.floatingactionbutton.FloatingActionButton
+                android:id="@+id/lower_hand_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentBottom="true"
+                android:layout_marginEnd="@dimen/standard_margin"
+                android:layout_marginBottom="@dimen/standard_half_margin"
+                android:contentDescription="@string/lower_hand"
+                android:visibility="gone"
+                app:backgroundTint="@color/call_buttons_background"
+                app:borderWidth="0dp"
+                app:fabCustomSize="40dp"
+                app:shapeAppearance="@style/fab_3_rounded"
+                app:srcCompat="@drawable/ic_baseline_do_not_touch_24"
+                app:tint="@color/white"
+                tools:visibility="visible" />
+
+        </RelativeLayout>
 
         <com.google.android.flexbox.FlexboxLayout
             android:id="@+id/callControls"

+ 14 - 0
app/src/main/res/layout/dialog_more_call_actions.xml

@@ -27,7 +27,21 @@
     android:orientation="vertical"
     android:paddingBottom="@dimen/standard_half_padding">
 
+    <LinearLayout
+        android:id="@+id/call_emoji_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:layout_marginBottom="@dimen/standard_half_margin"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:weightSum="10">
+    </LinearLayout>
+
     <TextView
+        android:id="@+id/advanced_call_options_title"
         android:layout_width="wrap_content"
         android:layout_height="@dimen/bottom_sheet_item_height"
         android:gravity="start|center_vertical"