Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.ActivityServerSelectionBinding
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.Status
Expand Down Expand Up @@ -66,9 +65,6 @@ class ServerSelectionActivity : BaseActivity() {
@Inject
lateinit var userManager: UserManager

@Inject
lateinit var networkMonitor: NetworkMonitor

private var statusQueryDisposable: Disposable? = null

private val onBackPressedCallback = object : OnBackPressedCallback(true) {
Expand Down
89 changes: 89 additions & 0 deletions app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,22 @@ import android.view.inputmethod.EditorInfo
import android.webkit.SslErrorHandler
import android.widget.EditText
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.R
Expand All @@ -38,9 +49,15 @@ import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.SwitchAccountActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.StatusBannerRow
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.events.RemoteWipeEvent
import com.nextcloud.talk.events.ServerStatus
import com.nextcloud.talk.events.ServerStatusEvent
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.lock.LockedActivity
import com.nextcloud.talk.utils.HttpStatusInterceptor
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
Expand All @@ -54,6 +71,9 @@ import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.utils.ssl.TrustManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
Expand Down Expand Up @@ -96,6 +116,48 @@ open class BaseActivity : AppCompatActivity() {
@Inject
lateinit var logger: Logger

@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var httpStatusInterceptor: HttpStatusInterceptor

private val maintenanceModeState = MutableStateFlow(false)
val maintenanceModeFlow: StateFlow<Boolean> = maintenanceModeState.asStateFlow()

/**
* [setContent] with the offline/maintenance-mode [StatusBannerRow] pushed above [content],
* for screens that are fully written in Compose. Chat is XML-rooted with Compose islands and
* keeps its own banner instead of using this.
*/
protected fun setContentWithStatusBanner(content: @Composable () -> Unit) {
setContent {
val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle()
val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle()
val showBanner = !isOnline || isMaintenanceMode
Column(modifier = Modifier.fillMaxSize()) {
StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode)
// content() itself emits bare sibling composables (e.g. ColoredStatusBar() next to
// the screen), which rely on being at the composition root to overlay rather than
// stack. Confining them to a single weighted Box here preserves that overlay
// behavior while still reserving exactly the space below the banner for them.
//
// Several screens also apply their own statusBarsPadding()/Scaffold insets that
// assume they sit at the true top of the window. Once the banner is visible it has
// already claimed that inset (StatusBannerRow pads itself for it), so mark it
// consumed here β€” any statusBarsPadding() further down then adds nothing extra,
// avoiding a doubled gap under the banner.
Box(
modifier = Modifier
.weight(1f)
.let { if (showBanner) it.consumeWindowInsets(WindowInsets.statusBars) else it }
) {
content()
}
}
}
}

open val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.TOOLBAR

Expand Down Expand Up @@ -136,6 +198,33 @@ open class BaseActivity : AppCompatActivity() {
cleanTempCertPreference()
}

/**
* The account whose [ServerStatusEvent]s should drive [maintenanceModeFlow] for this screen.
* Defaults to the globally active account; override when a screen shows a different,
* specific account (e.g. a non-active account selected via an intent extra).
*/
protected open fun accountIdForStatusBanner(): Long? = currentUserProviderOld.currentUser.blockingGet()?.id

override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
maintenanceModeState.value = accountIdForStatusBanner()?.let {
httpStatusInterceptor.currentStatus(it) == ServerStatus.MAINTENANCE_MODE
} ?: false
}

@Subscribe(threadMode = ThreadMode.MAIN)
open fun onServerStatusEvent(event: ServerStatusEvent) {
if (event.accountId != accountIdForStatusBanner()) return

when (event.status) {
ServerStatus.MAINTENANCE_MODE -> maintenanceModeState.value = true
ServerStatus.OK -> maintenanceModeState.value = false
else -> {
// UNAUTHORIZED / CLIENT_UPDATE_REQUIRED are handled by ConversationsListActivity's dialogs
}
}
}

public override fun onStart() {
super.onStart()
eventBus.register(this)
Expand Down
30 changes: 26 additions & 4 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ import com.nextcloud.talk.chat.ui.buildMessageActionsState
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.components.StatusBannerRow
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.dagger.modules.ViewModelFactoryWithParams
import com.nextcloud.talk.data.database.model.SendStatus
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.ServerStatus
import com.nextcloud.talk.events.ServerStatusEvent
import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.jobs.DeleteConversationWorker
Expand Down Expand Up @@ -287,9 +289,6 @@ class ChatActivity :
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory

Expand Down Expand Up @@ -595,6 +594,7 @@ class ChatActivity :
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)

setupStatusBannerView()
setupChatToolbarView()
setupChatEmptyStateView()
setupTypingIndicatorView()
Expand Down Expand Up @@ -639,6 +639,11 @@ class ChatActivity :
currentUserProvider.getCurrentUser()
.onSuccess { user ->
conversationUser = user
user.id?.let { accountId ->
chatViewModel.setMaintenanceMode(
httpStatusInterceptor.currentStatus(accountId) == ServerStatus.MAINTENANCE_MODE
)
}
handleIntent(intent)
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
Expand Down Expand Up @@ -2072,6 +2077,16 @@ class ChatActivity :
updateToolbarState()
}

private fun setupStatusBannerView() {
binding.statusBannerComposeView.setContent {
val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle()
val isMaintenanceMode by chatViewModel.maintenanceModeFlow.collectAsStateWithLifecycle()
MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(this@ChatActivity)) {
StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode)
}
}
}

private fun setupChatToolbarView() {
binding.chatToolbarComposeView.setContent {
MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(this@ChatActivity)) {
Expand Down Expand Up @@ -4058,6 +4073,13 @@ class ChatActivity :
}
}

@Subscribe(threadMode = ThreadMode.MAIN)
override fun onServerStatusEvent(event: ServerStatusEvent) {
super.onServerStatusEvent(event)
if (!::conversationUser.isInitialized || event.accountId != conversationUser.id) return
chatViewModel.setMaintenanceMode(event.status == ServerStatus.MAINTENANCE_MODE)
}

fun sendPictureFromCamIntent() {
if (!permissionUtil.isCameraPermissionGranted()) {
requestCameraPermissions()
Expand Down
43 changes: 6 additions & 37 deletions app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.LinearInterpolator
import android.view.inputmethod.InputMethodManager
import android.widget.Chronometer
Expand Down Expand Up @@ -296,12 +295,12 @@ class MessageInputFragment : Fragment() {
}

viewLifecycleOwner.lifecycleScope.launch {
var wasOnline: Boolean
var previousOnline: Boolean? = null
networkMonitor.isOnline
.onEach { isOnline ->
wasOnline = !binding.fragmentConnectionLost.isShown
val connectionGained = (!wasOnline && isOnline)
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
val connectionGained = (previousOnline == false && isOnline)
previousOnline = isOnline
Log.d(TAG, "isOnline: $isOnline\nconnectionGained: $connectionGained")
if (connectionGained) {
messageInputViewModel.sendUnsentMessages(
chatActivity.conversationUser!!.getCredentials(),
Expand All @@ -312,7 +311,7 @@ class MessageInputFragment : Fragment() {
)
)
}
handleUI(isOnline, connectionGained)
handleUI(isOnline)
}.collect()
}

Expand Down Expand Up @@ -344,33 +343,8 @@ class MessageInputFragment : Fragment() {
}
}

private fun handleUI(isOnline: Boolean, connectionGained: Boolean) {
private fun handleUI(isOnline: Boolean) {
if (isOnline) {
if (connectionGained) {
val animation: Animation = AlphaAnimation(FULLY_OPAQUE, FULLY_TRANSPARENT)
animation.duration = CONNECTION_ESTABLISHED_ANIM_DURATION
animation.interpolator = LinearInterpolator()
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityGreen))
binding.fragmentConnectionLost.text = getString(R.string.connection_established)
binding.fragmentConnectionLost.startAnimation(animation)
binding.fragmentConnectionLost.animation.setAnimationListener(object : AnimationListener {
override fun onAnimationStart(animation: Animation?) {
// unused atm
}

override fun onAnimationEnd(animation: Animation?) {
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)
}

override fun onAnimationRepeat(animation: Animation?) {
// unused atm
}
})
}

binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
binding.fragmentMessageInputView.recordAudioButton.visibility =
if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) View.VISIBLE else View.GONE
Expand All @@ -382,10 +356,6 @@ class MessageInputFragment : Fragment() {
} 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.visibility = View.VISIBLE
binding.fragmentMessageInputView.scheduledMessagesButton.visibility = View.GONE
}
}
Expand Down Expand Up @@ -1282,7 +1252,6 @@ class MessageInputFragment : Fragment() {
private const val VOICE_RECORD_LOCK_THRESHOLD: Float = 100f
private const val INCREMENT = 8f
private const val CURSOR_KEY = "_cursor"
private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000
private const val FULLY_OPAQUE: Float = 1.0f
private const val FULLY_TRANSPARENT: Float = 0.0f
private const val OPACITY_DISABLED = 0.7f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
Expand Down Expand Up @@ -93,7 +92,6 @@ import com.nextcloud.talk.chat.ui.model.MessageTypeContent
import com.nextcloud.talk.chat.ui.model.toScheduledMessageUiModel
import com.nextcloud.talk.chat.viewmodels.ScheduledMessagesViewModel
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
import com.nextcloud.talk.models.json.chat.ChatUtils
Expand Down Expand Up @@ -134,9 +132,6 @@ class ScheduledMessagesActivity : BaseActivity() {

private lateinit var scheduledMessagesViewModel: ScheduledMessagesViewModel

@Inject
lateinit var networkMonitor: NetworkMonitor

private val roomToken: String by lazy {
intent.getStringExtra(ROOM_TOKEN).orEmpty()
}
Expand Down Expand Up @@ -166,7 +161,7 @@ class ScheduledMessagesActivity : BaseActivity() {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
scheduledMessagesViewModel = ViewModelProvider(this, viewModelFactory)[ScheduledMessagesViewModel::class.java]

setContent {
setContentWithStatusBanner {
val colorScheme = viewThemeUtils.getColorScheme(this)
val currentUser by scheduledMessagesViewModel.currentUserState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
Expand Down Expand Up @@ -564,6 +565,13 @@ class ChatViewModel @AssistedInject constructor(

val isLoadingFlow = chatRepository.isLoadingFlow

private val _maintenanceModeFlow = MutableStateFlow(false)
val maintenanceModeFlow: StateFlow<Boolean> = _maintenanceModeFlow.asStateFlow()

fun setMaintenanceMode(active: Boolean) {
_maintenanceModeFlow.value = active
}

sealed interface ViewState

object GetReminderStartState : ViewState
Expand Down
Loading
Loading