mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-22 04:55:29 +03:00
Merge pull request #3075 from nextcloud/feature/3055/alignedTypingIndicator
Align typing indicator to new concept
This commit is contained in:
commit
87a4de7f5b
8 changed files with 208 additions and 60 deletions
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 -> {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
// {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue