From 569be55395981143e8db8c2b9bd7e0ff0638b994 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Tue, 10 Sep 2024 07:38:12 -0500 Subject: [PATCH 1/2] Implement queued messages for offline support Signed-off-by: rapterjet2004 --- .../talk/chat/MessageInputFragment.kt | 22 ++++++++++++++----- .../chat/viewmodels/MessageInputViewModel.kt | 15 ++++++++++++- .../utils/preferences/AppPreferencesImpl.kt | 22 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index 4a49ea8d6..5bd77d9b1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -73,6 +73,7 @@ import com.nextcloud.talk.utils.text.Spans import com.otaliastudios.autocomplete.Autocomplete import com.stfalcon.chatkit.commons.models.IMessage import com.vanniktech.emoji.EmojiPopup +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -141,6 +142,11 @@ class MessageInputFragment : Fragment() { saveState() } + override fun onResume() { + super.onResume() + chatActivity.messageInputViewModel.restoreMessageQueue(chatActivity.roomToken) + } + override fun onDestroyView() { super.onDestroyView() if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { @@ -178,12 +184,19 @@ class MessageInputFragment : Fragment() { val connectionGained = (!wasOnline && isOnline) wasOnline = !binding.fragmentMessageInputView.isShown Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") - - // FIXME timeout exception - maybe something to do with the room? - // handleMessageQueue(isOnline) + delay(500) + handleMessageQueue(isOnline) handleUI(isOnline, connectionGained) }.collect() } + + chatActivity.messageInputViewModel.messageQueueSizeFlow.observe(viewLifecycleOwner) { size -> + if (size > 0) { + binding.fragmentConnectionLost.text = getString(R.string.connection_lost_queued, size) + } else { + binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued) + } + } } private fun handleUI(isOnline: Boolean, connectionGained: Boolean) { @@ -220,12 +233,9 @@ class MessageInputFragment : Fragment() { binding.fragmentConnectionLost.clearAnimation() binding.fragmentConnectionLost.visibility = View.GONE binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) - binding.fragmentConnectionLost.text = - getString(R.string.connection_lost_sent_messages_are_queued) binding.fragmentConnectionLost.visibility = View.VISIBLE binding.fragmentMessageInputView.attachmentButton.isEnabled = false binding.fragmentMessageInputView.recordAudioButton.isEnabled = false - binding.fragmentMessageInputView.messageInput.isEnabled = false } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt index 4489ca830..a3e641b52 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager import com.nextcloud.talk.chat.data.io.AudioRecorderManager import com.nextcloud.talk.chat.data.io.MediaPlayerManager @@ -26,6 +27,8 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import javax.inject.Inject class MessageInputViewModel @Inject constructor( @@ -51,7 +54,7 @@ class MessageInputViewModel @Inject constructor( ) private var isQueueing: Boolean = false - private val messageQueue: MutableList = mutableListOf() + private var messageQueue: MutableList = mutableListOf() override fun onResume(owner: LifecycleOwner) { super.onResume(owner) @@ -119,6 +122,10 @@ class MessageInputViewModel @Inject constructor( val isVoicePreviewPlaying: LiveData get() = _isVoicePreviewPlaying + private val _messageQueueSizeFlow = MutableStateFlow(messageQueue.size) + val messageQueueSizeFlow: LiveData + get() = _messageQueueSizeFlow.asLiveData() + @Suppress("LongParameterList") fun sendChatMessage( roomToken: String, @@ -132,6 +139,7 @@ class MessageInputViewModel @Inject constructor( if (isQueueing) { messageQueue.add(QueuedMessage(message, displayName, replyTo, sendWithoutNotification)) dataStore.saveMessageQueue(roomToken, messageQueue) + _messageQueueSizeFlow.update { messageQueue.size } return } @@ -259,4 +267,9 @@ class MessageInputViewModel @Inject constructor( fun switchToMessageQueue(shouldQueue: Boolean) { isQueueing = shouldQueue } + + fun restoreMessageQueue(roomToken: String) { + messageQueue = dataStore.getMessageQueue(roomToken) + _messageQueueSizeFlow.tryEmit(messageQueue.size) + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt index 267c86eb4..b1b948bbd 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -484,7 +484,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { var queueStr = "" queue?.let { for (msg in queue) { - val msgStr = "[${msg.message},${msg.replyTo},${msg.displayName},${msg.sendWithoutNotification}]" + val msgStr = "${msg.message},${msg.replyTo},${msg.displayName},${msg.sendWithoutNotification}^" queueStr += msgStr } } @@ -500,18 +500,20 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { val queue: MutableList = mutableListOf() if (queueStr.isEmpty()) return queue - for (msgStr in queueStr.split("]")) { + for (msgStr in queueStr.split("^")) { try { - val msgArray = msgStr.replace("[", "").split(",") - val message = msgArray[MESSAGE_INDEX] - val replyTo = msgArray[REPLY_TO_INDEX].toInt() - val displayName = msgArray[DISPLY_NAME_INDEX] - val silent = msgArray[SILENT_INDEX].toBoolean() + if (msgStr.isNotEmpty()) { + val msgArray = msgStr.split(",") + val message = msgArray[MESSAGE_INDEX] + val replyTo = msgArray[REPLY_TO_INDEX].toInt() + val displayName = msgArray[DISPLY_NAME_INDEX] + val silent = msgArray[SILENT_INDEX].toBoolean() - val qMsg = MessageInputViewModel.QueuedMessage(message, displayName, replyTo, silent) - queue.add(qMsg) + val qMsg = MessageInputViewModel.QueuedMessage(message, displayName, replyTo, silent) + queue.add(qMsg) + } } catch (e: IndexOutOfBoundsException) { - Log.e(TAG, "Message string: $msgStr\n $e") + Log.e(TAG, "Message string: $msgStr\n Queue String: $queueStr \n$e") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13ed2b442..c23acb5fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -802,6 +802,7 @@ How to translate with transifex: Show banned participants Bans list Connection lost - Sent messages are queued + Connection lost - %1$d are queued Connection established Message deleted by you Unban From dc32a4a2f11a060f15f632c94ced258f8999582d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 13 Sep 2024 15:26:22 +0200 Subject: [PATCH 2/2] fix order of queued messages add delay between sending of queued messages to increase the chance they are received on server in the correct order. This is not the best solution though as it blocks the UI a bit so may have to be improved! Signed-off-by: Marcel Hibbe --- .../talk/chat/viewmodels/MessageInputViewModel.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt index a3e641b52..e74ebfc15 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -29,6 +29,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import java.lang.Thread.sleep import javax.inject.Inject class MessageInputViewModel @Inject constructor( @@ -79,9 +80,6 @@ class MessageInputViewModel @Inject constructor( mediaPlayerManager.handleOnStop() } - companion object { - private val TAG = MessageInputViewModel::class.java.simpleName - } val getAudioFocusChange: LiveData get() = audioFocusRequestManager.getManagerState @@ -252,6 +250,7 @@ class MessageInputViewModel @Inject constructor( dataStore.saveMessageQueue(roomToken, null) // empties the queue while (queue.size > 0) { val msg = queue.removeFirst() + sleep(DELAY_BETWEEN_QUEUED_MESSAGES) sendChatMessage( roomToken, credentials, @@ -272,4 +271,9 @@ class MessageInputViewModel @Inject constructor( messageQueue = dataStore.getMessageQueue(roomToken) _messageQueueSizeFlow.tryEmit(messageQueue.size) } + + companion object { + private val TAG = MessageInputViewModel::class.java.simpleName + private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 100 + } }