misc: display offline banner directly in LoggedInFlowNode (and fix window insets)

This commit is contained in:
ganfra 2025-10-21 16:05:20 +02:00
parent ea2fc290b7
commit 68daf8d2cf
13 changed files with 111 additions and 204 deletions

View file

@ -53,6 +53,7 @@ import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
@ -77,6 +78,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
@ -122,6 +124,7 @@ class LoggedInFlowNode(
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val notificationConversationService: NotificationConversationService, private val notificationConversationService: NotificationConversationService,
private val syncService: SyncService,
snackbarDispatcher: SnackbarDispatcher, snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>( ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -538,11 +541,17 @@ class LoggedInFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
Box(modifier = modifier) { val isOnline by syncService.isOnline.collectAsState()
val ftueState by ftueService.state.collectAsState() ConnectivityIndicatorContainer(
BackstackView() isOnline = isOnline,
if (ftueState is FtueState.Complete) { modifier = modifier,
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) ) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
} }
} }
} }

View file

@ -9,7 +9,6 @@ package io.element.android.appnav.room
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -48,7 +47,6 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -71,7 +69,6 @@ class RoomFlowNode(
private val client: MatrixClient, private val client: MatrixClient,
private val joinRoomEntryPoint: JoinRoomEntryPoint, private val joinRoomEntryPoint: JoinRoomEntryPoint,
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver, private val membershipObserver: RoomMembershipObserver,
private val spaceEntryPoint: SpaceEntryPoint, private val spaceEntryPoint: SpaceEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>( ) : BaseFlowNode<RoomFlowNode.NavTarget>(
@ -222,10 +219,8 @@ class RoomFlowNode(
} }
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier -> private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView( LoadingRoomNodeView(
state = LoadingRoomState.Loading, state = LoadingRoomState.Loading,
hasNetworkConnection = isOnline,
onBackClick = { navigateUp() }, onBackClick = { navigateUp() },
modifier = modifier, modifier = modifier,
) )

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -50,7 +49,6 @@ class JoinedRoomFlowNode(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
private val syncService: SyncService,
) : ) :
BaseFlowNode<JoinedRoomFlowNode.NavTarget>( BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -116,12 +114,10 @@ class JoinedRoomFlowNode(
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier -> private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
val loadingRoomState by loadingRoomStateStateFlow.collectAsState() val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView( LoadingRoomNodeView(
state = loadingRoomState, state = loadingRoomState,
hasNetworkConnection = isOnline, onBackClick = onBackClick,
modifier = modifier, modifier = modifier
onBackClick = onBackClick
) )
} }

View file

@ -8,8 +8,6 @@
package io.element.android.appnav.room.joined package io.element.android.appnav.room.joined
import androidx.compose.foundation.layout.Box 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.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -21,7 +19,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@ -38,17 +35,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun LoadingRoomNodeView( fun LoadingRoomNodeView(
state: LoadingRoomState, state: LoadingRoomState,
hasNetworkConnection: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
Column { LoadingRoomTopBar(onBackClick)
ConnectivityIndicatorView(isOnline = hasNetworkConnection)
LoadingRoomTopBar(onBackClick)
}
}, },
content = { padding -> content = { padding ->
Box( Box(
@ -85,7 +78,6 @@ private fun LoadingRoomTopBar(
title = { title = {
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
}, },
windowInsets = WindowInsets(0.dp),
) )
} }
@ -94,7 +86,6 @@ private fun LoadingRoomTopBar(
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview { internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView( LoadingRoomNodeView(
state = state, state = state,
onBackClick = {}, onBackClick = {}
hasNetworkConnection = false
) )
} }

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
@ -50,7 +49,6 @@ import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.home.impl.spaces.HomeSpacesView import io.element.android.features.home.impl.spaces.HomeSpacesView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -84,56 +82,47 @@ fun HomeView(
val state: RoomListState = homeState.roomListState val state: RoomListState = homeState.roomListState
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val firstThrottler = remember { FirstThrottler(300, coroutineScope) } val firstThrottler = remember { FirstThrottler(300, coroutineScope) }
Box(modifier) {
ConnectivityIndicatorContainer( if (state.contextMenu is RoomListState.ContextMenu.Shown) {
modifier = modifier, RoomListContextMenu(
isOnline = homeState.hasNetworkConnection, contextMenu = state.contextMenu,
) { topPadding -> canReportRoom = state.canReportRoom,
Box {
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
RoomListContextMenu(
contextMenu = state.contextMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
onRoomSettingsClick = onRoomSettingsClick,
onReportRoomClick = onReportRoomClick,
)
}
if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) {
RoomListDeclineInviteMenu(
menu = state.declineInviteMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
onDeclineAndBlockClick = onDeclineInviteAndBlockUser,
)
}
leaveRoomView()
HomeScaffold(
state = homeState,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onMenuActionClick = onMenuActionClick,
modifier = Modifier.padding(top = topPadding),
)
// This overlaid view will only be visible when state.displaySearchResults is true
RoomListSearchView(
state = state.searchState,
eventSink = state.eventSink, eventSink = state.eventSink,
hideInvitesAvatars = state.hideInvitesAvatars, onRoomSettingsClick = onRoomSettingsClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) }, onReportRoomClick = onReportRoomClick,
modifier = Modifier
.statusBarsPadding()
.padding(top = topPadding)
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault)
) )
acceptDeclineInviteView()
} }
if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) {
RoomListDeclineInviteMenu(
menu = state.declineInviteMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
onDeclineAndBlockClick = onDeclineInviteAndBlockUser,
)
}
leaveRoomView()
HomeScaffold(
state = homeState,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onMenuActionClick = onMenuActionClick,
)
// This overlaid view will only be visible when state.displaySearchResults is true
RoomListSearchView(
state = state.searchState,
eventSink = state.eventSink,
hideInvitesAvatars = state.hideInvitesAvatars,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
modifier = Modifier
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault)
)
acceptDeclineInviteView()
} }
} }

View file

@ -81,7 +81,6 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -112,7 +111,6 @@ class MessagesPresenter(
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>, private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
private val roomCallStatePresenter: Presenter<RoomCallState>, private val roomCallStatePresenter: Presenter<RoomCallState>,
private val roomMemberModerationPresenter: Presenter<RoomMemberModerationState>, private val roomMemberModerationPresenter: Presenter<RoomMemberModerationState>,
private val syncService: SyncService,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper, private val clipboardHelper: ClipboardHelper,
@ -193,7 +191,6 @@ class MessagesPresenter(
showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L
} }
} }
val isOnline by syncService.isOnline.collectAsState()
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
@ -250,8 +247,8 @@ class MessagesPresenter(
roomName = roomInfo.name, roomName = roomInfo.name,
roomAvatar = roomAvatar, roomAvatar = roomAvatar,
heroes = heroes, heroes = heroes,
composerState = composerState,
userEventPermissions = userEventPermissions, userEventPermissions = userEventPermissions,
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState, voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState, timelineState = timelineState,
timelineProtectionState = timelineProtectionState, timelineProtectionState = timelineProtectionState,
@ -261,19 +258,17 @@ class MessagesPresenter(
customReactionState = customReactionState, customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState, reactionSummaryState = reactionSummaryState,
readReceiptBottomSheetState = readReceiptBottomSheetState, readReceiptBottomSheetState = readReceiptBottomSheetState,
hasNetworkConnection = isOnline,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value, inviteProgress = inviteProgress.value,
showReinvitePrompt = showReinvitePrompt,
enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING, enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
appName = buildMeta.applicationName,
roomCallState = roomCallState, roomCallState = roomCallState,
appName = buildMeta.applicationName,
pinnedMessagesBannerState = pinnedMessagesBannerState, pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState, dmUserVerificationState = dmUserVerificationState,
roomMemberModerationState = roomMemberModerationState, roomMemberModerationState = roomMemberModerationState,
successorRoom = roomInfo.successorRoom, successorRoom = roomInfo.successorRoom
eventSink = { handleEvents(it) } ) { handleEvents(it) }
)
} }
@Composable @Composable

View file

@ -44,7 +44,6 @@ data class MessagesState(
val customReactionState: CustomReactionState, val customReactionState: CustomReactionState,
val reactionSummaryState: ReactionSummaryState, val reactionSummaryState: ReactionSummaryState,
val readReceiptBottomSheetState: ReadReceiptBottomSheetState, val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val inviteProgress: AsyncData<Unit>, val inviteProgress: AsyncData<Unit>,
val showReinvitePrompt: Boolean, val showReinvitePrompt: Boolean,

View file

@ -56,7 +56,6 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState> override val values: Sequence<MessagesState>
get() = sequenceOf( get() = sequenceOf(
aMessagesState(), aMessagesState(),
aMessagesState(hasNetworkConnection = false),
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)), aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)), aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
aMessagesState(showReinvitePrompt = true), aMessagesState(showReinvitePrompt = true),
@ -108,7 +107,6 @@ fun aMessagesState(
actionListState: ActionListState = anActionListState(), actionListState: ActionListState = anActionListState(),
customReactionState: CustomReactionState = aCustomReactionState(), customReactionState: CustomReactionState = aCustomReactionState(),
reactionSummaryState: ReactionSummaryState = aReactionSummaryState(), reactionSummaryState: ReactionSummaryState = aReactionSummaryState(),
hasNetworkConnection: Boolean = true,
showReinvitePrompt: Boolean = false, showReinvitePrompt: Boolean = false,
roomCallState: RoomCallState = aStandByCallState(), roomCallState: RoomCallState = aStandByCallState(),
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
@ -132,7 +130,6 @@ fun aMessagesState(
actionListState = actionListState, actionListState = actionListState,
customReactionState = customReactionState, customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState, reactionSummaryState = reactionSummaryState,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = null, snackbarMessage = null,
inviteProgress = AsyncData.Uninitialized, inviteProgress = AsyncData.Uninitialized,
showReinvitePrompt = showReinvitePrompt, showReinvitePrompt = showReinvitePrompt,

View file

@ -71,7 +71,6 @@ import io.element.android.features.messages.impl.topbars.MessagesViewTopBar
import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.topbars.ThreadTopBar
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout
@ -84,6 +83,7 @@ import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@ -123,6 +123,8 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn) KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
HideKeyboardWhenDisposed()
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
@ -180,8 +182,6 @@ fun MessagesView(
Scaffold( Scaffold(
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
topBar = { topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
if (state.timelineState.timelineMode is Timeline.Mode.Thread) { if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
ThreadTopBar( ThreadTopBar(
roomName = state.roomName, roomName = state.roomName,
@ -203,7 +203,6 @@ fun MessagesView(
onJoinCallClick = onJoinCallClick, onJoinCallClick = onJoinCallClick,
) )
} }
}
}, },
content = { padding -> content = { padding ->
Box( Box(

View file

@ -84,7 +84,6 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
@ -130,7 +129,6 @@ class MessagesPresenterTest {
.isEqualTo(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)) .isEqualTo(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))
assertThat(initialState.userEventPermissions.canSendMessage).isTrue() assertThat(initialState.userEventPermissions.canSendMessage).isTrue()
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue() assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
@ -1274,31 +1272,30 @@ class MessagesPresenterTest {
addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()), addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
): MessagesPresenter { ): MessagesPresenter {
return MessagesPresenter( return MessagesPresenter(
navigator = navigator,
room = joinedRoom, room = joinedRoom,
composerPresenter = messageComposerPresenter, composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenterFactory = FakeDefaultVoiceMessageComposerPresenterFactory(backgroundScope), voiceMessageComposerPresenterFactory = FakeDefaultVoiceMessageComposerPresenterFactory(backgroundScope),
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) }, timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() }, timelineProtectionPresenter = { aTimelineProtectionState() },
identityChangeStatePresenter = { anIdentityChangeState() },
linkPresenter = { aLinkState() },
actionListPresenter = { anActionListState(eventSink = actionListEventSink) }, actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
customReactionPresenter = { aCustomReactionState() }, customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = { aReactionSummaryState() }, reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
identityChangeStatePresenter = { anIdentityChangeState() },
linkPresenter = { aLinkState() },
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
roomCallStatePresenter = { aStandByCallState() }, roomCallStatePresenter = { aStandByCallState() },
roomMemberModerationPresenter = roomMemberModerationPresenter, roomMemberModerationPresenter = roomMemberModerationPresenter,
syncService = FakeSyncService(),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
navigator = navigator,
clipboardHelper = clipboardHelper,
buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
clipboardHelper = clipboardHelper,
htmlConverterProvider = FakeHtmlConverterProvider(), htmlConverterProvider = FakeHtmlConverterProvider(),
buildMeta = aBuildMeta(),
timelineController = TimelineController(joinedRoom, timeline), timelineController = TimelineController(joinedRoom, timeline),
permalinkParser = permalinkParser, permalinkParser = permalinkParser,
encryptionService = encryptionService,
analyticsService = analyticsService, analyticsService = analyticsService,
encryptionService = encryptionService,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
addRecentEmoji = addRecentEmoji, addRecentEmoji = addRecentEmoji,
) )

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -32,7 +33,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
internal fun Indicator( internal fun ConnectivityIndicator(
verticalPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
@ -40,7 +42,7 @@ internal fun Indicator(
.fillMaxWidth() .fillMaxWidth()
.background(ElementTheme.colors.bgSubtlePrimary) .background(ElementTheme.colors.bgSubtlePrimary)
.statusBarsPadding() .statusBarsPadding()
.padding(vertical = 6.dp), .padding(vertical = verticalPadding),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -61,6 +63,6 @@ internal fun Indicator(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun IndicatorPreview() = ElementPreview { internal fun ConnectivityIndicatorPreview() = ElementPreview {
Indicator() ConnectivityIndicator(verticalPadding = 6.dp)
} }

View file

@ -16,55 +16,58 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private val INDICATOR_VERTICAL_PADDING = 6.dp
/** /**
* A view that displays a connectivity indicator when the device is offline, passing the padding * A view that displays a connectivity indicator when the device is offline.
* needed to make sure the status bar is not overlapped to its content views.
*/ */
@Composable @Composable
fun ConnectivityIndicatorContainer( fun ConnectivityIndicatorContainer(
isOnline: Boolean, isOnline: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable (topPadding: Dp) -> Unit = {}, content: @Composable (Modifier) -> Unit = {},
) { ) {
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
Column(modifier = modifier) {
val statusBarTopPadding = if (LocalInspectionMode.current) { val statusBarTopPadding = if (LocalInspectionMode.current) {
// Needed to get valid UI previews // Needed to get valid UI previews
24.dp 24.dp
} else { } else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + INDICATOR_VERTICAL_PADDING
} }
val target = remember(isIndicatorVisible.targetState, statusBarTopPadding) { val target = if (isIndicatorVisible.targetState) statusBarTopPadding else 0.dp
if (!isIndicatorVisible.targetState) 0.dp else statusBarTopPadding val topWindowInset by animateDpAsState(
} targetValue = target,
val animationStateOffset by animateDpAsState( animationSpec = spring(
targetValue = target, stiffness = Spring.StiffnessMediumLow,
animationSpec = spring( visibilityThreshold = 1.dp,
stiffness = Spring.StiffnessMediumLow, ),
visibilityThreshold = 1.dp, label = "insets-animation",
), )
label = "insets-animation", // Display the network indicator with an animation
) AnimatedVisibility(
visibleState = isIndicatorVisible,
content(animationStateOffset) enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
// Display the network indicator with an animation ) {
AnimatedVisibility( ConnectivityIndicator(verticalPadding = INDICATOR_VERTICAL_PADDING)
visibleState = isIndicatorVisible, }
enter = fadeIn() + expandVertically(), // Consume the window insets to avoid double padding.
exit = fadeOut() + shrinkVertically(), content(
) { Modifier.consumeWindowInsets(PaddingValues(top = topWindowInset))
Indicator(modifier) )
} }
} }

View file

@ -1,65 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.networkmonitor.api.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
/**
* A view that displays a connectivity indicator when the device is offline, adding a default
* padding to make sure the status bar is not overlapped.
*/
@Composable
fun ConnectivityIndicatorView(
isOnline: Boolean,
) {
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline }
// Display the network indicator with an animation
AnimatedVisibility(
visibleState = isIndicatorVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Indicator()
}
// Show missing status bar padding when the indicator is not visible
AnimatedVisibility(
visibleState = isStatusBarPaddingVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
StatusBarPaddingSpacer()
}
}
@Composable
private fun StatusBarPaddingSpacer(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.statusBarsPadding())
}
@PreviewsDayNight
@Composable
internal fun ConnectivityIndicatorViewPreview() {
ElementPreview {
ConnectivityIndicatorView(isOnline = false)
}
}