From 6e869e3edb3324520370a030faf04fa04547a6b5 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 11:25:39 +0200 Subject: [PATCH 1/8] feat(chat): show server maintenance mode banner in chat Detects HTTP 503 responses carrying the X-Nextcloud-Maintenance-Mode header during chat message polling and surfaces a red banner in the message input area, mirroring the "Connection lost" banner and the maintenance-mode banner already shown in the conversation list. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../talk/chat/MessageInputFragment.kt | 6 +++++ .../talk/chat/data/ChatMessageRepository.kt | 2 ++ .../chat/data/network/ChatMessageSyncer.kt | 24 ++++++++++++++++++- .../network/OfflineFirstChatRepository.kt | 9 +++++++ .../talk/chat/domain/ChatPullResult.kt | 1 + .../talk/chat/viewmodels/ChatViewModel.kt | 2 ++ .../res/layout/fragment_message_input.xml | 11 +++++++++ 7 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index b542b984499..e1424a6a597 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -308,6 +308,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) { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index c50c84e8aad..a148fa809b5 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -46,6 +46,8 @@ interface ChatMessageRepository : LifecycleAwareManager { val isLoadingFlow: Flow + val maintenanceModeFlow: Flow + // /** // * Used for informing the user of the underlying processing behind offline support, [String] is the key // * which is handled in a switch statement in ChatActivity. diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt index 85118eacdb1..b05ac269954 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt @@ -87,6 +87,10 @@ class ChatMessageSyncer @Inject constructor( suspend fun onIncomingMessagesFromOthers() { // no-op by default } + + suspend fun onMaintenanceModeChanged(active: Boolean) { + // no-op by default + } } /** @@ -590,6 +594,12 @@ class ChatMessageSyncer @Inject constructor( ) HTTP_CODE_NOT_MODIFIED -> ChatPullResult.NotModified HTTP_CODE_PRECONDITION_FAILED -> ChatPullResult.PreconditionFailed + HTTP_CODE_SERVICE_UNAVAILABLE -> + if (response.headers()[MAINTENANCE_MODE_HEADER] == "1") { + ChatPullResult.MaintenanceMode + } else { + ChatPullResult.Error(HttpException(response)) + } else -> ChatPullResult.Error(HttpException(response)) } @@ -631,11 +641,14 @@ class ChatMessageSyncer @Inject constructor( val lookIntoFuture = fieldMap["lookIntoFuture"] == 1 return when (val result = pullMessagesFlow(target, fieldMap).first()) { - is ChatPullResult.Success -> + is ChatPullResult.Success -> { + events.onMaintenanceModeChanged(false) handleSuccessfulPull(target, result, queriedMessageId, lookIntoFuture, events) + } is ChatPullResult.NotModified -> { Log.d(TAG, "Server returned NOT_MODIFIED, nothing to update") + events.onMaintenanceModeChanged(false) if (lookIntoFuture && queriedMessageId != null) { // the server confirmed there is nothing newer than the queried message, so // the queried message is a valid HTTP-synced anchor @@ -646,9 +659,16 @@ class ChatMessageSyncer @Inject constructor( is ChatPullResult.PreconditionFailed -> { Log.d(TAG, "Server returned PRECONDITION_FAILED, nothing to update") + events.onMaintenanceModeChanged(false) NOTHING_SYNCED } + is ChatPullResult.MaintenanceMode -> { + Log.d(TAG, "Server is currently in maintenance mode") + events.onMaintenanceModeChanged(true) + SYNC_FAILED + } + is ChatPullResult.Error -> { Log.e(TAG, "Error pulling messages from server", result.throwable) SYNC_FAILED @@ -1003,6 +1023,8 @@ class ChatMessageSyncer @Inject constructor( private const val HTTP_CODE_OK: Int = 200 private const val HTTP_CODE_NOT_MODIFIED = 304 private const val HTTP_CODE_PRECONDITION_FAILED = 412 + private const val HTTP_CODE_SERVICE_UNAVAILABLE = 503 + private const val MAINTENANCE_MODE_HEADER = "X-Nextcloud-Maintenance-Mode" private const val MAX_PULL_ATTEMPTS = 5 private const val RETRY_LIMIT_SECOND_ATTEMPT = 50 private const val RETRY_LIMIT_THIRD_ATTEMPT = 10 diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 70d515a795c..4a10ce93f71 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -130,6 +130,11 @@ class OfflineFirstChatRepository @Inject constructor( private val _isLoadingFlow: MutableStateFlow = MutableStateFlow(false) + override val maintenanceModeFlow: Flow + get() = _maintenanceModeFlow + + private val _maintenanceModeFlow: MutableStateFlow = MutableStateFlow(false) + private var newXChatLastCommonRead: Int? = null private var itIsPaused = false @@ -188,6 +193,10 @@ class OfflineFirstChatRepository @Inject constructor( override suspend fun onIncomingMessagesFromOthers() { _incomingMessageFlow.emit(Unit) } + + override suspend fun onMaintenanceModeChanged(active: Boolean) { + _maintenanceModeFlow.value = active + } } override suspend fun loadInitialMessages(withNetworkParams: Bundle) { diff --git a/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt b/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt index 50e5c29a0bb..daef6c8a3e7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt @@ -14,5 +14,6 @@ sealed class ChatPullResult { object NotModified : ChatPullResult() object PreconditionFailed : ChatPullResult() + object MaintenanceMode : ChatPullResult() data class Error(val throwable: Throwable) : ChatPullResult() } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index fe4c48e6b1b..2af541ac64e 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -558,6 +558,8 @@ class ChatViewModel @AssistedInject constructor( val isLoadingFlow = chatRepository.isLoadingFlow + val maintenanceModeFlow = chatRepository.maintenanceModeFlow + sealed interface ViewState object GetReminderStartState : ViewState diff --git a/app/src/main/res/layout/fragment_message_input.xml b/app/src/main/res/layout/fragment_message_input.xml index aec5fc67534..df4eeba7117 100644 --- a/app/src/main/res/layout/fragment_message_input.xml +++ b/app/src/main/res/layout/fragment_message_input.xml @@ -49,6 +49,17 @@ android:visibility="gone" tools:visibility="visible" /> + + Date: Wed, 26 Aug 2026 11:43:21 +0200 Subject: [PATCH 2/8] fix(conv-list): propagate room-sync network errors to the UI getRoomsFromServer() caught and only logged exceptions from the background room-list sync, so GetRoomsErrorState was never produced by the current getRooms() path (roomListFlow only observes the local database, not the network call). This made handleHttpExceptions unreachable, silently breaking the unauthorized/outdated-client dialogs and the maintenance-mode banner. Adds a getRoomsErrorFlow that the repository emits into instead of swallowing the exception, collected by the view model into the existing GetRoomsErrorState. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../data/OfflineConversationsRepository.kt | 7 +++++++ .../data/network/OfflineFirstConversationsRepository.kt | 5 +++++ .../viewmodels/ConversationsListViewModel.kt | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt index 72d9efe6131..e36b8ed1aad 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt @@ -31,6 +31,13 @@ interface OfflineConversationsRepository { */ val syncErrorFlow: Flow + /** + * Network/HTTP failures from [getRooms]'s background sync (e.g. 401, 426, 503 with the + * maintenance-mode header). [roomListFlow] only observes the local database and never fails + * from a network error, so the UI needs this separate stream to react to them. + */ + val getRoomsErrorFlow: Flow + /** * Stream of a single conversation, for use in each conversations settings. */ diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt index 1c2bf927cff..90a66caff68 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -88,6 +88,10 @@ class OfflineFirstConversationsRepository @Inject constructor( get() = _syncErrorFlow private val _syncErrorFlow: MutableSharedFlow = MutableSharedFlow() + override val getRoomsErrorFlow: Flow + get() = _getRoomsErrorFlow + private val _getRoomsErrorFlow: MutableSharedFlow = MutableSharedFlow() + private val scope = CoroutineScope(Dispatchers.IO) sealed interface ConversationResult { @@ -217,6 +221,7 @@ class OfflineFirstConversationsRepository @Inject constructor( scope.launch { catchUpRoomsWithNewMessages(user, roomsWithNewMessages) } } catch (e: Exception) { Log.e(TAG, "Something went wrong when fetching conversations", e) + _getRoomsErrorFlow.emit(e) val hasCachedConversations = dao.getConversationsForUser(user.id!!).first().isNotEmpty() if (!hasCachedConversations) { _syncErrorFlow.emit(e) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 31547455b0e..35cd338c764 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -55,6 +55,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -173,6 +174,14 @@ class ConversationsListViewModel @Inject constructor( } .stateIn(viewModelScope, SharingStarted.Eagerly, listOf()) + init { + viewModelScope.launch { + repository.getRoomsErrorFlow.collect { throwable -> + _getRoomsViewState.value = GetRoomsErrorState(throwable) + } + } + } + /** * Drives the shimmer skeleton visibility. Visible while rooms are still being loaded for * the first time (no data in the list yet). Hidden as soon as either the room list is From 49de690ebfce150e44a934e0e720a192077b0a6d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 12:51:32 +0200 Subject: [PATCH 3/8] refactor(network): centralize maintenance-mode/HTTP-status detection in a shared interceptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat and conversation list each had their own bespoke detection of server maintenance mode (HTTP 503 + X-Nextcloud-Maintenance-Mode header), each only covering their own network call. Replaces both with a single OkHttpClient interceptor that sees every request for every screen and account, and publishes status changes (maintenance mode, unauthorized, client-update-required) via EventBus for any screen to react to — mirroring how the iOS app's NCAPISessionManager.checkStatusCode works. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/chat/ChatActivity.kt | 8 +++ .../talk/chat/data/ChatMessageRepository.kt | 2 - .../chat/data/network/ChatMessageSyncer.kt | 24 +------ .../network/OfflineFirstChatRepository.kt | 9 --- .../talk/chat/domain/ChatPullResult.kt | 1 - .../talk/chat/viewmodels/ChatViewModel.kt | 8 ++- .../ConversationsListActivity.kt | 44 ++++--------- .../viewmodels/ConversationsListViewModel.kt | 9 --- .../talk/dagger/modules/RestModule.java | 9 +++ .../talk/events/ServerStatusEvent.kt | 17 +++++ .../talk/utils/HttpStatusInterceptor.kt | 65 +++++++++++++++++++ 11 files changed, 120 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/events/ServerStatusEvent.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 7e17316b0df..5186972f6c1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -135,6 +135,8 @@ 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 @@ -4030,6 +4032,12 @@ class ChatActivity : } } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onServerStatusEvent(event: ServerStatusEvent) { + if (!::conversationUser.isInitialized || event.accountId != conversationUser.id) return + chatViewModel.setMaintenanceMode(event.status == ServerStatus.MAINTENANCE_MODE) + } + fun sendPictureFromCamIntent() { if (!permissionUtil.isCameraPermissionGranted()) { requestCameraPermissions() diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index a148fa809b5..c50c84e8aad 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -46,8 +46,6 @@ interface ChatMessageRepository : LifecycleAwareManager { val isLoadingFlow: Flow - val maintenanceModeFlow: Flow - // /** // * Used for informing the user of the underlying processing behind offline support, [String] is the key // * which is handled in a switch statement in ChatActivity. diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt index b05ac269954..85118eacdb1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatMessageSyncer.kt @@ -87,10 +87,6 @@ class ChatMessageSyncer @Inject constructor( suspend fun onIncomingMessagesFromOthers() { // no-op by default } - - suspend fun onMaintenanceModeChanged(active: Boolean) { - // no-op by default - } } /** @@ -594,12 +590,6 @@ class ChatMessageSyncer @Inject constructor( ) HTTP_CODE_NOT_MODIFIED -> ChatPullResult.NotModified HTTP_CODE_PRECONDITION_FAILED -> ChatPullResult.PreconditionFailed - HTTP_CODE_SERVICE_UNAVAILABLE -> - if (response.headers()[MAINTENANCE_MODE_HEADER] == "1") { - ChatPullResult.MaintenanceMode - } else { - ChatPullResult.Error(HttpException(response)) - } else -> ChatPullResult.Error(HttpException(response)) } @@ -641,14 +631,11 @@ class ChatMessageSyncer @Inject constructor( val lookIntoFuture = fieldMap["lookIntoFuture"] == 1 return when (val result = pullMessagesFlow(target, fieldMap).first()) { - is ChatPullResult.Success -> { - events.onMaintenanceModeChanged(false) + is ChatPullResult.Success -> handleSuccessfulPull(target, result, queriedMessageId, lookIntoFuture, events) - } is ChatPullResult.NotModified -> { Log.d(TAG, "Server returned NOT_MODIFIED, nothing to update") - events.onMaintenanceModeChanged(false) if (lookIntoFuture && queriedMessageId != null) { // the server confirmed there is nothing newer than the queried message, so // the queried message is a valid HTTP-synced anchor @@ -659,16 +646,9 @@ class ChatMessageSyncer @Inject constructor( is ChatPullResult.PreconditionFailed -> { Log.d(TAG, "Server returned PRECONDITION_FAILED, nothing to update") - events.onMaintenanceModeChanged(false) NOTHING_SYNCED } - is ChatPullResult.MaintenanceMode -> { - Log.d(TAG, "Server is currently in maintenance mode") - events.onMaintenanceModeChanged(true) - SYNC_FAILED - } - is ChatPullResult.Error -> { Log.e(TAG, "Error pulling messages from server", result.throwable) SYNC_FAILED @@ -1023,8 +1003,6 @@ class ChatMessageSyncer @Inject constructor( private const val HTTP_CODE_OK: Int = 200 private const val HTTP_CODE_NOT_MODIFIED = 304 private const val HTTP_CODE_PRECONDITION_FAILED = 412 - private const val HTTP_CODE_SERVICE_UNAVAILABLE = 503 - private const val MAINTENANCE_MODE_HEADER = "X-Nextcloud-Maintenance-Mode" private const val MAX_PULL_ATTEMPTS = 5 private const val RETRY_LIMIT_SECOND_ATTEMPT = 50 private const val RETRY_LIMIT_THIRD_ATTEMPT = 10 diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 4a10ce93f71..70d515a795c 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -130,11 +130,6 @@ class OfflineFirstChatRepository @Inject constructor( private val _isLoadingFlow: MutableStateFlow = MutableStateFlow(false) - override val maintenanceModeFlow: Flow - get() = _maintenanceModeFlow - - private val _maintenanceModeFlow: MutableStateFlow = MutableStateFlow(false) - private var newXChatLastCommonRead: Int? = null private var itIsPaused = false @@ -193,10 +188,6 @@ class OfflineFirstChatRepository @Inject constructor( override suspend fun onIncomingMessagesFromOthers() { _incomingMessageFlow.emit(Unit) } - - override suspend fun onMaintenanceModeChanged(active: Boolean) { - _maintenanceModeFlow.value = active - } } override suspend fun loadInitialMessages(withNetworkParams: Bundle) { diff --git a/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt b/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt index daef6c8a3e7..50e5c29a0bb 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/domain/ChatPullResult.kt @@ -14,6 +14,5 @@ sealed class ChatPullResult { object NotModified : ChatPullResult() object PreconditionFailed : ChatPullResult() - object MaintenanceMode : ChatPullResult() data class Error(val throwable: Throwable) : ChatPullResult() } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 2af541ac64e..523e6007ed7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -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 @@ -558,7 +559,12 @@ class ChatViewModel @AssistedInject constructor( val isLoadingFlow = chatRepository.isLoadingFlow - val maintenanceModeFlow = chatRepository.maintenanceModeFlow + private val _maintenanceModeFlow = MutableStateFlow(false) + val maintenanceModeFlow: StateFlow = _maintenanceModeFlow.asStateFlow() + + fun setMaintenanceMode(active: Boolean) { + _maintenanceModeFlow.value = active + } sealed interface ViewState diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 059bba60338..f93a3cc5318 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -63,6 +63,8 @@ import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.events.ServerStatus +import com.nextcloud.talk.events.ServerStatusEvent import com.nextcloud.talk.invitation.InvitationsActivity import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.run @@ -120,7 +122,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import retrofit2.HttpException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -203,7 +204,6 @@ class ConversationsListActivity : BaseActivity() { } else { currentUserProviderOld.currentUser.blockingGet() } - conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java] conversationTagsViewModel = ViewModelProvider(this, viewModelFactory)[ConversationTagsViewModel::class.java] @@ -452,7 +452,8 @@ class ConversationsListActivity : BaseActivity() { when (state) { is ConversationsListViewModel.GetRoomsErrorState -> { isRefreshingState.value = false - handleHttpExceptions(state.throwable) + Log.e(TAG, "Error observing the conversation list", state.throwable) + showErrorDialog() } else -> {} @@ -671,22 +672,15 @@ class ConversationsListActivity : BaseActivity() { } } - private fun handleHttpExceptions(throwable: Throwable) { - if (!networkMonitor.isOnline.value) return - - if (throwable is HttpException) { - when (throwable.code()) { - HTTP_UNAUTHORIZED -> showUnauthorizedDialog() - HTTP_CLIENT_UPGRADE_REQUIRED -> showOutdatedClientDialog() - HTTP_SERVICE_UNAVAILABLE -> showServiceUnavailableDialog(throwable) - else -> { - Log.e(TAG, "Http Exception in ConversationListActivity", throwable) - showErrorDialog() - } - } - } else { - Log.e(TAG, "Exception in ConversationListActivity", throwable) - showErrorDialog() + @Subscribe(threadMode = ThreadMode.MAIN) + fun onServerStatusEvent(event: ServerStatusEvent) { + if (event.accountId != currentUser?.id) return + + when (event.status) { + ServerStatus.UNAUTHORIZED -> showUnauthorizedDialog() + ServerStatus.CLIENT_UPDATE_REQUIRED -> showOutdatedClientDialog() + ServerStatus.MAINTENANCE_MODE -> isMaintenanceModeState.value = true + ServerStatus.OK -> isMaintenanceModeState.value = false } } @@ -1450,14 +1444,6 @@ class ConversationsListActivity : BaseActivity() { ) } - private fun showServiceUnavailableDialog(httpException: HttpException) { - if (httpException.response()?.headers()?.get(MAINTENANCE_MODE_HEADER_KEY) == "1") { - isMaintenanceModeState.value = true - } else { - showErrorDialog() - } - } - private fun showServerEOLDialog() { val dialogBuilder = MaterialAlertDialogBuilder(this) .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.ic_warning_white)) @@ -1556,12 +1542,8 @@ class ConversationsListActivity : BaseActivity() { private val TAG = ConversationsListActivity::class.java.simpleName const val BOTTOM_SHEET_DELAY: Long = 2500 const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 - const val HTTP_UNAUTHORIZED = 401 - const val HTTP_CLIENT_UPGRADE_REQUIRED = 426 const val CLIENT_UPGRADE_MARKET_LINK = "market://details?id=" const val CLIENT_UPGRADE_GPLAY_LINK = "https://play.google.com/store/apps/details?id=" - const val HTTP_SERVICE_UNAVAILABLE = 503 - const val MAINTENANCE_MODE_HEADER_KEY = "X-Nextcloud-Maintenance-Mode" const val REQUEST_POST_NOTIFICATIONS_PERMISSION = 111 const val DAYS_FOR_NOTIFICATION_WARNING = 5L const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 35cd338c764..31547455b0e 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -55,7 +55,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -174,14 +173,6 @@ class ConversationsListViewModel @Inject constructor( } .stateIn(viewModelScope, SharingStarted.Eagerly, listOf()) - init { - viewModelScope.launch { - repository.getRoomsErrorFlow.collect { throwable -> - _getRoomsViewState.value = GetRoomsErrorState(throwable) - } - } - } - /** * Drives the shimmer skeleton visibility. Visible while rooms are still being loaded for * the first time (no data in the list yet). Hidden as soon as either the room list is diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java index c98fd2e82ff..e4e37648979 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java @@ -17,6 +17,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.users.UserManager; import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.HttpStatusInterceptor; import com.nextcloud.talk.utils.RemoteWipeInterceptor; import com.nextcloud.talk.utils.LoggingUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; @@ -180,6 +181,12 @@ Dispatcher provideDispatcher() { return dispatcher; } + @Singleton + @Provides + HttpStatusInterceptor provideHttpStatusInterceptor(UserManager userManager) { + return new HttpStatusInterceptor(userManager); + } + @Singleton @Provides OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences, @@ -188,6 +195,7 @@ OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences, CookieManager cookieManager, Dispatcher dispatcher, UserManager userManager, + HttpStatusInterceptor httpStatusInterceptor, LoggingHttpInterceptor loggingHttpInterceptor) { OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); @@ -222,6 +230,7 @@ OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences, httpClient.addInterceptor(new HeadersInterceptor()); httpClient.addInterceptor(new RemoteWipeInterceptor(userManager, context, sslSocketFactoryCompat, trustManager)); + httpClient.addInterceptor(httpStatusInterceptor); httpClient.addInterceptor(loggingHttpInterceptor); return httpClient.build(); diff --git a/app/src/main/java/com/nextcloud/talk/events/ServerStatusEvent.kt b/app/src/main/java/com/nextcloud/talk/events/ServerStatusEvent.kt new file mode 100644 index 00000000000..2d9ffe3d5e6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/ServerStatusEvent.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.events + +enum class ServerStatus { + OK, + UNAUTHORIZED, + CLIENT_UPDATE_REQUIRED, + MAINTENANCE_MODE +} + +data class ServerStatusEvent(val accountId: Long, val status: ServerStatus) diff --git a/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt b/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt new file mode 100644 index 00000000000..67f2b8953af --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import com.nextcloud.talk.events.ServerStatus +import com.nextcloud.talk.events.ServerStatusEvent +import com.nextcloud.talk.users.UserManager +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import org.greenrobot.eventbus.EventBus +import java.util.concurrent.ConcurrentHashMap + +class HttpStatusInterceptor(private val userManager: UserManager) : Interceptor { + + private val lastKnownStatus = ConcurrentHashMap() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + val accountId = resolveAccountId(request) ?: return response + val newStatus = statusFor(response) + val previousStatus = lastKnownStatus.put(accountId, newStatus) ?: ServerStatus.OK + + if (newStatus != previousStatus) { + EventBus.getDefault().post(ServerStatusEvent(accountId, newStatus)) + } + + return response + } + + private fun statusFor(response: Response): ServerStatus = + when (response.code) { + HTTP_UNAUTHORIZED -> ServerStatus.UNAUTHORIZED + HTTP_UPGRADE_REQUIRED -> ServerStatus.CLIENT_UPDATE_REQUIRED + HTTP_SERVICE_UNAVAILABLE -> + if (response.header(MAINTENANCE_MODE_HEADER) == "1") { + ServerStatus.MAINTENANCE_MODE + } else { + ServerStatus.OK + } + else -> ServerStatus.OK + } + + private fun resolveAccountId(request: Request): Long? { + val authorization = request.header("Authorization") + val users = userManager.users.blockingGet() + val user = users.firstOrNull { ApiUtils.getCredentials(it.username, it.token) == authorization } + ?: users.firstOrNull { it.baseUrl != null && request.url.toString().startsWith(it.baseUrl!!) } + return user?.id + } + + companion object { + private const val HTTP_UNAUTHORIZED = 401 + private const val HTTP_UPGRADE_REQUIRED = 426 + private const val HTTP_SERVICE_UNAVAILABLE = 503 + private const val MAINTENANCE_MODE_HEADER = "X-Nextcloud-Maintenance-Mode" + } +} From 2c31927a93489b437dc4841c0bf8662923a026d7 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 12:53:45 +0200 Subject: [PATCH 4/8] fix(network): seed HTTP status from interceptor for screens that start observing late HttpStatusInterceptor only posts a ServerStatusEvent on a status transition, so a screen that starts observing after the transition already happened never learns the current status. In practice, the conversation list is usually first to detect maintenance mode; by the time a chat is opened, no new transition occurs since the account is already flagged, so the chat banner never appeared even though the server was still in maintenance mode. Adds HttpStatusInterceptor.currentStatus(accountId) so ChatActivity and ConversationsListActivity can seed their state as soon as the account is known, in addition to reacting to future transitions. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/chat/ChatActivity.kt | 9 +++++++++ .../ConversationsListActivity.kt | 9 +++++++++ .../talk/utils/HttpStatusInterceptor.kt | 19 ++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 5186972f6c1..1267cdee25f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -197,6 +197,7 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.FileViewerUtils +import com.nextcloud.talk.utils.HttpStatusInterceptor import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions @@ -292,6 +293,9 @@ class ChatActivity : @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var httpStatusInterceptor: HttpStatusInterceptor + @Inject lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory @@ -622,6 +626,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) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index f93a3cc5318..053f5f8b952 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -91,6 +91,7 @@ import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils.checkIfVoiceRoom import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.HttpStatusInterceptor import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions @@ -145,6 +146,9 @@ class ConversationsListActivity : BaseActivity() { @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var httpStatusInterceptor: HttpStatusInterceptor + @Inject lateinit var contactsViewModel: ContactsViewModel @@ -204,6 +208,11 @@ class ConversationsListActivity : BaseActivity() { } else { currentUserProviderOld.currentUser.blockingGet() } + currentUser?.id?.let { accountId -> + isMaintenanceModeState.value = + httpStatusInterceptor.currentStatus(accountId) == ServerStatus.MAINTENANCE_MODE + } + conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java] conversationTagsViewModel = ViewModelProvider(this, viewModelFactory)[ConversationTagsViewModel::class.java] diff --git a/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt b/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt index 67f2b8953af..e3e7f0af9c6 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/HttpStatusInterceptor.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.utils +import android.util.Log import com.nextcloud.talk.events.ServerStatus import com.nextcloud.talk.events.ServerStatusEvent import com.nextcloud.talk.users.UserManager @@ -24,17 +25,32 @@ class HttpStatusInterceptor(private val userManager: UserManager) : Interceptor val request = chain.request() val response = chain.proceed(request) - val accountId = resolveAccountId(request) ?: return response + val accountId = resolveAccountId(request) + if (accountId == null) { + Log.w(TAG, "Could not resolve account for ${request.url}, skipping status check") + return response + } + val newStatus = statusFor(response) val previousStatus = lastKnownStatus.put(accountId, newStatus) ?: ServerStatus.OK if (newStatus != previousStatus) { + Log.d(TAG, "Status for account $accountId changed from $previousStatus to $newStatus (${request.url})") EventBus.getDefault().post(ServerStatusEvent(accountId, newStatus)) } return response } + /** + * The last known status for [accountId], for a screen that starts observing after the + * transition already happened (e.g. the conversation list detects maintenance mode before + * the user opens a chat) — [intercept] only posts an event *on change*, so a late observer + * needs to fetch the current status once instead of waiting for a transition that already + * happened. + */ + fun currentStatus(accountId: Long): ServerStatus = lastKnownStatus[accountId] ?: ServerStatus.OK + private fun statusFor(response: Response): ServerStatus = when (response.code) { HTTP_UNAUTHORIZED -> ServerStatus.UNAUTHORIZED @@ -57,6 +73,7 @@ class HttpStatusInterceptor(private val userManager: UserManager) : Interceptor } companion object { + private const val TAG = "HttpStatusInterceptor" private const val HTTP_UNAUTHORIZED = 401 private const val HTTP_UPGRADE_REQUIRED = 426 private const val HTTP_SERVICE_UNAVAILABLE = 503 From 33cd0501dfef98c24e8590134ddc6f3c669da685 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 14:46:20 +0200 Subject: [PATCH 5/8] feat(ui): show the status banner on every fully-Compose screen via BaseActivity Centralizes maintenance-mode/offline detection into BaseActivity (account resolution, EventBus subscription, HttpStatusInterceptor seeding) instead of each screen wiring its own state, and adds the shared StatusBannerRow (moved to components/, self-padding for the status bar only when visible) to the conversation list plus the 12 other screens that are fully written in Compose. ChatActivity keeps its existing XML-based banner untouched, since it mixes XML and Compose rather than being fully Compose. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../talk/account/ServerSelectionActivity.kt | 4 - .../nextcloud/talk/activities/BaseActivity.kt | 44 +++++++++++ .../com/nextcloud/talk/chat/ChatActivity.kt | 11 +-- .../talk/chat/ScheduledMessagesActivity.kt | 72 ++++++++--------- .../ui => components}/StatusBanner.kt | 13 +++- .../talk/contacts/ContactsActivity.kt | 17 +++- .../ConversationCreationActivity.kt | 9 ++- .../ConversationInfoActivity.kt | 16 +++- .../ConversationInfoEditActivity.kt | 60 ++++++++------ .../ConversationsListActivity.kt | 26 ++----- .../ui/ConversationsListScreen.kt | 6 +- .../talk/diagnosis/DiagnosisActivity.kt | 33 ++++---- .../talk/location/GeocodingActivity.kt | 20 +++-- .../talk/location/LocationPickerActivity.kt | 22 ++++-- .../nextcloud/talk/logger/ui/LogsActivity.kt | 53 +++++++------ .../ListOpenConversationsActivity.kt | 32 +++++--- .../nextcloud/talk/profile/ProfileActivity.kt | 78 +++++++++++-------- .../talk/settings/SettingsActivity.kt | 4 - .../ThreadsOverviewActivity.kt | 57 ++++++++------ 19 files changed, 349 insertions(+), 228 deletions(-) rename app/src/main/java/com/nextcloud/talk/{conversationlist/ui => components}/StatusBanner.kt (80%) diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt index 35d2c1e1ffd..a264255e148 100644 --- a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt @@ -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 @@ -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) { diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index 366c7989b2b..5f3a14aff81 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -38,9 +38,14 @@ 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.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 @@ -54,6 +59,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 @@ -96,6 +104,15 @@ 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 = maintenanceModeState.asStateFlow() + open val appBarLayoutType: AppBarLayoutType get() = AppBarLayoutType.TOOLBAR @@ -136,6 +153,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) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 1267cdee25f..630026489a9 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -132,7 +132,6 @@ 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 @@ -197,7 +196,6 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.FileViewerUtils -import com.nextcloud.talk.utils.HttpStatusInterceptor import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions @@ -290,12 +288,6 @@ class ChatActivity : @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - @Inject - lateinit var networkMonitor: NetworkMonitor - - @Inject - lateinit var httpStatusInterceptor: HttpStatusInterceptor - @Inject lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory @@ -4042,7 +4034,8 @@ class ChatActivity : } @Subscribe(threadMode = ThreadMode.MAIN) - fun onServerStatusEvent(event: ServerStatusEvent) { + override fun onServerStatusEvent(event: ServerStatusEvent) { + super.onServerStatusEvent(event) if (!::conversationUser.isInitialized || event.accountId != conversationUser.id) return chatViewModel.setMaintenanceMode(event.status == ServerStatus.MAINTENANCE_MODE) } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt index fa18d31a9cd..72db535ceb6 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt @@ -93,7 +93,7 @@ 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.components.StatusBannerRow import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.extensions.toIntOrZero import com.nextcloud.talk.models.json.chat.ChatUtils @@ -134,9 +134,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() } @@ -169,6 +166,8 @@ class ScheduledMessagesActivity : BaseActivity() { setContent { val colorScheme = viewThemeUtils.getColorScheme(this) val currentUser by scheduledMessagesViewModel.currentUserState.collectAsStateWithLifecycle() + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() LaunchedEffect(Unit) { scheduledMessagesViewModel.loadCurrentUser() } @@ -179,37 +178,40 @@ class ScheduledMessagesActivity : BaseActivity() { LocalShowThreadButton provides false ) { ColoredStatusBar() - currentUser?.let { user -> - ScheduledMessagesScreen( - user = user, - conversationName = conversationName, - scheduledMessagesViewModel = scheduledMessagesViewModel, - dateUtils = dateUtils, - viewThemeUtils = viewThemeUtils, - onBack = { finish() }, - onLoadScheduledMessages = { loadScheduledMessages(user) }, - onSendNow = { message -> - sendNow(message, user) - }, - onReschedule = { message, sendAt, sendWithoutNotification -> - reschedule(message, sendAt, sendWithoutNotification, user) - }, - onEdit = { message, sendAt -> - edit(message, sendAt, user) - }, - onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, user) }, - onOpenParentMessage = { messageId -> - openParentMessage(messageId) - }, - onOpenThread = { threadId -> - openThread(threadId) - }, - threadTitle = threadTitle, - isThreadView = isThreadView, - onCopyScheduledMessage = { message -> - copyScheduledMessage(message) - } - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + currentUser?.let { user -> + ScheduledMessagesScreen( + user = user, + conversationName = conversationName, + scheduledMessagesViewModel = scheduledMessagesViewModel, + dateUtils = dateUtils, + viewThemeUtils = viewThemeUtils, + onBack = { finish() }, + onLoadScheduledMessages = { loadScheduledMessages(user) }, + onSendNow = { message -> + sendNow(message, user) + }, + onReschedule = { message, sendAt, sendWithoutNotification -> + reschedule(message, sendAt, sendWithoutNotification, user) + }, + onEdit = { message, sendAt -> + edit(message, sendAt, user) + }, + onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, user) }, + onOpenParentMessage = { messageId -> + openParentMessage(messageId) + }, + onOpenThread = { threadId -> + openThread(threadId) + }, + threadTitle = threadTitle, + isThreadView = isThreadView, + onCopyScheduledMessage = { message -> + copyScheduledMessage(message) + } + ) + } } } // CompositionLocalProvider } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/StatusBanner.kt b/app/src/main/java/com/nextcloud/talk/components/StatusBanner.kt similarity index 80% rename from app/src/main/java/com/nextcloud/talk/conversationlist/ui/StatusBanner.kt rename to app/src/main/java/com/nextcloud/talk/components/StatusBanner.kt index 6aec84d2351..1043d98901d 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/StatusBanner.kt +++ b/app/src/main/java/com/nextcloud/talk/components/StatusBanner.kt @@ -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 @@ -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 @@ -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(), diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index e20ee7b61b2..f41a4b07cf3 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -11,7 +11,9 @@ package com.nextcloud.talk.contacts import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -19,6 +21,7 @@ import autodagger.AutoInjector import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser @@ -62,14 +65,20 @@ class ContactsActivity : BaseActivity() { }.toSet().toMutableList() contactsViewModel.updateSelectedParticipants(selectedParticipants) + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme( colorScheme = colorScheme ) { ColoredStatusBar() - ContactsScreen( - contactsViewModel = contactsViewModel, - uiState = uiState.value - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + ContactsScreen( + contactsViewModel = contactsViewModel, + uiState = uiState.value + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index 0638284ba94..e345fb60017 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -89,6 +89,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import coil.compose.AsyncImage import com.nextcloud.talk.R @@ -99,6 +100,7 @@ import com.nextcloud.talk.components.AvatarEditPanel import com.nextcloud.talk.components.AvatarEditPanelCallbacks import com.nextcloud.talk.components.AvatarEditPanelState import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.loadImage import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel @@ -131,10 +133,15 @@ class ConversationCreationActivity : BaseActivity() { setContent { val colorScheme = viewThemeUtils.getColorScheme(this) val context = LocalContext.current + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() MaterialTheme( colorScheme = colorScheme ) { - ConversationCreationScreen(conversationCreationViewModel, context, pickImage) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + ConversationCreationScreen(conversationCreationViewModel, context, pickImage) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index bddda18c82e..5ccd33adb37 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -15,6 +15,7 @@ import androidx.activity.compose.setContent import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.LaunchedEffect @@ -44,6 +45,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.conversationinfo.model.ParticipantModel @@ -239,12 +241,18 @@ class ConversationInfoActivity : BaseActivity() { } } + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - ConversationInfoScreen( - state = uiState, - callbacks = buildCallbacks() - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + ConversationInfoScreen( + state = uiState, + callbacks = buildCallbacks() + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt index 529728d8904..d0bb432d989 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -12,6 +12,7 @@ import android.util.Log import androidx.activity.compose.setContent import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.LaunchedEffect @@ -27,6 +28,7 @@ import com.github.dhaval2404.imagepicker.ImagePicker import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.conversationinfoedit.ui.ConversationInfoEditCallbacks import com.nextcloud.talk.conversationinfoedit.ui.ConversationInfoEditScreen import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel @@ -122,33 +124,41 @@ class ConversationInfoEditActivity : BaseActivity() { } } + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - ConversationInfoEditScreen( - uiState = uiState, - callbacks = ConversationInfoEditCallbacks( - onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, - onSaveClick = { conversationInfoEditViewModel.saveNameAndDescription() }, - onAvatarUploadClick = { - pickImage?.selectLocal(startImagePickerForResult = startImagePickerForResult) - }, - onAvatarChooseClick = { - pickImage?.selectRemote( - startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult - ) - }, - onAvatarCameraClick = { - pickImage?.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) - }, - onAvatarDeleteClick = { conversationInfoEditViewModel.deleteAvatar() }, - onAvatarEmojiAvatarConfirmed = { emoji, color -> - conversationInfoEditViewModel.onEmojiAvatarConfirmed(emoji, color) - }, - onNameChange = { conversationInfoEditViewModel.updateConversationName(it) }, - onDescriptionChange = { conversationInfoEditViewModel.updateConversationDescription(it) } - ), - snackbarHostState = snackbarHostState - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + ConversationInfoEditScreen( + uiState = uiState, + callbacks = ConversationInfoEditCallbacks( + onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, + onSaveClick = { conversationInfoEditViewModel.saveNameAndDescription() }, + onAvatarUploadClick = { + pickImage?.selectLocal(startImagePickerForResult = startImagePickerForResult) + }, + onAvatarChooseClick = { + pickImage?.selectRemote( + startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult + ) + }, + onAvatarCameraClick = { + pickImage?.takePicture( + startTakePictureIntentForResult = startTakePictureIntentForResult + ) + }, + onAvatarDeleteClick = { conversationInfoEditViewModel.deleteAvatar() }, + onAvatarEmojiAvatarConfirmed = { emoji, color -> + conversationInfoEditViewModel.onEmojiAvatarConfirmed(emoji, color) + }, + onNameChange = { conversationInfoEditViewModel.updateConversationName(it) }, + onDescriptionChange = { conversationInfoEditViewModel.updateConversationDescription(it) } + ), + snackbarHostState = snackbarHostState + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 053f5f8b952..6e0647c8b54 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -59,7 +59,6 @@ import com.nextcloud.talk.conversationlist.ui.ConversationsListScreenCallbacks import com.nextcloud.talk.conversationlist.ui.ConversationsListScreenState import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel import com.nextcloud.talk.conversationtags.viewmodels.ConversationTagsViewModel -import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.events.EventStatus @@ -91,7 +90,6 @@ import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils.checkIfVoiceRoom import com.nextcloud.talk.utils.FileUtils -import com.nextcloud.talk.utils.HttpStatusInterceptor import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions @@ -143,12 +141,6 @@ class ConversationsListActivity : BaseActivity() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - @Inject - lateinit var networkMonitor: NetworkMonitor - - @Inject - lateinit var httpStatusInterceptor: HttpStatusInterceptor - @Inject lateinit var contactsViewModel: ContactsViewModel @@ -158,7 +150,6 @@ class ConversationsListActivity : BaseActivity() { private var currentUser: User? = null private val snackbarHostState = SnackbarHostState() - private val isMaintenanceModeState = MutableStateFlow(false) private val showUnreadBubbleState = MutableStateFlow(false) private val isFabVisibleState = MutableStateFlow(true) private val showNotificationWarningState = MutableStateFlow(false) @@ -208,10 +199,6 @@ class ConversationsListActivity : BaseActivity() { } else { currentUserProviderOld.currentUser.blockingGet() } - currentUser?.id?.let { accountId -> - isMaintenanceModeState.value = - httpStatusInterceptor.currentStatus(accountId) == ServerStatus.MAINTENANCE_MODE - } conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java] conversationTagsViewModel = ViewModelProvider(this, viewModelFactory)[ConversationTagsViewModel::class.java] @@ -238,6 +225,8 @@ class ConversationsListActivity : BaseActivity() { initObservers() } + override fun accountIdForStatusBanner(): Long? = currentUser?.id + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(KEY_ACCOUNT_DIALOG_VISIBLE, showAccountDialogState.value) @@ -259,7 +248,7 @@ class ConversationsListActivity : BaseActivity() { viewThemeUtils = viewThemeUtils, isShowEcosystem = appPreferences.isShowEcosystem && !resources.getBoolean(R.bool.is_branded_client), snackbarHostState = snackbarHostState, - isMaintenanceModeFlow = isMaintenanceModeState, + isMaintenanceModeFlow = maintenanceModeFlow, isOnlineFlow = networkMonitor.isOnline, showUnreadBubbleFlow = showUnreadBubbleState, isFabVisibleFlow = isFabVisibleState, @@ -284,7 +273,6 @@ class ConversationsListActivity : BaseActivity() { onContactClick = { contactsViewModel.createRoom(ROOM_TYPE_ONE_ONE, null, it.actorId!!, null) }, onLoadMoreClick = { conversationsListViewModel.loadMoreMessages(context) }, onRefresh = { - isMaintenanceModeState.value = false isRefreshingState.value = true appPreferences.setConversationListPositionAndOffset(0, 0) fetchRooms() @@ -682,14 +670,16 @@ class ConversationsListActivity : BaseActivity() { } @Subscribe(threadMode = ThreadMode.MAIN) - fun onServerStatusEvent(event: ServerStatusEvent) { + override fun onServerStatusEvent(event: ServerStatusEvent) { + super.onServerStatusEvent(event) if (event.accountId != currentUser?.id) return when (event.status) { ServerStatus.UNAUTHORIZED -> showUnauthorizedDialog() ServerStatus.CLIENT_UPDATE_REQUIRED -> showOutdatedClientDialog() - ServerStatus.MAINTENANCE_MODE -> isMaintenanceModeState.value = true - ServerStatus.OK -> isMaintenanceModeState.value = false + else -> { + // MAINTENANCE_MODE / OK are handled by BaseActivity's maintenanceModeFlow + } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt index a726cb35b92..e144c4bcde3 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.talk.R import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel import com.nextcloud.talk.conversationtags.viewmodels.ConversationTagsViewModel import com.nextcloud.talk.data.user.model.User @@ -247,7 +248,7 @@ fun ConversationsListScreen( Scaffold( contentWindowInsets = WindowInsets.safeDrawing, topBar = { - Column(modifier = Modifier.fillMaxWidth().statusBarsPadding()) { + Column(modifier = Modifier.fillMaxWidth()) { StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) ConversationListTopBar( state = ConversationListTopBarState( @@ -271,7 +272,8 @@ fun ConversationsListScreen( onAvatarClick = callbacks.onAvatarClick, onNavigateBack = callbacks.onNavigateBack, onAccountChooserClick = callbacks.onAccountChooserClick - ) + ), + modifier = Modifier.statusBarsPadding() ) } } diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt index ebd4d0c0ed2..0bc786dbc7f 100644 --- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt @@ -41,6 +41,7 @@ import com.nextcloud.talk.components.ColoredStatusBar import com.nextcloud.talk.components.StandardAppBar import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.errorhandling.saveLogsAsZip +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.logger.LogsRepository import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ClosedInterfaceImpl @@ -65,9 +66,6 @@ class DiagnosisActivity : BaseActivity() { @Inject lateinit var userManager: UserManager - @Inject - lateinit var networkMonitor: NetworkMonitor - @Inject lateinit var platformPermissionUtil: PlatformPermissionUtil @@ -182,19 +180,22 @@ private fun DiagnosisScreen( colorScheme = colorScheme ) { val isOnline = networkMonitor.isOnline.collectAsState().value - ColoredStatusBar() - Scaffold( - modifier = Modifier - .statusBarsPadding() - .displayCutoutPadding(), - topBar = { - StandardAppBar( - title = stringResource(R.string.nc_settings_diagnosis_title), - menuItems - ) - }, - content = { paddingValues -> - val viewState = diagnosisViewModel.notificationViewState.collectAsState().value + val isMaintenanceMode = maintenanceModeFlow.collectAsState().value + ColoredStatusBar() + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + StandardAppBar( + title = stringResource(R.string.nc_settings_diagnosis_title), + menuItems + ) + }, + content = { paddingValues -> + val viewState = diagnosisViewModel.notificationViewState.collectAsState().value Column( Modifier diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt index b8a1e019f2d..2f55cf3a5c5 100644 --- a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt @@ -9,14 +9,18 @@ package com.nextcloud.talk.location import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.location.components.GeocodingScreen import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.location.viewmodels.GeoCodingViewModel @@ -59,13 +63,19 @@ class GeocodingActivity : BaseActivity() { setContent { val colorScheme = viewThemeUtils.getColorScheme(this) + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - GeocodingScreen( - viewModel = viewModel, - onBack = { onBackPressedDispatcher.onBackPressed() }, - onAddressSelected = { address -> navigateToLocationPicker(address) } - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + GeocodingScreen( + viewModel = viewModel, + onBack = { onBackPressedDispatcher.onBackPressed() }, + onAddressSelected = { address -> navigateToLocationPicker(address) } + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt index 80b3aaa0b1f..0c6bf219075 100644 --- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -11,13 +11,17 @@ import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.extensions.getParcelableExtraProvider import com.nextcloud.talk.location.components.LocationPickerScreen import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CHAT_API_VERSION @@ -99,14 +103,20 @@ class LocationPickerActivity : BaseActivity() { val colorScheme = viewThemeUtils.getColorScheme(this) setContent { + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - LocationPickerScreen( - viewModel = viewModel, - onSearchClick = { navigateToGeocoding() }, - onBack = { onBackPressedDispatcher.onBackPressed() }, - onFinish = { finish() } - ) + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + LocationPickerScreen( + viewModel = viewModel, + onSearchClick = { navigateToGeocoding() }, + onBack = { onBackPressedDispatcher.onBackPressed() }, + onFinish = { finish() } + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt index 827a9a67e86..993317bf9ac 100644 --- a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt @@ -63,6 +63,7 @@ import com.nextcloud.talk.components.StandardAppBar import com.nextcloud.talk.diagnosis.buildDiagnosisReportText import com.nextcloud.talk.diagnosis.showShareReportDialog import com.nextcloud.talk.errorhandling.saveLogsAsZip +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.logger.Level import com.nextcloud.talk.logger.LogEntry import com.nextcloud.talk.logger.LogsRepository @@ -125,32 +126,38 @@ class LogsActivity : BaseActivity() { } ) + val isOnline by networkMonitor.isOnline.collectAsState() + val isMaintenanceMode by maintenanceModeFlow.collectAsState() + ColoredStatusBar() - Scaffold( - modifier = Modifier - .statusBarsPadding() - .displayCutoutPadding(), - topBar = { - StandardAppBar( - title = stringResource(R.string.nc_logs_title), - menuItems = menuItems + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + StandardAppBar( + title = stringResource(R.string.nc_logs_title), + menuItems = menuItems + ) + } + ) { paddingValues -> + LogsContent( + state = LogsUiState( + entries = entries, + isLoading = isLoading, + totalSize = totalSize, + lostEntries = viewModel.lostEntries, + loggingEnabled = loggingEnabled, + advancedLogging = advancedLogging + ), + onLoggingEnabledChange = { viewModel.setLoggingEnabled(it) }, + onAdvancedLoggingChange = { viewModel.setAdvancedLogging(it) }, + onDisable = { deleteExisting -> viewModel.setLoggingEnabled(false, deleteExisting) }, + paddingValues = paddingValues ) } - ) { paddingValues -> - LogsContent( - state = LogsUiState( - entries = entries, - isLoading = isLoading, - totalSize = totalSize, - lostEntries = viewModel.lostEntries, - loggingEnabled = loggingEnabled, - advancedLogging = advancedLogging - ), - onLoggingEnabledChange = { viewModel.setLoggingEnabled(it) }, - onAdvancedLoggingChange = { viewModel.setAdvancedLogging(it) }, - onDisable = { deleteExisting -> viewModel.setLoggingEnabled(false, deleteExisting) }, - paddingValues = paddingValues - ) } LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt index 3b91c1b1804..3dd4d1b4a65 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt @@ -9,6 +9,7 @@ package com.nextcloud.talk.openconversations import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider @@ -18,6 +19,7 @@ import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.utils.adjustUIForAPILevel35 @@ -47,21 +49,27 @@ class ListOpenConversationsActivity : BaseActivity() { val viewState by openConversationsViewModel.viewState.collectAsStateWithLifecycle() val searchTerm by openConversationsViewModel.searchTerm.collectAsStateWithLifecycle() + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - OpenConversationsScreen( - viewState = viewState, - searchTerm = searchTerm, - userBaseUrl = user?.baseUrl, - listenerInput = OpenConversationsScreenListenerInput( - onSearchTermChange = { term -> - openConversationsViewModel.updateSearchTerm(term) - openConversationsViewModel.fetchConversations() - }, - onConversationClick = { conversation -> navigateToChat(conversation) }, - onBackClick = { onBackPressedDispatcher.onBackPressed() } + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + OpenConversationsScreen( + viewState = viewState, + searchTerm = searchTerm, + userBaseUrl = user?.baseUrl, + listenerInput = OpenConversationsScreenListenerInput( + onSearchTermChange = { term -> + openConversationsViewModel.updateSearchTerm(term) + openConversationsViewModel.fetchConversations() + }, + onConversationClick = { conversation -> navigateToChat(conversation) }, + onBackClick = { onBackPressedDispatcher.onBackPressed() } + ) ) - ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt index df36d6bbf37..2e0fdb30462 100644 --- a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt @@ -14,12 +14,14 @@ import androidx.activity.compose.setContent import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.net.toFile import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError @@ -29,6 +31,7 @@ import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.userprofile.Scope @@ -128,47 +131,56 @@ class ProfileActivity : BaseActivity() { val colorScheme = viewThemeUtils.getColorScheme(this) setContent { + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() + MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - ProfileScreen( - state = profileUiState, - callbacks = ProfileCallbacks( - onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, - onEditSave = ::handleEditSave, - onAvatarUploadClick = { - pickImage.selectLocal(startImagePickerForResult = startImagePickerForResult) - }, - onAvatarChooseClick = { - pickImage.selectRemote( - startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult - ) - }, - onAvatarCameraClick = { - pickImage.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) - }, - onAvatarDeleteClick = ::deleteAvatar, - onProfileEnabledChange = { enabled -> - profileUiState = profileUiState.copy(isProfileEnabled = enabled) - }, - onTextChange = { position, newText -> - profileItems.getOrNull(position)?.text = newText - }, - onScopeClick = { position, field -> - scopeSheetRequest = ScopeSheetRequest(position, field) - } - ) - ) - scopeSheetRequest?.let { req -> - ScopeModalBottomSheet( - showPrivate = req.field != Field.DISPLAYNAME && req.field != Field.EMAIL, - onScopeSelected = { scope -> updateItemScope(req.position, scope) }, - onDismiss = { scopeSheetRequest = null } + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + ProfileScreen( + state = profileUiState, + callbacks = buildProfileCallbacks() ) + scopeSheetRequest?.let { req -> + ScopeModalBottomSheet( + showPrivate = req.field != Field.DISPLAYNAME && req.field != Field.EMAIL, + onScopeSelected = { scope -> updateItemScope(req.position, scope) }, + onDismiss = { scopeSheetRequest = null } + ) + } } } } } + private fun buildProfileCallbacks() = + ProfileCallbacks( + onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, + onEditSave = ::handleEditSave, + onAvatarUploadClick = { + pickImage.selectLocal(startImagePickerForResult = startImagePickerForResult) + }, + onAvatarChooseClick = { + pickImage.selectRemote( + startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult + ) + }, + onAvatarCameraClick = { + pickImage.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) + }, + onAvatarDeleteClick = ::deleteAvatar, + onProfileEnabledChange = { enabled -> + profileUiState = profileUiState.copy(isProfileEnabled = enabled) + }, + onTextChange = { position, newText -> + profileItems.getOrNull(position)?.text = newText + }, + onScopeClick = { position, field -> + scopeSheetRequest = ScopeSheetRequest(position, field) + } + ) + override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index d93fa5fd4b7..aaf96b39486 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -61,7 +61,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.conversationlist.ConversationsListActivity.Companion.NOTIFICATION_WARNING_DATE_NOT_SET -import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivitySettingsBinding import com.nextcloud.talk.diagnosis.DiagnosisActivity @@ -127,9 +126,6 @@ class SettingsActivity : @Inject lateinit var ncApi: NcApi - @Inject - lateinit var networkMonitor: NetworkMonitor - @Inject lateinit var ncApiCoroutines: NcApiCoroutines diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt index eabb185de20..09580f0a03c 100644 --- a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity @@ -47,6 +48,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity.Companion.TAG import com.nextcloud.talk.components.ColoredStatusBar import com.nextcloud.talk.components.StandardAppBar +import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.data.database.mappers.toDomainModel import com.nextcloud.talk.models.json.threads.ThreadInfo import com.nextcloud.talk.threadsoverview.components.ThreadRow @@ -89,38 +91,43 @@ class ThreadsOverviewActivity : BaseActivity() { setContent { val backgroundColor = colorResource(id = R.color.bg_default) + val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() + val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() MaterialTheme( colorScheme = colorScheme ) { ColoredStatusBar() - Scaffold( - modifier = Modifier - .statusBarsPadding(), - topBar = { - StandardAppBar( - title = appbarTitle, - null - ) - }, - content = { paddingValues -> - val uiState by threadsOverviewViewModel.threadsListState.collectAsState() - - Column( - Modifier - .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) - .background(backgroundColor) - .fillMaxSize() - ) { - ThreadsOverviewScreen( - uiState, - onThreadClick = { roomToken, threadId -> - navigateToChatActivity(roomToken, threadId) - } + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + Scaffold( + modifier = Modifier + .statusBarsPadding(), + topBar = { + StandardAppBar( + title = appbarTitle, + null ) + }, + content = { paddingValues -> + val uiState by threadsOverviewViewModel.threadsListState.collectAsState() + + Column( + Modifier + .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) + .background(backgroundColor) + .fillMaxSize() + ) { + ThreadsOverviewScreen( + uiState, + onThreadClick = { roomToken, threadId -> + navigateToChatActivity(roomToken, threadId) + } + ) + } } - } - ) + ) + } } } } From f4ac7800b7b66d0a4ebdca9e31579e3cdd9a7072 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 16:29:09 +0200 Subject: [PATCH 6/8] refactor(ui): extract status-banner wiring into BaseActivity.setContentWithStatusBanner Each fully-Compose screen repeated the same isOnline/isMaintenanceMode state collection and Column-wrapping to show the shared status banner. Adds BaseActivity.setContentWithStatusBanner(content), a drop-in replacement for setContent that collects the state once and renders StatusBannerRow above content, and switches all 12 screens to use it instead of duplicating the wiring. ConversationsListActivity is unchanged: its banner is embedded inside its own Scaffold's topBar alongside the conversation list's top bar, not the generic "wrap everything" shape the helper assumes. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/BaseActivity.kt | 22 ++++++ .../talk/chat/ScheduledMessagesActivity.kt | 71 +++++++++---------- .../talk/contacts/ContactsActivity.kt | 20 ++---- .../ConversationCreationActivity.kt | 12 +--- .../ConversationInfoActivity.kt | 19 ++--- .../ConversationInfoEditActivity.kt | 65 ++++++++--------- .../talk/diagnosis/DiagnosisActivity.kt | 37 +++++----- .../talk/location/GeocodingActivity.kt | 22 ++---- .../talk/location/LocationPickerActivity.kt | 25 ++----- .../nextcloud/talk/logger/ui/LogsActivity.kt | 54 +++++++------- .../ListOpenConversationsActivity.kt | 35 ++++----- .../nextcloud/talk/profile/ProfileActivity.kt | 30 +++----- .../ThreadsOverviewActivity.kt | 60 +++++++--------- 13 files changed, 197 insertions(+), 275 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index 5f3a14aff81..20ebc56b295 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -24,11 +24,16 @@ 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.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue 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 @@ -38,6 +43,7 @@ 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 @@ -113,6 +119,22 @@ open class BaseActivity : AppCompatActivity() { private val maintenanceModeState = MutableStateFlow(false) val maintenanceModeFlow: StateFlow = 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() + Column { + StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) + content() + } + } + } + open val appBarLayoutType: AppBarLayoutType get() = AppBarLayoutType.TOOLBAR diff --git a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt index 72db535ceb6..6ed3e971cdd 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt @@ -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 @@ -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.components.StatusBannerRow import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.extensions.toIntOrZero import com.nextcloud.talk.models.json.chat.ChatUtils @@ -163,11 +161,9 @@ 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() - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() LaunchedEffect(Unit) { scheduledMessagesViewModel.loadCurrentUser() } @@ -178,40 +174,37 @@ class ScheduledMessagesActivity : BaseActivity() { LocalShowThreadButton provides false ) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - currentUser?.let { user -> - ScheduledMessagesScreen( - user = user, - conversationName = conversationName, - scheduledMessagesViewModel = scheduledMessagesViewModel, - dateUtils = dateUtils, - viewThemeUtils = viewThemeUtils, - onBack = { finish() }, - onLoadScheduledMessages = { loadScheduledMessages(user) }, - onSendNow = { message -> - sendNow(message, user) - }, - onReschedule = { message, sendAt, sendWithoutNotification -> - reschedule(message, sendAt, sendWithoutNotification, user) - }, - onEdit = { message, sendAt -> - edit(message, sendAt, user) - }, - onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, user) }, - onOpenParentMessage = { messageId -> - openParentMessage(messageId) - }, - onOpenThread = { threadId -> - openThread(threadId) - }, - threadTitle = threadTitle, - isThreadView = isThreadView, - onCopyScheduledMessage = { message -> - copyScheduledMessage(message) - } - ) - } + currentUser?.let { user -> + ScheduledMessagesScreen( + user = user, + conversationName = conversationName, + scheduledMessagesViewModel = scheduledMessagesViewModel, + dateUtils = dateUtils, + viewThemeUtils = viewThemeUtils, + onBack = { finish() }, + onLoadScheduledMessages = { loadScheduledMessages(user) }, + onSendNow = { message -> + sendNow(message, user) + }, + onReschedule = { message, sendAt, sendWithoutNotification -> + reschedule(message, sendAt, sendWithoutNotification, user) + }, + onEdit = { message, sendAt -> + edit(message, sendAt, user) + }, + onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, user) }, + onOpenParentMessage = { messageId -> + openParentMessage(messageId) + }, + onOpenThread = { threadId -> + openThread(threadId) + }, + threadTitle = threadTitle, + isThreadView = isThreadView, + onCopyScheduledMessage = { message -> + copyScheduledMessage(message) + } + ) } } // CompositionLocalProvider } diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index f41a4b07cf3..36789c271d9 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -10,10 +10,7 @@ package com.nextcloud.talk.contacts import android.annotation.SuppressLint import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -21,7 +18,6 @@ import autodagger.AutoInjector import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser @@ -40,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() @@ -65,20 +61,14 @@ class ContactsActivity : BaseActivity() { }.toSet().toMutableList() contactsViewModel.updateSelectedParticipants(selectedParticipants) - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - MaterialTheme( colorScheme = colorScheme ) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - ContactsScreen( - contactsViewModel = contactsViewModel, - uiState = uiState.value - ) - } + ContactsScreen( + contactsViewModel = contactsViewModel, + uiState = uiState.value + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index e345fb60017..421650d02d7 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -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 @@ -89,7 +88,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import coil.compose.AsyncImage import com.nextcloud.talk.R @@ -100,7 +98,6 @@ import com.nextcloud.talk.components.AvatarEditPanel import com.nextcloud.talk.components.AvatarEditPanelCallbacks import com.nextcloud.talk.components.AvatarEditPanelState import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.loadImage import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel @@ -130,18 +127,13 @@ class ConversationCreationActivity : BaseActivity() { val conversationUser = conversationCreationViewModel.currentUser pickImage = PickImage(this, conversationUser) - setContent { + setContentWithStatusBanner { val colorScheme = viewThemeUtils.getColorScheme(this) val context = LocalContext.current - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() MaterialTheme( colorScheme = colorScheme ) { - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - ConversationCreationScreen(conversationCreationViewModel, context, pickImage) - } + ConversationCreationScreen(conversationCreationViewModel, context, pickImage) } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 5ccd33adb37..f1b7f98fdc1 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -11,11 +11,9 @@ 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 -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.LaunchedEffect @@ -45,7 +43,6 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.conversationinfo.model.ParticipantModel @@ -216,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() } @@ -241,18 +238,12 @@ class ConversationInfoActivity : BaseActivity() { } } - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - ConversationInfoScreen( - state = uiState, - callbacks = buildCallbacks() - ) - } + ConversationInfoScreen( + state = uiState, + callbacks = buildCallbacks() + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt index d0bb432d989..0bbb0f079c7 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -9,10 +9,8 @@ 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.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.LaunchedEffect @@ -28,7 +26,6 @@ import com.github.dhaval2404.imagepicker.ImagePicker import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.conversationinfoedit.ui.ConversationInfoEditCallbacks import com.nextcloud.talk.conversationinfoedit.ui.ConversationInfoEditScreen import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel @@ -103,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 @@ -124,41 +121,35 @@ class ConversationInfoEditActivity : BaseActivity() { } } - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - ConversationInfoEditScreen( - uiState = uiState, - callbacks = ConversationInfoEditCallbacks( - onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, - onSaveClick = { conversationInfoEditViewModel.saveNameAndDescription() }, - onAvatarUploadClick = { - pickImage?.selectLocal(startImagePickerForResult = startImagePickerForResult) - }, - onAvatarChooseClick = { - pickImage?.selectRemote( - startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult - ) - }, - onAvatarCameraClick = { - pickImage?.takePicture( - startTakePictureIntentForResult = startTakePictureIntentForResult - ) - }, - onAvatarDeleteClick = { conversationInfoEditViewModel.deleteAvatar() }, - onAvatarEmojiAvatarConfirmed = { emoji, color -> - conversationInfoEditViewModel.onEmojiAvatarConfirmed(emoji, color) - }, - onNameChange = { conversationInfoEditViewModel.updateConversationName(it) }, - onDescriptionChange = { conversationInfoEditViewModel.updateConversationDescription(it) } - ), - snackbarHostState = snackbarHostState - ) - } + ConversationInfoEditScreen( + uiState = uiState, + callbacks = ConversationInfoEditCallbacks( + onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, + onSaveClick = { conversationInfoEditViewModel.saveNameAndDescription() }, + onAvatarUploadClick = { + pickImage?.selectLocal(startImagePickerForResult = startImagePickerForResult) + }, + onAvatarChooseClick = { + pickImage?.selectRemote( + startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult + ) + }, + onAvatarCameraClick = { + pickImage?.takePicture( + startTakePictureIntentForResult = startTakePictureIntentForResult + ) + }, + onAvatarDeleteClick = { conversationInfoEditViewModel.deleteAvatar() }, + onAvatarEmojiAvatarConfirmed = { emoji, color -> + conversationInfoEditViewModel.onEmojiAvatarConfirmed(emoji, color) + }, + onNameChange = { conversationInfoEditViewModel.updateConversationName(it) }, + onDescriptionChange = { conversationInfoEditViewModel.updateConversationDescription(it) } + ), + snackbarHostState = snackbarHostState + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt index 0bc786dbc7f..1f9568128d4 100644 --- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt @@ -101,7 +101,7 @@ class DiagnosisActivity : BaseActivity() { val useEmbeddedDistrib = UnifiedPushUtils.hasEmbeddedDistributor(context) && !useUnifiedPush val showTestPushButton = isGooglePlayServicesAvailable || useUnifiedPush || useEmbeddedDistrib - setContent { + setContentWithStatusBanner { DiagnosisScreen( colorScheme = colorScheme, networkMonitor = networkMonitor, @@ -176,26 +176,23 @@ private fun DiagnosisScreen( stringResource(R.string.nc_settings_share_report_title) to onShareReportClick ) - MaterialTheme( - colorScheme = colorScheme - ) { - val isOnline = networkMonitor.isOnline.collectAsState().value - val isMaintenanceMode = maintenanceModeFlow.collectAsState().value + MaterialTheme( + colorScheme = colorScheme + ) { + val isOnline = networkMonitor.isOnline.collectAsState().value ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - Scaffold( - modifier = Modifier - .statusBarsPadding() - .displayCutoutPadding(), - topBar = { - StandardAppBar( - title = stringResource(R.string.nc_settings_diagnosis_title), - menuItems - ) - }, - content = { paddingValues -> - val viewState = diagnosisViewModel.notificationViewState.collectAsState().value + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + StandardAppBar( + title = stringResource(R.string.nc_settings_diagnosis_title), + menuItems + ) + }, + content = { paddingValues -> + val viewState = diagnosisViewModel.notificationViewState.collectAsState().value Column( Modifier diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt index 2f55cf3a5c5..fc831f16f7d 100644 --- a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt @@ -8,19 +8,14 @@ package com.nextcloud.talk.location import android.content.Intent import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.location.components.GeocodingScreen import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.location.viewmodels.GeoCodingViewModel @@ -61,21 +56,16 @@ class GeocodingActivity : BaseActivity() { val email = context.getString(R.string.osm_geocoder_contact) TalkJsonNominatimClient(baseUrl, okHttpClient, email) - setContent { + setContentWithStatusBanner { val colorScheme = viewThemeUtils.getColorScheme(this) - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - GeocodingScreen( - viewModel = viewModel, - onBack = { onBackPressedDispatcher.onBackPressed() }, - onAddressSelected = { address -> navigateToLocationPicker(address) } - ) - } + GeocodingScreen( + viewModel = viewModel, + onBack = { onBackPressedDispatcher.onBackPressed() }, + onAddressSelected = { address -> navigateToLocationPicker(address) } + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt index 0c6bf219075..8fa37d2ab05 100644 --- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -9,19 +9,14 @@ package com.nextcloud.talk.location import android.content.Intent import android.os.Bundle import androidx.activity.OnBackPressedCallback -import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.extensions.getParcelableExtraProvider import com.nextcloud.talk.location.components.LocationPickerScreen import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CHAT_API_VERSION @@ -102,21 +97,15 @@ class LocationPickerActivity : BaseActivity() { onBackPressedDispatcher.addCallback(this, onBackPressedCallback) val colorScheme = viewThemeUtils.getColorScheme(this) - setContent { - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - + setContentWithStatusBanner { MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - LocationPickerScreen( - viewModel = viewModel, - onSearchClick = { navigateToGeocoding() }, - onBack = { onBackPressedDispatcher.onBackPressed() }, - onFinish = { finish() } - ) - } + LocationPickerScreen( + viewModel = viewModel, + onSearchClick = { navigateToGeocoding() }, + onBack = { onBackPressedDispatcher.onBackPressed() }, + onFinish = { finish() } + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt index 993317bf9ac..d342a9555a4 100644 --- a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt @@ -105,7 +105,7 @@ class LogsActivity : BaseActivity() { val viewModel = ViewModelProvider(this, viewModelFactory)[LogsViewModel::class.java] val colorScheme = viewThemeUtils.getColorScheme(this) - setContent { + setContentWithStatusBanner { MaterialTheme(colorScheme = colorScheme) { val entries = viewModel.entries.collectAsState().value val isLoading = viewModel.isLoading.collectAsState().value @@ -126,38 +126,32 @@ class LogsActivity : BaseActivity() { } ) - val isOnline by networkMonitor.isOnline.collectAsState() - val isMaintenanceMode by maintenanceModeFlow.collectAsState() - ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - Scaffold( - modifier = Modifier - .statusBarsPadding() - .displayCutoutPadding(), - topBar = { - StandardAppBar( - title = stringResource(R.string.nc_logs_title), - menuItems = menuItems - ) - } - ) { paddingValues -> - LogsContent( - state = LogsUiState( - entries = entries, - isLoading = isLoading, - totalSize = totalSize, - lostEntries = viewModel.lostEntries, - loggingEnabled = loggingEnabled, - advancedLogging = advancedLogging - ), - onLoggingEnabledChange = { viewModel.setLoggingEnabled(it) }, - onAdvancedLoggingChange = { viewModel.setAdvancedLogging(it) }, - onDisable = { deleteExisting -> viewModel.setLoggingEnabled(false, deleteExisting) }, - paddingValues = paddingValues + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + StandardAppBar( + title = stringResource(R.string.nc_logs_title), + menuItems = menuItems ) } + ) { paddingValues -> + LogsContent( + state = LogsUiState( + entries = entries, + isLoading = isLoading, + totalSize = totalSize, + lostEntries = viewModel.lostEntries, + loggingEnabled = loggingEnabled, + advancedLogging = advancedLogging + ), + onLoggingEnabledChange = { viewModel.setLoggingEnabled(it) }, + onAdvancedLoggingChange = { viewModel.setAdvancedLogging(it) }, + onDisable = { deleteExisting -> viewModel.setLoggingEnabled(false, deleteExisting) }, + paddingValues = paddingValues + ) } LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt index 3dd4d1b4a65..639bcc4f659 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt @@ -8,8 +8,6 @@ package com.nextcloud.talk.openconversations import android.content.Intent import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider @@ -19,7 +17,6 @@ import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.utils.adjustUIForAPILevel35 @@ -44,32 +41,26 @@ class ListOpenConversationsActivity : BaseActivity() { val user = currentUserProviderOld.currentUser.blockingGet() - setContent { + setContentWithStatusBanner { val colorScheme = viewThemeUtils.getColorScheme(this) val viewState by openConversationsViewModel.viewState.collectAsStateWithLifecycle() val searchTerm by openConversationsViewModel.searchTerm.collectAsStateWithLifecycle() - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - OpenConversationsScreen( - viewState = viewState, - searchTerm = searchTerm, - userBaseUrl = user?.baseUrl, - listenerInput = OpenConversationsScreenListenerInput( - onSearchTermChange = { term -> - openConversationsViewModel.updateSearchTerm(term) - openConversationsViewModel.fetchConversations() - }, - onConversationClick = { conversation -> navigateToChat(conversation) }, - onBackClick = { onBackPressedDispatcher.onBackPressed() } - ) + OpenConversationsScreen( + viewState = viewState, + searchTerm = searchTerm, + userBaseUrl = user?.baseUrl, + listenerInput = OpenConversationsScreenListenerInput( + onSearchTermChange = { term -> + openConversationsViewModel.updateSearchTerm(term) + openConversationsViewModel.fetchConversations() + }, + onConversationClick = { conversation -> navigateToChat(conversation) }, + onBackClick = { onBackPressedDispatcher.onBackPressed() } ) - } + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt index 2e0fdb30462..bfc7827c840 100644 --- a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt @@ -10,18 +10,15 @@ import android.app.Activity import android.content.pm.PackageManager 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.annotation.DrawableRes -import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.net.toFile import androidx.core.net.toUri -import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError @@ -31,7 +28,6 @@ import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.ColoredStatusBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.userprofile.Scope @@ -130,25 +126,19 @@ class ProfileActivity : BaseActivity() { } val colorScheme = viewThemeUtils.getColorScheme(this) - setContent { - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - + setContentWithStatusBanner { MaterialTheme(colorScheme = colorScheme) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - ProfileScreen( - state = profileUiState, - callbacks = buildProfileCallbacks() + ProfileScreen( + state = profileUiState, + callbacks = buildProfileCallbacks() + ) + scopeSheetRequest?.let { req -> + ScopeModalBottomSheet( + showPrivate = req.field != Field.DISPLAYNAME && req.field != Field.EMAIL, + onScopeSelected = { scope -> updateItemScope(req.position, scope) }, + onDismiss = { scopeSheetRequest = null } ) - scopeSheetRequest?.let { req -> - ScopeModalBottomSheet( - showPrivate = req.field != Field.DISPLAYNAME && req.field != Field.EMAIL, - onScopeSelected = { scope -> updateItemScope(req.position, scope) }, - onDismiss = { scopeSheetRequest = null } - ) - } } } } diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt index 09580f0a03c..46224447a12 100644 --- a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt @@ -11,7 +11,6 @@ import android.content.Intent import android.os.Bundle import android.text.format.DateUtils import android.util.Log -import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,7 +37,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity @@ -48,7 +46,6 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity.Companion.TAG import com.nextcloud.talk.components.ColoredStatusBar import com.nextcloud.talk.components.StandardAppBar -import com.nextcloud.talk.components.StatusBannerRow import com.nextcloud.talk.data.database.mappers.toDomainModel import com.nextcloud.talk.models.json.threads.ThreadInfo import com.nextcloud.talk.threadsoverview.components.ThreadRow @@ -89,45 +86,40 @@ class ThreadsOverviewActivity : BaseActivity() { threadsSourceUrl = extras?.getString(KEY_THREADS_SOURCE_URL).orEmpty() appbarTitle = extras?.getString(KEY_APPBAR_TITLE).orEmpty() - setContent { + setContentWithStatusBanner { val backgroundColor = colorResource(id = R.color.bg_default) - val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() - val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() MaterialTheme( colorScheme = colorScheme ) { ColoredStatusBar() - Column { - StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - Scaffold( - modifier = Modifier - .statusBarsPadding(), - topBar = { - StandardAppBar( - title = appbarTitle, - null - ) - }, - content = { paddingValues -> - val uiState by threadsOverviewViewModel.threadsListState.collectAsState() + Scaffold( + modifier = Modifier + .statusBarsPadding(), + topBar = { + StandardAppBar( + title = appbarTitle, + null + ) + }, + content = { paddingValues -> + val uiState by threadsOverviewViewModel.threadsListState.collectAsState() - Column( - Modifier - .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) - .background(backgroundColor) - .fillMaxSize() - ) { - ThreadsOverviewScreen( - uiState, - onThreadClick = { roomToken, threadId -> - navigateToChatActivity(roomToken, threadId) - } - ) - } + Column( + Modifier + .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) + .background(backgroundColor) + .fillMaxSize() + ) { + ThreadsOverviewScreen( + uiState, + onThreadClick = { roomToken, threadId -> + navigateToChatActivity(roomToken, threadId) + } + ) } - ) - } + } + ) } } } From 340e63d214c08a911160d4b32888a9c523aac574 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 16:33:40 +0200 Subject: [PATCH 7/8] fix(conv-list): remove double status-bar gap when the banner is visible ConversationListTopBar always applied statusBarsPadding regardless of whether StatusBannerRow above it was already visible and self-padding for the status bar, producing a visible gap between the banner and the search/top bar. Only pad the top bar when the banner is hidden. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../talk/conversationlist/ui/ConversationsListScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt index e144c4bcde3..43ddd2fc93b 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt @@ -248,6 +248,7 @@ fun ConversationsListScreen( Scaffold( contentWindowInsets = WindowInsets.safeDrawing, topBar = { + val showBanner = !isOnline || isMaintenanceMode Column(modifier = Modifier.fillMaxWidth()) { StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) ConversationListTopBar( @@ -273,7 +274,7 @@ fun ConversationsListScreen( onNavigateBack = callbacks.onNavigateBack, onAccountChooserClick = callbacks.onAccountChooserClick ), - modifier = Modifier.statusBarsPadding() + modifier = if (showBanner) Modifier else Modifier.statusBarsPadding() ) } } From becddaa5f1652a8a19072013723184a82263d596 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 26 Aug 2026 16:57:18 +0200 Subject: [PATCH 8/8] fix(ui): stop the status banner doubling screens' own status-bar padding Several screens apply their own statusBarsPadding()/Scaffold inset (ThreadsOverviewActivity, DiagnosisActivity, LogsActivity, ConversationCreationActivity, ContactsScreen, LocationPickerScreen) assuming they sit at the true top of the window. Once StatusBannerRow is visible it already claims that inset for itself, so those screens' own padding added a second, redundant gap on top of it (e.g. an oversized app bar in "new conversation"). setContentWithStatusBanner now marks the status-bar inset consumed for content() whenever the banner is showing, so screens' existing statusBarsPadding() calls add nothing extra in that case. Fixed once centrally since some of the affected composables (ContactsScreen, LocationPickerScreen) have no access to the banner state to guard against it themselves. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/BaseActivity.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index 20ebc56b295..88dcf446bee 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -27,9 +27,15 @@ 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 @@ -128,9 +134,26 @@ open class BaseActivity : AppCompatActivity() { setContent { val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() val isMaintenanceMode by maintenanceModeFlow.collectAsStateWithLifecycle() - Column { + val showBanner = !isOnline || isMaintenanceMode + Column(modifier = Modifier.fillMaxSize()) { StatusBannerRow(isOffline = !isOnline, isMaintenanceMode = isMaintenanceMode) - content() + // 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() + } } } }