Skip to content
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
18 changes: 14 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 @@ -132,9 +132,10 @@ 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 @@ -284,9 +285,6 @@ class ChatActivity :
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory

Expand Down Expand Up @@ -617,6 +615,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 @@ -3999,6 +4002,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
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ class MessageInputFragment : Fragment() {
binding.fragmentCallStarted.visibility = if (hasCall) View.VISIBLE else View.GONE
}
}

viewLifecycleOwner.lifecycleScope.launch {
chatActivity.chatViewModel.maintenanceModeFlow.collect { isMaintenanceMode ->
binding.fragmentMaintenanceMode.visibility = if (isMaintenanceMode) View.VISIBLE else View.GONE
}
}
}

private fun setReactionsOnly(spreedCapabilities: SpreedCapability) {
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 @@ -96,6 +96,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 @@ -405,6 +406,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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.nextcloud.talk.conversationlist.ui
package com.nextcloud.talk.components

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
Expand All @@ -14,6 +14,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand All @@ -25,9 +26,17 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R

/**
* Row of full-width red banners for offline/maintenance-mode status, meant to sit at the very
* top of a screen. Self-contained regarding the status bar inset: [Modifier.statusBarsPadding] is
* only applied when a row is actually visible, so a screen that drops this in gets zero extra
* space reserved in the common case (nothing showing) and correct clearance from the status bar
* whenever a row appears.
*/
@Composable
fun StatusBannerRow(isOffline: Boolean, isMaintenanceMode: Boolean) {
Column {
val showBanner = isOffline || isMaintenanceMode
Column(modifier = if (showBanner) Modifier.statusBarsPadding() else Modifier) {
AnimatedVisibility(
visible = isOffline,
enter = expandVertically(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package com.nextcloud.talk.contacts

import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModelProvider
Expand All @@ -37,7 +36,7 @@ class ContactsActivity : BaseActivity() {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
setContent {
setContentWithStatusBanner {
val isAddParticipants = intent.getBooleanExtra(BundleKeys.KEY_ADD_PARTICIPANTS, false)
val hideAlreadyAddedParticipants = intent.getBooleanExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, false)
contactsViewModel.getContactsFromSearchParams()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
Expand Down Expand Up @@ -128,7 +127,7 @@ class ConversationCreationActivity : BaseActivity() {
val conversationUser = conversationCreationViewModel.currentUser
pickImage = PickImage(this, conversationUser)

setContent {
setContentWithStatusBanner {
val colorScheme = viewThemeUtils.getColorScheme(this)
val context = LocalContext.current
MaterialTheme(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
Expand Down Expand Up @@ -214,7 +213,7 @@ class ConversationInfoActivity : BaseActivity() {

private fun setupCompose() {
val colorScheme = viewThemeUtils.getColorScheme(this)
setContent {
setContentWithStatusBanner {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package com.nextcloud.talk.conversationinfoedit

import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.MaterialTheme
Expand Down Expand Up @@ -101,7 +100,7 @@ class ConversationInfoEditActivity : BaseActivity() {

private fun setupCompose() {
val colorScheme = viewThemeUtils.getColorScheme(this)
setContent {
setContentWithStatusBanner {
val uiState by conversationInfoEditViewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val userMessage = uiState.userMessage
Expand Down Expand Up @@ -138,7 +137,9 @@ class ConversationInfoEditActivity : BaseActivity() {
)
},
onAvatarCameraClick = {
pickImage?.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult)
pickImage?.takePicture(
startTakePictureIntentForResult = startTakePictureIntentForResult
)
},
onAvatarDeleteClick = { conversationInfoEditViewModel.deleteAvatar() },
onAvatarEmojiAvatarConfirmed = { emoji, color ->
Expand Down
Loading
Loading