Performance Improvements

This commit is contained in:
Vitor Pamplona 2023-06-26 14:25:46 -04:00
parent 743a4c9d87
commit f9d652f849
15 changed files with 465 additions and 389 deletions

View File

@ -1134,7 +1134,7 @@ class Account(
user.countReportAuthorsBy(followingKeySet()) < 5
}
fun isAcceptableDirect(note: Note): Boolean {
private fun isAcceptableDirect(note: Note): Boolean {
if (!warnAboutPostsWithReports) {
return note.reportsBy(userProfile()).isEmpty()
}

View File

@ -33,7 +33,7 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
}
override fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) return null
if (info.picture.isNullOrBlank()) return super.profilePicture()
return info.picture ?: super.profilePicture()
}
@ -65,7 +65,7 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
}
override fun profilePicture(): String? {
return info?.image()?.ifBlank { null } ?: super.profilePicture()
return info?.image()?.ifBlank { null }
}
override fun anyNameStartsWith(prefix: String): Boolean {

View File

@ -12,6 +12,8 @@ import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
@ -237,6 +239,7 @@ class User(val pubkeyHex: String) {
info = newUserInfo
info?.latestMetadata = latestMetadata
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
try {
@ -421,6 +424,7 @@ class UserMetadata {
var updatedMetadataAt: Long = 0
var latestMetadata: MetadataEvent? = null
var tags: ImmutableListOfLists<String>? = null
fun anyName(): String? {
return display_name ?: displayName ?: name ?: username
@ -434,6 +438,23 @@ class UserMetadata {
fun lnAddress(): String? {
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
}
fun bestUsername(): String? {
return name?.ifBlank { null } ?: username?.ifBlank { null }
}
fun bestDisplayName(): String? {
return displayName?.ifBlank { null } ?: display_name?.ifBlank { null }
}
fun nip05(): String? {
return nip05?.ifBlank { null }
}
fun profilePicture(): String? {
if (picture.isNullOrBlank()) picture = null
return picture
}
}
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {

View File

@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@ -389,7 +390,7 @@ fun UserComposeForChat(
),
verticalAlignment = Alignment.CenterVertically
) {
ClickableUserPicture(baseUser, 55.dp, accountViewModel)
ClickableUserPicture(baseUser, Size55dp, accountViewModel)
Column(
modifier = Modifier

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.LruCache
import android.util.Patterns
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
@ -26,6 +27,8 @@ import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.HeadingStyle
@ -40,7 +43,6 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
@ -751,7 +753,11 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
}
}
loadedLink = LoadedBechLink(returningNote, it)
val newLink = LoadedBechLink(returningNote, it)
launch(Dispatchers.Main) {
loadedLink = newLink
}
}
}
}
@ -838,10 +844,12 @@ fun HashTag(word: String, nav: (String) -> Unit) {
}
if (myTag != null) {
launch(Dispatchers.Main) {
tagSuffixPair = Pair(myTag, mySuffix)
}
}
}
}
tagSuffixPair?.let { tagPair ->
val hashtagIcon = remember(tagPair.first) { checkForHashtagWithIcon(tagPair.first) }
@ -922,10 +930,13 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
launch(Dispatchers.Main) {
loadedTag = LoadedTag(it, null, suffix)
}
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let {
launch(Dispatchers.Main) {
loadedTag = LoadedTag(null, it, suffix)
}
}
@ -934,6 +945,7 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
}
}
}
}
if (loadedTag == null) {
Text(
@ -995,25 +1007,23 @@ private fun DisplayUserFromTag(
) {
val route = remember { "User/${baseUser.pubkeyHex}" }
val suffix = remember { "$addedChars " }
val hex = remember { baseUser.pubkeyDisplayHex() }
val innerUserState by baseUser.live().metadata.observeAsState()
val displayName by remember(innerUserState) {
derivedStateOf {
innerUserState?.user?.toBestDisplayName() ?: ""
}
}
val userTags by remember(innerUserState) {
derivedStateOf {
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}
val meta by baseUser.live().metadata.map {
it.user.info
}.distinctUntilChanged().observeAsState(baseUser.info)
Crossfade(targetState = meta) {
val displayName = remember(it) {
it?.bestDisplayName() ?: hex
}
CreateClickableTextWithEmoji(
clickablePart = displayName,
suffix = suffix,
maxLines = 1,
route = route,
nav = nav,
tags = userTags
tags = it?.tags ?: ImmutableListOfLists()
)
}
}

View File

@ -50,7 +50,7 @@ fun RobohashAsyncImage(
@Composable
fun RobohashFallbackAsyncImage(
robot: String,
model: String,
model: String?,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
@ -91,18 +91,6 @@ fun RobohashAsyncImageProxy(
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
) {
if (model == null) {
RobohashAsyncImage(
robot = robot,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
} else {
RobohashFallbackAsyncImage(
robot = robot,
model = model,
@ -115,4 +103,3 @@ fun RobohashAsyncImageProxy(
filterQuality = filterQuality
)
}
}

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -28,6 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@ -109,31 +111,28 @@ fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifie
@Composable
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
val userState by baseUser.live().metadata.observeAsState()
val isValidNIP05 by remember(userState) {
derivedStateOf {
userState?.user?.nip05()?.split("@")?.size == 2
}
}
val nip05 by remember(userState) {
derivedStateOf {
userState?.user?.nip05()
}
}
val nip05 by baseUser.live().metadata.map {
it.user.nip05()
}.observeAsState()
if (isValidNIP05) {
nip05?.let {
Crossfade(targetState = nip05, modifier = columnModifier) {
if (it != null) {
val isValid = it.split("@").size == 2
if (isValid) {
DisplayNIP05Line(it, baseUser, columnModifier)
}
}
}
}
@Composable
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
DisplayNIP05(nip05, nip05Verified)
Crossfade(targetState = nip05Verified) {
Row(verticalAlignment = Alignment.CenterVertically) {
DisplayNIP05(nip05, it)
}
}
}
}
@ -150,7 +149,7 @@ private fun DisplayNIP05(
if (user != "_") {
Text(
text = AnnotatedString(user),
text = remember(nip05) { AnnotatedString(user) },
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -160,7 +159,7 @@ private fun DisplayNIP05(
NIP05VerifiedSymbol(nip05Verified)
ClickableText(
text = AnnotatedString(domain),
text = remember(nip05) { AnnotatedString(domain) },
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
maxLines = 1,

View File

@ -83,6 +83,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.get
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
@ -160,6 +161,8 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30dp
@ -168,12 +171,16 @@ import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.repostProfileBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
@ -195,7 +202,7 @@ import java.util.Locale
fun NoteCompose(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = remember { Modifier },
modifier: Modifier = Modifier,
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
@ -207,9 +214,10 @@ fun NoteCompose(
) {
val isBlank by baseNote.live().metadata.map {
it.note.event == null
}.observeAsState(baseNote.event == null)
}.distinctUntilChanged().observeAsState(baseNote.event == null)
if (isBlank) {
Crossfade(targetState = isBlank) {
if (it) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
@ -237,6 +245,7 @@ fun NoteCompose(
)
}
}
}
@Composable
fun CheckHiddenNoteCompose(
@ -261,7 +270,8 @@ fun CheckHiddenNoteCompose(
}
}
if (!isHidden) {
Crossfade(targetState = isHidden) {
if (!it) {
LoadedNoteCompose(
note,
routeForLastRead,
@ -277,6 +287,7 @@ fun CheckHiddenNoteCompose(
)
}
}
}
@Immutable
data class NoteComposeReportState(
@ -309,20 +320,52 @@ fun LoadedNoteCompose(
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
scope.launch(Dispatchers.Main) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
}
}
}
Crossfade(targetState = state) {
RenderReportState(
it,
note,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
parentBackgroundColor,
accountViewModel,
nav
)
}
}
@Composable
fun RenderReportState(
state: NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by remember(state, showReportedNote) {
derivedStateOf {
!state.isAcceptable && !showReportedNote
}
}
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
@ -333,11 +376,7 @@ fun LoadedNoteCompose(
onClick = { showReportedNote = true }
)
} else {
val canPreview by remember(state, showReportedNote) {
derivedStateOf {
(!state.isAcceptable && showReportedNote) || state.canPreview
}
}
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
NormalNote(
note,
@ -355,6 +394,7 @@ fun LoadedNoteCompose(
)
}
}
}
@Composable
fun WatchForReports(
@ -368,13 +408,23 @@ fun WatchForReports(
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState) {
launch(Dispatchers.Default) {
accountViewModel.account.let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.userProfile().pubkeyHex ||
(note.author?.let { loggedIn.userProfile().isFollowingCached(it) } ?: true) ||
noteReportsState?.note?.hasAnyReports() != true
val isFromLoggedIn = note.author?.pubkeyHex == loggedIn.userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { loggedIn.userProfile().isFollowingCached(it) } ?: true
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onChange(true, true, persistentSetOf())
} else {
val newCanPreview = noteReportsState?.note?.hasAnyReports() != true
val newIsAcceptable = noteReportsState?.note?.let {
loggedIn.isAcceptable(it)
} ?: true
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onChange(true, true, persistentSetOf())
} else {
val newRelevantReports = noteReportsState?.note?.let {
loggedIn.getRelevantReports(it)
} ?: emptySet()
@ -384,6 +434,8 @@ fun WatchForReports(
}
}
}
}
}
@Composable
fun NormalNote(
@ -400,29 +452,18 @@ fun NormalNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = remember { baseNote.event }
val isChannelHeader by remember {
derivedStateOf {
(baseNote.event is ChannelCreateEvent || baseNote.event is ChannelMetadataEvent) && baseNote.channelHex() != null
}
}
if (isChannelHeader) {
ChannelHeader(
when (baseNote.event) {
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
channelNote = baseNote,
showVideo = !makeItShort,
showBottomDiviser = true,
accountViewModel = accountViewModel,
nav = nav
)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote)
} else {
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
baseNote,
@ -534,7 +575,7 @@ private fun ClickableNote(
) {
val scope = rememberCoroutineScope()
val updatedModifier = remember(backgroundColor.value) {
val updatedModifier = remember(backgroundColor) {
modifier
.combinedClickable(
onClick = {
@ -569,17 +610,8 @@ fun InnerNoteWithReactions(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val notBoostedNorQuote by remember {
derivedStateOf {
!isBoostedNote && !isQuotedNote
}
}
val showSecondRow by remember {
derivedStateOf {
baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote
}
}
val notBoostedNorQuote = !isBoostedNote && !isQuotedNote
val showSecondRow = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote
Row(
modifier = remember {
@ -593,10 +625,13 @@ fun InnerNoteWithReactions(
}
) {
if (notBoostedNorQuote) {
Column(WidthAuthorPictureModifier) {
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
}
Spacer(modifier = DoubleHorzSpacer)
}
Column(Modifier.fillMaxWidth()) {
NoteBody(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
@ -609,12 +644,9 @@ fun InnerNoteWithReactions(
nav = nav
)
}
}
val isNotRepost by remember {
derivedStateOf {
baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent
}
}
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent
if (isNotRepost) {
if (makeItShort) {
@ -651,7 +683,6 @@ private fun NoteBody(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(Modifier.fillMaxWidth()) {
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = showAuthorPicture,
@ -688,7 +719,6 @@ private fun NoteBody(
nav
)
}
}
@Composable
private fun RenderNoteRow(
@ -1711,6 +1741,15 @@ private fun ReplyRow(
}
}
if (showReply) {
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull() }
if (replyingDirectlyTo != null && unPackReply) {
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
Spacer(modifier = StdVertSpacer)
} else {
// ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
}
} else {
val showChannelReply by remember {
derivedStateOf {
(noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) ||
@ -1718,15 +1757,7 @@ private fun ReplyRow(
}
}
if (showReply) {
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull() }
if (replyingDirectlyTo != null && unPackReply) {
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
Spacer(modifier = Modifier.height(5.dp))
} else {
// ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
}
} else if (showChannelReply) {
if (showChannelReply) {
val channelHex = note.channelHex()
channelHex?.let {
ChannelHeader(
@ -1745,6 +1776,7 @@ private fun ReplyRow(
}
}
}
}
@Composable
private fun ReplyNoteComposition(
@ -1759,7 +1791,7 @@ private fun ReplyNoteComposition(
val defaultReplyBackground = MaterialTheme.colors.replyBackground
LaunchedEffect(key1 = backgroundColor.value, key2 = defaultReplyBackground) {
launch(Dispatchers.IO) {
launch(Dispatchers.Default) {
val newReplyBackgroundColor =
defaultReplyBackground.compositeOver(backgroundColor.value)
if (replyBackgroundColor.value != newReplyBackgroundColor) {
@ -1768,22 +1800,10 @@ private fun ReplyNoteComposition(
}
}
val borderColor = MaterialTheme.colors.subtleBorder
NoteCompose(
baseNote = replyingDirectlyTo,
isQuotedNote = true,
modifier = remember {
Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
borderColor,
QuoteBorder
)
},
modifier = MaterialTheme.colors.replyModifier,
unPackReply = false,
makeItShort = true,
parentBackgroundColor = replyBackgroundColor,
@ -1801,7 +1821,7 @@ private fun SecondUserInfoRow(
val noteEvent = remember { note.event } ?: return
val noteAuthor = remember { note.author } ?: return
Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
@ -1823,7 +1843,7 @@ private fun FirstUserInfoRow(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = CenterVertically, modifier = remember { UserNameRowHeight }) {
val isRepost by remember {
derivedStateOf {
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
@ -1869,13 +1889,13 @@ private fun MoreOptionsButton(
var moreActionsExpanded by remember { mutableStateOf(false) }
IconButton(
modifier = remember { Modifier.size(24.dp) },
modifier = Size24Modifier,
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = remember { Modifier.size(15.dp) },
modifier = Size15Modifier,
tint = MaterialTheme.colors.placeholderText
)
@ -1908,7 +1928,6 @@ fun TimeAgo(time: Long) {
@Composable
private fun AuthorAndRelayInformation(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
Column(WidthAuthorPictureModifier) {
// Draws the boosted picture outside the boosted card.
Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) {
RenderAuthorImages(baseNote, nav, accountViewModel)
@ -1916,7 +1935,6 @@ private fun AuthorAndRelayInformation(baseNote: Note, accountViewModel: AccountV
BadgeBox(baseNote, accountViewModel, nav)
}
}
@Composable
private fun BadgeBox(
@ -1950,11 +1968,7 @@ private fun RenderAuthorImages(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val isRepost by remember {
derivedStateOf {
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
}
}
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
@ -1962,11 +1976,7 @@ private fun RenderAuthorImages(
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
}
val isChannel by remember {
derivedStateOf {
baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
}
}
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
if (isChannel) {
val baseChannelHex = remember { baseNote.channelHex() }
@ -2001,7 +2011,7 @@ fun LoadChannel(baseChannelHex: String, content: @Composable (Channel) -> Unit)
private fun ChannelNotePicture(baseChannel: Channel) {
val model by baseChannel.live.map {
it.channel.profilePicture()
}.observeAsState()
}.distinctUntilChanged().observeAsState()
val backgroundColor = MaterialTheme.colors.background
@ -2879,7 +2889,10 @@ private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav:
mutableStateOf(lazyRelayList.take(3).toImmutableList())
}
val scope = rememberCoroutineScope()
WatchRelayLists(baseNote) { relayList ->
scope.launch(Dispatchers.Main) {
if (!equalImmutableLists(relayList, lazyRelayList)) {
lazyRelayList = relayList
shortRelayList = relayList.take(3).toImmutableList()
@ -2891,6 +2904,7 @@ private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav:
showShowMore = nextShowMore
}
}
}
Spacer(DoubleVertSpacer)
@ -2982,12 +2996,14 @@ fun NoteAuthorPicture(
) {
val author by baseNote.live().metadata.map {
it.note.author
}.observeAsState()
}.distinctUntilChanged().observeAsState(baseNote.author)
if (author == null) {
Crossfade(targetState = author) {
if (it == null) {
DisplayBlankAuthor(size, modifier)
} else {
ClickableUserPicture(author!!, size, accountViewModel, modifier, onClick)
ClickableUserPicture(it, size, accountViewModel, modifier, onClick)
}
}
}
@ -3111,16 +3127,24 @@ fun BaseUserPicture(
val userProfile by baseUser.live().metadata.map {
it.user.profilePicture()
}.observeAsState()
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
val myBoxModifier = remember {
Modifier.size(size)
}
Crossfade(targetState = userProfile) {
Box(myBoxModifier, contentAlignment = TopEnd) {
PictureAndFollowingMark(
userHex = userPubkey,
userPicture = userProfile,
userPicture = it,
size = size,
modifier = modifier,
accountViewModel = accountViewModel
)
}
}
}
@Composable
fun PictureAndFollowingMark(
@ -3157,14 +3181,18 @@ fun PictureAndFollowingMark(
private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
val showFollowingMark by accountViewModel.userFollows.map {
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
}.observeAsState(
}.distinctUntilChanged().observeAsState(
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
)
if (showFollowingMark) {
Crossfade(targetState = showFollowingMark) {
if (it) {
Box(contentAlignment = TopEnd) {
FollowingIcon(iconSize)
}
}
}
}
@Composable
private fun FollowingIcon(iconSize: Dp) {

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp
@Composable
fun UserCompose(
@ -38,9 +39,9 @@ fun UserCompose(
modifier = overallModifier,
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseUser, 55.dp, accountViewModel = accountViewModel, nav = nav)
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseUser)
}
@ -48,7 +49,7 @@ fun UserCompose(
AboutDisplay(baseUser)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
UserActionOptions(baseUser, accountViewModel)
}
}

View File

@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@ -20,13 +22,11 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.theme.StdButtonSizeModifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@ -36,38 +36,34 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
val authorState by baseNote.live().metadata.map {
it.note.author
}.distinctUntilChanged().observeAsState()
}.observeAsState(baseNote.author)
authorState?.let {
Crossfade(targetState = authorState, modifier = weight) {
it?.let {
UsernameDisplay(it, weight)
}
}
}
@Composable
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val userState by baseUser.live().metadata.observeAsState()
val bestUserName by remember(userState) {
derivedStateOf {
userState?.user?.bestUsername()
}
}
val bestDisplayName by remember(userState) {
derivedStateOf {
userState?.user?.bestDisplayName()
}
}
val npubDisplay by remember {
derivedStateOf {
baseUser.pubkeyDisplayHex()
}
}
val tags by remember(userState) {
derivedStateOf {
userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
val userMetadata by baseUser.live().metadata.map {
it.user.info
}.observeAsState(baseUser.info)
Crossfade(targetState = userMetadata, modifier = weight) {
if (it != null) {
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight)
} else {
NPubDisplay(npubDisplay, weight)
}
}
}
@Composable
@ -106,6 +102,7 @@ private fun UserDisplay(
tags: ImmutableListOfLists<String>?,
modifier: Modifier
) {
Row(modifier = modifier) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
@ -117,6 +114,7 @@ private fun UserDisplay(
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
}
}
@Composable
private fun UserAndUsernameDisplay(
@ -125,6 +123,7 @@ private fun UserAndUsernameDisplay(
bestUserName: String,
modifier: Modifier
) {
Row(modifier = modifier) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
@ -142,6 +141,7 @@ private fun UserAndUsernameDisplay(
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
}
}
@Composable
fun DrawPlayName(name: String) {

View File

@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -106,7 +107,7 @@ private fun RenderZapNote(
},
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseAuthor, 55.dp, accountViewModel = accountViewModel, nav = nav)
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(
modifier = remember {

View File

@ -197,8 +197,7 @@ private fun FeedLoaded(
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember {
Modifier
.fillMaxWidth().animateItemPlacement()
Modifier.fillMaxWidth().animateItemPlacement()
}
Row(defaultModifier) {

View File

@ -586,9 +586,10 @@ fun ChannelHeader(
)
}
channel.profilePicture()?.let {
RobohashAsyncImageProxy(
robot = channel.idHex,
model = channel.profilePicture(),
model = it,
contentDescription = stringResource(R.string.profile_image),
modifier = Modifier
.width(Size35dp)
@ -596,6 +597,7 @@ fun ChannelHeader(
.padding(start = 10.dp)
.clip(shape = CircleShape)
)
}
Column(
modifier = Modifier

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.foundation.layout.defaultMinSize
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.size
import androidx.compose.foundation.layout.width
@ -62,6 +63,7 @@ val StdPadding = Modifier.padding(10.dp)
val Size15Modifier = Modifier.size(15.dp)
val Size20Modifier = Modifier.size(20.dp)
val Size22Modifier = Modifier.size(22.dp)
val Size24Modifier = Modifier.size(24.dp)
val Size30Modifier = Modifier.size(30.dp)
val Size55Modifier = Modifier.size(55.dp)
@ -76,6 +78,8 @@ val DiviserThickness = 0.25.dp
val ReactionRowHeight = Modifier.height(24.dp).padding(start = 10.dp)
val ReactionRowHeightChat = Modifier.height(25.dp)
val UserNameRowHeight = Modifier.height(22.dp).fillMaxWidth()
val UserNameMaxRowHeight = Modifier.heightIn(max = 22.dp).fillMaxWidth()
val Height4dpModifier = Modifier.height(4.dp)

View File

@ -121,6 +121,26 @@ val LightImageModifier = Modifier
QuoteBorder
)
val DarkReplyBorderModifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
DarkSubtleBorder,
QuoteBorder
)
val LightReplyBorderModifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
LightSubtleBorder,
QuoteBorder
)
val MarkDownStyleOnDark = richTextDefaults.copy(
paragraphSpacing = DefaultParagraphSpacing,
headingStyle = DefaultHeadingStyle,
@ -243,6 +263,9 @@ val Colors.repostProfileBorder: Modifier
val Colors.imageModifier: Modifier
get() = if (isLight) LightImageModifier else DarkImageModifier
val Colors.replyModifier: Modifier
get() = if (isLight) LightReplyBorderModifier else DarkReplyBorderModifier
@Composable
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {