Merge pull request #4415 from nextcloud/issue-4376-hide-features-in-offline-mode

Disabling/Hiding features when offline
This commit is contained in:
Marcel Hibbe 2024-11-21 10:32:42 +01:00 committed by GitHub
commit 6637e8c9d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 141 additions and 105 deletions

View file

@ -113,6 +113,7 @@ import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityChatBinding
import com.nextcloud.talk.events.UserMentionClickEvent
@ -237,6 +238,9 @@ class ChatActivity :
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
lateinit var networkMonitor: NetworkMonitor
lateinit var chatViewModel: ChatViewModel
lateinit var messageInputViewModel: MessageInputViewModel
@ -2916,6 +2920,14 @@ class ChatActivity :
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
this.lifecycleScope.launch {
networkMonitor.isOnline.onEach { isOnline ->
conversationVoiceCallMenuItem?.isVisible = isOnline
searchItem?.isVisible = isOnline
conversationVideoMenuItem?.isVisible = isOnline
}.collect()
}
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
Handler().post {
findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {

View file

@ -280,17 +280,15 @@ class MessageInputFragment : Fragment() {
})
}
binding.fragmentMessageInputView.attachmentButton.isEnabled = true
binding.fragmentMessageInputView.recordAudioButton.isEnabled = true
binding.fragmentMessageInputView.messageInput.isEnabled = true
binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
binding.fragmentMessageInputView.recordAudioButton.visibility = View.VISIBLE
} else {
binding.fragmentMessageInputView.attachmentButton.visibility = View.INVISIBLE
binding.fragmentMessageInputView.recordAudioButton.visibility = View.INVISIBLE
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
}
}

View file

@ -21,8 +21,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Bundle
@ -43,6 +41,7 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -136,6 +135,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.apache.commons.lang3.builder.CompareToBuilder
@ -307,6 +307,7 @@ class ConversationsListActivity :
this.lifecycleScope.launch {
networkMonitor.isOnline.onEach { isOnline ->
showNetworkErrorDialog(!isOnline)
handleUI(isOnline)
}.collect()
}
@ -891,15 +892,10 @@ class ConversationsListActivity :
binding.chatListConnectionLost.visibility = if (show) View.VISIBLE else View.GONE
}
@Suppress("ReturnCount")
private fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
private fun handleUI(show: Boolean) {
binding.floatingActionButton.isEnabled = show
binding.searchText.isEnabled = show
binding.searchText.isVisible = show
}
private fun sortConversations(conversationItems: MutableList<AbstractFlexibleItem<*>>) {
@ -1345,18 +1341,20 @@ class ConversationsListActivity :
}
override fun onItemLongClick(position: Int) {
if (showShareToScreen) {
Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.")
} else {
val clickedItem: Any? = adapter!!.getItem(position)
if (clickedItem != null && clickedItem is ConversationItem) {
val conversation = clickedItem.model
conversationsListBottomDialog = ConversationsListBottomDialog(
this,
userManager.currentUser.blockingGet(),
conversation
)
conversationsListBottomDialog!!.show()
this.lifecycleScope.launch {
if (showShareToScreen || !networkMonitor.isOnline.first()) {
Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.")
} else {
val clickedItem: Any? = adapter!!.getItem(position)
if (clickedItem != null && clickedItem is ConversationItem) {
val conversation = clickedItem.model
conversationsListBottomDialog = ConversationsListBottomDialog(
this@ConversationsListActivity,
userManager.currentUser.blockingGet(),
conversation
)
conversationsListBottomDialog!!.show()
}
}
}
}

View file

@ -7,11 +7,20 @@
package com.nextcloud.talk.data.network
import androidx.lifecycle.LiveData
import kotlinx.coroutines.flow.Flow
/**
* Utility for reporting app connectivity status.
*/
interface NetworkMonitor {
/**
* Returns the device's current connectivity status.
*/
val isOnline: Flow<Boolean>
/**
* Returns the device's current connectivity status as LiveData for better interop with Java code.
*/
val isOnlineLiveData: LiveData<Boolean>
}

View file

@ -11,10 +11,10 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkRequest.Builder
import androidx.core.content.getSystemService
import androidx.core.os.trace
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@ -29,49 +29,41 @@ import javax.inject.Singleton
class NetworkMonitorImpl @Inject constructor(
private val context: Context
) : NetworkMonitor {
override val isOnlineLiveData: LiveData<Boolean>
get() = isOnline.asLiveData()
override val isOnline: Flow<Boolean> = callbackFlow {
trace("NetworkMonitorImpl.callbackFlow") {
val connectivityManager = context.getSystemService<ConnectivityManager>()
if (connectivityManager == null) {
channel.trySend(false)
channel.close()
return@callbackFlow
val connectivityManager = context.getSystemService<ConnectivityManager>()
if (connectivityManager == null) {
channel.trySend(false)
channel.close()
return@callbackFlow
}
val networkRequest = Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
val networkCallback = object : ConnectivityManager.NetworkCallback() {
private val networks = mutableSetOf<Network>()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
}
/**
* The callback's methods are invoked on changes to *any* network matching the [NetworkRequest],
* not just the active network. So we can simply track the presence (or absence) of such [Network].
*/
val callback = object : ConnectivityManager.NetworkCallback() {
private val networks = mutableSetOf<Network>()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
}
override fun onLost(network: Network) {
networks -= network
channel.trySend(networks.isNotEmpty())
}
override fun onLost(network: Network) {
networks -= network
channel.trySend(networks.isNotEmpty())
}
}
trace("NetworkMonitorImpl.registerNetworkCallback") {
val request = Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
}
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
/**
* Sends the latest connectivity status to the underlying channel.
*/
channel.trySend(connectivityManager.isCurrentlyConnected())
channel.trySend(connectivityManager.isCurrentlyConnected())
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
awaitClose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
.distinctUntilChanged()

View file

@ -24,6 +24,7 @@ import com.nextcloud.talk.adapters.items.AdvancedUserItem;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.conversationlist.ConversationsListActivity;
import com.nextcloud.talk.data.network.NetworkMonitor;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.databinding.DialogChooseAccountBinding;
import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
@ -83,6 +84,9 @@ public class ChooseAccountDialogFragment extends DialogFragment {
@Inject
InvitationsRepository invitationsRepository;
@Inject
NetworkMonitor networkMonitor;
private DialogChooseAccountBinding binding;
private View dialogView;
@ -111,7 +115,7 @@ public class ChooseAccountDialogFragment extends DialogFragment {
setupCurrentUser(user);
setupListeners();
setupAdapter();
prepareViews();
networkMonitor.isOnlineLiveData().observe(this, this::prepareViews);
}
private void setupCurrentUser(User user) {
@ -309,13 +313,17 @@ public class ChooseAccountDialogFragment extends DialogFragment {
}
}
private void prepareViews() {
private void prepareViews(Boolean isOnline) {
if (getActivity() != null) {
LinearLayoutManager layoutManager = new SmoothScrollLinearLayoutManager(getActivity());
binding.accountsList.setLayoutManager(layoutManager);
}
binding.accountsList.setHasFixedSize(true);
binding.accountsList.setAdapter(adapter);
if (!isOnline) {
binding.addAccount.setVisibility(View.GONE);
}
}
public static ChooseAccountDialogFragment newInstance() {

View file

@ -17,6 +17,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -25,6 +26,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogMessageActionsBinding
import com.nextcloud.talk.models.domain.ConversationModel
@ -49,6 +51,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.first
import kotlinx.coroutines.launch
import java.util.Date
import javax.inject.Inject
@ -72,6 +76,9 @@ class MessageActionsDialog(
@Inject
lateinit var dateUtils: DateUtils
@Inject
lateinit var networkMonitor: NetworkMonitor
private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
private lateinit var popup: EmojiPopup
@ -123,10 +130,14 @@ class MessageActionsDialog(
chatActivity.chatViewModel.getNoteToSelfAvailability.observe(this) { state ->
when (state) {
is ChatViewModel.NoteToSelfAvailableState -> {
initMenuAddToNote(
!message.isDeleted && !ConversationUtils.isNoteToSelfConversation(currentConversation),
state.roomToken
)
this.lifecycleScope.launch {
initMenuAddToNote(
!message.isDeleted &&
!ConversationUtils.isNoteToSelfConversation(currentConversation) &&
networkMonitor.isOnline.first(),
state.roomToken
)
}
}
else -> {
initMenuAddToNote(
@ -136,39 +147,47 @@ class MessageActionsDialog(
}
}
initMenuItemTranslate(
!message.isDeleted &&
this.lifecycleScope.launch {
initMenuItemTranslate(
!message.isDeleted &&
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) &&
networkMonitor.isOnline.first()
)
initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted)
initMenuReplyToMessage(message.replyable && hasChatPermission)
initMenuReplyPrivately(
message.replyable &&
hasUserId(user) &&
hasUserActorId(message) &&
currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
networkMonitor.isOnline.first()
)
initMenuEditMessage(isMessageEditable)
initMenuDeleteMessage(showMessageDeletionButton && networkMonitor.isOnline.first())
initMenuForwardMessage(
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
CapabilitiesUtil.isTranslationsSupported(spreedCapabilities)
)
initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted)
initMenuReplyToMessage(message.replyable && hasChatPermission)
initMenuReplyPrivately(
message.replyable &&
hasUserId(user) &&
hasUserActorId(message) &&
currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
)
initMenuEditMessage(isMessageEditable)
initMenuDeleteMessage(showMessageDeletionButton)
initMenuForwardMessage(
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
!(message.isDeletedCommentMessage || message.isDeleted)
)
initMenuRemindMessage(
!message.isDeleted &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) &&
currentConversation!!.remoteServer.isNullOrEmpty()
)
initMenuMarkAsUnread(
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType()
)
initMenuShare(messageHasFileAttachment || messageHasRegularText)
initMenuItemOpenNcApp(
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType()
)
initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)
!(message.isDeletedCommentMessage || message.isDeleted) &&
networkMonitor.isOnline.first()
)
initMenuRemindMessage(
!message.isDeleted &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) &&
currentConversation!!.remoteServer.isNullOrEmpty() &&
networkMonitor.isOnline.first()
)
initMenuMarkAsUnread(
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
networkMonitor.isOnline.first()
)
initMenuShare(messageHasFileAttachment || messageHasRegularText && networkMonitor.isOnline.first())
initMenuItemOpenNcApp(
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() &&
networkMonitor.isOnline.first()
)
initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)
}
}
override fun onStart() {