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

Merge pull request #3075 from nextcloud/feature/3055/alignedTypingIndicator

Align typing indicator to new concept
Marcel Hibbe 2 жил өмнө
parent
commit
87a4de7f5b

+ 72 - 35
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -159,8 +159,8 @@ import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.repositories.reactions.ReactionsRepository
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
 import com.nextcloud.talk.signaling.SignalingMessageReceiver
-import com.nextcloud.talk.translate.ui.TranslateActivity
 import com.nextcloud.talk.signaling.SignalingMessageSender
+import com.nextcloud.talk.translate.ui.TranslateActivity
 import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@@ -319,7 +319,8 @@ class ChatActivity :
     }
 
     var typingTimer: CountDownTimer? = null
-    val typingParticipants = HashMap<String, String>()
+    var typedWhileTypingTimerIsRunning: Boolean = false
+    val typingParticipants = HashMap<String, TypingParticipant>()
 
     private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
         override fun onSwitchTo(token: String?) {
@@ -334,23 +335,38 @@ class ChatActivity :
     }
 
     private val conversationMessageListener = object : SignalingMessageReceiver.ConversationMessageListener {
-        override fun onStartTyping(session: String) {
-            if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) {
-                var name = webSocketInstance?.getDisplayNameForSession(session)
+        override fun onStartTyping(userId: String?, session: String?) {
+            val userIdOrGuestSession = userId ?: session
+
+            if (isTypingStatusEnabled() && conversationUser?.userId != userIdOrGuestSession) {
+                var displayName = webSocketInstance?.getDisplayNameForSession(session)
+
+                if (displayName != null && !typingParticipants.contains(userIdOrGuestSession)) {
+                    if (displayName == "") {
+                        displayName = context.resources?.getString(R.string.nc_guest)!!
+                    }
+
+                    runOnUiThread {
+                        val typingParticipant = TypingParticipant(userIdOrGuestSession!!, displayName) {
+                            typingParticipants.remove(userIdOrGuestSession)
+                            updateTypingIndicator()
+                        }
 
-                if (name != null && !typingParticipants.contains(session)) {
-                    if (name == "") {
-                        name = context.resources?.getString(R.string.nc_guest)!!
+                        typingParticipants[userIdOrGuestSession] = typingParticipant
+                        updateTypingIndicator()
                     }
-                    typingParticipants[session] = name
-                    updateTypingIndicator()
+                } else if (typingParticipants.contains(userIdOrGuestSession)) {
+                    typingParticipants[userIdOrGuestSession]?.restartTimer()
                 }
             }
         }
 
-        override fun onStopTyping(session: String) {
-            if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) {
-                typingParticipants.remove(session)
+        override fun onStopTyping(userId: String?, session: String?) {
+            val userIdOrGuestSession = userId ?: session
+
+            if (isTypingStatusEnabled() && conversationUser?.userId != userId) {
+                typingParticipants[userIdOrGuestSession]?.cancelTimer()
+                typingParticipants.remove(userIdOrGuestSession)
                 updateTypingIndicator()
             }
         }
@@ -544,7 +560,7 @@ class ChatActivity :
 
             @Suppress("Detekt.TooGenericExceptionCaught")
             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                sendStartTypingMessage()
+                updateOwnTypingStatus(s)
 
                 if (s.length >= lengthFilter) {
                     binding?.messageInputView?.inputEditText?.error = String.format(
@@ -922,7 +938,11 @@ class ChatActivity :
             return DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH)
         }
 
-        val participantNames = ArrayList(typingParticipants.values)
+        val participantNames = ArrayList<String>()
+
+        for (typingParticipant in typingParticipants.values) {
+            participantNames.add(typingParticipant.name)
+        }
 
         val typingString: SpannableStringBuilder
         when (typingParticipants.size) {
@@ -998,42 +1018,51 @@ class ChatActivity :
         }
     }
 
-    fun sendStartTypingMessage() {
-        if (webSocketInstance == null) {
-            return
+    fun updateOwnTypingStatus(typedText: CharSequence) {
+        fun sendStartTypingSignalingMessage() {
+            for ((sessionId, participant) in webSocketInstance?.getUserMap()!!) {
+                val ncSignalingMessage = NCSignalingMessage()
+                ncSignalingMessage.to = sessionId
+                ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE
+                signalingMessageSender!!.send(ncSignalingMessage)
+            }
         }
 
-        if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) {
-            if (typingTimer == null) {
-                for ((sessionId, participant) in webSocketInstance?.getUserMap()!!) {
-                    val ncSignalingMessage = NCSignalingMessage()
-                    ncSignalingMessage.to = sessionId
-                    ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE
-                    signalingMessageSender!!.send(ncSignalingMessage)
-                }
+        if (isTypingStatusEnabled()) {
+            if (typedText.isEmpty()) {
+                sendStopTypingMessage()
+            } else if (typingTimer == null) {
+                sendStartTypingSignalingMessage()
 
                 typingTimer = object : CountDownTimer(
-                    TYPING_DURATION_BEFORE_SENDING_STOP,
-                    TYPING_DURATION_BEFORE_SENDING_STOP
+                    TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE,
+                    TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE
                 ) {
                     override fun onTick(millisUntilFinished: Long) {
-                        // unused atm
+                        // unused
                     }
 
                     override fun onFinish() {
-                        sendStopTypingMessage()
+                        if (typedWhileTypingTimerIsRunning) {
+                            sendStartTypingSignalingMessage()
+                            cancel()
+                            start()
+                            typedWhileTypingTimerIsRunning = false
+                        } else {
+                            sendStopTypingMessage()
+                        }
                     }
                 }.start()
             } else {
-                typingTimer?.cancel()
-                typingTimer?.start()
+                typedWhileTypingTimerIsRunning = true
             }
         }
     }
 
-    fun sendStopTypingMessage() {
-        if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) {
+    private fun sendStopTypingMessage() {
+        if (isTypingStatusEnabled()) {
             typingTimer = null
+            typedWhileTypingTimerIsRunning = false
 
             for ((sessionId, participant) in webSocketInstance?.getUserMap()!!) {
                 val ncSignalingMessage = NCSignalingMessage()
@@ -1044,6 +1073,11 @@ class ChatActivity :
         }
     }
 
+    private fun isTypingStatusEnabled(): Boolean {
+        return webSocketInstance != null &&
+            !CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)
+    }
+
     private fun getRoomInfo() {
         logConversationInfos("getRoomInfo")
 
@@ -2347,6 +2381,8 @@ class ChatActivity :
                     Log.d(TAG, "leaveRoom - leaveRoom - got response: $startNanoTime")
                     logConversationInfos("leaveRoom#onNext")
 
+                    sendStopTypingMessage()
+
                     checkingLobbyStatus = false
 
                     if (getRoomInfoTimerHandler != null) {
@@ -3810,7 +3846,8 @@ class ChatActivity :
         private const val COMMA = ", "
         private const val TYPING_INDICATOR_ANIMATION_DURATION = 200L
         private const val TYPING_INDICATOR_MAX_NAME_LENGTH = 14
-        private const val TYPING_DURATION_BEFORE_SENDING_STOP = 4000L
+        private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L
+        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"
     }

+ 59 - 0
app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt

@@ -0,0 +1,59 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021-2022 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.chat
+
+import android.os.CountDownTimer
+
+class TypingParticipant(val userId: String, val name: String, val funToCallWhenTimeIsUp: (userId: String) -> Unit) {
+    var timer: CountDownTimer? = null
+
+    init {
+        startTimer()
+    }
+
+    private fun startTimer() {
+        timer = object : CountDownTimer(
+            TYPING_DURATION_TO_HIDE_TYPING_MESSAGE,
+            TYPING_DURATION_TO_HIDE_TYPING_MESSAGE
+        ) {
+            override fun onTick(millisUntilFinished: Long) {
+                // unused
+            }
+
+            override fun onFinish() {
+                funToCallWhenTimeIsUp(userId)
+            }
+        }.start()
+    }
+
+    fun restartTimer() {
+        timer?.cancel()
+        timer?.start()
+    }
+
+    fun cancelTimer() {
+        timer?.cancel()
+    }
+
+    companion object {
+        private const val TYPING_DURATION_TO_HIDE_TYPING_MESSAGE = 15000L
+    }
+}

+ 25 - 6
app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt

@@ -629,6 +629,7 @@ class SettingsActivity : BaseActivity() {
                     PorterDuff.Mode.SRC_IN
                 )
             }
+
             CapabilitiesUtilNew.isServerAlmostEOL(currentUser!!) -> {
                 binding.serverAgeWarningText.setTextColor(
                     ContextCompat.getColor((context), R.color.nc_darkYellow)
@@ -639,6 +640,7 @@ class SettingsActivity : BaseActivity() {
                     PorterDuff.Mode.SRC_IN
                 )
             }
+
             else -> {
                 binding.serverAgeWarningTextCard.visibility = View.GONE
             }
@@ -664,17 +666,31 @@ class SettingsActivity : BaseActivity() {
             binding.settingsReadPrivacy.visibility = View.GONE
         }
 
-        if (CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)) {
-            (binding.settingsTypingStatus.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
-                !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!)
-        } else {
-            binding.settingsTypingStatus.visibility = View.GONE
-        }
+        setupTypingStatusSetting()
 
         (binding.settingsPhoneBookIntegration.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
             appPreferences.isPhoneBookIntegrationEnabled
     }
 
+    private fun setupTypingStatusSetting() {
+        if (currentUser!!.externalSignalingServer?.externalSignalingServer?.isNotEmpty() == true) {
+            binding.settingsTypingStatusOnlyWithHpb.visibility = View.GONE
+
+            if (CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)) {
+                (binding.settingsTypingStatus.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
+                    !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!)
+            } else {
+                binding.settingsTypingStatus.visibility = View.GONE
+            }
+        } else {
+            (binding.settingsTypingStatus.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked = false
+            binding.settingsTypingStatusOnlyWithHpb.visibility = View.VISIBLE
+            binding.settingsTypingStatus.isEnabled = false
+            binding.settingsTypingStatusOnlyWithHpb.alpha = DISABLED_ALPHA
+            binding.settingsTypingStatus.alpha = DISABLED_ALPHA
+        }
+    }
+
     private fun setupScreenLockSetting() {
         val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
         if (keyguardManager.isKeyguardSecure) {
@@ -846,10 +862,13 @@ class SettingsActivity : BaseActivity() {
                 when (newValue) {
                     "HTTP" ->
                         binding.settingsProxyPortEdit.value = "3128"
+
                     "DIRECT" ->
                         binding.settingsProxyPortEdit.value = "8080"
+
                     "SOCKS" ->
                         binding.settingsProxyPortEdit.value = "1080"
+
                     else -> {
                     }
                 }

+ 4 - 4
app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt

@@ -36,15 +36,15 @@ internal class ConversationMessageNotifier {
     }
 
     @Synchronized
-    fun notifyStartTyping(sessionId: String?) {
+    fun notifyStartTyping(userId: String?, sessionId: String?) {
         for (listener in ArrayList(conversationMessageListeners)) {
-            listener.onStartTyping(sessionId)
+            listener.onStartTyping(userId, sessionId)
         }
     }
 
-    fun notifyStopTyping(sessionId: String?) {
+    fun notifyStopTyping(userId: String?, sessionId: String?) {
         for (listener in ArrayList(conversationMessageListeners)) {
-            listener.onStopTyping(sessionId)
+            listener.onStopTyping(userId, sessionId)
         }
     }
 }

+ 23 - 10
app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java

@@ -24,6 +24,7 @@ import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
 import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
 import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
+import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -169,8 +170,8 @@ public abstract class SignalingMessageReceiver {
      * Listener for conversation messages.
      */
     public interface ConversationMessageListener {
-        void onStartTyping(String session);
-        void onStopTyping(String session);
+        void onStartTyping(String userId, String session);
+        void onStopTyping(String userId,String session);
     }
 
     /**
@@ -515,6 +516,26 @@ public abstract class SignalingMessageReceiver {
         return participant;
     }
 
+    protected void processCallWebSocketMessage(CallWebSocketMessage callWebSocketMessage) {
+
+        NCSignalingMessage signalingMessage = callWebSocketMessage.getNcSignalingMessage();
+
+        if (callWebSocketMessage.getSenderWebSocketMessage() != null && signalingMessage != null) {
+            String type = signalingMessage.getType();
+
+            String userId = callWebSocketMessage.getSenderWebSocketMessage().getUserid();
+            String sessionId = signalingMessage.getFrom();
+
+            if ("startedTyping".equals(type)) {
+                conversationMessageNotifier.notifyStartTyping(userId, sessionId);
+            }
+
+            if ("stoppedTyping".equals(type)) {
+                conversationMessageNotifier.notifyStopTyping(userId, sessionId);
+            }
+        }
+    }
+
     protected void processSignalingMessage(NCSignalingMessage signalingMessage) {
         // Note that in the internal signaling server message "data" is the String representation of a JSON
         // object, although it is already decoded when used here.
@@ -581,14 +602,6 @@ public abstract class SignalingMessageReceiver {
             return;
         }
 
-        if ("startedTyping".equals(type)) {
-            conversationMessageNotifier.notifyStartTyping(sessionId);
-        }
-
-        if ("stoppedTyping".equals(type)) {
-            conversationMessageNotifier.notifyStopTyping(sessionId);
-        }
-
         if ("reaction".equals(type)) {
             // Message schema (external signaling server):
             // {

+ 13 - 5
app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt

@@ -35,6 +35,7 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
 import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage
+import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage
@@ -182,15 +183,16 @@ class WebSocketInstance internal constructor(
     private fun processMessage(text: String) {
         val (_, callWebSocketMessage) = LoganSquare.parse(text, CallOverallWebSocketMessage::class.java)
         if (callWebSocketMessage != null) {
-            val ncSignalingMessage = callWebSocketMessage
-                .ncSignalingMessage
+            val ncSignalingMessage = callWebSocketMessage.ncSignalingMessage
+
             if (ncSignalingMessage != null &&
                 TextUtils.isEmpty(ncSignalingMessage.from) &&
                 callWebSocketMessage.senderWebSocketMessage != null
             ) {
                 ncSignalingMessage.from = callWebSocketMessage.senderWebSocketMessage!!.sessionId
             }
-            signalingMessageReceiver.process(ncSignalingMessage)
+
+            signalingMessageReceiver.process(callWebSocketMessage)
         }
     }
 
@@ -453,8 +455,14 @@ class WebSocketInstance internal constructor(
             processEvent(eventMap)
         }
 
-        fun process(message: NCSignalingMessage?) {
-            processSignalingMessage(message)
+        fun process(message: CallWebSocketMessage?) {
+            if (message?.ncSignalingMessage?.type == "startedTyping" ||
+                message?.ncSignalingMessage?.type == "stoppedTyping"
+            ) {
+                processCallWebSocketMessage(message)
+            } else {
+                processSignalingMessage(message?.ncSignalingMessage)
+            }
         }
     }
 

+ 10 - 0
app/src/main/res/layout/activity_settings.xml

@@ -272,6 +272,16 @@
             apc:mp_key="@string/nc_settings_read_privacy_key"
             apc:mp_summary="@string/nc_settings_typing_status_desc"
             apc:mp_title="@string/nc_settings_typing_status_title" />
+
+        <TextView
+            android:id="@+id/settings_typing_status_only_with_hpb"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:textColor="@color/disabled_text"
+            android:text="@string/nc_settings_typing_status_hpb_description">
+        </TextView>
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
     <com.yarolegovich.mp.MaterialPreferenceCategory

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -153,6 +153,8 @@ How to translate with transifex:
     <string name="nc_settings_read_privacy_title">Read status</string>
     <string name="nc_settings_typing_status_desc">Share my typing-status and show the typing-status of others</string>
     <string name="nc_settings_typing_status_title">Typing status</string>
+    <string name="nc_settings_typing_status_hpb_description">Typing status is only available when using a high
+        performance backend (HPB)</string>
 
     <string name="nc_screen_lock_timeout_30">30 seconds</string>
     <string name="nc_screen_lock_timeout_60">1 minute</string>