diff --git a/app/src/androidTest/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenTest.kt b/app/src/androidTest/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenTest.kt index ed39ea6a534..2813ccc02ba 100644 --- a/app/src/androidTest/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenTest.kt +++ b/app/src/androidTest/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenTest.kt @@ -17,13 +17,11 @@ */ package com.wire.android.ui.userprofile.other -import androidx.compose.material3.MaterialTheme import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import com.wire.android.ui.WireTestTheme import com.wire.android.ui.connection.CONNECTION_ACTION_BUTTONS_TEST_TAG import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.userprofile.other.OtherUserStubs.provideState import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant @@ -40,7 +38,6 @@ class OtherUserProfileScreenTest { WireTestTheme { ContentFooter( state = provideState(withExpireAt = Instant.DISTANT_FUTURE.toEpochMilliseconds()), - maxBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation ) } } @@ -54,7 +51,6 @@ class OtherUserProfileScreenTest { WireTestTheme { ContentFooter( state = provideState(withUserName = "", withFullName = ""), - maxBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation ) } } @@ -68,7 +64,6 @@ class OtherUserProfileScreenTest { WireTestTheme { ContentFooter( state = provideState(withMembership = Membership.Service), - maxBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index ad012d2753b..791a58e8727 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt @@ -197,7 +197,6 @@ private fun MainLoginContent( start = MaterialTheme.wireDimensions.spacing16x, end = MaterialTheme.wireDimensions.spacing16x ), - divider = {} // no divider ) } }, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/CollapsingTopBarScaffold.kt b/app/src/main/kotlin/com/wire/android/ui/common/CollapsingTopBarScaffold.kt index 1b83267cfcc..86a632da08e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/CollapsingTopBarScaffold.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/CollapsingTopBarScaffold.kt @@ -18,23 +18,29 @@ package com.wire.android.ui.common +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.rememberSwipeableState -import androidx.compose.material.swipeable +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.FabPosition import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -43,54 +49,82 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions +import kotlin.math.min import kotlin.math.roundToInt /** - * @param maxBarElevation maximum elevation value available * @param topBarHeader topmost part of the top bar, usually the TopAppBar, [topBarCollapsing] element slides under it, * the lambda receives elevation value for the [topBarHeader] * @param topBarCollapsing collapsing part of the top bar + * @param modifier modifier for the scaffold + * @param maxBarElevation maximum elevation value available + * @param topBarBackgroundColor background color of the top bar * @param topBarFooter bar under the [topBarCollapsing], moves with it and ends up directly under [topBarHeader] when collapsed - * @param bottomBar bottom bar of the screen, typically a [NavigationBar] + * @param bottomBar bottom bar of the screen * @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton] * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition]. - * @param isSwipeable if true then collapsing is enabled + * @param collapsingEnabled if true then collapsing is enabled * @param snapOnFling on collapsing fling, only close the collapsible and don't carry the velocity to the scrollable - * @param keepElevationWhenCollapsed if true then keep showing elevation also when scrolling children after top bar is already collapsed; - * if false then hide elevation when approaching the end of the collapsing and don't show it when scrolling children + * @param contentLazyListState state of the content lazy list, used for calculating elevations * @param content content of the screen */ -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun CollapsingTopBarScaffold( - topBarHeader: @Composable (elevation: Dp) -> Unit, + topBarHeader: @Composable () -> Unit, topBarCollapsing: @Composable () -> Unit, modifier: Modifier = Modifier, maxBarElevation: Dp = MaterialTheme.wireDimensions.topBarShadowElevation, + topBarBackgroundColor: Color = MaterialTheme.wireColorScheme.background, topBarFooter: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, - isSwipeable: Boolean = true, + collapsingEnabled: Boolean = true, snapOnFling: Boolean = true, - keepElevationWhenCollapsed: Boolean = false, + contentLazyListState: LazyListState? = null, content: @Composable () -> Unit, ) { + val density = LocalDensity.current + var hasFooterSegment by remember { mutableStateOf(false) } + var hasCollapsingSegment by remember { mutableStateOf(false) } val maxBarElevationPx = with(LocalDensity.current) { maxBarElevation.toPx() } - val swipeableState = rememberSwipeableState(initialValue = State.EXPANDED) // TODO: migrate to AnchoredDraggable - var nestedOffsetState by rememberSaveable { mutableStateOf(0f) } - var collapsingHeight by rememberSaveable { mutableStateOf(0) } - val topBarElevationState by remember { + val anchoredDraggableState = remember { + AnchoredDraggableState( + initialValue = State.EXPANDED, + anchors = calculateAnchors(collapsingEnabled, 0), + positionalThreshold = { totalDistance: Float -> totalDistance * 0.5f }, + velocityThreshold = { with(density) { 125.dp.toPx() } }, + snapAnimationSpec = SpringSpec(), + decayAnimationSpec = splineBasedDecay(density), + ) + } + val topBarElevationState by remember(contentLazyListState, maxBarElevationPx) { derivedStateOf { - if (keepElevationWhenCollapsed) { - val value = -(swipeableState.offset.value + nestedOffsetState) - listOf(value, maxBarElevationPx).minOrNull() ?: 0f - } else { - // hide elevation when approaching the end of the collapsing and don't show it when scrolling children - val value = -swipeableState.offset.value - listOf(value, collapsingHeight - value, maxBarElevationPx).minOrNull() ?: 0f + with(density) { + val collapsingHeight = anchoredDraggableState.calculateCollapsingHeight() + val offset = -anchoredDraggableState.offset + val scaledOffset = if (collapsingHeight > 0f && collapsingHeight < maxBarElevationPx) { + // if collapsingHeight is less than maxBarElevationPx then the offset needs to be scaled + (offset / collapsingHeight) * maxBarElevationPx + } else { + offset + } + + // hide top bar elevation when approaching the end of the collapsing + listOf(maxBarElevationPx, scaledOffset, collapsingHeight - scaledOffset).min().toDp() + } + } + } + val topBarContainerElevationState by remember(contentLazyListState, maxBarElevationPx) { + derivedStateOf { + with(density) { + // start adding elevation to the whole container after fully collapsed + contentLazyListState.calculateContentOffset(maxBarElevationPx).toDp() } } } @@ -98,21 +132,31 @@ fun CollapsingTopBarScaffold( val nestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = - if (available.y < 0) swipeableState.performDrag(available.y).toOffset() - else Offset.Zero + if (available.y < 0 && collapsingEnabled) { + anchoredDraggableState.dispatchRawDelta(delta = available.y).toOffset() + } else { + Offset.Zero + } override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = - swipeableState.performDrag(available.y).toOffset().also { nestedOffsetState += consumed.y } + if (collapsingEnabled) { + anchoredDraggableState.dispatchRawDelta(delta = available.y).toOffset() + } else { + Offset.Zero + } override suspend fun onPreFling(available: Velocity): Velocity = - if (available.y < 0 && swipeableState.currentValue != State.COLLAPSED) { - swipeableState.performFling(available.y) - if (snapOnFling) available - else Velocity.Zero - } else Velocity.Zero + if (available.y < 0 && anchoredDraggableState.currentValue != State.COLLAPSED && collapsingEnabled) { + anchoredDraggableState.settle(velocity = available.y) + if (snapOnFling) available else Velocity.Zero + } else { + Velocity.Zero + } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - swipeableState.performFling(velocity = available.y) + if (collapsingEnabled) { + anchoredDraggableState.settle(velocity = available.y) + } return super.onPostFling(consumed, available) } @@ -121,22 +165,25 @@ fun CollapsingTopBarScaffold( WireScaffold( modifier = modifier, - topBar = { topBarHeader(with(LocalDensity.current) { topBarElevationState.toDp() }) }, + topBar = { + Surface( + color = topBarBackgroundColor, + shadowElevation = if (hasFooterSegment || hasCollapsingSegment) topBarElevationState else topBarContainerElevationState, + ) { + topBarHeader() + } + }, bottomBar = bottomBar, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, content = { internalPadding -> Layout( - modifier = if (isSwipeable) { + modifier = if (collapsingEnabled && anchoredDraggableState.anchors.size > 1) { Modifier .padding(internalPadding) - .swipeable( - state = swipeableState, + .anchoredDraggable( + state = anchoredDraggableState, orientation = Orientation.Vertical, - anchors = mapOf(0f to State.EXPANDED).let { - if (collapsingHeight > 0) it.plus(-collapsingHeight.toFloat() to State.COLLAPSED) - else it - } ) .nestedScroll(nestedScrollConnection) } else { @@ -144,20 +191,47 @@ fun CollapsingTopBarScaffold( .padding(internalPadding) }, content = { - Box(modifier = Modifier.layoutId("topBarCollapsing")) { topBarCollapsing() } - Box(modifier = Modifier.layoutId("topBarFooter")) { topBarFooter() } - Box(modifier = Modifier.layoutId("content")) { content() } + Surface( + modifier = Modifier.fillMaxWidth().layoutId("topBarContainer"), + color = topBarBackgroundColor, + shadowElevation = topBarContainerElevationState + ) {} + Box(modifier = Modifier.fillMaxWidth().layoutId("topBarCollapsing")) { + topBarCollapsing() + } + Box(modifier = Modifier.fillMaxWidth().layoutId("topBarFooter")) { + topBarFooter() + } + Box(modifier = Modifier.fillMaxWidth().layoutId("content")) { + content() + } }, measurePolicy = { measurables, constraints -> val measureConstraints = constraints.copy(minWidth = 0, minHeight = 0) val collapsingPlaceable = measurables.first { it.layoutId == "topBarCollapsing" }.measure(measureConstraints) val footerPlaceable = measurables.first { it.layoutId == "topBarFooter" }.measure(measureConstraints) - val contentPlaceable = measurables.first { it.layoutId == "content" } - .measure(measureConstraints.copy(maxHeight = constraints.maxHeight - footerPlaceable.height)) - collapsingHeight = collapsingPlaceable.height + val containerPlaceable = measurables.first { it.layoutId == "topBarContainer" }.measure( + measureConstraints.copy( + minHeight = collapsingPlaceable.height + footerPlaceable.height, + maxHeight = collapsingPlaceable.height + footerPlaceable.height + ) + ) + val contentPlaceable = measurables.first { it.layoutId == "content" }.measure( + measureConstraints.copy( + maxHeight = if (collapsingEnabled) { + constraints.maxHeight - footerPlaceable.height + } else { + constraints.maxHeight - collapsingPlaceable.height - footerPlaceable.height + } + ) + ) + hasCollapsingSegment = collapsingPlaceable.height > 0 + hasFooterSegment = footerPlaceable.height > 0 + anchoredDraggableState.updateAnchors(calculateAnchors(collapsingEnabled, collapsingPlaceable.height)) layout(constraints.maxWidth, constraints.maxHeight) { - val swipeOffset = swipeableState.offset.value.roundToInt() + val swipeOffset = anchoredDraggableState.offset.roundToInt() contentPlaceable.placeRelative(0, collapsingPlaceable.height + footerPlaceable.height + swipeOffset) + containerPlaceable.placeRelative(0, swipeOffset) footerPlaceable.placeRelative(0, collapsingPlaceable.height + swipeOffset) collapsingPlaceable.placeRelative(0, swipeOffset) } @@ -167,6 +241,23 @@ fun CollapsingTopBarScaffold( ) } +private fun LazyListState?.calculateContentOffset(maxValue: Float) = when { + this == null -> 0f + firstVisibleItemIndex == 0 -> min(firstVisibleItemScrollOffset.toFloat(), maxValue) + else -> maxValue +} + +@OptIn(ExperimentalFoundationApi::class) +private fun AnchoredDraggableState.calculateCollapsingHeight() = anchors.positionOf(State.COLLAPSED).let { + if (it.isNaN()) 0f else -it +} + +@OptIn(ExperimentalFoundationApi::class) +private fun calculateAnchors(isSwipeable: Boolean, collapsingHeight: Int) = DraggableAnchors { + State.EXPANDED at 0f + if (isSwipeable && collapsingHeight > 0) State.COLLAPSED at -collapsingHeight.toFloat() +} + private enum class State { EXPANDED, COLLAPSED; diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireTabRow.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireTabRow.kt index 2e3bd25fa5d..f64ca4a9203 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireTabRow.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireTabRow.kt @@ -48,7 +48,12 @@ fun WireTabRow( onTabChange: (Int) -> Unit, modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.background, - divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, + divider: @Composable () -> Unit = @Composable { + HorizontalDivider( + color = colorsScheme().outline, + thickness = dimensions().dividerThickness + ) + }, upperCaseTitles: Boolean = true ) { TabRow( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 413facb7b79..74dcec0ba5b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -21,12 +21,13 @@ package com.wire.android.ui.home import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.animateContentSize +import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn @@ -276,19 +277,21 @@ fun HomeContent( content = { CollapsingTopBarScaffold( snapOnFling = false, - keepElevationWhenCollapsed = true, - topBarHeader = { elevation -> - Column(modifier = Modifier.animateContentSize()) { - AnimatedVisibility(visible = !searchBarState.isSearchActive) { - HomeTopBar( - userAvatarData = homeState.userAvatarData, - title = stringResource(currentNavigationItem.title), - elevation = elevation, - withLegalHoldIndicator = homeState.shouldDisplayLegalHoldIndicator, - onHamburgerMenuClick = ::openDrawer, - onNavigateToSelfUserProfile = onSelfUserClick - ) - } + topBarHeader = { + AnimatedVisibility( + modifier = Modifier.background(MaterialTheme.colorScheme.background), + visible = !searchBarState.isSearchActive, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + HomeTopBar( + userAvatarData = homeState.userAvatarData, + title = stringResource(currentNavigationItem.title), + elevation = dimensions().spacing0x, // CollapsingTopBarScaffold manages applied elevation + withLegalHoldIndicator = homeState.shouldDisplayLegalHoldIndicator, + onHamburgerMenuClick = ::openDrawer, + onNavigateToSelfUserProfile = onSelfUserClick, + ) } }, topBarCollapsing = { @@ -301,7 +304,8 @@ fun HomeContent( ) } }, - isSwipeable = !searchBarState.isSearchActive, + collapsingEnabled = !searchBarState.isSearchActive, + contentLazyListState = homeStateHolder.currentLazyListState, content = { /** * This "if" is a workaround, otherwise it can crash because of the SubcomposeLayout's nature. diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt index 68616554be7..459a3127f52 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.home +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.rememberDrawerState @@ -42,8 +44,10 @@ class HomeStateHolder( val drawerState: DrawerState, val currentNavigationItem: HomeDestination, val searchBarState: SearchBarState, - val navigator: Navigator + val navigator: Navigator, + lazyListStates: Map, ) { + val currentLazyListState = lazyListStates[currentNavigationItem] ?: error("No LazyListState found for $currentNavigationItem") fun closeDrawer() { coroutineScope.launch { @@ -67,11 +71,11 @@ fun rememberHomeScreenState( }, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), ): HomeStateHolder { + val searchBarState = rememberSearchbarState() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route val currentNavigationItem = currentRoute?.let { HomeDestination.fromRoute(it) } ?: HomeDestination.Conversations - - val searchBarState = rememberSearchbarState() + val lazyListStates = HomeDestination.values().associateWith { rememberLazyListState() } val homeState = remember( currentNavigationItem @@ -82,7 +86,8 @@ fun rememberHomeScreenState( drawerState, currentNavigationItem, searchBarState, - navigator + navigator, + lazyListStates ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt index d2043071b3e..fb227326079 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt @@ -61,15 +61,16 @@ fun ArchiveScreen(homeStateHolder: HomeStateHolder) { navigator = navigator, conversationItemType = ConversationItemType.ALL_CONVERSATIONS, searchBarState = searchBarState, - conversationsSource = ConversationsSource.ARCHIVE + conversationsSource = ConversationsSource.ARCHIVE, + lazyListState = currentLazyListState, ) } } @Composable -fun ArchivedConversationsEmptyStateScreen() { +fun ArchivedConversationsEmptyStateScreen(modifier: Modifier = Modifier) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index c159c1a1e52..ef8a870b355 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -86,7 +85,6 @@ import com.wire.android.ui.common.calculateCurrentTab import com.wire.android.ui.common.dialogs.ArchiveConversationDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState -import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle @@ -266,7 +264,7 @@ fun GroupConversationDetailsScreen( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable private fun GroupConversationDetailsContent( conversationSheetContent: ConversationSheetContent?, @@ -290,9 +288,7 @@ private fun GroupConversationDetailsContent( val lazyListStates: List = GroupConversationDetailsTabItem.entries.map { rememberLazyListState() } val initialPageIndex = GroupConversationDetailsTabItem.OPTIONS.ordinal val pagerState = rememberPagerState(initialPage = initialPageIndex, pageCount = { GroupConversationDetailsTabItem.entries.size }) - val maxAppBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation val currentTabState by remember { derivedStateOf { pagerState.calculateCurrentTab() } } - val elevationState by remember { derivedStateOf { lazyListStates[currentTabState].topBarElevation(maxAppBarElevation) } } val conversationSheetState = rememberConversationSheetState(conversationSheetContent) @@ -331,7 +327,7 @@ private fun GroupConversationDetailsContent( CollapsingTopBarScaffold( topBarHeader = { WireCenterAlignedTopAppBar( - elevation = elevationState, + elevation = dimensions().spacing0x, // CollapsingTopBarScaffold already manages elevation titleContent = { WireTopAppBarTitle( title = stringResource(R.string.conversation_details_title), @@ -355,7 +351,8 @@ private fun GroupConversationDetailsContent( onSearchConversationMessagesClick = onSearchConversationMessagesClick, onConversationMediaClick = onConversationMediaClick, isUnderLegalHold = it.isUnderLegalHold, - onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } } + onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, + modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.spacing16x) ) } }, @@ -364,8 +361,6 @@ private fun GroupConversationDetailsContent( tabs = GroupConversationDetailsTabItem.entries, selectedTabIndex = currentTabState, onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, - modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x), - divider = {} // no divider ) }, bottomBar = { @@ -380,6 +375,7 @@ private fun GroupConversationDetailsContent( modifier = Modifier.fillMaxWidth() ) { currentTabState -> Surface( + shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation, color = MaterialTheme.wireColorScheme.background, modifier = Modifier.fillMaxWidth(), ) { @@ -401,7 +397,8 @@ private fun GroupConversationDetailsContent( } } } - } + }, + contentLazyListState = lazyListStates[currentTabState], ) { var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) } val keyboardController = LocalSoftwareKeyboardController.current diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt index 1b0f15999f2..88eda234d55 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt @@ -55,16 +55,17 @@ import com.wire.kalium.logic.data.user.SupportedProtocol fun GroupConversationParticipants( onProfilePressed: (UIParticipant) -> Unit, groupParticipantsState: GroupConversationParticipantsState, + modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState() ) { val context = LocalContext.current - Column { + Column(modifier = modifier) { LazyColumn( state = lazyListState, modifier = Modifier.fillMaxSize() ) { - item(key = "participants_list_header") { - if (BuildConfig.MLS_SUPPORT_ENABLED && BuildConfig.DEVELOPER_FEATURES_ENABLED) { + if (BuildConfig.MLS_SUPPORT_ENABLED && BuildConfig.DEVELOPER_FEATURES_ENABLED) { + item(key = "participants_list_header") { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt index ab8755310c1..7056604c256 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt @@ -137,7 +137,6 @@ private fun MessageDetailsScreenContent( selectedTabIndex = currentTabState, onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x), - divider = {} // no divider ) } }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt index 1a8703e76ff..3aa2b6e6c6a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchAllServicesScreen.kt @@ -46,17 +46,17 @@ fun SearchAllServicesScreen( searchQuery: String, onServiceClicked: (Contact) -> Unit, searchServicesViewModel: SearchServicesViewModel = hiltViewModel(), + lazyListState: LazyListState = rememberLazyListState(), ) { LaunchedEffect(key1 = searchQuery) { searchServicesViewModel.searchQueryChanged(searchQuery) } - val lazyState = rememberLazyListState() SearchAllServicesContent( searchQuery = searchServicesViewModel.state.searchQuery, onServiceClicked = onServiceClicked, result = searchServicesViewModel.state.result, - lazyListState = lazyState, + lazyListState = lazyListState, error = searchServicesViewModel.state.error, isLoading = searchServicesViewModel.state.isLoading ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt index 1fcdc698f11..f4e2e3b3746 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt @@ -21,14 +21,16 @@ package com.wire.android.ui.home.conversations.search import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -50,6 +52,7 @@ import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.TabItem import com.wire.android.ui.common.WireTabRow import com.wire.android.ui.common.calculateCurrentTab +import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchTopBar @@ -77,28 +80,38 @@ fun SearchUsersAndServicesScreen( screenType: SearchPeopleScreenType, modifier: Modifier = Modifier, isGroupSubmitVisible: Boolean = true, - isServicesAllowed: Boolean = false + isServicesAllowed: Boolean = false, + initialPage: SearchPeopleTabItem = SearchPeopleTabItem.PEOPLE, ) { val searchBarState = rememberSearchbarState() val scope = rememberCoroutineScope() - val initialPageIndex = SearchPeopleTabItem.PEOPLE.ordinal + val tabs = remember(isServicesAllowed) { + if (isServicesAllowed) SearchPeopleTabItem.entries else listOf(SearchPeopleTabItem.PEOPLE) + } val pagerState = rememberPagerState( - initialPage = initialPageIndex, - pageCount = { if (isServicesAllowed) SearchPeopleTabItem.entries.size else 1 } + initialPage = tabs.indexOf(initialPage), + pageCount = { tabs.size } ) - val currentTabState by remember { derivedStateOf { pagerState.calculateCurrentTab() } } + val currentTabState by remember { + derivedStateOf { + pagerState.calculateCurrentTab() + } + } + val lazyListStates: List = List(tabs.size) { + rememberLazyListState() + } CollapsingTopBarScaffold( modifier = modifier, - topBarHeader = { elevation -> + topBarHeader = { AnimatedVisibility( visible = !searchBarState.isSearchActive, - enter = expandVertically(), - exit = shrinkVertically(), + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), ) { Box(modifier = Modifier.wrapContentSize()) { WireCenterAlignedTopAppBar( - elevation = elevation, + elevation = dimensions().spacing0x, // CollapsingTopBarScaffold already manages elevation title = searchTitle, navigationIconType = when (screenType) { SearchPeopleScreenType.CONVERSATION_DETAILS -> NavigationIconType.Close @@ -116,67 +129,54 @@ fun SearchUsersAndServicesScreen( searchBarHint = stringResource(R.string.label_search_people), searchQueryTextState = searchBarState.searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, - ) { - if (screenType == SearchPeopleScreenType.CONVERSATION_DETAILS && isServicesAllowed) { - WireTabRow( - tabs = SearchPeopleTabItem.entries, - selectedTabIndex = currentTabState, - onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, - divider = {} // no divider - ) - } + ) + }, + topBarFooter = { + if (screenType == SearchPeopleScreenType.CONVERSATION_DETAILS && isServicesAllowed) { + WireTabRow( + tabs = SearchPeopleTabItem.entries, + selectedTabIndex = currentTabState, + onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, + ) } }, - isSwipeable = !searchBarState.isSearchActive, + collapsingEnabled = !searchBarState.isSearchActive, + contentLazyListState = lazyListStates[pagerState.currentPage], content = { - Crossfade( - targetState = searchBarState.isSearchActive, label = "" - ) { isSearchActive -> - val actionType = when (screenType) { - SearchPeopleScreenType.NEW_CONVERSATION -> ItemActionType.CLICK - SearchPeopleScreenType.NEW_GROUP_CONVERSATION -> ItemActionType.CHECK - SearchPeopleScreenType.CONVERSATION_DETAILS -> ItemActionType.CHECK - } + val actionType = when (screenType) { + SearchPeopleScreenType.NEW_CONVERSATION -> ItemActionType.CLICK + SearchPeopleScreenType.NEW_GROUP_CONVERSATION -> ItemActionType.CHECK + SearchPeopleScreenType.CONVERSATION_DETAILS -> ItemActionType.CHECK + } - if (screenType == SearchPeopleScreenType.CONVERSATION_DETAILS) { - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() - ) { pageIndex -> - when (SearchPeopleTabItem.entries[pageIndex]) { - SearchPeopleTabItem.PEOPLE -> { - SearchAllPeopleOrContactsScreen( - searchQuery = searchBarState.searchQueryTextState.text.toString(), - contactsSelected = selectedContacts, - onOpenUserProfile = onOpenUserProfile, - onContactChecked = onContactChecked, - isSearchActive = isSearchActive, - isLoading = false, // TODO: update correctly - actionType = actionType, - ) - } + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + ) { pageIndex -> + when (tabs[pageIndex]) { + SearchPeopleTabItem.PEOPLE -> { + SearchAllPeopleOrContactsScreen( + searchQuery = searchBarState.searchQueryTextState.text.toString(), + contactsSelected = selectedContacts, + onOpenUserProfile = onOpenUserProfile, + onContactChecked = onContactChecked, + isSearchActive = searchBarState.isSearchActive, + isLoading = false, // TODO: update correctly + actionType = actionType, + lazyListState = lazyListStates[pageIndex], + ) + } - SearchPeopleTabItem.SERVICES -> { - SearchAllServicesScreen( - searchQuery = searchBarState.searchQueryTextState.text.toString(), - onServiceClicked = onServiceClicked, - ) - } - } + SearchPeopleTabItem.SERVICES -> { + SearchAllServicesScreen( + searchQuery = searchBarState.searchQueryTextState.text.toString(), + onServiceClicked = onServiceClicked, + lazyListState = lazyListStates[pageIndex], + ) } } - } else { - SearchAllPeopleOrContactsScreen( - searchQuery = searchBarState.searchQueryTextState.text.toString(), - contactsSelected = selectedContacts, - onContactChecked = onContactChecked, - onOpenUserProfile = onOpenUserProfile, - isSearchActive = isSearchActive, - isLoading = false, // TODO: update correctly - actionType = actionType, - ) } } BackHandler(enabled = searchBarState.isSearchActive) { @@ -202,7 +202,7 @@ fun SearchUsersAndServicesScreen( } SearchPeopleScreenType.CONVERSATION_DETAILS -> { - if (pagerState.currentPage != SearchPeopleTabItem.SERVICES.ordinal) { + if (tabs[pagerState.currentPage] != SearchPeopleTabItem.SERVICES) { SelectParticipantsButtonsRow( selectedParticipantsCount = selectedContacts.size, mainButtonText = actionButtonTitle, @@ -214,7 +214,6 @@ fun SearchUsersAndServicesScreen( } }, snapOnFling = false, - keepElevationWhenCollapsed = true ) } @@ -240,6 +239,7 @@ private fun SearchAllPeopleOrContactsScreen( onOpenUserProfile: (Contact) -> Unit, onContactChecked: (Boolean, Contact) -> Unit, searchUserViewModel: SearchUserViewModel = hiltViewModel(), + lazyListState: LazyListState = rememberLazyListState(), ) { LaunchedEffect(key1 = searchQuery) { @@ -254,7 +254,6 @@ private fun SearchAllPeopleOrContactsScreen( ) } - val lazyState = rememberLazyListState() var selectedContactResultsExpanded by remember { mutableStateOf(false) } var contactResultsExpanded by remember { mutableStateOf(true) } var publicResultsExpanded by remember { mutableStateOf(true) } @@ -265,7 +264,7 @@ private fun SearchAllPeopleOrContactsScreen( contactsSelectedSearchResult = searchUserViewModel.state.selectedResult, onChecked = onContactChecked, onOpenUserProfile = onOpenUserProfile, - lazyListState = lazyState, + lazyListState = lazyListState, isSearchActive = isSearchActive, isLoading = isLoading, actionType = actionType, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt index 5b0d2cb1ee3..8573df4b346 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.home.conversations.search.messages import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.paging.PagingData @@ -40,9 +42,10 @@ fun SearchConversationMessagesResultsScreen( lazyPagingMessages: LazyPagingItems, onMessageClick: (messageId: String) -> Unit, modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), searchQuery: String = "" ) { - LazyColumn(modifier = modifier) { + LazyColumn(state = lazyListState, modifier = modifier) { items( count = lazyPagingMessages.itemCount, key = lazyPagingMessages.itemKey { it.header.messageId }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt index f79c284bf36..7351d7abd90 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt @@ -19,7 +19,10 @@ package com.wire.android.ui.home.conversations.search.messages import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -32,12 +35,14 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination import com.wire.android.navigation.style.PopUpNavigationAnimation -import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId @@ -79,17 +84,23 @@ fun SearchConversationMessagesResultContent( onCloseSearchClicked: () -> Unit, modifier: Modifier = Modifier, ) { + val lazyListState = rememberLazyListState() + val maxAppBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation WireScaffold( modifier = modifier, topBar = { - SearchTopBar( - isSearchActive = true, // we want the search to be always active and back arrow visible on this particular screen - searchBarHint = stringResource(id = R.string.label_search_messages), - searchQueryTextState = searchQueryTextState, - modifier = Modifier.padding(top = dimensions().spacing24x), - onCloseSearchClicked = onCloseSearchClicked, - isLoading = state.isLoading - ) + Surface( + shadowElevation = lazyListState.topBarElevation(maxAppBarElevation), + color = MaterialTheme.wireColorScheme.background + ) { + SearchTopBar( + isSearchActive = true, // we want the search to be always active and back arrow visible on this particular screen + searchBarHint = stringResource(id = R.string.label_search_messages), + searchQueryTextState = searchQueryTextState, + onCloseSearchClicked = onCloseSearchClicked, + isLoading = state.isLoading + ) + } }, content = { internalPadding -> Column(modifier = Modifier.padding(internalPadding)) { @@ -101,6 +112,7 @@ fun SearchConversationMessagesResultContent( if (lazyPagingMessages.itemCount > 0) { SearchConversationMessagesResultsScreen( lazyPagingMessages = lazyPagingMessages, + lazyListState = lazyListState, searchQuery = state.searchQuery, onMessageClick = onMessageClick ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt index 8ae4f590c96..7db38696fd0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt @@ -19,6 +19,8 @@ package com.wire.android.ui.home.conversationslist import androidx.activity.compose.BackHandler +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -79,6 +81,7 @@ fun ConversationRouterHomeBridge( conversationsSource: ConversationsSource = ConversationsSource.MAIN, conversationListViewModel: ConversationListViewModel = hiltViewModel(), conversationCallListViewModel: ConversationCallListViewModel = hiltViewModel(), + lazyListState: LazyListState = rememberLazyListState(), ) { var currentConversationOptionNavigation by remember { mutableStateOf(ConversationOptionNavigation.Home) @@ -182,7 +185,8 @@ fun ConversationRouterHomeBridge( conversationListCallState = conversationCallListViewModel.conversationListCallState, dismissJoinCallAnywayDialog = conversationCallListViewModel::dismissJoinCallAnywayDialog, joinCallAnyway = conversationCallListViewModel::joinAnyway, - joinOngoingCall = conversationCallListViewModel::joinOngoingCall + joinOngoingCall = conversationCallListViewModel::joinOngoingCall, + lazyListState = lazyListState, ) ConversationItemType.SEARCH -> { @@ -196,7 +200,8 @@ fun ConversationRouterHomeBridge( onJoinCall = { conversationCallListViewModel.joinOngoingCall(it, onJoinedCall) }, - onAudioPermissionPermanentlyDenied = { } + onAudioPermissionPermanentlyDenied = { }, + lazyListState = lazyListState, ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt index 25c74e11a83..ca7844b02e5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -66,6 +67,7 @@ fun AllConversationScreen(homeStateHolder: HomeStateHolder) { navigator = navigator, conversationItemType = ConversationItemType.ALL_CONVERSATIONS, searchBarState = searchBarState, + lazyListState = currentLazyListState, ) } } @@ -82,10 +84,10 @@ fun AllConversationScreenContent( onAudioPermissionPermanentlyDenied: () -> Unit, dismissJoinCallAnywayDialog: () -> Unit, joinCallAnyway: (conversationId: ConversationId, onJoinedCall: (ConversationId) -> Unit) -> Unit, + joinOngoingCall: (conversationId: ConversationId, onJoinedCall: (ConversationId) -> Unit) -> Unit, isFromArchive: Boolean = false, - joinOngoingCall: (conversationId: ConversationId, onJoinedCall: (ConversationId) -> Unit) -> Unit + lazyListState: LazyListState = rememberLazyListState(), ) { - val lazyListState = rememberLazyListState() val callConversationIdToJoin = remember { mutableStateOf(ConversationId("", "")) } if (conversationListCallState.shouldShowJoinAnywayDialog) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/search/SearchConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/search/SearchConversationScreen.kt index 4fe8134c2d5..da20181cb13 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/search/SearchConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/search/SearchConversationScreen.kt @@ -18,12 +18,14 @@ package com.wire.android.ui.home.conversationslist.search +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -57,6 +59,7 @@ fun SearchConversationScreen( onJoinCall: (ConversationId) -> Unit, onAudioPermissionPermanentlyDenied: () -> Unit, modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), ) { Box(modifier.fillMaxSize()) { if (conversationSearchResult.values.isEmpty()) { @@ -70,6 +73,7 @@ fun SearchConversationScreen( onOpenUserProfile = onOpenUserProfile, onJoinCall = onJoinCall, onAudioPermissionPermanentlyDenied = onAudioPermissionPermanentlyDenied, + lazyListState = lazyListState, ) } } @@ -78,9 +82,9 @@ fun SearchConversationScreen( @Composable private fun EmptySearchResult(onNewConversationCLick: () -> Unit) { Column( - horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() ) { VerticalSpace.x8() Column( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt index 7828b8e7381..3617c3028d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R @@ -41,8 +40,10 @@ import com.wire.android.navigation.handleNavigation import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.SetLockCodeScreenDestination import com.wire.android.ui.home.HomeStateHolder +import com.wire.android.ui.theme.WireTheme import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.extension.folderWithElements +import com.wire.android.util.ui.PreviewMultipleThemes @HomeNavGraph @WireDestination @@ -51,7 +52,6 @@ fun SettingsScreen( homeStateHolder: HomeStateHolder, viewModel: SettingsViewModel = hiltViewModel() ) { - val lazyListState: LazyListState = rememberLazyListState() val turnAppLockOffDialogState = rememberVisibilityState() val onAppLockSwitchClicked: (Boolean) -> Unit = remember { { isChecked -> @@ -62,7 +62,7 @@ fun SettingsScreen( val context = LocalContext.current SettingsScreenContent( - lazyListState = lazyListState, + lazyListState = homeStateHolder.currentLazyListState, settingsState = viewModel.state, onItemClicked = remember { { @@ -79,9 +79,10 @@ fun SettingsScreen( @Composable fun SettingsScreenContent( - lazyListState: LazyListState = rememberLazyListState(), settingsState: SettingsState, onItemClicked: (SettingsItem.DirectionItem) -> Unit, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), onAppLockSwitchChanged: (Boolean) -> Unit ) { val context = LocalContext.current @@ -90,7 +91,7 @@ fun SettingsScreenContent( with(featureVisibilityFlags) { LazyColumn( state = lazyListState, - modifier = Modifier.fillMaxSize() + modifier = modifier.fillMaxSize() ) { folderWithElements( header = context.getString(R.string.settings_account_settings_label), @@ -177,8 +178,8 @@ private fun LazyListScope.folderWithElements( } } -@Preview(showBackground = false) +@PreviewMultipleThemes @Composable -fun PreviewSettingsScreen() { - SettingsScreenContent(rememberLazyListState(), SettingsState(), {}, {}) +fun PreviewSettingsScreen() = WireTheme { + SettingsScreenContent(settingsState = SettingsState(), onItemClicked = {}, onAppLockSwitchChanged = {},) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt index d1973dd6bbb..a42d569a4cc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt @@ -46,11 +46,10 @@ fun WhatsNewScreen( homeStateHolder: HomeStateHolder, whatsNewViewModel: WhatsNewViewModel = hiltViewModel() ) { - val lazyListState: LazyListState = rememberLazyListState() val context = LocalContext.current WhatsNewScreenContent( state = whatsNewViewModel.state, - lazyListState = lazyListState, + lazyListState = homeStateHolder.currentLazyListState, onItemClicked = remember { { it.direction.handleNavigation( diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index dc9df9e4ff1..9cdb9b39b28 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState @@ -76,6 +77,7 @@ import com.wire.android.ui.common.error.ErrorIcon import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.remove.RemoveIcon import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchBarState @@ -97,6 +99,7 @@ import com.wire.android.ui.home.newconversation.common.SendContentButton import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper import com.wire.android.util.extension.getActivity @@ -295,12 +298,14 @@ fun ImportMediaRegularContent( ) { val importMediaScreenState = rememberImportMediaScreenState() + val lazyListState = rememberLazyListState() + val maxAppBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation with(importMediaAuthenticatedState) { WireScaffold( topBar = { WireCenterAlignedTopAppBar( - elevation = dimensions().spacing0x, + elevation = lazyListState.topBarElevation(maxAppBarElevation), onNavigationPressed = navigateBack, navigationIconType = NavigationIconType.Close, title = stringResource(id = R.string.import_media_content_title), @@ -309,6 +314,14 @@ fun ImportMediaRegularContent( avatarData = UserAvatarData(avatarAsset), clickable = remember { Clickable(enabled = false) { } } ) + }, + bottomContent = { + ImportMediaTopBarContent( + state = importMediaAuthenticatedState, + searchBarState = importMediaScreenState.searchBarState, + searchQueryTextState = searchQueryTextState, + onRemoveAsset = onRemoveAsset, + ) } ) }, @@ -317,10 +330,8 @@ fun ImportMediaRegularContent( ImportMediaContent( state = this, internalPadding = internalPadding, - searchQueryTextState = searchQueryTextState, onConversationClicked = onConversationClicked, - searchBarState = importMediaScreenState.searchBarState, - onRemoveAsset = onRemoveAsset + lazyListState = lazyListState, ) }, bottomBar = { @@ -331,6 +342,9 @@ fun ImportMediaRegularContent( ) } ) + BackHandler(enabled = importMediaScreenState.searchBarState.isSearchActive) { + importMediaScreenState.searchBarState.closeSearch() + } WireModalSheetLayout( sheetState = importMediaScreenState.bottomSheetState, sheetContent = { @@ -457,25 +471,16 @@ private fun ImportMediaBottomBar( } @Composable -private fun ImportMediaContent( +fun ImportMediaTopBarContent( state: ImportMediaAuthenticatedState, - internalPadding: PaddingValues, + searchBarState: SearchBarState, searchQueryTextState: TextFieldState, - onConversationClicked: (conversationId: ConversationId) -> Unit, onRemoveAsset: (index: Int) -> Unit, - searchBarState: SearchBarState + modifier: Modifier = Modifier, ) { - val importedItemsList: PersistentList = state.importedAssets - val itemsToImport = importedItemsList.size + val isMultipleImport = state.importedAssets.size != 1 - val isMultipleImport = itemsToImport != 1 - - Column( - modifier = Modifier - .padding(internalPadding) - .fillMaxSize() - ) { - val lazyListState = rememberLazyListState() + Column(modifier = modifier) { if (state.isImporting) { Box( Modifier @@ -497,14 +502,14 @@ private fun ImportMediaContent( ) { AssetTilePreview( modifier = Modifier.fillMaxHeight(), - assetBundle = importedItemsList.first().assetBundle, + assetBundle = state.importedAssets.first().assetBundle, showOnlyExtension = false, onClick = {} ) } } else { when (state.importedText.isNullOrBlank()) { - true -> ImportAssetsCarrousel(importedItemsList, onRemoveAsset) + true -> ImportAssetsCarrousel(state.importedAssets, onRemoveAsset) false -> ImportText(state.importedText) } } @@ -513,17 +518,30 @@ private fun ImportMediaContent( thickness = 1.dp, modifier = Modifier.padding(top = dimensions().spacing12x) ) - Box(Modifier.padding(dimensions().spacing6x)) { - SearchTopBar( - isSearchActive = searchBarState.isSearchActive, - searchBarHint = stringResource( - R.string.search_bar_conversations_hint, - stringResource(id = R.string.conversations_screen_title).lowercase() - ), - searchQueryTextState = searchQueryTextState, - onActiveChanged = searchBarState::searchActiveChanged, - ) - } + SearchTopBar( + isSearchActive = searchBarState.isSearchActive, + searchBarHint = stringResource( + R.string.search_bar_conversations_hint, + stringResource(id = R.string.conversations_screen_title).lowercase() + ), + searchQueryTextState = searchQueryTextState, + onActiveChanged = searchBarState::searchActiveChanged, + ) + } +} + +@Composable +private fun ImportMediaContent( + state: ImportMediaAuthenticatedState, + internalPadding: PaddingValues, + onConversationClicked: (conversationId: ConversationId) -> Unit, + lazyListState: LazyListState = rememberLazyListState(), +) { + Column( + modifier = Modifier + .padding(internalPadding) + .fillMaxSize() + ) { ConversationList( modifier = Modifier.weight(1f), lazyListState = lazyListState, @@ -541,9 +559,6 @@ private fun ImportMediaContent( onAudioPermissionPermanentlyDenied = {} ) } - BackHandler(enabled = searchBarState.isSearchActive) { - searchBarState.closeSearch() - } } @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 6c8b840795d..40b5900e539 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.result.ResultBackNavigator @@ -83,7 +82,6 @@ import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.spacers.VerticalSpace -import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState @@ -100,7 +98,6 @@ import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectProfileDialog import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme -import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.userprofile.common.EditableState import com.wire.android.ui.userprofile.common.UserProfileInfo import com.wire.android.ui.userprofile.group.RemoveConversationMemberState @@ -285,16 +282,10 @@ fun OtherProfileScreenContent( } val initialPage = 0 val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { tabItems.size }) - val lazyListStates = OtherUserProfileTabItem.values().associateWith { rememberLazyListState() } + val lazyListStates = OtherUserProfileTabItem.entries.associateWith { rememberLazyListState() } val currentTabState by remember(state, pagerState) { derivedStateOf { if (state.isDataLoading) 0 else pagerState.calculateCurrentTab() } } - val maxBarElevation = MaterialTheme.wireDimensions.topBarShadowElevation - val tabBarElevationState by remember(tabItems, lazyListStates, currentTabState) { - derivedStateOf { - lazyListStates[tabItems[currentTabState]]?.topBarElevation(maxBarElevation) ?: 0.dp - } - } if (!requestInProgress) { blockUserDialogState.dismiss() @@ -305,11 +296,11 @@ fun OtherProfileScreenContent( } CollapsingTopBarScaffold( - topBarHeader = { elevation -> + topBarHeader = { TopBarHeader( state = state, navigationIconType = navigationIconType, - elevation = elevation, + elevation = dimensions().spacing0x, // CollapsingTopBarScaffold already manages elevation onNavigateBack = navigateBack, openConversationBottomSheet = openConversationBottomSheet ) @@ -326,12 +317,12 @@ fun OtherProfileScreenContent( TopBarFooter( state = state, pagerState = pagerState, - tabBarElevation = tabBarElevationState, tabItems = tabItems, currentTab = currentTabState, scope = scope ) }, + contentLazyListState = lazyListStates[tabItems[currentTabState]], content = { Content( state = state, @@ -348,12 +339,11 @@ fun OtherProfileScreenContent( bottomBar = { ContentFooter( state, - maxBarElevation, onIgnoreConnectionRequest, onOpenConversation ) }, - isSwipeable = state.connectionState != ConnectionState.BLOCKED + collapsingEnabled = state.connectionState != ConnectionState.BLOCKED ) WireModalSheetLayout( @@ -475,7 +465,6 @@ private fun TopBarCollapsing( private fun TopBarFooter( state: OtherUserProfileState, pagerState: PagerState, - tabBarElevation: Dp, tabItems: List, currentTab: Int, scope: CoroutineScope @@ -486,17 +475,11 @@ private fun TopBarFooter( enter = fadeIn(), exit = fadeOut(), ) { - Surface( - shadowElevation = tabBarElevation, - color = MaterialTheme.wireColorScheme.background - ) { - WireTabRow( - tabs = tabItems, - selectedTabIndex = currentTab, - onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, - divider = {} // no divider - ) - } + WireTabRow( + tabs = tabItems, + selectedTabIndex = currentTab, + onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } }, + ) } } } @@ -578,7 +561,6 @@ private fun Content( @Composable fun ContentFooter( state: OtherUserProfileState, - maxBarElevation: Dp, onIgnoreConnectionRequest: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {} ) { @@ -590,7 +572,7 @@ fun ContentFooter( // TODO show open conversation button for service bots after AR-2135 if (!state.isMetadataEmpty() && state.membership != Membership.Service && !state.isTemporaryUser()) { Surface( - shadowElevation = maxBarElevation, + shadowElevation = dimensions().bottomNavigationShadowElevation, color = MaterialTheme.wireColorScheme.background ) { Box(modifier = Modifier.padding(all = dimensions().spacing16x)) {