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

View File

@ -280,17 +280,15 @@ class MessageInputFragment : Fragment() {
}) })
} }
binding.fragmentMessageInputView.attachmentButton.isEnabled = true binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
binding.fragmentMessageInputView.recordAudioButton.isEnabled = true binding.fragmentMessageInputView.recordAudioButton.visibility = View.VISIBLE
binding.fragmentMessageInputView.messageInput.isEnabled = true
} else { } else {
binding.fragmentMessageInputView.attachmentButton.visibility = View.INVISIBLE
binding.fragmentMessageInputView.recordAudioButton.visibility = View.INVISIBLE
binding.fragmentConnectionLost.clearAnimation() binding.fragmentConnectionLost.clearAnimation()
binding.fragmentConnectionLost.visibility = View.GONE binding.fragmentConnectionLost.visibility = View.GONE
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) 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.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.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -43,6 +41,7 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -136,6 +135,7 @@ 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
@ -307,6 +307,7 @@ class ConversationsListActivity :
this.lifecycleScope.launch { this.lifecycleScope.launch {
networkMonitor.isOnline.onEach { isOnline -> networkMonitor.isOnline.onEach { isOnline ->
showNetworkErrorDialog(!isOnline) showNetworkErrorDialog(!isOnline)
handleUI(isOnline)
}.collect() }.collect()
} }
@ -891,15 +892,10 @@ class ConversationsListActivity :
binding.chatListConnectionLost.visibility = if (show) View.VISIBLE else View.GONE binding.chatListConnectionLost.visibility = if (show) View.VISIBLE else View.GONE
} }
@Suppress("ReturnCount") private fun handleUI(show: Boolean) {
private fun isNetworkAvailable(context: Context): Boolean { binding.floatingActionButton.isEnabled = show
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager binding.searchText.isEnabled = show
val network = connectivityManager.activeNetwork ?: return false binding.searchText.isVisible = show
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 sortConversations(conversationItems: MutableList<AbstractFlexibleItem<*>>) { private fun sortConversations(conversationItems: MutableList<AbstractFlexibleItem<*>>) {
@ -1345,18 +1341,20 @@ class ConversationsListActivity :
} }
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
if (showShareToScreen) { this.lifecycleScope.launch {
Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.") if (showShareToScreen || !networkMonitor.isOnline.first()) {
} else { Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.")
val clickedItem: Any? = adapter!!.getItem(position) } else {
if (clickedItem != null && clickedItem is ConversationItem) { val clickedItem: Any? = adapter!!.getItem(position)
val conversation = clickedItem.model if (clickedItem != null && clickedItem is ConversationItem) {
conversationsListBottomDialog = ConversationsListBottomDialog( val conversation = clickedItem.model
this, conversationsListBottomDialog = ConversationsListBottomDialog(
userManager.currentUser.blockingGet(), this@ConversationsListActivity,
conversation userManager.currentUser.blockingGet(),
) conversation
conversationsListBottomDialog!!.show() )
conversationsListBottomDialog!!.show()
}
} }
} }
} }

View File

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

View File

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

View File

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