Increases the speed of the Zap Tab in Profiles.

This commit is contained in:
Vitor Pamplona
2023-06-05 18:19:23 -04:00
parent d6a6a52821
commit d32d2da280
17 changed files with 275 additions and 204 deletions
@@ -2,17 +2,21 @@ package com.vitorpamplona.amethyst.service.model.zaps
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEventInterface import com.vitorpamplona.amethyst.service.model.LnZapEventInterface
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
object UserZaps { object UserZaps {
fun forProfileFeed(zaps: Map<Note, Note?>?): List<Pair<Note, Note>> { fun forProfileFeed(zaps: Map<Note, Note?>?): List<ZapReqResponse> {
if (zaps == null) return emptyList() if (zaps == null) return emptyList()
return ( return (
zaps zaps
.filter { it.value != null } .mapNotNull { entry ->
.toList() entry.value?.let {
.sortedBy { (it.second?.event as? LnZapEventInterface)?.amount() } ZapReqResponse(entry.key, it)
}
}
.sortedBy { (it.zapEvent.event as? LnZapEventInterface)?.amount() }
.reversed() .reversed()
) as List<Pair<Note, Note>> )
} }
} }
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -385,11 +386,13 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
mentions.forEachIndexed { idx, user -> mentions.forEachIndexed { idx, user ->
val innerUserState by user.live().metadata.observeAsState() val innerUserState by user.live().metadata.observeAsState()
val innerUser = innerUserState?.user innerUserState?.user?.let { myUser ->
innerUser?.let { myUser ->
Spacer(modifier = Modifier.width(5.dp)) Spacer(modifier = Modifier.width(5.dp))
val tags = remember(innerUserState) {
myUser.info?.latestMetadata?.tags?.toImmutableList()
}
Button( Button(
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -400,8 +403,8 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
} }
) { ) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = "${myUser.toBestDisplayName()}", text = remember(innerUserState) { "${myUser.toBestDisplayName()}" },
tags = myUser.info?.latestMetadata?.tags, tags = tags,
color = Color.White, color = Color.White,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -13,6 +13,7 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadChannel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -269,7 +271,7 @@ private fun DisplayUser(
val userState by it.live().metadata.observeAsState() val userState by it.live().metadata.observeAsState()
val route = remember(userState) { "User/${it.pubkeyHex}" } val route = remember(userState) { "User/${it.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val addedCharts = remember { val addedCharts = remember {
"${nip19.additionalChars} " "${nip19.additionalChars} "
} }
@@ -323,7 +325,7 @@ fun CreateClickableText(
@Composable @Composable
fun CreateTextWithEmoji( fun CreateTextWithEmoji(
text: String, text: String,
tags: List<List<String>>?, tags: ImmutableList<List<String>>?,
color: Color = Color.Unspecified, color: Color = Color.Unspecified,
textAlign: TextAlign? = null, textAlign: TextAlign? = null,
fontWeight: FontWeight? = null, fontWeight: FontWeight? = null,
@@ -382,7 +384,7 @@ fun CreateTextWithEmoji(
@Composable @Composable
fun CreateTextWithEmoji( fun CreateTextWithEmoji(
text: String, text: String,
emojis: Map<String, String>, emojis: ImmutableMap<String, String>,
color: Color = Color.Unspecified, color: Color = Color.Unspecified,
textAlign: TextAlign? = null, textAlign: TextAlign? = null,
fontWeight: FontWeight? = null, fontWeight: FontWeight? = null,
@@ -438,7 +440,7 @@ fun CreateTextWithEmoji(
@Composable @Composable
fun CreateClickableTextWithEmoji( fun CreateClickableTextWithEmoji(
clickablePart: String, clickablePart: String,
tags: List<List<String>>?, tags: ImmutableList<List<String>>?,
style: TextStyle, style: TextStyle,
onClick: (Int) -> Unit onClick: (Int) -> Unit
) { ) {
@@ -475,7 +477,7 @@ fun CreateClickableTextWithEmoji(
fun CreateClickableTextWithEmoji( fun CreateClickableTextWithEmoji(
clickablePart: String, clickablePart: String,
suffix: String, suffix: String,
tags: List<List<String>>?, tags: ImmutableList<List<String>>?,
overrideColor: Color? = null, overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal, fontWeight: FontWeight = FontWeight.Normal,
route: String, route: String,
@@ -512,7 +514,7 @@ fun CreateClickableTextWithEmoji(
} }
} }
suspend fun assembleAnnotatedList(text: String, emojis: Map<String, String>): List<Renderable> { suspend fun assembleAnnotatedList(text: String, emojis: Map<String, String>): ImmutableList<Renderable> {
return NIP30Parser().buildArray(text).map { return NIP30Parser().buildArray(text).map {
val url = emojis[it] val url = emojis[it]
if (url != null) { if (url != null) {
@@ -520,15 +522,20 @@ suspend fun assembleAnnotatedList(text: String, emojis: Map<String, String>): Li
} else { } else {
TextType(it) TextType(it)
} }
} }.toImmutableList()
} }
@Immutable
open class Renderable() open class Renderable()
@Immutable
class TextType(val text: String) : Renderable() class TextType(val text: String) : Renderable()
@Immutable
class ImageUrlType(val url: String) : Renderable() class ImageUrlType(val url: String) : Renderable()
@Composable @Composable
fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle, onClick: (Int) -> Unit) { fun ClickableInLineIconRenderer(wordsInOrder: ImmutableList<Renderable>, style: SpanStyle, onClick: (Int) -> Unit) {
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value -> val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
if (value is ImageUrlType) { if (value is ImageUrlType) {
Pair( Pair(
@@ -589,7 +596,7 @@ fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle
@Composable @Composable
fun InLineIconRenderer( fun InLineIconRenderer(
wordsInOrder: List<Renderable>, wordsInOrder: ImmutableList<Renderable>,
style: SpanStyle, style: SpanStyle,
maxLines: Int = Int.MAX_VALUE, maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip, overflow: TextOverflow = TextOverflow.Clip,
@@ -702,55 +702,90 @@ fun startsWithNIP19Scheme(word: String): Boolean {
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) } return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
} }
@Immutable
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)
@Composable @Composable
fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var nip19Route by remember { mutableStateOf<Nip19.Return?>(null) } var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
LaunchedEffect(key1 = word) { LaunchedEffect(key1 = word) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
Nip19.uriToRoute(word)?.let { Nip19.uriToRoute(word)?.let {
var returningNote: Note? = null
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNotePair = Pair(note, it.additionalChars) returningNote = note
} }
} }
nip19Route = it loadedLink = LoadedBechLink(returningNote, it)
} }
} }
} }
if (canPreview) { if (canPreview) {
baseNotePair?.let { loadedLink?.let { loadedLink ->
NoteCompose( loadedLink.baseNote?.let {
baseNote = it.first, DisplayFullNote(it, accountViewModel, backgroundColor, nav, loadedLink)
accountViewModel = accountViewModel, } ?: run {
modifier = Modifier ClickableRoute(loadedLink.nip19, nav)
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
)
if (!it.second.isNullOrEmpty()) {
Text(
"${it.second} "
)
} }
} ?: nip19Route?.let { } ?: run {
ClickableRoute(it, nav) Text(text = remember { "$word " })
} ?: Text(text = "$word ") }
} else { } else {
nip19Route?.let { loadedLink?.let {
ClickableRoute(it, nav) ClickableRoute(it.nip19, nav)
} ?: Text(text = "$word ") } ?: run {
Text(text = remember { "$word " })
}
}
}
@Composable
private fun DisplayFullNote(
it: Note,
accountViewModel: AccountViewModel,
backgroundColor: Color,
nav: (String) -> Unit,
loadedLink: LoadedBechLink
) {
val borderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
val modifier = remember {
Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
borderColor,
RoundedCornerShape(15.dp)
)
}
NoteCompose(
baseNote = it,
accountViewModel = accountViewModel,
modifier = modifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
)
val extraChars = remember(loadedLink) {
if (loadedLink.nip19.additionalChars.isNotBlank()) {
"${loadedLink.nip19.additionalChars} "
} else {
null
}
}
extraChars?.let {
Text(
it
)
} }
} }
@@ -876,12 +911,12 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
"User/${it.first.pubkeyHex}" "User/${it.first.pubkeyHex}"
} }
val userTags = remember(innerUserState) { val userTags = remember(innerUserState) {
innerUserState?.user?.info?.latestMetadata?.tags innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList()
} }
CreateClickableTextWithEmoji( CreateClickableTextWithEmoji(
clickablePart = displayName, clickablePart = displayName,
suffix = "${it.second} ", suffix = remember { "${it.second} " },
tags = userTags, tags = userTags,
route = route, route = route,
nav = nav nav = nav
@@ -1,17 +1,11 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.zaps.UserZaps import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() { class UserProfileZapsFeedFilter(val user: User) : FeedFilter<ZapReqResponse>() {
var user: User? = null override fun feed(): List<ZapReqResponse> {
return UserZaps.forProfileFeed(user.zaps)
fun loadUserProfile(user: User?) {
this.user = user
}
override fun feed(): List<Pair<Note, Note>> {
return UserZaps.forProfileFeed(user?.zaps)
} }
} }
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -227,7 +228,7 @@ private fun AccountName(
} }
val tags by remember(userState) { val tags by remember(userState) {
derivedStateOf { derivedStateOf {
user.info?.latestMetadata?.tags user.info?.latestMetadata?.tags?.toImmutableList()
} }
} }
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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 kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -128,7 +129,7 @@ fun ProfileContent(
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture()?.ifBlank { null }?.let { ResizeImage(it, 100.dp) } } val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture()?.ifBlank { null }?.let { ResizeImage(it, 100.dp) } }
val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() } val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() } val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() }
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags } val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" } val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" }
Box { Box {
@@ -193,8 +194,8 @@ fun ProfileContent(
} }
if (bestUserName != null) { if (bestUserName != null) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = " @$bestUserName", text = remember { " @$bestUserName" },
tags = accountUser.info?.latestMetadata?.tags, tags = tags,
color = Color.LightGray, color = Color.LightGray,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -418,7 +418,7 @@ private fun RenderChangeChannelMetadataNote(
CreateTextWithEmoji( CreateTextWithEmoji(
text = text, text = text,
tags = note.author?.info?.latestMetadata?.tags tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableList() }
) )
} }
@@ -441,7 +441,7 @@ private fun RenderCreateChannelNote(note: Note) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = text, text = text,
tags = note.author?.info?.latestMetadata?.tags tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableList() }
) )
} }
@@ -457,7 +457,7 @@ private fun DrawAuthorInfo(
val route = remember { "User/$pubkeyHex" } val route = remember { "User/$pubkeyHex" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) } val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -468,17 +468,19 @@ private fun DrawAuthorInfo(
robot = pubkeyHex, robot = pubkeyHex,
model = userProfilePicture, model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image), contentDescription = stringResource(id = R.string.profile_image),
modifier = Modifier modifier = remember {
.width(25.dp) Modifier
.height(25.dp) .width(25.dp)
.clip(shape = CircleShape) .height(25.dp)
.clickable(onClick = { .clip(shape = CircleShape)
nav(route) .clickable(onClick = {
}) nav(route)
})
}
) )
CreateClickableTextWithEmoji( CreateClickableTextWithEmoji(
clickablePart = " $userDisplayName", clickablePart = remember { " $userDisplayName" },
suffix = "", suffix = "",
tags = userTags, tags = userTags,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -792,18 +792,19 @@ fun RenderAppDefinition(
} }
} }
it.anyName()?.let { val name = remember(it) { it.anyName() }
name?.let {
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) { Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = it, text = it,
tags = remember { note.event?.tags() ?: emptyList() }, tags = remember { (note.event?.tags() ?: emptyList()).toImmutableList() },
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 25.sp fontSize = 25.sp
) )
} }
} }
val website = it.website val website = remember(it) { it.website }
if (!website.isNullOrEmpty()) { if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
@@ -1717,7 +1718,7 @@ fun DisplayHighlight(
val userState by userBase.live().metadata.observeAsState() val userState by userBase.live().metadata.observeAsState()
val route = remember { "User/${userBase.pubkeyHex}" } val route = remember { "User/${userBase.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
if (userDisplayName != null) { if (userDisplayName != null) {
CreateClickableTextWithEmoji( CreateClickableTextWithEmoji(
@@ -20,6 +20,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -231,17 +232,14 @@ private fun ReplyInfoMention(
onUserTagClick: (User) -> Unit onUserTagClick: (User) -> Unit
) { ) {
val innerUserState by user.live().metadata.observeAsState() val innerUserState by user.live().metadata.observeAsState()
val innerUser = remember(innerUserState) {
innerUserState?.user
} ?: return
CreateClickableTextWithEmoji( CreateClickableTextWithEmoji(
clickablePart = "$prefix${innerUser.toBestDisplayName()}", clickablePart = remember(innerUserState) { "$prefix${innerUserState?.user?.toBestDisplayName()}" },
tags = innerUser.info?.latestMetadata?.tags, tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList() },
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(alpha = 0.52f), color = MaterialTheme.colors.primary.copy(alpha = 0.52f),
fontSize = 13.sp fontSize = 13.sp
), ),
onClick = { onUserTagClick(innerUser) } onClick = { onUserTagClick(user) }
) )
} }
@@ -12,6 +12,8 @@ import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) { fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@@ -29,7 +31,7 @@ fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val bestUserName = remember(userState) { userState?.user?.bestUsername() } val bestUserName = remember(userState) { userState?.user?.bestUsername() }
val bestDisplayName = remember(userState) { userState?.user?.bestDisplayName() } val bestDisplayName = remember(userState) { userState?.user?.bestDisplayName() }
val npubDisplay = remember { baseUser.pubkeyDisplayHex() } val npubDisplay = remember { baseUser.pubkeyDisplayHex() }
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight) UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
} }
@@ -39,7 +41,7 @@ private fun UserNameDisplay(
bestUserName: String?, bestUserName: String?,
bestDisplayName: String?, bestDisplayName: String?,
npubDisplay: String, npubDisplay: String,
tags: List<List<String>>?, tags: ImmutableList<List<String>>?,
modifier: Modifier modifier: Modifier
) { ) {
if (bestUserName != null && bestDisplayName != null) { if (bestUserName != null && bestDisplayName != null) {
@@ -49,7 +51,7 @@ private fun UserNameDisplay(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
CreateTextWithEmoji( CreateTextWithEmoji(
text = "@$bestUserName", text = remember { "@$bestUserName" },
tags = tags, tags = tags,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1, maxLines = 1,
@@ -67,7 +69,7 @@ private fun UserNameDisplay(
) )
} else if (bestUserName != null) { } else if (bestUserName != null) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = "@$bestUserName", text = remember { "@$bestUserName" },
tags = tags, tags = tags,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
@@ -38,37 +39,44 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun ZapNoteCompose(baseReqResponse: ZapReqResponse, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseNoteRequest by baseNote.first.live().metadata.observeAsState() val baseNoteRequest by baseReqResponse.request.live().metadata.observeAsState()
val noteZapRequest = remember(baseNoteRequest) { baseNoteRequest?.note } ?: return
var baseAuthor by remember { var baseAuthor by remember {
mutableStateOf(noteZapRequest.author) mutableStateOf<User?>(null)
}
LaunchedEffect(baseNoteRequest) {
launch(Dispatchers.Default) {
(baseNoteRequest?.note?.event as? LnZapRequestEvent)?.let {
baseNoteRequest?.note?.let {
val decryptedContent = accountViewModel.decryptZap(it)
if (decryptedContent != null) {
baseAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
} else {
baseAuthor = it.author
}
}
}
}
} }
if (baseAuthor == null) { if (baseAuthor == null) {
BlankNote() BlankNote()
} else { } else {
val route = remember(baseAuthor) {
"User/${baseAuthor?.pubkeyHex}"
}
Column( Column(
modifier = modifier =
Modifier.clickable( Modifier.clickable(
onClick = { nav("User/${baseAuthor?.pubkeyHex}") } onClick = { nav(route) }
), ),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
LaunchedEffect(Unit) {
launch(Dispatchers.Default) {
(noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = accountViewModel.decryptZap(noteZapRequest)
if (decryptedContent != null) {
baseAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
}
}
}
}
baseAuthor?.let { baseAuthor?.let {
RenderZapNote(it, baseNote.second, nav, accountViewModel) RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel)
} }
Divider( Divider(
@@ -87,30 +95,37 @@ private fun RenderZapNote(
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
Row( Row(
modifier = Modifier modifier = remember {
.padding( Modifier
start = 12.dp, .padding(
end = 12.dp, start = 12.dp,
top = 10.dp end = 12.dp,
), top = 10.dp
)
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
UserPicture(baseAuthor, nav, accountViewModel, 55.dp) UserPicture(baseAuthor, nav, accountViewModel, 55.dp)
Column( Column(
modifier = Modifier modifier = remember {
.padding(start = 10.dp) Modifier
.weight(1f) .padding(start = 10.dp)
.weight(1f)
}
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseAuthor) UsernameDisplay(baseAuthor)
} }
Row(verticalAlignment = Alignment.CenterVertically) {
AboutDisplay(baseAuthor) AboutDisplay(baseAuthor)
}
} }
Column( Column(
modifier = Modifier.padding(start = 10.dp), modifier = remember {
Modifier.padding(start = 10.dp)
},
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
ZapAmount(zapNote) ZapAmount(zapNote)
@@ -125,13 +140,15 @@ private fun RenderZapNote(
@Composable @Composable
private fun ZapAmount(zapEventNote: Note) { private fun ZapAmount(zapEventNote: Note) {
val noteState by zapEventNote.live().metadata.observeAsState() val noteState by zapEventNote.live().metadata.observeAsState()
val noteZap = remember(noteState) { noteState?.note } ?: return
var zapAmount by remember { mutableStateOf<String?>(null) } var zapAmount by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteZap) { LaunchedEffect(key1 = noteState) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
zapAmount = showAmountAxis((noteZap.event as? LnZapEvent)?.amount) val newZapAmount = showAmountAxis((noteState?.note?.event as? LnZapEvent)?.amount)
if (zapAmount != newZapAmount) {
zapAmount = newZapAmount
}
} }
} }
@@ -151,7 +168,6 @@ fun UserActionOptions(
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val isHidden by remember(accountState) { val isHidden by remember(accountState) {
derivedStateOf { derivedStateOf {
@@ -159,20 +175,39 @@ fun UserActionOptions(
} }
} }
val userState by accountViewModel.account.userProfile().live().follows.observeAsState()
val isFollowing by remember(userState) {
derivedStateOf {
userState?.user?.isFollowingCached(baseAuthor) ?: false
}
}
if (isHidden) { if (isHidden) {
ShowUserButton { ShowUserButton {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.show(baseAuthor) accountViewModel.show(baseAuthor)
} }
} }
} else if (isFollowing) { } else {
ShowFollowingOrUnfollowingButton(baseAuthor, accountViewModel)
}
}
@Composable
fun ShowFollowingOrUnfollowingButton(
baseAuthor: User,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
var isFollowing by remember { mutableStateOf(false) }
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
LaunchedEffect(key1 = accountFollowsState) {
launch(Dispatchers.Default) {
val newShowFollowingMark =
accountFollowsState?.user?.isFollowing(baseAuthor) == true
if (newShowFollowingMark != isFollowing) {
isFollowing = newShowFollowingMark
}
}
}
if (isFollowing) {
UnfollowButton { scope.launch(Dispatchers.IO) { accountViewModel.unfollow(baseAuthor) } } UnfollowButton { scope.launch(Dispatchers.IO) { accountViewModel.unfollow(baseAuthor) } }
} else { } else {
FollowButton({ scope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } }) FollowButton({ scope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } })
@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
@@ -87,7 +88,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = user.bestDisplayName() ?: user.bestUsername() ?: "", text = user.bestDisplayName() ?: user.bestUsername() ?: "",
tags = user.info?.latestMetadata?.tags, tags = user.info?.latestMetadata?.tags?.toImmutableList(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp fontSize = 18.sp
) )
@@ -1,11 +1,18 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class ZapReqResponse(val request: Note, val zapEvent: Note)
@Stable
sealed class LnZapFeedState { sealed class LnZapFeedState {
object Loading : LnZapFeedState() object Loading : LnZapFeedState()
class Loaded(val feed: MutableState<List<Pair<Note, Note>>>) : LnZapFeedState() class Loaded(val feed: MutableState<ImmutableList<ZapReqResponse>>) : LnZapFeedState()
object Empty : LnZapFeedState() object Empty : LnZapFeedState()
class FeedError(val errorMessage: String) : LnZapFeedState() class FeedError(val errorMessage: String) : LnZapFeedState()
} }
@@ -2,78 +2,44 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun LnZapFeedView( fun LnZapFeedView(
viewModel: LnZapFeedViewModel, viewModel: LnZapFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit
enablePullRefresh: Boolean = true
) { ) {
val feedState by viewModel.feedContent.collectAsState() val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) } Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false } when (state) {
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) is LnZapFeedState.Empty -> {
FeedEmpty {
val modifier = if (enablePullRefresh) { viewModel.invalidateData()
Modifier.pullRefresh(pullRefreshState)
} else {
Modifier
}
Box(modifier) {
Column() {
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is LnZapFeedState.Empty -> {
FeedEmpty {
refreshing = true
}
}
is LnZapFeedState.FeedError -> {
FeedError(state.errorMessage) {
refreshing = true
}
}
is LnZapFeedState.Loaded -> {
if (refreshing) {
refreshing = false
}
LnZapFeedLoaded(state, accountViewModel, nav)
}
is LnZapFeedState.Loading -> {
LoadingFeed()
}
} }
} }
} is LnZapFeedState.FeedError -> {
FeedError(state.errorMessage) {
if (enablePullRefresh) { viewModel.invalidateData()
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) }
}
is LnZapFeedState.Loaded -> {
LnZapFeedLoaded(state, accountViewModel, nav)
}
is LnZapFeedState.Loading -> {
LoadingFeed()
}
} }
} }
} }
@@ -93,7 +59,7 @@ private fun LnZapFeedLoaded(
), ),
state = listState state = listState
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.second.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.zapEvent.idHex }) { _, item ->
ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav)
} }
} }
@@ -2,13 +2,16 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -17,9 +20,15 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class NostrUserProfileZapsFeedViewModel : LnZapFeedViewModel(UserProfileZapsFeedFilter) class NostrUserProfileZapsFeedViewModel(user: User) : LnZapFeedViewModel(UserProfileZapsFeedFilter(user)) {
class Factory(val user: User) : ViewModelProvider.Factory {
override fun <NostrUserProfileZapsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileZapsFeedViewModel>): NostrUserProfileZapsFeedViewModel {
return NostrUserProfileZapsFeedViewModel(user) as NostrUserProfileZapsFeedViewModel
}
}
}
open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : ViewModel() { open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : ViewModel() {
private val _feedContent = MutableStateFlow<LnZapFeedState>(LnZapFeedState.Loading) private val _feedContent = MutableStateFlow<LnZapFeedState>(LnZapFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
@@ -32,12 +41,12 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
private fun refreshSuspended() { private fun refreshSuspended() {
checkNotInMainThread() checkNotInMainThread()
val notes = dataSource.loadTop() val notes = dataSource.loadTop().toImmutableList()
val oldNotesState = _feedContent.value val oldNotesState = _feedContent.value
if (oldNotesState is LnZapFeedState.Loaded) { if (oldNotesState is LnZapFeedState.Loaded) {
// Using size as a proxy for has changed. // Using size as a proxy for has changed.
if (notes != oldNotesState.feed.value) { if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
updateFeed(notes) updateFeed(notes)
} }
} else { } else {
@@ -45,7 +54,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
} }
} }
private fun updateFeed(notes: List<Pair<Note, Note>>) { private fun updateFeed(notes: ImmutableList<ZapReqResponse>) {
val scope = CoroutineScope(Job() + Dispatchers.Main) val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { scope.launch {
val currentState = _feedContent.value val currentState = _feedContent.value
@@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedState
@@ -154,11 +153,19 @@ fun PrepareViewModels(baseUser: User, accountViewModel: AccountViewModel, nav: (
) )
) )
val zapFeedViewModel: NostrUserProfileZapsFeedViewModel = viewModel(
key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel",
factory = NostrUserProfileZapsFeedViewModel.Factory(
baseUser
)
)
ProfileScreen( ProfileScreen(
baseUser = baseUser, baseUser = baseUser,
followsFeedViewModel, followsFeedViewModel,
followersFeedViewModel, followersFeedViewModel,
appRecommendations, appRecommendations,
zapFeedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
@@ -171,12 +178,12 @@ fun ProfileScreen(
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel, followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
appRecommendations: NostrUserAppRecommendationsFeedViewModel, appRecommendations: NostrUserAppRecommendationsFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser) UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser) UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileZapsFeedFilter.loadUserProfile(baseUser)
UserProfileReportsFeedFilter.loadUserProfile(baseUser) UserProfileReportsFeedFilter.loadUserProfile(baseUser)
UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser) UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
@@ -293,10 +300,10 @@ fun ProfileScreen(
1 -> TabNotesConversations(accountViewModel, nav) 1 -> TabNotesConversations(accountViewModel, nav)
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) 2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav) 3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
4 -> TabReceivedZaps(baseUser, accountViewModel, nav) 4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
6 -> TabBookmarks(baseUser, accountViewModel, nav) 5 -> TabBookmarks(baseUser, accountViewModel, nav)
7 -> TabReports(baseUser, accountViewModel, nav) 6 -> TabReports(baseUser, accountViewModel, nav)
8 -> TabRelays(baseUser, accountViewModel) 7 -> TabRelays(baseUser, accountViewModel)
} }
} }
} }
@@ -579,7 +586,7 @@ private fun DrawAdditionalInfo(
) { ) {
val userState by baseUser.live().metadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = remember(userState) { userState?.user } ?: return val user = remember(userState) { userState?.user } ?: return
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
@@ -1120,16 +1127,14 @@ private fun WatchFollowChanges(
} }
@Composable @Composable
fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun TabReceivedZaps(baseUser: User, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel() WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel)
WatchZapsAndUpdateFeed(baseUser, feedViewModel)
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)
) { ) {
LnZapFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) LnZapFeedView(zapFeedViewModel, accountViewModel, nav)
} }
} }
} }