manual backport of PR #4598 (TooManyRequestsException)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-01-14 15:38:47 +01:00
parent 237b6fafac
commit 36f4c4f497
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
9 changed files with 71 additions and 73 deletions

View File

@ -180,7 +180,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
).first() ).first()
} }
parentChatMessage!!.activeUser = message.activeUser parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -207,7 +207,6 @@ class OutcomingTextMessageViewHolder(itemView: View) :
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.setOnClickListener { binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage) chatActivity.jumpToQuotedMessage(parentChatMessage)
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -191,7 +191,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -428,7 +427,7 @@ class ChatActivity :
this.lifecycleScope.launch { this.lifecycleScope.launch {
delay(DELAY_TO_SHOW_PROGRESS_BAR) delay(DELAY_TO_SHOW_PROGRESS_BAR)
if (adapter?.isEmpty == true && networkMonitor.isOnline.first()) { if (adapter?.isEmpty == true && networkMonitor.isOnline.value) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
} }
} }
@ -923,7 +922,7 @@ class ChatActivity :
chatViewModel.getGeneralUIFlow.onEach { key -> chatViewModel.getGeneralUIFlow.onEach { key ->
when (key) { when (key) {
NO_OFFLINE_MESSAGES_FOUND -> { NO_OFFLINE_MESSAGES_FOUND -> {
if (networkMonitor.isOnline.first().not()) { if (networkMonitor.isOnline.value.not()) {
binding.offline.root.visibility = View.VISIBLE binding.offline.root.visibility = View.VISIBLE
} }
} }

View File

@ -26,7 +26,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.chat.ChatOverall import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -45,8 +44,7 @@ class OfflineFirstChatRepository @Inject constructor(
private val chatDao: ChatMessagesDao, private val chatDao: ChatMessagesDao,
private val chatBlocksDao: ChatBlocksDao, private val chatBlocksDao: ChatBlocksDao,
private val network: ChatNetworkDataSource, private val network: ChatNetworkDataSource,
private val datastore: AppPreferences, private val networkMonitor: NetworkMonitor,
private val monitor: NetworkMonitor,
private val userProvider: CurrentUserProviderNew private val userProvider: CurrentUserProviderNew
) : ChatMessageRepository { ) : ChatMessageRepository {
@ -71,8 +69,7 @@ class OfflineFirstChatRepository @Inject constructor(
> >
> = MutableSharedFlow() > = MutableSharedFlow()
override val updateMessageFlow: override val updateMessageFlow: Flow<ChatMessage>
Flow<ChatMessage>
get() = _updateMessageFlow get() = _updateMessageFlow
private val _updateMessageFlow: private val _updateMessageFlow:
@ -85,8 +82,7 @@ class OfflineFirstChatRepository @Inject constructor(
private val _lastCommonReadFlow: private val _lastCommonReadFlow:
MutableSharedFlow<Int> = MutableSharedFlow() MutableSharedFlow<Int> = MutableSharedFlow()
override val lastReadMessageFlow: override val lastReadMessageFlow: Flow<Int>
Flow<Int>
get() = _lastReadMessageFlow get() = _lastReadMessageFlow
private val _lastReadMessageFlow: private val _lastReadMessageFlow:
@ -278,7 +274,7 @@ class OfflineFirstChatRepository @Inject constructor(
var showUnreadMessagesMarker = true var showUnreadMessagesMarker = true
while (true) { while (true) {
if (!monitor.isOnline.first() || itIsPaused) { if (!networkMonitor.isOnline.value || itIsPaused) {
Thread.sleep(HALF_SECOND) Thread.sleep(HALF_SECOND)
} else { } else {
// sync database with server // sync database with server
@ -469,7 +465,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? { private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? {
if (!monitor.isOnline.first()) { if (!networkMonitor.isOnline.value) {
Log.d(TAG, "Device is offline, can't load chat messages from server") Log.d(TAG, "Device is offline, can't load chat messages from server")
return null return null
} }

View File

@ -138,7 +138,6 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.lang3.builder.CompareToBuilder import org.apache.commons.lang3.builder.CompareToBuilder
@ -1359,7 +1358,7 @@ class ConversationsListActivity :
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
this.lifecycleScope.launch { this.lifecycleScope.launch {
if (showShareToScreen || !networkMonitor.isOnline.first()) { if (showShareToScreen || !networkMonitor.isOnline.value) {
Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.") Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.")
} else { } else {
val clickedItem: Any? = adapter!!.getItem(position) val clickedItem: Any? = adapter!!.getItem(position)

View File

@ -10,7 +10,6 @@ package com.nextcloud.talk.conversationlist.data.network
import android.util.Log import android.util.Log
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.data.database.dao.ConversationsDao import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.mappers.asEntity import com.nextcloud.talk.data.database.mappers.asEntity
@ -40,7 +39,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
private val dao: ConversationsDao, private val dao: ConversationsDao,
private val network: ConversationsNetworkDataSource, private val network: ConversationsNetworkDataSource,
private val chatNetworkDataSource: ChatNetworkDataSource, private val chatNetworkDataSource: ChatNetworkDataSource,
private val monitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val currentUserProviderNew: CurrentUserProviderNew private val currentUserProviderNew: CurrentUserProviderNew
) : OfflineConversationsRepository { ) : OfflineConversationsRepository {
override val roomListFlow: Flow<List<ConversationModel>> override val roomListFlow: Flow<List<ConversationModel>>
@ -59,7 +58,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
val initialConversationModels = getListOfConversations(user.id!!) val initialConversationModels = getListOfConversations(user.id!!)
_roomListFlow.emit(initialConversationModels) _roomListFlow.emit(initialConversationModels)
if (monitor.isOnline.first()) { if (networkMonitor.isOnline.value) {
val conversationEntitiesFromSync = getRoomsFromServer() val conversationEntitiesFromSync = getRoomsFromServer()
if (!conversationEntitiesFromSync.isNullOrEmpty()) { if (!conversationEntitiesFromSync.isNullOrEmpty()) {
val conversationModelsFromSync = conversationEntitiesFromSync.map(ConversationEntity::asModel) val conversationModelsFromSync = conversationEntitiesFromSync.map(ConversationEntity::asModel)
@ -106,8 +105,8 @@ class OfflineFirstConversationsRepository @Inject constructor(
private suspend fun getRoomsFromServer(): List<ConversationEntity>? { private suspend fun getRoomsFromServer(): List<ConversationEntity>? {
var conversationsFromSync: List<ConversationEntity>? = null var conversationsFromSync: List<ConversationEntity>? = null
if (!monitor.isOnline.first()) { if (!networkMonitor.isOnline.value) {
Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server") Log.d(TAG, "Device is offline, can't load conversations from server")
return null return null
} }

View File

@ -61,7 +61,6 @@ import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -180,7 +179,6 @@ class RepositoryModule {
chatMessagesDao: ChatMessagesDao, chatMessagesDao: ChatMessagesDao,
chatBlocksDao: ChatBlocksDao, chatBlocksDao: ChatBlocksDao,
dataSource: ChatNetworkDataSource, dataSource: ChatNetworkDataSource,
appPreferences: AppPreferences,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
userProvider: CurrentUserProviderNew userProvider: CurrentUserProviderNew
): ChatMessageRepository { ): ChatMessageRepository {
@ -188,7 +186,6 @@ class RepositoryModule {
chatMessagesDao, chatMessagesDao,
chatBlocksDao, chatBlocksDao,
dataSource, dataSource,
appPreferences,
networkMonitor, networkMonitor,
userProvider userProvider
) )

View File

@ -2,13 +2,14 @@
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.data.network package com.nextcloud.talk.data.network
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow
/** /**
* Utility for reporting app connectivity status. * Utility for reporting app connectivity status.
@ -17,7 +18,7 @@ interface NetworkMonitor {
/** /**
* Returns the device's current connectivity status. * Returns the device's current connectivity status.
*/ */
val isOnline: Flow<Boolean> val isOnline: StateFlow<Boolean>
/** /**
* Returns the device's current connectivity status as LiveData for better interop with Java code. * Returns the device's current connectivity status as LiveData for better interop with Java code.

View File

@ -2,6 +2,7 @@
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -11,17 +12,17 @@ import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest.Builder import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -29,49 +30,57 @@ import javax.inject.Singleton
class NetworkMonitorImpl @Inject constructor( class NetworkMonitorImpl @Inject constructor(
private val context: Context private val context: Context
) : NetworkMonitor { ) : NetworkMonitor {
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!
override val isOnlineLiveData: LiveData<Boolean> override val isOnlineLiveData: LiveData<Boolean>
get() = isOnline.asLiveData() get() = isOnline.asLiveData()
override val isOnline: Flow<Boolean> = callbackFlow { override val isOnline: StateFlow<Boolean> get() = _isOnline
val connectivityManager = context.getSystemService<ConnectivityManager>()
if (connectivityManager == null) { private val _isOnline: StateFlow<Boolean> = callbackFlow {
channel.trySend(false) val callback = object : ConnectivityManager.NetworkCallback() {
channel.close() override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
return@callbackFlow super.onCapabilitiesChanged(network, networkCapabilities)
val connected = networkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_VALIDATED
)
trySend(connected)
Log.d(TAG, "Network status changed: $connected")
} }
val networkRequest = Builder() override fun onUnavailable() {
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) super.onUnavailable()
.build() trySend(false)
Log.d(TAG, "Network status: onUnavailable")
val networkCallback = object : ConnectivityManager.NetworkCallback() {
private val networks = mutableSetOf<Network>()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
networks -= network super.onLost(network)
channel.trySend(networks.isNotEmpty()) trySend(false)
Log.d(TAG, "Network status: onLost")
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
trySend(true)
Log.d(TAG, "Network status: onAvailable")
} }
} }
connectivityManager.registerNetworkCallback(networkRequest, networkCallback) connectivityManager.registerDefaultNetworkCallback(callback)
channel.trySend(connectivityManager.isCurrentlyConnected())
awaitClose { awaitClose {
connectivityManager.unregisterNetworkCallback(networkCallback) connectivityManager.unregisterNetworkCallback(callback)
} }
} }.stateIn(
.distinctUntilChanged() CoroutineScope(Dispatchers.IO),
.flowOn(Dispatchers.IO) SharingStarted.WhileSubscribed(COROUTINE_TIMEOUT),
.conflate() false
)
private fun ConnectivityManager.isCurrentlyConnected() = companion object {
activeNetwork private val TAG = NetworkMonitorImpl::class.java.simpleName
?.let(::getNetworkCapabilities) private const val COROUTINE_TIMEOUT = 5000L
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false }
} }

View File

@ -51,7 +51,6 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -134,7 +133,7 @@ class MessageActionsDialog(
initMenuAddToNote( initMenuAddToNote(
!message.isDeleted && !message.isDeleted &&
!ConversationUtils.isNoteToSelfConversation(currentConversation) && !ConversationUtils.isNoteToSelfConversation(currentConversation) &&
networkMonitor.isOnline.first(), networkMonitor.isOnline.value,
state.roomToken state.roomToken
) )
} }
@ -147,16 +146,16 @@ class MessageActionsDialog(
} }
} }
initMenuItems() initMenuItems(networkMonitor.isOnline.value)
} }
private fun initMenuItems() { private fun initMenuItems(isOnline: Boolean) {
this.lifecycleScope.launch { this.lifecycleScope.launch {
initMenuItemTranslate( initMenuItemTranslate(
!message.isDeleted && !message.isDeleted &&
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) && CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) &&
networkMonitor.isOnline.first() isOnline
) )
initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted) initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted)
initMenuReplyToMessage(message.replyable && hasChatPermission) initMenuReplyToMessage(message.replyable && hasChatPermission)
@ -165,30 +164,30 @@ class MessageActionsDialog(
hasUserId(user) && hasUserId(user) &&
hasUserActorId(message) && hasUserActorId(message) &&
currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
networkMonitor.isOnline.first() isOnline
) )
initMenuEditMessage(isMessageEditable) initMenuEditMessage(isMessageEditable)
initMenuDeleteMessage(showMessageDeletionButton && networkMonitor.isOnline.first()) initMenuDeleteMessage(showMessageDeletionButton && isOnline)
initMenuForwardMessage( initMenuForwardMessage(
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
!(message.isDeletedCommentMessage || message.isDeleted) && !(message.isDeletedCommentMessage || message.isDeleted) &&
networkMonitor.isOnline.first() isOnline
) )
initMenuRemindMessage( initMenuRemindMessage(
!message.isDeleted && !message.isDeleted &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) && hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) &&
currentConversation!!.remoteServer.isNullOrEmpty() && currentConversation!!.remoteServer.isNullOrEmpty() &&
networkMonitor.isOnline.first() isOnline
) )
initMenuMarkAsUnread( initMenuMarkAsUnread(
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() && ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
networkMonitor.isOnline.first() isOnline
) )
initMenuShare(messageHasFileAttachment || messageHasRegularText && networkMonitor.isOnline.first()) initMenuShare(messageHasFileAttachment || messageHasRegularText && isOnline)
initMenuItemOpenNcApp( initMenuItemOpenNcApp(
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() && ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() &&
networkMonitor.isOnline.first() isOnline
) )
initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)
} }