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.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<String>?,
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
)
}
}
@@ -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))
}
}
@@ -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<Boolean>::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<Boolean>::topBarTransitionSpec,
label = "TopBarAnimatedContent"
) { isVisible ->
if (isVisible) {
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
private fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
val context = LocalContext.current
@@ -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)
@@ -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)