diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 87e1d2f4e..c24a60510 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -74,12 +74,20 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Font18SP +import com.vitorpamplona.amethyst.ui.theme.IconRowModifier +import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier import com.vitorpamplona.amethyst.ui.theme.Size16dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size26Modifier +import com.vitorpamplona.amethyst.ui.theme.bannerModifier +import com.vitorpamplona.amethyst.ui.theme.drawerSpacing import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.events.toImmutableListOfLists +import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -90,6 +98,15 @@ fun DrawerContent( openSheet: () -> Unit, accountViewModel: AccountViewModel ) { + val coroutineScope = rememberCoroutineScope() + val onClickUser = { + nav("User/${accountViewModel.userProfile().pubkeyHex}") + coroutineScope.launch { + drawerState.close() + } + Unit + } + val automaticallyShowProfilePicture = remember { accountViewModel.settings.showProfilePictures.value } @@ -100,27 +117,31 @@ fun DrawerContent( ) { Column() { ProfileContent( - accountViewModel.account.userProfile(), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 25.dp) - .padding(top = 70.dp), - drawerState, + baseAccountUser = accountViewModel.account.userProfile(), + modifier = profileContentHeaderModifier, accountViewModel, - nav + onClickUser ) + + Column(drawerSpacing) { + EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel) + } + + FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser) + Divider( thickness = DividerThickness, modifier = Modifier.padding(top = 20.dp) ) + ListContent( - nav, - drawerState, - openSheet, modifier = Modifier .fillMaxWidth() .weight(1f), - accountViewModel + drawerState, + openSheet, + accountViewModel, + nav ) BottomContent( @@ -137,45 +158,48 @@ fun DrawerContent( fun ProfileContent( baseAccountUser: User, modifier: Modifier = Modifier, - drawerState: DrawerState, accountViewModel: AccountViewModel, - nav: (String) -> Unit + onClickUser: () -> Unit ) { - val coroutineScope = rememberCoroutineScope() + val userInfo by baseAccountUser.live().userMetadataInfo.observeAsState() - val accountUserState by baseAccountUser.live().metadata.observeAsState() - - val profilePubHex = remember(accountUserState) { accountUserState?.user?.pubkeyHex } ?: return - - val profileBanner = remember(accountUserState) { accountUserState?.user?.info?.banner?.ifBlank { null } } - val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() } - // val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() } - val bestDisplayName = remember(accountUserState) { accountUserState?.user?.toBestDisplayName() } - val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } - val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" } - - val automaticallyShowProfilePicture = remember { - accountViewModel.settings.showProfilePictures.value - } + ProfileContentTemplate( + profilePubHex = baseAccountUser.pubkeyHex, + profileBanner = userInfo?.banner, + profilePicture = userInfo?.profilePicture(), + bestDisplayName = userInfo?.bestDisplayName(), + tags = userInfo?.tags, + modifier = modifier, + accountViewModel = accountViewModel, + onClick = onClickUser + ) +} +@Composable +fun ProfileContentTemplate( + profilePubHex: HexKey, + profileBanner: String?, + profilePicture: String?, + bestDisplayName: String?, + tags: ImmutableListOfLists?, + modifier: Modifier, + accountViewModel: AccountViewModel, + onClick: () -> Unit +) { Box { if (profileBanner != null) { AsyncImage( model = profileBanner, contentDescription = stringResource(id = R.string.profile_image), contentScale = ContentScale.FillWidth, - modifier = Modifier - .fillMaxWidth() - .height(120.dp) + modifier = bannerModifier ) } else { Image( painter = painterResource(R.drawable.profile_banner), contentDescription = stringResource(R.string.profile_banner), contentScale = ContentScale.FillWidth, - modifier = Modifier - .fillMaxWidth() - .height(120.dp) + modifier = bannerModifier ) } @@ -190,13 +214,8 @@ fun ProfileContent( .clip(shape = CircleShape) .border(3.dp, MaterialTheme.colorScheme.background, CircleShape) .background(MaterialTheme.colorScheme.background) - .clickable(onClick = { - nav(route) - coroutineScope.launch { - drawerState.close() - } - }), - loadProfilePicture = automaticallyShowProfilePicture + .clickable(onClick = onClick), + loadProfilePicture = accountViewModel.settings.showProfilePictures.value ) if (bestDisplayName != null) { @@ -205,139 +224,98 @@ fun ProfileContent( tags = tags, modifier = Modifier .padding(top = 7.dp) - .clickable(onClick = { - nav(route) - coroutineScope.launch { - drawerState.close() - } - }), + .clickable(onClick = onClick), fontWeight = FontWeight.Bold, fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) } - - Column(Modifier.padding(top = Size10dp)) { - EditStatusBoxes(baseAccountUser, accountViewModel) - } - - Row( - modifier = Modifier - .padding(top = Size10dp) - .clickable(onClick = { - nav(route) - coroutineScope.launch { - drawerState.close() - } - }) - ) { - FollowingAndFollowerCounts(baseAccountUser) - } } } } @Composable private fun EditStatusBoxes(baseAccountUser: User, accountViewModel: AccountViewModel) { - val focusManager = LocalFocusManager.current - LoadStatuses(user = baseAccountUser, accountViewModel) { statuses -> if (statuses.isEmpty()) { - val currentStatus = remember { - mutableStateOf("") - } - val hasChanged by remember { - derivedStateOf { - currentStatus.value != "" - } - } - - OutlinedTextField( - value = currentStatus.value, - onValueChange = { currentStatus.value = it }, - label = { Text(text = stringResource(R.string.status_update)) }, - modifier = Modifier.fillMaxWidth(), - placeholder = { - Text( - text = stringResource(R.string.status_update), - color = MaterialTheme.colorScheme.placeholderText - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Send, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions = KeyboardActions( - onSend = { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } - ), - singleLine = true, - trailingIcon = { - if (hasChanged) { - SendButton() { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } - } - } - ) + StatusEditBar(accountViewModel = accountViewModel) } else { statuses.forEach { - val originalStatus by it.live().content.observeAsState("") + val originalStatus by it.live().content.observeAsState() - val thisStatus = remember { - mutableStateOf(originalStatus) - } - val hasChanged by remember { - derivedStateOf { - thisStatus.value != originalStatus - } - } - - OutlinedTextField( - value = thisStatus.value, - onValueChange = { thisStatus.value = it }, - label = { Text(text = stringResource(R.string.status_update)) }, - modifier = Modifier.fillMaxWidth(), - placeholder = { - Text( - text = stringResource(R.string.status_update), - color = MaterialTheme.colorScheme.placeholderText - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Send, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions = KeyboardActions( - onSend = { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } - ), - singleLine = true, - trailingIcon = { - if (hasChanged) { - SendButton() { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } - } else { - UserStatusDeleteButton() { - accountViewModel.deleteStatus(it) - focusManager.clearFocus(true) - } - } - } - ) + StatusEditBar(originalStatus, it.address, accountViewModel) } } } } +@Composable +fun StatusEditBar( + savedStatus: String? = null, + tag: ATag? = null, + accountViewModel: AccountViewModel +) { + val focusManager = LocalFocusManager.current + + val currentStatus = remember { + mutableStateOf(savedStatus ?: "") + } + val hasChanged = remember { + derivedStateOf { + currentStatus.value != (savedStatus ?: "") + } + } + + OutlinedTextField( + value = currentStatus.value, + onValueChange = { currentStatus.value = it }, + label = { Text(text = stringResource(R.string.status_update)) }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = stringResource(R.string.status_update), + color = MaterialTheme.colorScheme.placeholderText + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Send, + capitalization = KeyboardCapitalization.Sentences + ), + keyboardActions = KeyboardActions( + onSend = { + if (tag == null) { + accountViewModel.createStatus(currentStatus.value) + } else { + accountViewModel.updateStatus(tag, currentStatus.value) + } + + focusManager.clearFocus(true) + } + ), + singleLine = true, + trailingIcon = { + if (hasChanged.value) { + SendButton { + if (tag == null) { + accountViewModel.createStatus(currentStatus.value) + } else { + accountViewModel.updateStatus(tag, currentStatus.value) + } + focusManager.clearFocus(true) + } + } else { + if (tag != null) { + UserStatusDeleteButton { + accountViewModel.deleteStatus(tag) + focusManager.clearFocus(true) + } + } + } + } + ) +} + @Composable fun SendButton(onClick: () -> Unit) { IconButton( @@ -369,7 +347,7 @@ fun UserStatusDeleteButton(onClick: () -> Unit) { } @Composable -private fun FollowingAndFollowerCounts(baseAccountUser: User) { +private fun FollowingAndFollowerCounts(baseAccountUser: User, onClick: () -> Unit) { var followingCount by remember { mutableStateOf("--") } var followerCount by remember { mutableStateOf("--") } @@ -385,21 +363,25 @@ private fun FollowingAndFollowerCounts(baseAccountUser: User) { } } - Text( - text = followingCount, - fontWeight = FontWeight.Bold - ) + Row( + modifier = drawerSpacing.clickable(onClick = onClick) + ) { + Text( + text = followingCount, + fontWeight = FontWeight.Bold + ) - Text(stringResource(R.string.following)) + Text(stringResource(R.string.following)) - Spacer(modifier = DoubleHorzSpacer) + Spacer(modifier = DoubleHorzSpacer) - Text( - text = followerCount, - fontWeight = FontWeight.Bold - ) + Text( + text = followerCount, + fontWeight = FontWeight.Bold + ) - Text(stringResource(R.string.followers)) + Text(stringResource(R.string.followers)) + } } @Composable @@ -426,20 +408,18 @@ fun WatchFollower(baseAccountUser: User, onReady: (String) -> Unit) { @Composable fun ListContent( - nav: (String) -> Unit, + modifier: Modifier, drawerState: DrawerState, openSheet: () -> Unit, - modifier: Modifier, - accountViewModel: AccountViewModel + accountViewModel: AccountViewModel, + nav: (String) -> Unit ) { val route = remember(accountViewModel) { "User/${accountViewModel.userProfile().pubkeyHex}" } val coroutineScope = rememberCoroutineScope() - var wantsToEditRelays by remember { - mutableStateOf(false) - } + var wantsToEditRelays by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) } var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) } @@ -504,9 +484,8 @@ fun ListContent( ) } - val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short) IconRow( - title = textTorProxy, + title = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short), icon = R.drawable.ic_tor, tint = MaterialTheme.colorScheme.onBackground, onLongClick = { @@ -672,21 +651,19 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl ) ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 15.dp, horizontal = 25.dp), + modifier = IconRowModifier, verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(icon), null, - modifier = Modifier.size(22.dp), + modifier = Size22Modifier, tint = tint ) Text( - modifier = Modifier.padding(start = 16.dp), + modifier = IconRowTextModifier, text = title, - fontSize = 18.sp + fontSize = Font18SP ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 4830f9585..cd83ca014 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -675,15 +675,15 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun updateStatus(it: AddressableNote, newStatus: String) { + fun updateStatus(it: ATag, newStatus: String) { viewModelScope.launch(Dispatchers.IO) { - account.updateStatus(it, newStatus) + account.updateStatus(LocalCache.getOrCreateAddressableNote(it), newStatus) } } - fun deleteStatus(it: AddressableNote) { + fun deleteStatus(it: ATag) { viewModelScope.launch(Dispatchers.IO) { - account.deleteStatus(it) + account.deleteStatus(LocalCache.getOrCreateAddressableNote(it)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index fc0f6ef5a..14810b779 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -3,7 +3,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically @@ -302,10 +305,8 @@ fun MainScreen( bottomBar = { AnimatedContent( targetState = shouldShow.value, - transitionSpec = { - slideInVertically { height -> height } togetherWith - slideOutVertically { height -> height } - } + transitionSpec = AnimatedContentTransitionScope::bottomBarTransitionSpec, + label = "BottomBarAnimatedContent" ) { isVisible -> if (isVisible) { AppBottomBar(accountViewModel, navState, navBottomRow) @@ -315,10 +316,8 @@ fun MainScreen( topBar = { AnimatedContent( targetState = shouldShow.value, - transitionSpec = { - slideInVertically { height -> 0 } togetherWith - slideOutVertically { height -> 0 } - } + transitionSpec = AnimatedContentTransitionScope::topBarTransitionSpec, + label = "TopBarAnimatedContent" ) { isVisible -> if (isVisible) { AppTopBar( @@ -396,6 +395,23 @@ fun MainScreen( } } +@OptIn(ExperimentalAnimationApi::class) +private fun AnimatedContentTransitionScope.topBarTransitionSpec(): ContentTransform { + return topBarAnimation +} + +@OptIn(ExperimentalAnimationApi::class) +private fun AnimatedContentTransitionScope.bottomBarTransitionSpec(): ContentTransform { + return bottomBarAnimation +} + +@ExperimentalAnimationApi +val topBarAnimation: ContentTransform = + slideInVertically { height -> 0 } togetherWith slideOutVertically { height -> 0 } + +val bottomBarAnimation: ContentTransform = + slideInVertically { height -> height } togetherWith slideOutVertically { height -> height } + @Composable private fun DisplayErrorMessages(accountViewModel: AccountViewModel) { val context = LocalContext.current diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index eefb1ce07..f9a7ede87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -148,3 +148,12 @@ val EditFieldLeadingIconModifier = Modifier val ZeroPadding = PaddingValues(0.dp) val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp) val ButtonPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) + +val profileContentHeaderModifier = Modifier.fillMaxWidth().padding(top = 70.dp, start = Size25dp, end = Size25dp) +val bannerModifier = Modifier.fillMaxWidth().height(120.dp) +val drawerSpacing = Modifier.padding(top = Size10dp, start = Size25dp, end = Size25dp) + +val IconRowTextModifier = Modifier.padding(start = 16.dp) +val IconRowModifier = Modifier + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 25.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt index 24824f181..09747ac3f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt @@ -33,6 +33,7 @@ val Typography = Typography( val Font12SP = 12.sp val Font14SP = 14.sp val Font17SP = 17.sp +val Font18SP = 18.sp val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)