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

Align typing indicator to new concept
This commit is contained in:
Marcel Hibbe 2023-06-02 16:08:15 +02:00 committed by GitHub
commit 87a4de7f5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 208 additions and 60 deletions

View file

@ -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 (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<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"
}

View file

@ -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
}
}

View file

@ -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 -> {
}
}

View file

@ -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)
}
}
}

View file

@ -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):
// {

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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>