Improves rendering performance of Chat Screens

pull/465/head
Vitor Pamplona 2023-06-21 11:57:49 -04:00
rodzic 6fe66986be
commit cce9d6cf68
7 zmienionych plików z 297 dodań i 140 usunięć

Wyświetl plik

@ -275,9 +275,10 @@ private fun RenderUserAsClickableText(
CreateClickableTextWithEmoji(
clickablePart = it,
suffix = addedCharts,
tags = userTags,
maxLines = 1,
route = route,
nav = nav
nav = nav,
tags = userTags
)
}
}
@ -286,24 +287,38 @@ private fun RenderUserAsClickableText(
fun CreateClickableText(
clickablePart: String,
suffix: String,
maxLines: Int = Int.MAX_VALUE,
overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal,
route: String,
nav: (String) -> Unit
) {
ClickableText(
text = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()
) {
val currentStyle = LocalTextStyle.current
val primaryColor = MaterialTheme.colors.primary
val onBackgroundColor = MaterialTheme.colors.onBackground
val clickablePartStyle = remember(primaryColor, overrideColor) {
currentStyle.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight).toSpanStyle()
}
val nonClickablePartStyle = remember(onBackgroundColor, overrideColor) {
currentStyle.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight).toSpanStyle()
}
val text = remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) {
buildAnnotatedString {
withStyle(clickablePartStyle) {
append(clickablePart)
}
withStyle(
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle()
) {
withStyle(nonClickablePartStyle) {
append(suffix)
}
},
}
}
ClickableText(
text = text,
maxLines = maxLines,
onClick = { nav(route) }
)
}
@ -410,14 +425,17 @@ fun CreateTextWithEmoji(
modifier = modifier
)
} else {
val style = LocalTextStyle.current.merge(
TextStyle(
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize
)
).toSpanStyle()
val currentStyle = LocalTextStyle.current
val style = remember(currentStyle) {
currentStyle.merge(
TextStyle(
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize
)
).toSpanStyle()
}
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
}
@ -426,6 +444,7 @@ fun CreateTextWithEmoji(
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
maxLines: Int = Int.MAX_VALUE,
tags: ImmutableListOfLists<String>?,
style: TextStyle,
onClick: (Int) -> Unit
@ -448,29 +467,37 @@ fun CreateClickableTextWithEmoji(
if (emojiList.isEmpty()) {
ClickableText(
AnnotatedString(clickablePart),
text = AnnotatedString(clickablePart),
style = style,
maxLines = maxLines,
onClick = onClick
)
} else {
ClickableInLineIconRenderer(emojiList, style.toSpanStyle()) {
ClickableInLineIconRenderer(emojiList, maxLines, style.toSpanStyle()) {
onClick(it)
}
}
}
@Immutable
data class DoubleEmojiList(
val part1: ImmutableList<Renderable>,
val part2: ImmutableList<Renderable>
)
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
suffix: String,
tags: ImmutableListOfLists<String>?,
maxLines: Int = Int.MAX_VALUE,
overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal,
route: String,
nav: (String) -> Unit
nav: (String) -> Unit,
tags: ImmutableListOfLists<String>?
) {
var emojiLists by remember(clickablePart) {
mutableStateOf<Pair<ImmutableList<Renderable>, ImmutableList<Renderable>>?>(null)
mutableStateOf<DoubleEmojiList?>(null)
}
LaunchedEffect(key1 = clickablePart) {
@ -483,20 +510,28 @@ fun CreateClickableTextWithEmoji(
val newEmojiList2 = assembleAnnotatedList(suffix, emojis)
if (newEmojiList1.isNotEmpty() || newEmojiList2.isNotEmpty()) {
emojiLists = Pair(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList())
emojiLists = DoubleEmojiList(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList())
}
}
}
}
if (emojiLists == null) {
CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, nav)
CreateClickableText(clickablePart, suffix, maxLines, overrideColor, fontWeight, route, nav)
} else {
ClickableInLineIconRenderer(emojiLists!!.first, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
ClickableInLineIconRenderer(
emojiLists!!.part1,
maxLines,
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()
) {
nav(route)
}
InLineIconRenderer(emojiLists!!.second, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle())
InLineIconRenderer(
emojiLists!!.part2,
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
maxLines
)
}
}
@ -521,7 +556,12 @@ class TextType(val text: String) : Renderable()
class ImageUrlType(val url: String) : Renderable()
@Composable
fun ClickableInLineIconRenderer(wordsInOrder: ImmutableList<Renderable>, style: SpanStyle, onClick: (Int) -> Unit) {
fun ClickableInLineIconRenderer(
wordsInOrder: ImmutableList<Renderable>,
maxLines: Int = Int.MAX_VALUE,
style: SpanStyle,
onClick: (Int) -> Unit
) {
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
if (value is ImageUrlType) {
Pair(
@ -574,6 +614,7 @@ fun ClickableInLineIconRenderer(wordsInOrder: ImmutableList<Renderable>, style:
text = annotatedText,
modifier = pressIndicator,
inlineContent = inlineContent,
maxLines = maxLines,
onTextLayout = {
layoutResult.value = it
}

Wyświetl plik

@ -1041,8 +1041,9 @@ private fun DisplayUserFromTag(
CreateClickableTextWithEmoji(
clickablePart = displayName,
suffix = remember { "$addedChars " },
tags = userTags,
maxLines = 1,
route = route,
nav = nav
nav = nav,
tags = userTags
)
}

Wyświetl plik

@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
@ -72,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size13dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
@ -261,11 +263,11 @@ fun NormalChatNote(
}
}
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
if (routeForLastRead != null) {
LaunchedEffect(key1 = routeForLastRead) {
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(it, createdAt)
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
}
}
}
@ -369,43 +371,72 @@ private fun RenderBubble(
}
Column(modifier = bubbleModifier) {
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = StdVertSpacer)
}
MessageBubbleLines(
drawAuthorInfo,
baseNote,
alignment,
nav,
innerQuote,
backgroundBubbleColor,
accountViewModel,
onWantsToReply,
canPreview,
bubbleSize,
availableBubbleSize
)
}
}
RenderReplyRow(
note = baseNote,
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
@Composable
private fun MessageBubbleLines(
drawAuthorInfo: Boolean,
baseNote: Note,
alignment: Arrangement.Horizontal,
nav: (String) -> Unit,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
onWantsToReply: (Note) -> Unit,
canPreview: Boolean,
bubbleSize: MutableState<IntSize>,
availableBubbleSize: MutableState<IntSize>
) {
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = StdVertSpacer)
}
RenderReplyRow(
note = baseNote,
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
NoteRow(
note = baseNote,
canPreview = canPreview,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
ConstrainedStatusRow(
bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize
) {
StatusRow(
baseNote = baseNote,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
NoteRow(
note = baseNote,
canPreview = canPreview,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
ConstrainedStatusRow(
bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize
) {
StatusRow(
baseNote = baseNote,
accountViewModel = accountViewModel,
onWantsToReply = onWantsToReply
)
}
}
}
@ -418,25 +449,42 @@ private fun RenderReplyRow(
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val replyTo by remember {
val hasReply by remember {
derivedStateOf {
note.replyTo?.lastOrNull()
innerQuote && note.replyTo?.lastOrNull() != null
}
}
if (!innerQuote && replyTo != null) {
Row(verticalAlignment = Alignment.CenterVertically) {
replyTo?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
if (hasReply) {
RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply)
}
}
@Composable
private fun RenderReply(
note: Note,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically) {
val replyTo by remember {
derivedStateOf {
note.replyTo?.lastOrNull()
}
}
replyTo?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
}
}
}
@ -501,9 +549,7 @@ private fun StatusRow(
accountViewModel: AccountViewModel,
onWantsToReply: (Note) -> Unit
) {
val grayTint = MaterialTheme.colors.placeholderText
Column() {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
ChatTimeAgo(baseNote)
RelayBadges(baseNote)
@ -511,16 +557,16 @@ private fun StatusRow(
}
}
Column() {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
LikeReaction(baseNote, grayTint, accountViewModel)
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote, grayTint, accountViewModel)
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
Spacer(modifier = StdHorzSpacer)
ReplyReaction(
baseNote,
grayTint,
accountViewModel,
baseNote = baseNote,
grayTint = MaterialTheme.colors.placeholderText,
accountViewModel = accountViewModel,
showCounter = false,
iconSize = Size16dp
) {
@ -644,51 +690,107 @@ private fun DrawAuthorInfo(
alignment: Arrangement.Horizontal,
nav: (String) -> Unit
) {
val userState by baseNote.author!!.live().metadata.observeAsState()
val pubkeyHex = remember { baseNote.author?.pubkeyHex } ?: return
val route = remember { "User/$pubkeyHex" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = alignment,
modifier = Modifier.padding(top = 5.dp)
) {
RobohashAsyncImageProxy(
robot = pubkeyHex,
model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = remember {
Modifier
.width(25.dp)
.height(25.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
nav(route)
})
}
)
DisplayAndWatchNoteAuthor(baseNote, nav)
}
}
userDisplayName?.let {
Spacer(modifier = StdHorzSpacer)
@Composable
private fun DisplayAndWatchNoteAuthor(
baseNote: Note,
nav: (String) -> Unit
) {
val author = remember {
baseNote.author
}
author?.let {
WatchAndDisplayUser(it, nav)
}
}
CreateClickableTextWithEmoji(
clickablePart = it,
suffix = "",
tags = userTags,
fontWeight = FontWeight.Bold,
overrideColor = MaterialTheme.colors.onBackground,
route = route,
nav = nav
)
@Composable
private fun WatchAndDisplayUser(
author: User,
nav: (String) -> Unit
) {
val pubkeyHex = remember { author.pubkeyHex }
val route = remember { "User/${author.pubkeyHex}" }
Spacer(modifier = StdHorzSpacer)
DrawPlayName(it)
val userState by author.live().metadata.observeAsState()
val userDisplayName by remember(userState) {
derivedStateOf {
userState?.user?.toBestDisplayName()
}
}
val userProfilePicture by remember(userState) {
derivedStateOf {
ResizeImage(userState?.user?.profilePicture(), Size25dp)
}
}
val userTags by remember(userState) {
derivedStateOf {
userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}
UserIcon(pubkeyHex, userProfilePicture, nav, route)
userDisplayName?.let {
DisplayMessageUsername(it, userTags, route, nav)
}
}
@Composable
private fun UserIcon(
pubkeyHex: String,
userProfilePicture: ResizeImage,
nav: (String) -> Unit,
route: String
) {
RobohashAsyncImageProxy(
robot = pubkeyHex,
model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = remember {
Modifier
.width(Size25dp)
.height(Size25dp)
.clip(shape = CircleShape)
.clickable(onClick = {
nav(route)
})
}
)
}
@Composable
private fun DisplayMessageUsername(
userDisplayName: String,
userTags: ImmutableListOfLists<String>?,
route: String,
nav: (String) -> Unit
) {
Spacer(modifier = StdHorzSpacer)
CreateClickableTextWithEmoji(
clickablePart = userDisplayName,
suffix = "",
maxLines = 1,
tags = userTags,
fontWeight = FontWeight.Bold,
overrideColor = MaterialTheme.colors.onBackground, // we do not want clickable names in purple here.
route = route,
nav = nav
)
Spacer(modifier = StdHorzSpacer)
DrawPlayName(userDisplayName)
}
@Immutable

Wyświetl plik

@ -2062,9 +2062,10 @@ private fun LoadAndDisplayUser(
CreateClickableTextWithEmoji(
clickablePart = userDisplayName,
suffix = " ",
tags = userTags,
maxLines = 1,
route = route,
nav = nav
nav = nav,
tags = userTags
)
}
}

Wyświetl plik

@ -65,14 +65,16 @@ private fun UserNameDisplay(
fontWeight = FontWeight.Bold,
maxLines = 1
)
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
if (bestDisplayName != bestUserName) {
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
}
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
} else if (bestDisplayName != null) {
@ -88,7 +90,7 @@ private fun UserNameDisplay(
DrawPlayName(bestDisplayName)
} else if (bestUserName != null) {
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
text = bestUserName,
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1,
@ -99,7 +101,7 @@ private fun UserNameDisplay(
DrawPlayName(bestUserName)
} else {
Text(
npubDisplay,
text = npubDisplay,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -113,8 +115,15 @@ fun DrawPlayName(name: String) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
DrawPlayNameIcon {
speak(name, context, lifecycleOwner)
}
}
@Composable
fun DrawPlayNameIcon(onClick: () -> Unit) {
IconButton(
onClick = { speak(name, context, lifecycleOwner) },
onClick = onClick,
modifier = StdButtonSizeModifier
) {
Icon(

Wyświetl plik

@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@ -236,7 +237,7 @@ fun ChannelScreen(
)
}
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = DoubleVertSpacer)
replyTo.value?.let {
DisplayReplyingToNote(it, accountViewModel, nav) {

Wyświetl plik

@ -26,10 +26,12 @@ val StdButtonSizeModifier = Modifier.size(20.dp)
val StdHorzSpacer = Modifier.width(5.dp)
val StdVertSpacer = Modifier.height(5.dp)
val DoubleHorzSpacer = Modifier.width(10.dp)
val DoubleVertSpacer = Modifier.width(10.dp)
val Size35dp = 35.dp
val Size13dp = 13.dp
val Size16dp = 16.dp
val Size25dp = 25.dp
val Size35dp = 35.dp
val StdPadding = Modifier.padding(10.dp)