Merge remote-tracking branch 'origin/develop' into bma/brandColorFix

This commit is contained in:
Benoit Marty 2025-10-22 12:27:00 +02:00
commit 24fc74caf4
44 changed files with 159 additions and 267 deletions

View file

@ -56,6 +56,7 @@ import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.networkmonitor.api.NetworkMonitor
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.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
@ -82,6 +83,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.toRoomIdOrAlias
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.VerificationRequest
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -128,6 +130,7 @@ class LoggedInFlowNode(
private val sessionEnterpriseService: SessionEnterpriseService,
private val networkMonitor: NetworkMonitor,
private val notificationConversationService: NotificationConversationService,
private val syncService: SyncService,
private val enterpriseService: EnterpriseService,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
@ -556,11 +559,17 @@ class LoggedInFlowNode(
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
Box(modifier = modifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { 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 androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.RoomMembershipObserver
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 kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@ -71,7 +69,6 @@ class RoomFlowNode(
private val client: MatrixClient,
private val joinRoomEntryPoint: JoinRoomEntryPoint,
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver,
private val spaceEntryPoint: SpaceEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>(
@ -222,10 +219,8 @@ class RoomFlowNode(
}
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView(
state = LoadingRoomState.Loading,
hasNetworkConnection = isOnline,
onBackClick = { navigateUp() },
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.di.SessionScope
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.LoadingRoomStateFlowFactory
import kotlinx.coroutines.flow.distinctUntilChanged
@ -50,7 +49,6 @@ class JoinedRoomFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
private val syncService: SyncService,
) :
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
backstack = BackStack(
@ -116,12 +114,10 @@ class JoinedRoomFlowNode(
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView(
state = loadingRoomState,
hasNetworkConnection = isOnline,
modifier = modifier,
onBackClick = onBackClick
onBackClick = onBackClick,
modifier = modifier
)
}

View file

@ -8,8 +8,6 @@
package io.element.android.appnav.room.joined
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.padding
@ -21,7 +19,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
@ -38,17 +35,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LoadingRoomNodeView(
state: LoadingRoomState,
hasNetworkConnection: Boolean,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = {
Column {
ConnectivityIndicatorView(isOnline = hasNetworkConnection)
LoadingRoomTopBar(onBackClick)
}
LoadingRoomTopBar(onBackClick)
},
content = { padding ->
Box(
@ -85,7 +78,6 @@ private fun LoadingRoomTopBar(
title = {
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 {
LoadingRoomNodeView(
state = state,
onBackClick = {},
hasNetworkConnection = false
onBackClick = {}
)
}

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
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.search.RoomListSearchView
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.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -84,56 +82,47 @@ fun HomeView(
val state: RoomListState = homeState.roomListState
val coroutineScope = rememberCoroutineScope()
val firstThrottler = remember { FirstThrottler(300, coroutineScope) }
ConnectivityIndicatorContainer(
modifier = modifier,
isOnline = homeState.hasNetworkConnection,
) { topPadding ->
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,
Box(modifier) {
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
RoomListContextMenu(
contextMenu = state.contextMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
hideInvitesAvatars = state.hideInvitesAvatars,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
modifier = Modifier
.statusBarsPadding()
.padding(top = topPadding)
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault)
onRoomSettingsClick = onRoomSettingsClick,
onReportRoomClick = onReportRoomClick,
)
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

@ -20,7 +20,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -51,7 +50,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.copy
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@ -157,7 +155,6 @@ private fun RoomListSearchContent(
focusRequester.saveFocusedChild()
}
},
windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)
)
}
) { padding ->

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

View file

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

View file

@ -56,7 +56,6 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
aMessagesState(hasNetworkConnection = false),
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
aMessagesState(showReinvitePrompt = true),
@ -108,7 +107,6 @@ fun aMessagesState(
actionListState: ActionListState = anActionListState(),
customReactionState: CustomReactionState = aCustomReactionState(),
reactionSummaryState: ReactionSummaryState = aReactionSummaryState(),
hasNetworkConnection: Boolean = true,
showReinvitePrompt: Boolean = false,
roomCallState: RoomCallState = aStandByCallState(),
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
@ -132,7 +130,6 @@ fun aMessagesState(
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = null,
inviteProgress = AsyncData.Uninitialized,
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.voicemessages.composer.VoiceMessagePermissionRationaleDialog
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.designsystem.atomic.molecules.ComposerAlertMolecule
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.Scaffold
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.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@ -123,6 +123,8 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
HideKeyboardWhenDisposed()
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
@ -180,8 +182,6 @@ fun MessagesView(
Scaffold(
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
ThreadTopBar(
roomName = state.roomName,
@ -203,7 +203,6 @@ fun MessagesView(
onJoinCallClick = onJoinCallClick,
)
}
}
},
content = { padding ->
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.aRoomInfo
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.aTimelineItemDebugInfo
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))
assertThat(initialState.userEventPermissions.canSendMessage).isTrue()
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.showReinvitePrompt).isFalse()
@ -1274,31 +1272,30 @@ class MessagesPresenterTest {
addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
): MessagesPresenter {
return MessagesPresenter(
navigator = navigator,
room = joinedRoom,
composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenterFactory = FakeDefaultVoiceMessageComposerPresenterFactory(backgroundScope),
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
identityChangeStatePresenter = { anIdentityChangeState() },
linkPresenter = { aLinkState() },
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
identityChangeStatePresenter = { anIdentityChangeState() },
linkPresenter = { aLinkState() },
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
roomCallStatePresenter = { aStandByCallState() },
roomMemberModerationPresenter = roomMemberModerationPresenter,
syncService = FakeSyncService(),
snackbarDispatcher = SnackbarDispatcher(),
navigator = navigator,
clipboardHelper = clipboardHelper,
buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers,
clipboardHelper = clipboardHelper,
htmlConverterProvider = FakeHtmlConverterProvider(),
buildMeta = aBuildMeta(),
timelineController = TimelineController(joinedRoom, timeline),
permalinkParser = permalinkParser,
encryptionService = encryptionService,
analyticsService = analyticsService,
encryptionService = encryptionService,
featureFlagService = featureFlagService,
addRecentEmoji = addRecentEmoji,
)

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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
@Composable
internal fun Indicator(
internal fun ConnectivityIndicator(
verticalPadding: Dp,
modifier: Modifier = Modifier,
) {
Row(
@ -40,7 +42,7 @@ internal fun Indicator(
.fillMaxWidth()
.background(ElementTheme.colors.bgSubtlePrimary)
.statusBarsPadding()
.padding(vertical = 6.dp),
.padding(vertical = verticalPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
@ -61,6 +63,6 @@ internal fun Indicator(
@PreviewsDayNight
@Composable
internal fun IndicatorPreview() = ElementPreview {
Indicator()
internal fun ConnectivityIndicatorPreview() = ElementPreview {
ConnectivityIndicator(verticalPadding = 6.dp)
}

View file

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

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)
}
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:513b6de9643106b0c021a60a2791e6227ce81ff1089338a5d3de8ba525e353ab
size 8389
oid sha256:c9583a29e680bee98819258e5b7356dd8677144957d5869da72c47685d986e87
size 6494

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7aebe39a70a79e025d392ffb6c97793c828cc29acf7700461399e6cbea0d27a3
size 10512
oid sha256:f5c19a8b26c9187c9bfd630242b15370ec8f0b874f9d3913ce53c743c0e0bdcd
size 8574

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89bd61aa2d74f720a2b1d73fa5379e712fc61e423cbcbfab83cb291bdbd75766
size 8013
oid sha256:74ad11143fc70164f67a8326cb87ce90a2c8730317540810ea138e39fc9093fa
size 6306

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:831058152c5c6e691afecab498ae07f4b35f7b75438a4efa1841cc04922bb7be
size 10037
oid sha256:bde612ade7298aeef44bbc480ee9d742ccb8c94108e21dcae9e804c744032a08
size 8308

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc9668d1caef19e9a4fe76ff4ed0eab8107aa493c3adcaef7ebbbb878925ac62
size 66408
oid sha256:6a249702f230f3dc0c9ee097fc11496df64c11436680e3f790d1fbf9051c4119
size 65098

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bfc8aff1305c39b400c95831a6b07f00c376c328b3ad2281cf4b6b74970f67c
size 61538
oid sha256:ea48147772f8fa99aca0345a5a98f8d2ea6fd26793edab55f7d920a7e3f56414
size 60213

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67981be91a4736680a69b9d2dfe11cab3a69e0d375cf2589e38bed3630b9fc12
size 53199

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4dd9bacf4f467ac0f2b1050fcf5a4246e520674009471c05d2e32eb5d1d1f7bf
size 57599
oid sha256:3917d9d2d5c67e840e613cc8dfc39212e11853ac8710e87af554e20dea31d34d
size 39439

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3917d9d2d5c67e840e613cc8dfc39212e11853ac8710e87af554e20dea31d34d
size 39439
oid sha256:432fae13ece80f287afc16411ca162c2ea0b19d66b148adcb13c7710ac442b5d
size 60866

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:432fae13ece80f287afc16411ca162c2ea0b19d66b148adcb13c7710ac442b5d
size 60866
oid sha256:cc380e2b1c75c5b38ac410a271ddb9c02d41ebf9ff037fc59f20d23d87241590
size 56864

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc380e2b1c75c5b38ac410a271ddb9c02d41ebf9ff037fc59f20d23d87241590
size 56864
oid sha256:0cb2168ef240a788416f0f7d5cf949b282b9c4cf76338c4d547562b15ff3dba1
size 55322

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cb2168ef240a788416f0f7d5cf949b282b9c4cf76338c4d547562b15ff3dba1
size 55322
oid sha256:17ec77b5053bff9057d6b122140f53c5384abac7998bbc8d770db95a04bc0062
size 59963

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17ec77b5053bff9057d6b122140f53c5384abac7998bbc8d770db95a04bc0062
size 59963
oid sha256:ac595ce33affb3eec6c465fd68ab1fa57eb942f58944cc16e5e01892a21ba4a7
size 50323

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac595ce33affb3eec6c465fd68ab1fa57eb942f58944cc16e5e01892a21ba4a7
size 50323
oid sha256:2831b5841aac7cdec50829002aa46fabf530163e0600185f32da0793c3637fb5
size 61695

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2831b5841aac7cdec50829002aa46fabf530163e0600185f32da0793c3637fb5
size 61695
oid sha256:ac8dd9bc435999370adc7b3d4a0eb96c7eeedcb500818b44e05aafe759b720ba
size 63860

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac8dd9bc435999370adc7b3d4a0eb96c7eeedcb500818b44e05aafe759b720ba
size 63860
oid sha256:67981be91a4736680a69b9d2dfe11cab3a69e0d375cf2589e38bed3630b9fc12
size 53199

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fb56dba32bfc9db1ddf201d118c580b21143f1f74575bda95ff43fa0914b2a3
size 52344

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e64f40aac075bd075f92cf98ffa864ced12b3a43a874d72cd5f293202859623
size 55090
oid sha256:beb8fc32a4b048ba5c5053ad5d62da55aac8072234af59a2a1af6dd3b03be74c
size 37401

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:beb8fc32a4b048ba5c5053ad5d62da55aac8072234af59a2a1af6dd3b03be74c
size 37401
oid sha256:d654cda7c0f4df547ccf00fc3a8121d0c5bd2b337be17b71f7b95e5987833032
size 58473

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d654cda7c0f4df547ccf00fc3a8121d0c5bd2b337be17b71f7b95e5987833032
size 58473
oid sha256:794ef2d62ae364d456930dc7879741848c8f65696a5b7676b032da70e3ef7330
size 51156

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:794ef2d62ae364d456930dc7879741848c8f65696a5b7676b032da70e3ef7330
size 51156
oid sha256:96bdb7695db3a0476480e4e675dfc5d088d210b519a3f6b9592bbe6ec18f4ec6
size 52985

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96bdb7695db3a0476480e4e675dfc5d088d210b519a3f6b9592bbe6ec18f4ec6
size 52985
oid sha256:e12bc7c029daee03dc31bc889c37ebbd597ac61bf18bf13867a867983a79bb2b
size 54085

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e12bc7c029daee03dc31bc889c37ebbd597ac61bf18bf13867a867983a79bb2b
size 54085
oid sha256:72023d9b9eaade788d33ef6c123a89edc079c4cc543421c256676b75fba6adc1
size 44505

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72023d9b9eaade788d33ef6c123a89edc079c4cc543421c256676b75fba6adc1
size 44505
oid sha256:1ad3ae1b79bf30293e306ebac5806d328569f03b1370adb39c6dec143d8789c1
size 59027

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1ad3ae1b79bf30293e306ebac5806d328569f03b1370adb39c6dec143d8789c1
size 59027
oid sha256:83a7cce4a92980220d2afd24d895154d1a463e685c9f81ee91707ae78c12809d
size 64594

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83a7cce4a92980220d2afd24d895154d1a463e685c9f81ee91707ae78c12809d
size 64594
oid sha256:7fb56dba32bfc9db1ddf201d118c580b21143f1f74575bda95ff43fa0914b2a3
size 52344

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d7cbce14d66654ba69cd6a154eefecebacf1556f29c5ccefb1d6f1975270814
size 5427

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14927029a06e7f4f5346573d5020fa7582ff716dd552db969bb84304a879d4f0
size 5384