Optimizes the rendering of the side bar.

This commit is contained in:
Vitor Pamplona
2023-12-02 17:23:15 -05:00
parent 6ef1bee8a0
commit 7cd09261de
5 changed files with 195 additions and 192 deletions
@@ -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.screen.loggedIn.ConnectOrbotDialog
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer 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.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier 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.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.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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -90,6 +98,15 @@ fun DrawerContent(
openSheet: () -> Unit, openSheet: () -> Unit,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val coroutineScope = rememberCoroutineScope()
val onClickUser = {
nav("User/${accountViewModel.userProfile().pubkeyHex}")
coroutineScope.launch {
drawerState.close()
}
Unit
}
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -100,27 +117,31 @@ fun DrawerContent(
) { ) {
Column() { Column() {
ProfileContent( ProfileContent(
accountViewModel.account.userProfile(), baseAccountUser = accountViewModel.account.userProfile(),
modifier = Modifier modifier = profileContentHeaderModifier,
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 70.dp),
drawerState,
accountViewModel, accountViewModel,
nav onClickUser
) )
Column(drawerSpacing) {
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel)
}
FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser)
Divider( Divider(
thickness = DividerThickness, thickness = DividerThickness,
modifier = Modifier.padding(top = 20.dp) modifier = Modifier.padding(top = 20.dp)
) )
ListContent( ListContent(
nav,
drawerState,
openSheet,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f),
accountViewModel drawerState,
openSheet,
accountViewModel,
nav
) )
BottomContent( BottomContent(
@@ -137,45 +158,48 @@ fun DrawerContent(
fun ProfileContent( fun ProfileContent(
baseAccountUser: User, baseAccountUser: User,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
drawerState: DrawerState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit onClickUser: () -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val userInfo by baseAccountUser.live().userMetadataInfo.observeAsState()
val accountUserState by baseAccountUser.live().metadata.observeAsState() ProfileContentTemplate(
profilePubHex = baseAccountUser.pubkeyHex,
val profilePubHex = remember(accountUserState) { accountUserState?.user?.pubkeyHex } ?: return profileBanner = userInfo?.banner,
profilePicture = userInfo?.profilePicture(),
val profileBanner = remember(accountUserState) { accountUserState?.user?.info?.banner?.ifBlank { null } } bestDisplayName = userInfo?.bestDisplayName(),
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() } tags = userInfo?.tags,
// val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() } modifier = modifier,
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.toBestDisplayName() } accountViewModel = accountViewModel,
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } onClick = onClickUser
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" } )
}
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
@Composable
fun ProfileContentTemplate(
profilePubHex: HexKey,
profileBanner: String?,
profilePicture: String?,
bestDisplayName: String?,
tags: ImmutableListOfLists<String>?,
modifier: Modifier,
accountViewModel: AccountViewModel,
onClick: () -> Unit
) {
Box { Box {
if (profileBanner != null) { if (profileBanner != null) {
AsyncImage( AsyncImage(
model = profileBanner, model = profileBanner,
contentDescription = stringResource(id = R.string.profile_image), contentDescription = stringResource(id = R.string.profile_image),
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = Modifier modifier = bannerModifier
.fillMaxWidth()
.height(120.dp)
) )
} else { } else {
Image( Image(
painter = painterResource(R.drawable.profile_banner), painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner), contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = Modifier modifier = bannerModifier
.fillMaxWidth()
.height(120.dp)
) )
} }
@@ -190,13 +214,8 @@ fun ProfileContent(
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape) .border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.clickable(onClick = { .clickable(onClick = onClick),
nav(route) loadProfilePicture = accountViewModel.settings.showProfilePictures.value
coroutineScope.launch {
drawerState.close()
}
}),
loadProfilePicture = automaticallyShowProfilePicture
) )
if (bestDisplayName != null) { if (bestDisplayName != null) {
@@ -205,139 +224,98 @@ fun ProfileContent(
tags = tags, tags = tags,
modifier = Modifier modifier = Modifier
.padding(top = 7.dp) .padding(top = 7.dp)
.clickable(onClick = { .clickable(onClick = onClick),
nav(route)
coroutineScope.launch {
drawerState.close()
}
}),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp, fontSize = 18.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis 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 @Composable
private fun EditStatusBoxes(baseAccountUser: User, accountViewModel: AccountViewModel) { private fun EditStatusBoxes(baseAccountUser: User, accountViewModel: AccountViewModel) {
val focusManager = LocalFocusManager.current
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses -> LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
val currentStatus = remember { StatusEditBar(accountViewModel = accountViewModel)
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)
}
}
}
)
} else { } else {
statuses.forEach { statuses.forEach {
val originalStatus by it.live().content.observeAsState("") val originalStatus by it.live().content.observeAsState()
val thisStatus = remember { StatusEditBar(originalStatus, it.address, accountViewModel)
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)
}
}
}
)
} }
} }
} }
} }
@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 @Composable
fun SendButton(onClick: () -> Unit) { fun SendButton(onClick: () -> Unit) {
IconButton( IconButton(
@@ -369,7 +347,7 @@ fun UserStatusDeleteButton(onClick: () -> Unit) {
} }
@Composable @Composable
private fun FollowingAndFollowerCounts(baseAccountUser: User) { private fun FollowingAndFollowerCounts(baseAccountUser: User, onClick: () -> Unit) {
var followingCount by remember { mutableStateOf("--") } var followingCount by remember { mutableStateOf("--") }
var followerCount by remember { mutableStateOf("--") } var followerCount by remember { mutableStateOf("--") }
@@ -385,21 +363,25 @@ private fun FollowingAndFollowerCounts(baseAccountUser: User) {
} }
} }
Text( Row(
text = followingCount, modifier = drawerSpacing.clickable(onClick = onClick)
fontWeight = FontWeight.Bold ) {
) Text(
text = followingCount,
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.following)) Text(stringResource(R.string.following))
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
Text( Text(
text = followerCount, text = followerCount,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text(stringResource(R.string.followers)) Text(stringResource(R.string.followers))
}
} }
@Composable @Composable
@@ -426,20 +408,18 @@ fun WatchFollower(baseAccountUser: User, onReady: (String) -> Unit) {
@Composable @Composable
fun ListContent( fun ListContent(
nav: (String) -> Unit, modifier: Modifier,
drawerState: DrawerState, drawerState: DrawerState,
openSheet: () -> Unit, openSheet: () -> Unit,
modifier: Modifier, accountViewModel: AccountViewModel,
accountViewModel: AccountViewModel nav: (String) -> Unit
) { ) {
val route = remember(accountViewModel) { val route = remember(accountViewModel) {
"User/${accountViewModel.userProfile().pubkeyHex}" "User/${accountViewModel.userProfile().pubkeyHex}"
} }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var wantsToEditRelays by remember { var wantsToEditRelays by remember { mutableStateOf(false) }
mutableStateOf(false)
}
var backupDialogOpen by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) }
var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) } 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( 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, icon = R.drawable.ic_tor,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
onLongClick = { onLongClick = {
@@ -672,21 +651,19 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl
) )
) { ) {
Row( Row(
modifier = Modifier modifier = IconRowModifier,
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 25.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
painter = painterResource(icon), painter = painterResource(icon),
null, null,
modifier = Modifier.size(22.dp), modifier = Size22Modifier,
tint = tint tint = tint
) )
Text( Text(
modifier = Modifier.padding(start = 16.dp), modifier = IconRowTextModifier,
text = title, text = title,
fontSize = 18.sp fontSize = Font18SP
) )
} }
} }
@@ -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) { 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) { viewModelScope.launch(Dispatchers.IO) {
account.deleteStatus(it) account.deleteStatus(LocalCache.getOrCreateAddressableNote(it))
} }
} }
@@ -3,7 +3,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.res.Configuration import android.content.res.Configuration
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
@@ -302,10 +305,8 @@ fun MainScreen(
bottomBar = { bottomBar = {
AnimatedContent( AnimatedContent(
targetState = shouldShow.value, targetState = shouldShow.value,
transitionSpec = { transitionSpec = AnimatedContentTransitionScope<Boolean>::bottomBarTransitionSpec,
slideInVertically { height -> height } togetherWith label = "BottomBarAnimatedContent"
slideOutVertically { height -> height }
}
) { isVisible -> ) { isVisible ->
if (isVisible) { if (isVisible) {
AppBottomBar(accountViewModel, navState, navBottomRow) AppBottomBar(accountViewModel, navState, navBottomRow)
@@ -315,10 +316,8 @@ fun MainScreen(
topBar = { topBar = {
AnimatedContent( AnimatedContent(
targetState = shouldShow.value, targetState = shouldShow.value,
transitionSpec = { transitionSpec = AnimatedContentTransitionScope<Boolean>::topBarTransitionSpec,
slideInVertically { height -> 0 } togetherWith label = "TopBarAnimatedContent"
slideOutVertically { height -> 0 }
}
) { isVisible -> ) { isVisible ->
if (isVisible) { if (isVisible) {
AppTopBar( AppTopBar(
@@ -396,6 +395,23 @@ fun MainScreen(
} }
} }
@OptIn(ExperimentalAnimationApi::class)
private fun <S> AnimatedContentTransitionScope<S>.topBarTransitionSpec(): ContentTransform {
return topBarAnimation
}
@OptIn(ExperimentalAnimationApi::class)
private fun <S> AnimatedContentTransitionScope<S>.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 @Composable
private fun DisplayErrorMessages(accountViewModel: AccountViewModel) { private fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
val context = LocalContext.current val context = LocalContext.current
@@ -148,3 +148,12 @@ val EditFieldLeadingIconModifier = Modifier
val ZeroPadding = PaddingValues(0.dp) val ZeroPadding = PaddingValues(0.dp)
val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp) val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp)
val ButtonPadding = PaddingValues(vertical = 6.dp, horizontal = 16.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)
@@ -33,6 +33,7 @@ val Typography = Typography(
val Font12SP = 12.sp val Font12SP = 12.sp
val Font14SP = 14.sp val Font14SP = 14.sp
val Font17SP = 17.sp val Font17SP = 17.sp
val Font18SP = 18.sp
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em) val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)