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 5f7c67410..49ba7cf37 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/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() + var typedWhileTypingTimerIsRunning: Boolean = false + val typingParticipants = HashMap() 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 (name != null && !typingParticipants.contains(session)) { - if (name == "") { - name = context.resources?.getString(R.string.nc_guest)!! + 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)!! } - typingParticipants[session] = name - updateTypingIndicator() + + runOnUiThread { + val typingParticipant = TypingParticipant(userIdOrGuestSession!!, displayName) { + typingParticipants.remove(userIdOrGuestSession) + updateTypingIndicator() + } + + typingParticipants[userIdOrGuestSession] = typingParticipant + 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() + + 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" } diff --git a/app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt b/app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt new file mode 100644 index 000000000..a29a13fdc --- /dev/null +++ b/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 + * + * 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.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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index 55fd82bbf..23f0ddb40 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/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(R.id.mp_checkable) as Checkable).isChecked = - !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!) - } else { - binding.settingsTypingStatus.visibility = View.GONE - } + setupTypingStatusSetting() (binding.settingsPhoneBookIntegration.findViewById(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(R.id.mp_checkable) as Checkable).isChecked = + !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!) + } else { + binding.settingsTypingStatus.visibility = View.GONE + } + } else { + (binding.settingsTypingStatus.findViewById(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 -> { } } diff --git a/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt b/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt index 6b8fac543..e1207f52e 100644 --- a/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt +++ b/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) } } } diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java index 1455b1ec5..5a2ffe4bc 100644 --- a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java +++ b/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): // { diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt index b057a638a..b50859f7c 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt +++ b/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) + } } } diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 7c5c4890a..953a97166 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/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" /> + + + Read status Share my typing-status and show the typing-status of others Typing status + Typing status is only available when using a high + performance backend (HPB) 30 seconds 1 minute