From d090e4036d6bb82b130603cccdf9c4692cdd606b Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 24 Oct 2025 12:44:49 +0800 Subject: [PATCH 01/26] Initial component --- .../thoughtcrime/securesms/ui/Components.kt | 147 +++++++++++++++++- 1 file changed, 141 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 14e4d43a06..281bd6032e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.ui +import android.R.attr.data import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility @@ -88,6 +89,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -98,6 +100,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R import org.thoughtcrime.securesms.ui.components.AccentOutlineButton +import org.thoughtcrime.securesms.ui.components.DangerFillButtonRect import org.thoughtcrime.securesms.ui.components.SessionSwitch import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.components.TitledRadioButton @@ -106,6 +109,8 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.ui.theme.primaryGreen import org.thoughtcrime.securesms.ui.theme.primaryOrange @@ -286,7 +291,8 @@ fun ItemButton( painter = painterResource(id = iconRes), contentDescription = null, tint = iconTint ?: colors.contentColor, - modifier = Modifier.align(Alignment.Center) + modifier = Modifier + .align(Alignment.Center) .size(iconSize) ) }, @@ -318,7 +324,8 @@ fun ItemButton( onClick: () -> Unit ) { TextButton( - modifier = modifier.fillMaxWidth() + modifier = modifier + .fillMaxWidth() .heightIn(min = minHeight), colors = colors, onClick = onClick, @@ -351,7 +358,8 @@ fun ItemButton( subtitle?.let { Text( text = it, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .qaTag(subtitleQaTag), style = LocalType.current.small, ) @@ -392,7 +400,7 @@ fun Cell( Box( modifier = modifier .then( - if(dropShadow) + if (dropShadow) Modifier.dropShadow( shape = MaterialTheme.shapes.small, shadow = Shadow( @@ -731,6 +739,131 @@ fun SearchBar( ) } +/** + * CollapsingActionTray + */ +@Composable +fun CollapsibleActionTray( + modifier: Modifier = Modifier, + data: CollapsibleActionTrayData, + onCollapsedClicked: () -> Unit = {}, + onClosedClicked: () -> Unit = {} +) { + Column( + modifier = modifier + .background(LocalColors.current.backgroundSecondary) + .padding(LocalDimensions.current.smallSpacing) + .clip(RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier, + painter = painterResource(R.drawable.ic_chevron_down), + contentDescription = null + ) + Text( + "looooooooooooooooooooo000000oong", + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = LocalDimensions.current.smallSpacing), + style = LocalType.current.h8, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + modifier = Modifier, + painter = painterResource(R.drawable.ic_x), + contentDescription = null + ) + } + + // Rendered actions + AnimatedVisibility(visible = !data.collapsed) { + CategoryCell { + Column(modifier = Modifier.fillMaxWidth()) { + data.items.forEachIndexed { index, item -> + if (index != 0) Divider() + ActionRowItem( + modifier = Modifier.background(LocalColors.current.backgroundTertiary), + title = item.label, + onClick = item.onClick, + qaTag = 0, + endContent = { + DangerFillButtonRect( + item.buttonLabel, + modifier = Modifier + .width(100.dp) + ) { + item.onClick + } + } + ) + } + } + } + } + } +} + +data class CollapsibleActionTrayData( + val title: AnnotatedString, + val collapsed: Boolean, + val items: List +) + +data class CollapsibleActionTrayItemData( + val label: AnnotatedString, + val buttonLabel: String, + val buttonColor: Color, + val onClick: () -> Unit +) + + +@Preview +@Composable +fun PreviewCollapsibleActionTray( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + val demoItems = listOf( + CollapsibleActionTrayItemData( + label = annotatedStringResource("Mute notifications"), + buttonLabel = "Mute", + buttonColor = LocalColors.current.text, + onClick = {} + ), + CollapsibleActionTrayItemData( + label = annotatedStringResource("Pin conversation"), + buttonLabel = "Pin", + buttonColor = LocalColors.current.accent, + onClick = {} + ), + CollapsibleActionTrayItemData( + label = annotatedStringResource("Delete chat"), + buttonLabel = "Delete", + buttonColor = LocalColors.current.danger, + onClick = {} + ) + ) + + CollapsibleActionTray( + data = CollapsibleActionTrayData( + title = annotatedStringResource("Header"), + collapsed = false, + items = demoItems + ) + ) + } +} + @Preview @Composable fun PreviewSearchBar() { @@ -1020,7 +1153,8 @@ fun ActionRowItem( endContent: @Composable (() -> Unit)? = null ){ Row( - modifier = modifier.heightIn(min = minHeight) + modifier = modifier + .heightIn(min = minHeight) .clickable { onClick() } .padding(paddingValues) .qaTag(qaTag), @@ -1092,7 +1226,8 @@ fun IconActionRowItem( modifier = Modifier.size(LocalDimensions.current.itemButtonIconSpacing) ) { Icon( - modifier = Modifier.align(Alignment.Center) + modifier = Modifier + .align(Alignment.Center) .size(iconSize) .qaTag(R.string.qa_action_item_icon), painter = painterResource(id = icon), From 3762010dbc46c02eaed3c10569e8f597b1304a47 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 24 Oct 2025 14:44:18 +0800 Subject: [PATCH 02/26] fix crash --- .../thoughtcrime/securesms/ui/Components.kt | 42 +++++++++++++++---- .../src/main/res/values/strings.xml | 2 + 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 281bd6032e..25869451de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -5,12 +5,19 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -65,6 +72,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode @@ -762,13 +770,23 @@ fun CollapsibleActionTray( .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { + val rotation by animateFloatAsState( + targetValue = if (data.collapsed) 0f else 180f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ) + ) + Icon( - modifier = Modifier, + modifier = Modifier + .rotate(rotation) + .clickable(onClick = onCollapsedClicked), painter = painterResource(R.drawable.ic_chevron_down), contentDescription = null ) Text( - "looooooooooooooooooooo000000oong", + data.title, modifier = Modifier .fillMaxWidth() .weight(1f) @@ -779,14 +797,24 @@ fun CollapsibleActionTray( overflow = TextOverflow.Ellipsis ) Icon( - modifier = Modifier, + modifier = Modifier + .clickable(onClick = onClosedClicked), painter = painterResource(R.drawable.ic_x), contentDescription = null ) } // Rendered actions - AnimatedVisibility(visible = !data.collapsed) { + AnimatedVisibility( + visible = !data.collapsed, + enter = slideInVertically( + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing), + initialOffsetY = { it } // start just below and slide up into place + ), + exit = slideOutVertically( + animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing), + targetOffsetY = { it } // slide down out of view when collapsing + )) { CategoryCell { Column(modifier = Modifier.fillMaxWidth()) { data.items.forEachIndexed { index, item -> @@ -794,15 +822,15 @@ fun CollapsibleActionTray( ActionRowItem( modifier = Modifier.background(LocalColors.current.backgroundTertiary), title = item.label, - onClick = item.onClick, - qaTag = 0, + onClick = {}, + qaTag = R.string.qa_string_placeholder, endContent = { DangerFillButtonRect( item.buttonLabel, modifier = Modifier .width(100.dp) ) { - item.onClick + item.onClick() } } ) diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index dcd853dd0c..ad8658f248 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -291,4 +291,6 @@ action-item-icon qa-blocked-contacts-settings-item + qa-placeholder + \ No newline at end of file From 155b89a67415b4c737a34a1ac971b3fa11446036 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 24 Oct 2025 17:38:23 +0800 Subject: [PATCH 03/26] Slim fill button --- .../thoughtcrime/securesms/ui/Components.kt | 15 ++++++++---- .../securesms/ui/components/Button.kt | 23 ++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 25869451de..6095a2ed5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -110,6 +110,7 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.ui.components.AccentOutlineButton import org.thoughtcrime.securesms.ui.components.DangerFillButtonRect import org.thoughtcrime.securesms.ui.components.SessionSwitch +import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.components.TitledRadioButton import org.thoughtcrime.securesms.ui.components.annotatedStringResource @@ -759,10 +760,15 @@ fun CollapsibleActionTray( ) { Column( modifier = modifier + .fillMaxWidth() + .clip( + RoundedCornerShape( + topStart = LocalDimensions.current.contentSpacing, + topEnd = LocalDimensions.current.contentSpacing + ) + ) .background(LocalColors.current.backgroundSecondary) - .padding(LocalDimensions.current.smallSpacing) - .clip(RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)) - .fillMaxWidth(), + .padding(LocalDimensions.current.smallSpacing), verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { Row( @@ -825,8 +831,9 @@ fun CollapsibleActionTray( onClick = {}, qaTag = R.string.qa_string_placeholder, endContent = { - DangerFillButtonRect( + SlimFillButtonRect( item.buttonLabel, + color = item.buttonColor, modifier = Modifier .width(100.dp) ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt index ef9215cace..a1ab702564 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt @@ -73,7 +73,8 @@ fun Button( style.applyButtonConstraints { androidx.compose.material3.Button( onClick = onClick, - modifier = modifier.heightIn(min = style.minHeight) + modifier = modifier + .heightIn(min = style.minHeight) .defaultMinSize(minWidth = minWidth), enabled = enabled, interactionSource = interactionSource, @@ -352,6 +353,25 @@ fun BorderlessHtmlButton( } } +@Composable +fun SlimFillButtonRect( + text: String, + modifier: Modifier = Modifier, + color: Color = LocalColors.current.accent, + enabled: Boolean = true, + onClick: () -> Unit +) { + Button( + text, onClick, + ButtonType.Fill(color), + modifier, + enabled, + style = ButtonStyle.Slim, + shape = sessionShapes().extraSmall + ) +} + + val MutableInteractionSource.releases get() = interactions.filter { it is PressInteraction.Release } @@ -380,6 +400,7 @@ private fun VariousButtons( SlimOutlineButton("Slim Outline Disabled", enabled = false) {} SlimAccentOutlineButton("Slim Accent") {} SlimOutlineButton("Slim Danger", color = LocalColors.current.danger) {} + SlimFillButtonRect("Slim Fill", color = LocalColors.current.accent) {} BorderlessButton("Borderless Button") {} BorderlessButton("Borderless Secondary", color = LocalColors.current.textSecondary) {} FillButtonRect("Fill Rect") {} From 081e0fc40b69c109b0d408634b1e8a94c8a99e45 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 27 Oct 2025 07:16:30 +0800 Subject: [PATCH 04/26] fixed bg --- app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 6095a2ed5b..94db6623d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -822,7 +822,8 @@ fun CollapsibleActionTray( targetOffsetY = { it } // slide down out of view when collapsing )) { CategoryCell { - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.fillMaxWidth() + .background(LocalColors.current.backgroundTertiary)) { data.items.forEachIndexed { index, item -> if (index != 0) Divider() ActionRowItem( From 6a9e31f3473fbbc40bb178eddac5aff406e90373 Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 11:39:17 +0800 Subject: [PATCH 05/26] collapse ui state --- .../groups/SelectContactsViewModel.kt | 37 ++++++ .../groups/compose/InviteContactsScreen.kt | 104 +++++++++++----- .../group/CreateGroupViewModel.kt | 1 + .../preferences/BlockedContactsViewModel.kt | 1 + .../thoughtcrime/securesms/ui/Components.kt | 111 ++++++++++-------- 5 files changed, 176 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index 5784b8a723..e9e6947a33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -1,11 +1,13 @@ package org.thoughtcrime.securesms.groups +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -19,7 +21,9 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext +import network.loki.messenger.R import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.shouldShowProBadge @@ -27,6 +31,7 @@ import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.home.search.searchName import org.thoughtcrime.securesms.pro.ProStatusManager +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils @@ -39,6 +44,7 @@ open class SelectContactsViewModel @AssistedInject constructor( @Assisted private val excludingAccountIDs: Set
, @Assisted private val contactFiltering: (Recipient) -> Boolean, // default will filter out blocked and unapproved contacts private val recipientRepository: RecipientRepository, + @param:ApplicationContext private val context: Context, ) : ViewModel() { // Input: The search query private val mutableSearchQuery = MutableStateFlow("") @@ -71,6 +77,9 @@ open class SelectContactsViewModel @AssistedInject constructor( val currentSelected: Set
get() = mutableSelectedContactAccountIDs.value + private val _uiState = MutableStateFlow(InviteUiState()) + val uiState: StateFlow = _uiState + @OptIn(ExperimentalCoroutinesApi::class) private fun observeContacts() = (configFactory.configUpdateNotifications as Flow) .debounce(100L) @@ -134,6 +143,7 @@ open class SelectContactsViewModel @AssistedInject constructor( newSet.add(address) } mutableSelectedContactAccountIDs.value = newSet + updateUiState() } fun selectAccountIDs(accountIDs: Set
) { @@ -142,8 +152,35 @@ open class SelectContactsViewModel @AssistedInject constructor( fun clearSelection(){ mutableSelectedContactAccountIDs.value = emptySet() + updateUiState() + } + + fun toggleFooter() { + _uiState.update { + it.copy(collapsed = !it.collapsed) + } } + private fun updateUiState() { + val count = currentSelected.size + val visible = currentSelected.isNotEmpty() + // TODO: String from crowdin + val footerTitle = + GetString(context.resources.getQuantityString(R.plurals.membersInviteSend, count, count)) + + _uiState.value = InviteUiState( + collapsed = count == 0, + visible = visible, + footerActionTitle = footerTitle + ) + } + + data class InviteUiState( + val visible: Boolean = false, + val collapsed: Boolean = true, + val footerActionTitle : GetString = GetString("") + ) + @AssistedFactory interface Factory { fun create( diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt index 902c0bc4fe..aebdbfe0f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt @@ -4,10 +4,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api @@ -15,8 +18,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import network.loki.messenger.R @@ -24,9 +29,12 @@ import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.SelectContactsViewModel import org.thoughtcrime.securesms.ui.BottomFadingEdgeBox +import org.thoughtcrime.securesms.ui.CollapsibleActionTray +import org.thoughtcrime.securesms.ui.CollapsibleActionTrayData +import org.thoughtcrime.securesms.ui.CollapsibleActionTrayItemData +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.SearchBar import org.thoughtcrime.securesms.ui.components.BackAppBar -import org.thoughtcrime.securesms.ui.components.AccentOutlineButton import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -42,17 +50,23 @@ fun InviteContactsScreen( viewModel: SelectContactsViewModel, onDoneClicked: () -> Unit, onBack: () -> Unit, - banner: @Composable ()->Unit = {} + banner: @Composable () -> Unit = {} ) { + val footerData by viewModel.uiState.collectAsState() + InviteContacts( contacts = viewModel.contacts.collectAsState().value, onContactItemClicked = viewModel::onContactItemClicked, searchQuery = viewModel.searchQuery.collectAsState().value, onSearchQueryChanged = viewModel::onSearchQueryChanged, - onSearchQueryClear = {viewModel.onSearchQueryChanged("") }, + onSearchQueryClear = { viewModel.onSearchQueryChanged("") }, onDoneClicked = onDoneClicked, onBack = onBack, - banner = banner + banner = banner, + data = footerData, + onToggleFooter = viewModel::toggleFooter, + onCloseFooter = viewModel::clearSelection + ) } @@ -66,15 +80,47 @@ fun InviteContacts( onSearchQueryClear: () -> Unit, onDoneClicked: () -> Unit, onBack: () -> Unit, - banner: @Composable ()->Unit = {} + banner: @Composable () -> Unit = {}, + data: SelectContactsViewModel.InviteUiState, + onToggleFooter: () -> Unit, + onCloseFooter: () -> Unit, ) { + val colors = LocalColors.current + val trayItems = listOf( + CollapsibleActionTrayItemData( + label = GetString(LocalResources.current.getString(R.string.membersInvite)), + buttonLabel = GetString(LocalResources.current.getString(R.string.membersInviteTitle)), + buttonColor = colors.accent, + onClick = { onDoneClicked() } + ) + ) + Scaffold( + contentWindowInsets = WindowInsets.safeDrawing, topBar = { BackAppBar( title = stringResource(id = R.string.membersInvite), onBack = onBack, ) }, + bottomBar = { + Box( + modifier = Modifier + .fillMaxWidth() + .imePadding() + ) { + CollapsibleActionTray( + data = CollapsibleActionTrayData( + title = data.footerActionTitle, + collapsed = data.collapsed, + visible = data.visible, + items = trayItems + ), + onCollapsedClicked = onToggleFooter, + onClosedClicked = onCloseFooter + ) + } + } ) { paddings -> Column( modifier = Modifier @@ -101,10 +147,11 @@ fun InviteContacts( Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) BottomFadingEdgeBox(modifier = Modifier.weight(1f)) { bottomContentPadding -> - if(contacts.isEmpty() && searchQuery.isEmpty()){ + if (contacts.isEmpty() && searchQuery.isEmpty()) { Text( text = stringResource(id = R.string.contactNone), - modifier = Modifier.padding(top = LocalDimensions.current.spacing) + modifier = Modifier + .padding(top = LocalDimensions.current.spacing) .align(Alignment.TopCenter), style = LocalType.current.base.copy(color = LocalColors.current.textSecondary) ) @@ -120,27 +167,7 @@ fun InviteContacts( } } } - - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth() - ) { - AccentOutlineButton( - onClick = onDoneClicked, - enabled = contacts.any { it.selected }, - modifier = Modifier - .padding(vertical = LocalDimensions.current.spacing) - .qaTag(R.string.qa_invite_button), - ) { - Text( - stringResource(id = R.string.membersInviteTitle) - ) - } - } } - } } @@ -174,6 +201,13 @@ private fun PreviewSelectContacts() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, + data = SelectContactsViewModel.InviteUiState( + collapsed = false, + visible = true, + footerActionTitle = GetString("1 Contact Selected") + ), + onToggleFooter = { }, + onCloseFooter = { }, ) } } @@ -192,7 +226,13 @@ private fun PreviewSelectEmptyContacts() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, - banner = { GroupMinimumVersionBanner() } + data = SelectContactsViewModel.InviteUiState( + collapsed = true, + visible = false, + footerActionTitle = GetString("") + ), + onToggleFooter = { }, + onCloseFooter = { } ) } } @@ -211,7 +251,13 @@ private fun PreviewSelectEmptyContactsWithSearch() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, - banner = { GroupMinimumVersionBanner() } + data = SelectContactsViewModel.InviteUiState( + collapsed = true, + visible = false, + footerActionTitle = GetString("") + ), + onToggleFooter = { }, + onCloseFooter = { } ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt index 9a5ebeb878..2136ec045d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt @@ -46,6 +46,7 @@ class CreateGroupViewModel @AssistedInject constructor( excludingAccountIDs = emptySet(), contactFiltering = SelectContactsViewModel.Factory.defaultFiltering, recipientRepository = recipientRepository, + context = appContext ) { // Child view model to handle contact selection logic diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index 4dc04a007c..cf2666cd9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -34,6 +34,7 @@ class BlockedContactsViewModel @Inject constructor( avatarUtils = avatarUtils, proStatusManager = proStatusManager, recipientRepository = recipientRepository, + context = context ) { private val _unblockDialog = MutableStateFlow(false) val unblockDialog: StateFlow = _unblockDialog diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 94db6623d8..93270c8ba0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -139,11 +139,11 @@ fun AccountIdHeader( horizontal = LocalDimensions.current.contentSpacing, vertical = LocalDimensions.current.xxsSpacing ) -){ +) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, - ){ + ) { Box( modifier = Modifier .weight(1f) @@ -156,8 +156,7 @@ fun AccountIdHeader( .border( shape = MaterialTheme.shapes.large ) - .padding(textPaddingValues) - , + .padding(textPaddingValues), text = text, style = textStyle.copy(color = LocalColors.current.textSecondary) ) @@ -216,7 +215,7 @@ fun PathDot( @Preview @Composable -fun PreviewPathDot(){ +fun PreviewPathDot() { PreviewTheme { Box( modifier = Modifier.padding(20.dp) @@ -241,8 +240,11 @@ data class OptionsCardData( val title: GetString?, val options: List> ) { - constructor(title: GetString, vararg options: RadioOption): this(title, options.asList()) - constructor(@StringRes title: Int, vararg options: RadioOption): this(GetString(title), options.asList()) + constructor(title: GetString, vararg options: RadioOption) : this(title, options.asList()) + constructor(@StringRes title: Int, vararg options: RadioOption) : this( + GetString(title), + options.asList() + ) } @Composable @@ -375,7 +377,7 @@ fun ItemButton( } } - endIcon?.let{ + endIcon?.let { Spacer(Modifier.width(LocalDimensions.current.smallSpacing)) Box( @@ -442,7 +444,7 @@ fun getCellTopShape() = RoundedCornerShape( @Composable fun getCellBottomShape() = RoundedCornerShape( - topStart = 0.dp, + topStart = 0.dp, topEnd = 0.dp, bottomEnd = LocalDimensions.current.shapeSmall, bottomStart = LocalDimensions.current.shapeSmall @@ -456,11 +458,11 @@ fun CategoryCell( dropShadow: Boolean = false, content: @Composable () -> Unit, -){ + ) { Column( modifier = modifier.fillMaxWidth() ) { - if(!title.isNullOrEmpty() || titleIcon != null) { + if (!title.isNullOrEmpty() || titleIcon != null) { Row( modifier = Modifier.padding( start = LocalDimensions.current.smallSpacing, @@ -481,12 +483,12 @@ fun CategoryCell( } } - Cell( - modifier = Modifier.fillMaxWidth(), - dropShadow = dropShadow - ){ + Cell( + modifier = Modifier.fillMaxWidth(), + dropShadow = dropShadow + ) { content() - } + } } } @@ -527,9 +529,11 @@ private fun BottomFadingEdgeBoxPreview() { content = { bottomContentPadding -> LazyColumn(contentPadding = PaddingValues(bottom = bottomContentPadding)) { items(200) { - Text("Item $it", + Text( + "Item $it", color = LocalColors.current.text, - style = LocalType.current.base) + style = LocalType.current.base + ) } } }, @@ -648,7 +652,7 @@ fun SpeechBubbleTooltip( state = tooltipState, modifier = modifier, positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { + tooltip = { val bubbleColor = LocalColors.current.backgroundBubbleReceived Card( @@ -777,7 +781,7 @@ fun CollapsibleActionTray( verticalAlignment = Alignment.CenterVertically ) { val rotation by animateFloatAsState( - targetValue = if (data.collapsed) 0f else 180f, + targetValue = if (data.collapsed) 180f else 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow @@ -792,7 +796,7 @@ fun CollapsibleActionTray( contentDescription = null ) Text( - data.title, + text = data.title.string(), modifier = Modifier .fillMaxWidth() .weight(1f) @@ -822,18 +826,23 @@ fun CollapsibleActionTray( targetOffsetY = { it } // slide down out of view when collapsing )) { CategoryCell { - Column(modifier = Modifier.fillMaxWidth() - .background(LocalColors.current.backgroundTertiary)) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(LocalColors.current.backgroundTertiary) + ) { data.items.forEachIndexed { index, item -> + val titleText = item.label() + val annotatedTitle = remember(titleText) { AnnotatedString(titleText) } if (index != 0) Divider() ActionRowItem( modifier = Modifier.background(LocalColors.current.backgroundTertiary), - title = item.label, + title = annotatedTitle, onClick = {}, qaTag = R.string.qa_string_placeholder, endContent = { SlimFillButtonRect( - item.buttonLabel, + item.buttonLabel.string(), color = item.buttonColor, modifier = Modifier .width(100.dp) @@ -850,14 +859,15 @@ fun CollapsibleActionTray( } data class CollapsibleActionTrayData( - val title: AnnotatedString, + val title: GetString, val collapsed: Boolean, + val visible: Boolean, val items: List ) data class CollapsibleActionTrayItemData( - val label: AnnotatedString, - val buttonLabel: String, + val label: GetString, + val buttonLabel: GetString, val buttonColor: Color, val onClick: () -> Unit ) @@ -871,20 +881,20 @@ fun PreviewCollapsibleActionTray( PreviewTheme(colors) { val demoItems = listOf( CollapsibleActionTrayItemData( - label = annotatedStringResource("Mute notifications"), - buttonLabel = "Mute", + label = GetString("Mute notifications"), + buttonLabel = GetString("Mute"), buttonColor = LocalColors.current.text, onClick = {} ), CollapsibleActionTrayItemData( - label = annotatedStringResource("Pin conversation"), - buttonLabel = "Pin", + label = GetString("Pin conversation"), + buttonLabel = GetString("Pin"), buttonColor = LocalColors.current.accent, onClick = {} ), CollapsibleActionTrayItemData( - label = annotatedStringResource("Delete chat"), - buttonLabel = "Delete", + label = GetString("Delete chat"), + buttonLabel = GetString("Delete"), buttonColor = LocalColors.current.danger, onClick = {} ) @@ -892,8 +902,9 @@ fun PreviewCollapsibleActionTray( CollapsibleActionTray( data = CollapsibleActionTrayData( - title = annotatedStringResource("Header"), + title = GetString("Invite Contacts"), collapsed = false, + visible = true, items = demoItems ) ) @@ -930,14 +941,15 @@ fun ExpandableText( expandedMaxLines: Int = Int.MAX_VALUE, expandButtonText: String = stringResource(id = R.string.viewMore), collapseButtonText: String = stringResource(id = R.string.viewLess), -){ +) { var expanded by remember { mutableStateOf(false) } var showButton by remember { mutableStateOf(false) } var maxHeight by remember { mutableStateOf(Dp.Unspecified) } val density = LocalDensity.current - val enableScrolling = expanded && maxHeight != Dp.Unspecified && expandedMaxLines != Int.MAX_VALUE + val enableScrolling = + expanded && maxHeight != Dp.Unspecified && expandedMaxLines != Int.MAX_VALUE BaseExpandableText( text = text, @@ -958,10 +970,11 @@ fun ExpandableText( onTextMeasured = { textLayoutResult -> showButton = expanded || textLayoutResult.hasVisualOverflow val lastVisible = (expandedMaxLines - 1).coerceAtMost(textLayoutResult.lineCount - 1) - val px = textLayoutResult.getLineBottom(lastVisible) // bottom of that line in px + val px = + textLayoutResult.getLineBottom(lastVisible) // bottom of that line in px maxHeight = with(density) { px.toDp() } }, - onTap = if(showButton){ // only expand if there is enough text + onTap = if (showButton) { // only expand if there is enough text { expanded = !expanded } } else null ) @@ -1020,11 +1033,11 @@ fun BaseExpandableText( showScroll: Boolean = false, onTextMeasured: (TextLayoutResult) -> Unit = {}, onTap: (() -> Unit)? = null -){ +) { var textModifier: Modifier = Modifier - if(qaTag != null) textModifier = textModifier.qaTag(qaTag) - if(expanded) textModifier = textModifier.height(expandedMaxHeight) - if(showScroll){ + if (qaTag != null) textModifier = textModifier.qaTag(qaTag) + if (expanded) textModifier = textModifier.height(expandedMaxHeight) + if (showScroll) { val scrollState = rememberScrollState() val scrollEdge = LocalDimensions.current.xxxsSpacing val scrollWidth = 2.dp @@ -1040,7 +1053,7 @@ fun BaseExpandableText( Column( modifier = modifier.then( - if(onTap != null) Modifier.clickable { onTap() } else Modifier + if (onTap != null) Modifier.clickable { onTap() } else Modifier ), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -1057,7 +1070,7 @@ fun BaseExpandableText( overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis ) - if(showButton) { + if (showButton) { Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) Text( text = if (expanded) collapseButtonText else expandButtonText, @@ -1187,7 +1200,7 @@ fun ActionRowItem( minHeight: Dp = LocalDimensions.current.minItemButtonHeight, paddingValues: PaddingValues = PaddingValues(horizontal = LocalDimensions.current.smallSpacing), endContent: @Composable (() -> Unit)? = null -){ +) { Row( modifier = modifier .heightIn(min = minHeight) @@ -1244,7 +1257,7 @@ fun IconActionRowItem( iconSize: Dp = LocalDimensions.current.iconMedium, minHeight: Dp = LocalDimensions.current.minItemButtonHeight, paddingValues: PaddingValues = PaddingValues(horizontal = LocalDimensions.current.smallSpacing), -){ +) { ActionRowItem( modifier = modifier, title = title, @@ -1289,7 +1302,7 @@ fun SwitchActionRowItem( subtitleStyle: TextStyle = LocalType.current.small, paddingValues: PaddingValues = PaddingValues(horizontal = LocalDimensions.current.smallSpacing), minHeight: Dp = LocalDimensions.current.minItemButtonHeight, -){ +) { ActionRowItem( modifier = modifier, title = title, @@ -1313,7 +1326,7 @@ fun SwitchActionRowItem( @Preview @Composable -fun PreviewActionRowItems(){ +fun PreviewActionRowItems() { PreviewTheme { Column( modifier = Modifier.padding(20.dp), From dfa5b36cd4c98cb4cc4ccd7d27895f968b817f6a Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 11:41:21 +0800 Subject: [PATCH 06/26] Cleanup and renaming --- .../groups/compose/InviteContactsScreen.kt | 12 ++++----- .../thoughtcrime/securesms/ui/Components.kt | 25 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt index aebdbfe0f3..bb1b5f9012 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt @@ -29,9 +29,9 @@ import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.SelectContactsViewModel import org.thoughtcrime.securesms.ui.BottomFadingEdgeBox -import org.thoughtcrime.securesms.ui.CollapsibleActionTray -import org.thoughtcrime.securesms.ui.CollapsibleActionTrayData -import org.thoughtcrime.securesms.ui.CollapsibleActionTrayItemData +import org.thoughtcrime.securesms.ui.CollapsibleFooterAction +import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData +import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.SearchBar import org.thoughtcrime.securesms.ui.components.BackAppBar @@ -87,7 +87,7 @@ fun InviteContacts( ) { val colors = LocalColors.current val trayItems = listOf( - CollapsibleActionTrayItemData( + CollapsibleFooterItemData( label = GetString(LocalResources.current.getString(R.string.membersInvite)), buttonLabel = GetString(LocalResources.current.getString(R.string.membersInviteTitle)), buttonColor = colors.accent, @@ -109,8 +109,8 @@ fun InviteContacts( .fillMaxWidth() .imePadding() ) { - CollapsibleActionTray( - data = CollapsibleActionTrayData( + CollapsibleFooterAction( + data = CollapsibleFooterActionData( title = data.footerActionTitle, collapsed = data.collapsed, visible = data.visible, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 93270c8ba0..09a4ff118f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.ui -import android.R.attr.data import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility @@ -101,14 +100,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min import androidx.compose.ui.unit.times import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R import org.thoughtcrime.securesms.ui.components.AccentOutlineButton -import org.thoughtcrime.securesms.ui.components.DangerFillButtonRect import org.thoughtcrime.securesms.ui.components.SessionSwitch import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator @@ -753,12 +750,12 @@ fun SearchBar( } /** - * CollapsingActionTray + * CollapsibleFooterAction */ @Composable -fun CollapsibleActionTray( +fun CollapsibleFooterAction( modifier: Modifier = Modifier, - data: CollapsibleActionTrayData, + data: CollapsibleFooterActionData, onCollapsedClicked: () -> Unit = {}, onClosedClicked: () -> Unit = {} ) { @@ -858,14 +855,14 @@ fun CollapsibleActionTray( } } -data class CollapsibleActionTrayData( +data class CollapsibleFooterActionData( val title: GetString, val collapsed: Boolean, val visible: Boolean, - val items: List + val items: List ) -data class CollapsibleActionTrayItemData( +data class CollapsibleFooterItemData( val label: GetString, val buttonLabel: GetString, val buttonColor: Color, @@ -880,19 +877,19 @@ fun PreviewCollapsibleActionTray( ) { PreviewTheme(colors) { val demoItems = listOf( - CollapsibleActionTrayItemData( + CollapsibleFooterItemData( label = GetString("Mute notifications"), buttonLabel = GetString("Mute"), buttonColor = LocalColors.current.text, onClick = {} ), - CollapsibleActionTrayItemData( + CollapsibleFooterItemData( label = GetString("Pin conversation"), buttonLabel = GetString("Pin"), buttonColor = LocalColors.current.accent, onClick = {} ), - CollapsibleActionTrayItemData( + CollapsibleFooterItemData( label = GetString("Delete chat"), buttonLabel = GetString("Delete"), buttonColor = LocalColors.current.danger, @@ -900,8 +897,8 @@ fun PreviewCollapsibleActionTray( ) ) - CollapsibleActionTray( - data = CollapsibleActionTrayData( + CollapsibleFooterAction( + data = CollapsibleFooterActionData( title = GetString("Invite Contacts"), collapsed = false, visible = true, From e14c297b02c012693c6ca4dd1cf67d61c062db05 Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 14:03:36 +0800 Subject: [PATCH 07/26] cleanup --- .../groups/compose/InviteContactsScreen.kt | 4 + .../thoughtcrime/securesms/ui/Components.kt | 190 ++++++++++-------- 2 files changed, 112 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt index bb1b5f9012..2e79bd6e07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt @@ -5,12 +5,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api @@ -107,6 +110,7 @@ fun InviteContacts( Box( modifier = Modifier .fillMaxWidth() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) .imePadding() ) { CollapsibleFooterAction( diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 09a4ff118f..face44adb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -15,6 +15,8 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Canvas @@ -759,95 +761,119 @@ fun CollapsibleFooterAction( onCollapsedClicked: () -> Unit = {}, onClosedClicked: () -> Unit = {} ) { - Column( - modifier = modifier - .fillMaxWidth() - .clip( - RoundedCornerShape( - topStart = LocalDimensions.current.contentSpacing, - topEnd = LocalDimensions.current.contentSpacing - ) - ) - .background(LocalColors.current.backgroundSecondary) - .padding(LocalDimensions.current.smallSpacing), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + + // Bottomsheet-like enter/exit + val enterFromBottom = remember { + slideInVertically( + // start completely off-screen below + initialOffsetY = { it }, + animationSpec = tween(durationMillis = 350, easing = FastOutSlowInEasing) + ) + fadeIn() + } + val exitToBottom = remember { + slideOutVertically( + // leave sliding down out of view + targetOffsetY = { it }, + animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing) + ) + fadeOut() + } + + AnimatedVisibility( + // drives show/hide from bottom + visible = data.visible, + enter = enterFromBottom, + exit = exitToBottom ) { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val rotation by animateFloatAsState( - targetValue = if (data.collapsed) 180f else 0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow + Column( + modifier = modifier + .fillMaxWidth() + .clip( + RoundedCornerShape( + topStart = LocalDimensions.current.contentSpacing, + topEnd = LocalDimensions.current.contentSpacing + ) ) - ) - - Icon( - modifier = Modifier - .rotate(rotation) - .clickable(onClick = onCollapsedClicked), - painter = painterResource(R.drawable.ic_chevron_down), - contentDescription = null - ) - Text( - text = data.title.string(), - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = LocalDimensions.current.smallSpacing), - style = LocalType.current.h8, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Icon( + .background(LocalColors.current.backgroundSecondary) + .padding(LocalDimensions.current.smallSpacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + Row( modifier = Modifier - .clickable(onClick = onClosedClicked), - painter = painterResource(R.drawable.ic_x), - contentDescription = null - ) - } + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + val rotation by animateFloatAsState( + targetValue = if (data.collapsed) 180f else 0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ) + ) - // Rendered actions - AnimatedVisibility( - visible = !data.collapsed, - enter = slideInVertically( - animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing), - initialOffsetY = { it } // start just below and slide up into place - ), - exit = slideOutVertically( - animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing), - targetOffsetY = { it } // slide down out of view when collapsing - )) { - CategoryCell { - Column( + Icon( + modifier = Modifier + .rotate(rotation) + .clickable(onClick = onCollapsedClicked), + painter = painterResource(R.drawable.ic_chevron_down), + contentDescription = null + ) + Text( + text = data.title.string(), modifier = Modifier .fillMaxWidth() - .background(LocalColors.current.backgroundTertiary) - ) { - data.items.forEachIndexed { index, item -> - val titleText = item.label() - val annotatedTitle = remember(titleText) { AnnotatedString(titleText) } - if (index != 0) Divider() - ActionRowItem( - modifier = Modifier.background(LocalColors.current.backgroundTertiary), - title = annotatedTitle, - onClick = {}, - qaTag = R.string.qa_string_placeholder, - endContent = { - SlimFillButtonRect( - item.buttonLabel.string(), - color = item.buttonColor, - modifier = Modifier - .width(100.dp) - ) { - item.onClick() + .weight(1f) + .padding(horizontal = LocalDimensions.current.smallSpacing), + style = LocalType.current.h8, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + modifier = Modifier + .clickable(onClick = onClosedClicked), + painter = painterResource(R.drawable.ic_x), + contentDescription = null + ) + } + + // Rendered actions + AnimatedVisibility( + visible = !data.collapsed, + enter = slideInVertically( + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing), + initialOffsetY = { it } // start just below and slide up into place + ), + exit = slideOutVertically( + animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing), + targetOffsetY = { it } // slide down out of view when collapsing + )) { + CategoryCell { + Column( + modifier = Modifier + .fillMaxWidth() + .background(LocalColors.current.backgroundTertiary) + ) { + data.items.forEachIndexed { index, item -> + val titleText = item.label() + val annotatedTitle = remember(titleText) { AnnotatedString(titleText) } + if (index != 0) Divider() + ActionRowItem( + modifier = Modifier.background(LocalColors.current.backgroundTertiary), + title = annotatedTitle, + onClick = {}, + qaTag = R.string.qa_string_placeholder, + endContent = { + SlimFillButtonRect( + item.buttonLabel.string(), + color = item.buttonColor, + modifier = Modifier + .width(100.dp) + ) { + item.onClick() + } } - } - ) + ) + } } } } From d70a0dae693503330e1ab30c4b5c9a12407c276c Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 14:15:14 +0800 Subject: [PATCH 08/26] Fixed some animations --- .../thoughtcrime/securesms/ui/Components.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index face44adb9..737f117255 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -15,8 +15,10 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Canvas @@ -839,14 +841,15 @@ fun CollapsibleFooterAction( // Rendered actions AnimatedVisibility( visible = !data.collapsed, - enter = slideInVertically( - animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing), - initialOffsetY = { it } // start just below and slide up into place - ), - exit = slideOutVertically( - animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing), - targetOffsetY = { it } // slide down out of view when collapsing - )) { + enter = expandVertically( + animationSpec = tween(durationMillis = 220, easing = FastOutSlowInEasing), + expandFrom = Alignment.Top + ) + fadeIn(animationSpec = tween(durationMillis = 150)), + exit = shrinkVertically( + animationSpec = tween(durationMillis = 180, easing = FastOutLinearInEasing), + shrinkTowards = Alignment.Top + ) + fadeOut(animationSpec = tween(durationMillis = 120)) + ) { CategoryCell { Column( modifier = Modifier From cdb21da4e69790516682919eef68dd9f630f7982 Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 14:24:40 +0800 Subject: [PATCH 09/26] String from crowdin --- .../thoughtcrime/securesms/groups/SelectContactsViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index e9e6947a33..f3e0412794 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -164,9 +164,8 @@ open class SelectContactsViewModel @AssistedInject constructor( private fun updateUiState() { val count = currentSelected.size val visible = currentSelected.isNotEmpty() - // TODO: String from crowdin val footerTitle = - GetString(context.resources.getQuantityString(R.plurals.membersInviteSend, count, count)) + GetString(context.resources.getQuantityString(R.plurals.contactSelected, count, count)) _uiState.value = InviteUiState( collapsed = count == 0, From 6b90a93c905ca57c1055400c48b229dc0f2c137f Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 14:32:56 +0800 Subject: [PATCH 10/26] remoed qa placeholder --- app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt | 2 +- content-descriptions/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 737f117255..2af56dca34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -864,7 +864,7 @@ fun CollapsibleFooterAction( modifier = Modifier.background(LocalColors.current.backgroundTertiary), title = annotatedTitle, onClick = {}, - qaTag = R.string.qa_string_placeholder, + qaTag = R.string.qa_collapsing_footer_action, endContent = { SlimFillButtonRect( item.buttonLabel.string(), diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index ad8658f248..a727f999e5 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -291,6 +291,6 @@ action-item-icon qa-blocked-contacts-settings-item - qa-placeholder + qa-collapsing-footer-action \ No newline at end of file From f9192065a2b5a7cbb91174fb83721910cd04ae32 Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 14:52:15 +0800 Subject: [PATCH 11/26] updated behavior --- .../securesms/groups/SelectContactsViewModel.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index f3e0412794..8b4c49c5db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -164,19 +164,21 @@ open class SelectContactsViewModel @AssistedInject constructor( private fun updateUiState() { val count = currentSelected.size val visible = currentSelected.isNotEmpty() - val footerTitle = + val footerTitle = if(count == 0) GetString("") else GetString(context.resources.getQuantityString(R.plurals.contactSelected, count, count)) - _uiState.value = InviteUiState( - collapsed = count == 0, - visible = visible, - footerActionTitle = footerTitle - ) + _uiState.update { + it.copy( + visible = visible, + collapsed = if(!it.visible) false else it.collapsed, + footerActionTitle = footerTitle + ) + } } data class InviteUiState( val visible: Boolean = false, - val collapsed: Boolean = true, + val collapsed: Boolean = false, val footerActionTitle : GetString = GetString("") ) From b7c497df125506e3360234a8a2a52f817260a2cd Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 28 Oct 2025 19:23:00 +0800 Subject: [PATCH 12/26] Fixed rules for bottomsheet --- .../thoughtcrime/securesms/ui/Components.kt | 158 ++++++++++++++---- 1 file changed, 125 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 2af56dca34..fff911a210 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -37,11 +38,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState @@ -66,6 +69,7 @@ import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -91,6 +95,7 @@ import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -101,6 +106,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -850,35 +856,95 @@ fun CollapsibleFooterAction( shrinkTowards = Alignment.Top ) + fadeOut(animationSpec = tween(durationMillis = 120)) ) { - CategoryCell { - Column( - modifier = Modifier - .fillMaxWidth() - .background(LocalColors.current.backgroundTertiary) - ) { - data.items.forEachIndexed { index, item -> - val titleText = item.label() - val annotatedTitle = remember(titleText) { AnnotatedString(titleText) } - if (index != 0) Divider() - ActionRowItem( - modifier = Modifier.background(LocalColors.current.backgroundTertiary), - title = annotatedTitle, - onClick = {}, - qaTag = R.string.qa_collapsing_footer_action, - endContent = { - SlimFillButtonRect( - item.buttonLabel.string(), - color = item.buttonColor, - modifier = Modifier - .width(100.dp) - ) { - item.onClick() - } - } - ) - } + CategoryCell(modifier = Modifier.padding(bottom = LocalDimensions.current.smallSpacing)) { + CollapsibleFooterActions(items = data.items) + } + } + } + } +} + +@Composable +private fun CollapsibleFooterActions( + items: List, + buttonWidthCapFraction: Float = 1f / 3f // criteria +) { + // rules for this: + // Max width should be approx 1/3 of the available space (buttonWidthCapFraction) + // Buttons should have matching widths + + BoxWithConstraints(Modifier.fillMaxWidth()) { + val density = LocalDensity.current + val capPx = (constraints.maxWidth * buttonWidthCapFraction).toInt() + val capDp = with(density) { capPx.toDp() } + + val single = items.size == 1 + val measuredMaxButtonWidthPx = remember(items, capPx) { mutableIntStateOf(1) } + + // Only do the offscreen equal width computation when we have 2+ buttons. + if (!single) { + SubcomposeLayout { parentConstraints -> + val measurables = subcompose("measureButtons") { + items.forEach { item -> + SlimFillButtonRect(item.buttonLabel.string(), color = item.buttonColor) {} } } + val placeables = measurables.map { m -> + m.measure( + Constraints( + minWidth = 1, + maxWidth = capPx, + minHeight = 0, + maxHeight = parentConstraints.maxHeight + ) + ) + } + val natural = placeables.maxOfOrNull { it.width } ?: 1 + measuredMaxButtonWidthPx.intValue = natural.coerceIn(1, capPx) + layout(0, 0) {} + } + } + + val equalWidthDp = with(density) { measuredMaxButtonWidthPx.intValue.toDp() } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(LocalColors.current.backgroundTertiary) + ) { + items.forEachIndexed { index, item -> + if (index != 0) Divider() + + val titleText = item.label() + val annotatedTitle = remember(titleText) { AnnotatedString(titleText) } + + ActionRowItem( + modifier = Modifier.background(LocalColors.current.backgroundTertiary), + title = annotatedTitle, + onClick = {}, + qaTag = R.string.qa_collapsing_footer_action, + endContent = { + + val modifier: Modifier = Modifier + .padding(start = LocalDimensions.current.smallSpacing) + + if (single) { + modifier + .wrapContentWidth() // size to content + .widthIn(max = capDp) // but don't exceed the cap + } else { + modifier.width(equalWidthDp) + } + + Box(modifier = modifier) { + SlimFillButtonRect( + item.buttonLabel.string(), + color = item.buttonColor + ) { item.onClick() } + } + + } + ) } } } @@ -907,19 +973,45 @@ fun PreviewCollapsibleActionTray( PreviewTheme(colors) { val demoItems = listOf( CollapsibleFooterItemData( - label = GetString("Mute notifications"), - buttonLabel = GetString("Mute"), - buttonColor = LocalColors.current.text, + label = GetString("Invite "), + buttonLabel = GetString("Invite"), + buttonColor = LocalColors.current.accent, onClick = {} ), CollapsibleFooterItemData( - label = GetString("Pin conversation"), - buttonLabel = GetString("Pin"), + label = GetString("Delete"), + buttonLabel = GetString("2"), + buttonColor = LocalColors.current.danger, + onClick = {} + ) + ) + + CollapsibleFooterAction( + data = CollapsibleFooterActionData( + title = GetString("Invite Contacts"), + collapsed = false, + visible = true, + items = demoItems + ) + ) + } +} + +@Preview +@Composable +fun PreviewCollapsibleActionTrayLongText( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + val demoItems = listOf( + CollapsibleFooterItemData( + label = GetString("Looooooooooooooooooooooooooooooooooooooooooooooooooooooooong"), + buttonLabel = GetString("Long Loooooooooooooong"), buttonColor = LocalColors.current.accent, onClick = {} ), CollapsibleFooterItemData( - label = GetString("Delete chat"), + label = GetString("Delete"), buttonLabel = GetString("Delete"), buttonColor = LocalColors.current.danger, onClick = {} From ccc20e36072f2f069651a766a5af6e5418b52c11 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 07:06:31 +0800 Subject: [PATCH 13/26] Slim button textStyle changed to --- .../org/thoughtcrime/securesms/ui/components/ButtonStyle.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonStyle.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonStyle.kt index bd4a855fb7..e0b67fa2ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonStyle.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonStyle.kt @@ -52,7 +52,7 @@ interface ButtonStyle { object Slim: ButtonStyle { @Composable - override fun textStyle() = LocalType.current.extraSmall.bold() + override fun textStyle() = LocalType.current.small.bold() .copy(textAlign = TextAlign.Center) override val minHeight = 29.dp } From 9f0faa8533dd6574d01a47ee1c20b72e8b29dda8 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 07:38:15 +0800 Subject: [PATCH 14/26] Expose minWidth for slim fill button --- .../java/org/thoughtcrime/securesms/ui/components/Button.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt index a1ab702564..46987cbd51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt @@ -359,6 +359,7 @@ fun SlimFillButtonRect( modifier: Modifier = Modifier, color: Color = LocalColors.current.accent, enabled: Boolean = true, + minWidth: Dp = Dp.Unspecified, onClick: () -> Unit ) { Button( @@ -367,7 +368,8 @@ fun SlimFillButtonRect( modifier, enabled, style = ButtonStyle.Slim, - shape = sessionShapes().extraSmall + shape = sessionShapes().extraSmall, + minWidth = minWidth ) } From e666367f3c0612be2de75c9c8a91c839efe51074 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 07:39:19 +0800 Subject: [PATCH 15/26] Silde in and out duration changes for collapsible footer --- app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index fff911a210..ac10f840d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -775,14 +775,14 @@ fun CollapsibleFooterAction( slideInVertically( // start completely off-screen below initialOffsetY = { it }, - animationSpec = tween(durationMillis = 350, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) ) + fadeIn() } val exitToBottom = remember { slideOutVertically( // leave sliding down out of view targetOffsetY = { it }, - animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing) + animationSpec = tween(durationMillis = 200, easing = FastOutLinearInEasing) ) + fadeOut() } From 372b2b7e5b845d5d9dacc8f4df5f838706572fb6 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 07:51:21 +0800 Subject: [PATCH 16/26] Updated collapse animation --- .../thoughtcrime/securesms/ui/Components.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index ac10f840d1..f9bcf138a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -848,13 +848,20 @@ fun CollapsibleFooterAction( AnimatedVisibility( visible = !data.collapsed, enter = expandVertically( - animationSpec = tween(durationMillis = 220, easing = FastOutSlowInEasing), - expandFrom = Alignment.Top - ) + fadeIn(animationSpec = tween(durationMillis = 150)), - exit = shrinkVertically( - animationSpec = tween(durationMillis = 180, easing = FastOutLinearInEasing), - shrinkTowards = Alignment.Top - ) + fadeOut(animationSpec = tween(durationMillis = 120)) + expandFrom = Alignment.Top, + animationSpec = spring( + dampingRatio = 0.9f, + stiffness = Spring.StiffnessLow + ) + ) + fadeIn(animationSpec = tween(durationMillis = 120, delayMillis = 40)), + exit = fadeOut(animationSpec = tween(durationMillis = 90)) + + shrinkVertically( + shrinkTowards = Alignment.Top, + animationSpec = spring( + dampingRatio = 1.0f, + stiffness = Spring.StiffnessMedium + ) + ) ) { CategoryCell(modifier = Modifier.padding(bottom = LocalDimensions.current.smallSpacing)) { CollapsibleFooterActions(items = data.items) @@ -1006,7 +1013,7 @@ fun PreviewCollapsibleActionTrayLongText( val demoItems = listOf( CollapsibleFooterItemData( label = GetString("Looooooooooooooooooooooooooooooooooooooooooooooooooooooooong"), - buttonLabel = GetString("Long Loooooooooooooong"), + buttonLabel = GetString("Long Looooooooooooooooooooong"), buttonColor = LocalColors.current.accent, onClick = {} ), From 55115158d50ff9d57873b910dea68f8ed46fa9cc Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 07:58:22 +0800 Subject: [PATCH 17/26] Fixed modifier assignment, slimfill min width set to 0dp --- .../org/thoughtcrime/securesms/ui/Components.kt | 14 +++++--------- .../thoughtcrime/securesms/ui/components/Button.kt | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index f9bcf138a9..5ef2b03915 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -931,25 +931,21 @@ private fun CollapsibleFooterActions( onClick = {}, qaTag = R.string.qa_collapsing_footer_action, endContent = { + var modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing) - val modifier: Modifier = Modifier - .padding(start = LocalDimensions.current.smallSpacing) - - if (single) { - modifier - .wrapContentWidth() // size to content - .widthIn(max = capDp) // but don't exceed the cap + modifier = if (single) { + modifier.wrapContentWidth().widthIn(max = capDp) } else { modifier.width(equalWidthDp) } Box(modifier = modifier) { SlimFillButtonRect( - item.buttonLabel.string(), + modifier = Modifier.fillMaxWidth(), + text = item.buttonLabel.string(), color = item.buttonColor ) { item.onClick() } } - } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt index 46987cbd51..d54ceceb30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt @@ -359,7 +359,7 @@ fun SlimFillButtonRect( modifier: Modifier = Modifier, color: Color = LocalColors.current.accent, enabled: Boolean = true, - minWidth: Dp = Dp.Unspecified, + minWidth: Dp = 0.dp, onClick: () -> Unit ) { Button( From 29181f85bfa0a55da180ef5258a71c72d43cf815 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 08:12:00 +0800 Subject: [PATCH 18/26] Fixed spacings --- .../thoughtcrime/securesms/ui/Components.kt | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 5ef2b03915..22b451f75a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -58,6 +58,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -802,7 +803,7 @@ fun CollapsibleFooterAction( ) ) .background(LocalColors.current.backgroundSecondary) - .padding(LocalDimensions.current.smallSpacing), + .padding(horizontal = LocalDimensions.current.smallSpacing, vertical = LocalDimensions.current.xxsSpacing), verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { Row( @@ -818,13 +819,15 @@ fun CollapsibleFooterAction( ) ) - Icon( - modifier = Modifier - .rotate(rotation) - .clickable(onClick = onCollapsedClicked), - painter = painterResource(R.drawable.ic_chevron_down), - contentDescription = null - ) + IconButton( + modifier = Modifier.rotate(rotation), + onClick = onCollapsedClicked + ) { + Icon( + painter = painterResource(R.drawable.ic_chevron_down), + contentDescription = null + ) + } Text( text = data.title.string(), modifier = Modifier @@ -836,12 +839,15 @@ fun CollapsibleFooterAction( maxLines = 1, overflow = TextOverflow.Ellipsis ) - Icon( - modifier = Modifier - .clickable(onClick = onClosedClicked), - painter = painterResource(R.drawable.ic_x), - contentDescription = null - ) + IconButton( + modifier = Modifier.rotate(rotation), + onClick = onClosedClicked + ) { + Icon( + painter = painterResource(R.drawable.ic_x), + contentDescription = null + ) + } } // Rendered actions @@ -934,7 +940,9 @@ private fun CollapsibleFooterActions( var modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing) modifier = if (single) { - modifier.wrapContentWidth().widthIn(max = capDp) + modifier + .wrapContentWidth() + .widthIn(max = capDp) } else { modifier.width(equalWidthDp) } From 9135f5975b1fe0d4ef11b17f12aa352334f2803d Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 08:30:39 +0800 Subject: [PATCH 19/26] Updated state for Collapsible footer --- .../groups/SelectContactsViewModel.kt | 46 +++++++++---------- .../groups/compose/InviteContactsScreen.kt | 10 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index 8b4c49c5db..cc371c5164 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -77,8 +78,26 @@ open class SelectContactsViewModel @AssistedInject constructor( val currentSelected: Set
get() = mutableSelectedContactAccountIDs.value - private val _uiState = MutableStateFlow(InviteUiState()) - val uiState: StateFlow = _uiState + private val footerCollapsed = MutableStateFlow(false) + + val collapsibleFooterState: StateFlow = + combine(mutableSelectedContactAccountIDs, footerCollapsed) { selected, isCollapsed -> + val count = selected.size + val visible = count > 0 + val title = if (count == 0) GetString("") + else GetString( + context.resources.getQuantityString(R.plurals.contactSelected, count, count) + ) + + CollapsibleFooterState( + visible = visible, + // auto-expand when nothing is selected, otherwise keep user's choice + collapsed = if (!visible) false else isCollapsed, + footerActionTitle = title + ) + } + .distinctUntilChanged() + .stateIn(viewModelScope, SharingStarted.Eagerly, CollapsibleFooterState()) @OptIn(ExperimentalCoroutinesApi::class) private fun observeContacts() = (configFactory.configUpdateNotifications as Flow) @@ -143,7 +162,6 @@ open class SelectContactsViewModel @AssistedInject constructor( newSet.add(address) } mutableSelectedContactAccountIDs.value = newSet - updateUiState() } fun selectAccountIDs(accountIDs: Set
) { @@ -152,31 +170,13 @@ open class SelectContactsViewModel @AssistedInject constructor( fun clearSelection(){ mutableSelectedContactAccountIDs.value = emptySet() - updateUiState() } fun toggleFooter() { - _uiState.update { - it.copy(collapsed = !it.collapsed) - } - } - - private fun updateUiState() { - val count = currentSelected.size - val visible = currentSelected.isNotEmpty() - val footerTitle = if(count == 0) GetString("") else - GetString(context.resources.getQuantityString(R.plurals.contactSelected, count, count)) - - _uiState.update { - it.copy( - visible = visible, - collapsed = if(!it.visible) false else it.collapsed, - footerActionTitle = footerTitle - ) - } + footerCollapsed.update { !it } } - data class InviteUiState( + data class CollapsibleFooterState( val visible: Boolean = false, val collapsed: Boolean = false, val footerActionTitle : GetString = GetString("") diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt index 2e79bd6e07..927f86a30f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt @@ -55,7 +55,7 @@ fun InviteContactsScreen( onBack: () -> Unit, banner: @Composable () -> Unit = {} ) { - val footerData by viewModel.uiState.collectAsState() + val footerData by viewModel.collapsibleFooterState.collectAsState() InviteContacts( contacts = viewModel.contacts.collectAsState().value, @@ -84,7 +84,7 @@ fun InviteContacts( onDoneClicked: () -> Unit, onBack: () -> Unit, banner: @Composable () -> Unit = {}, - data: SelectContactsViewModel.InviteUiState, + data: SelectContactsViewModel.CollapsibleFooterState, onToggleFooter: () -> Unit, onCloseFooter: () -> Unit, ) { @@ -205,7 +205,7 @@ private fun PreviewSelectContacts() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, - data = SelectContactsViewModel.InviteUiState( + data = SelectContactsViewModel.CollapsibleFooterState( collapsed = false, visible = true, footerActionTitle = GetString("1 Contact Selected") @@ -230,7 +230,7 @@ private fun PreviewSelectEmptyContacts() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, - data = SelectContactsViewModel.InviteUiState( + data = SelectContactsViewModel.CollapsibleFooterState( collapsed = true, visible = false, footerActionTitle = GetString("") @@ -255,7 +255,7 @@ private fun PreviewSelectEmptyContactsWithSearch() { onSearchQueryClear = {}, onDoneClicked = {}, onBack = {}, - data = SelectContactsViewModel.InviteUiState( + data = SelectContactsViewModel.CollapsibleFooterState( collapsed = true, visible = false, footerActionTitle = GetString("") From ee5394e86714cb96792635df9e125b52a7dc7892 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 08:40:39 +0800 Subject: [PATCH 20/26] Remove fadingBox --- .../securesms/groups/compose/InviteContactsScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt index 927f86a30f..b0ece45a94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt @@ -150,7 +150,7 @@ fun InviteContacts( Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - BottomFadingEdgeBox(modifier = Modifier.weight(1f)) { bottomContentPadding -> + Box(modifier = Modifier.weight(1f)) { if (contacts.isEmpty() && searchQuery.isEmpty()) { Text( text = stringResource(id = R.string.contactNone), @@ -162,7 +162,7 @@ fun InviteContacts( } else { LazyColumn( state = scrollState, - contentPadding = PaddingValues(bottom = bottomContentPadding), + contentPadding = PaddingValues(bottom = LocalDimensions.current.spacing), ) { multiSelectMemberList( contacts = contacts, From c7949ca9520a727bf849f4691e2acaf7a8633b8f Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 09:28:24 +0800 Subject: [PATCH 21/26] Removed rotation for close button --- app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 22b451f75a..33d90c0903 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -840,7 +840,6 @@ fun CollapsibleFooterAction( overflow = TextOverflow.Ellipsis ) IconButton( - modifier = Modifier.rotate(rotation), onClick = onClosedClicked ) { Icon( From a85e31012e8534f8a39f7c497a4329fee044e8f4 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 11:28:38 +0800 Subject: [PATCH 22/26] Initial animation fix --- .../org/thoughtcrime/securesms/ui/Components.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 33d90c0903..3ce3077549 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -780,10 +780,9 @@ fun CollapsibleFooterAction( ) + fadeIn() } val exitToBottom = remember { - slideOutVertically( - // leave sliding down out of view - targetOffsetY = { it }, - animationSpec = tween(durationMillis = 200, easing = FastOutLinearInEasing) + shrinkVertically( + shrinkTowards = Alignment.Bottom, + animationSpec = tween(200, easing = FastOutLinearInEasing) ) + fadeOut() } @@ -796,6 +795,7 @@ fun CollapsibleFooterAction( Column( modifier = modifier .fillMaxWidth() + .animateContentSize() .clip( RoundedCornerShape( topStart = LocalDimensions.current.contentSpacing, @@ -803,7 +803,10 @@ fun CollapsibleFooterAction( ) ) .background(LocalColors.current.backgroundSecondary) - .padding(horizontal = LocalDimensions.current.smallSpacing, vertical = LocalDimensions.current.xxsSpacing), + .padding( + horizontal = LocalDimensions.current.smallSpacing, + vertical = LocalDimensions.current.xxsSpacing + ), verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { Row( @@ -861,7 +864,7 @@ fun CollapsibleFooterAction( ) + fadeIn(animationSpec = tween(durationMillis = 120, delayMillis = 40)), exit = fadeOut(animationSpec = tween(durationMillis = 90)) + shrinkVertically( - shrinkTowards = Alignment.Top, + shrinkTowards = Alignment.Bottom, animationSpec = spring( dampingRatio = 1.0f, stiffness = Spring.StiffnessMedium From a12c2198bd3695d8ed4588016476b249eaa81ca6 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 11:48:42 +0800 Subject: [PATCH 23/26] Fixed animation --- .../main/java/org/thoughtcrime/securesms/ui/Components.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 3ce3077549..4a7baee11f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing @@ -790,12 +791,11 @@ fun CollapsibleFooterAction( // drives show/hide from bottom visible = data.visible, enter = enterFromBottom, - exit = exitToBottom + exit = exitToBottom, ) { Column( modifier = modifier .fillMaxWidth() - .animateContentSize() .clip( RoundedCornerShape( topStart = LocalDimensions.current.contentSpacing, @@ -803,6 +803,7 @@ fun CollapsibleFooterAction( ) ) .background(LocalColors.current.backgroundSecondary) + .animateContentSize() .padding( horizontal = LocalDimensions.current.smallSpacing, vertical = LocalDimensions.current.xxsSpacing @@ -852,9 +853,10 @@ fun CollapsibleFooterAction( } } + val showActions = data.visible && !data.collapsed // Rendered actions AnimatedVisibility( - visible = !data.collapsed, + visible = showActions, enter = expandVertically( expandFrom = Alignment.Top, animationSpec = spring( From e1acce66f183b4007ee9e9660e660e0d6a1531d4 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 12:30:15 +0800 Subject: [PATCH 24/26] Fixed button mod --- .../main/java/org/thoughtcrime/securesms/ui/Components.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 4a7baee11f..476d2046e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -951,9 +951,13 @@ private fun CollapsibleFooterActions( modifier.width(equalWidthDp) } + val buttonModifier = + if (single) Modifier.wrapContentWidth() + else Modifier.fillMaxWidth() + Box(modifier = modifier) { SlimFillButtonRect( - modifier = Modifier.fillMaxWidth(), + modifier = buttonModifier, text = item.buttonLabel.string(), color = item.buttonColor ) { item.onClick() } From f9624d983b975fcf4db30cb35c0f2168fb5fdc02 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 29 Oct 2025 12:32:33 +0800 Subject: [PATCH 25/26] inline modifier --- .../thoughtcrime/securesms/ui/Components.kt | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 476d2046e0..33b131b98c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -941,23 +941,16 @@ private fun CollapsibleFooterActions( onClick = {}, qaTag = R.string.qa_collapsing_footer_action, endContent = { - var modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing) - - modifier = if (single) { - modifier - .wrapContentWidth() - .widthIn(max = capDp) - } else { - modifier.width(equalWidthDp) - } - - val buttonModifier = - if (single) Modifier.wrapContentWidth() - else Modifier.fillMaxWidth() - - Box(modifier = modifier) { + Box( + modifier = Modifier + .padding(start = LocalDimensions.current.smallSpacing) + .then( + if (single) Modifier.wrapContentWidth().widthIn(max = capDp) + else Modifier.width(equalWidthDp) + ) + ) { SlimFillButtonRect( - modifier = buttonModifier, + modifier = if (single) Modifier else Modifier.fillMaxWidth(), text = item.buttonLabel.string(), color = item.buttonColor ) { item.onClick() } From 5a9a6af2357bb362bbeba7c029881e8969b5b32e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 29 Oct 2025 15:40:12 +1100 Subject: [PATCH 26/26] animation tweaks --- .../thoughtcrime/securesms/ui/Components.kt | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 33b131b98c..2ad98b9ace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -781,9 +781,9 @@ fun CollapsibleFooterAction( ) + fadeIn() } val exitToBottom = remember { - shrinkVertically( - shrinkTowards = Alignment.Bottom, - animationSpec = tween(200, easing = FastOutLinearInEasing) + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(durationMillis = 200, easing = FastOutLinearInEasing) ) + fadeOut() } @@ -858,20 +858,13 @@ fun CollapsibleFooterAction( AnimatedVisibility( visible = showActions, enter = expandVertically( - expandFrom = Alignment.Top, - animationSpec = spring( - dampingRatio = 0.9f, - stiffness = Spring.StiffnessLow - ) - ) + fadeIn(animationSpec = tween(durationMillis = 120, delayMillis = 40)), - exit = fadeOut(animationSpec = tween(durationMillis = 90)) + - shrinkVertically( - shrinkTowards = Alignment.Bottom, - animationSpec = spring( - dampingRatio = 1.0f, - stiffness = Spring.StiffnessMedium - ) - ) + animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing), + expandFrom = Alignment.Top + ) + fadeIn(animationSpec = tween(durationMillis = 120)), + exit = shrinkVertically( + animationSpec = tween(durationMillis = 100, easing = FastOutLinearInEasing), + shrinkTowards = Alignment.Top + ) + fadeOut(animationSpec = tween(durationMillis = 80)) ) { CategoryCell(modifier = Modifier.padding(bottom = LocalDimensions.current.smallSpacing)) { CollapsibleFooterActions(items = data.items)