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

View File

@ -2,17 +2,21 @@ package com.vitorpamplona.amethyst.service.model.zaps
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEventInterface
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
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()
return (
zaps
.filter { it.value != null }
.toList()
.sortedBy { (it.second?.event as? LnZapEventInterface)?.amount() }
.mapNotNull { entry ->
entry.value?.let {
ZapReqResponse(entry.key, it)
}
}
.sortedBy { (it.zapEvent.event as? LnZapEventInterface)?.amount() }
.reversed()
) as List<Pair<Note, Note>>
)
}
}

View File

@ -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.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -385,11 +386,13 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
mentions.forEachIndexed { idx, user ->
val innerUserState by user.live().metadata.observeAsState()
val innerUser = innerUserState?.user
innerUser?.let { myUser ->
innerUserState?.user?.let { myUser ->
Spacer(modifier = Modifier.width(5.dp))
val tags = remember(innerUserState) {
myUser.info?.latestMetadata?.tags?.toImmutableList()
}
Button(
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors(
@ -400,8 +403,8 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
}
) {
CreateTextWithEmoji(
text = "${myUser.toBestDisplayName()}",
tags = myUser.info?.latestMetadata?.tags,
text = remember(innerUserState) { "${myUser.toBestDisplayName()}" },
tags = tags,
color = Color.White,
textAlign = TextAlign.Center
)

View File

@ -13,6 +13,7 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
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.ui.note.LoadChannel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@ -269,7 +271,7 @@ private fun DisplayUser(
val userState by it.live().metadata.observeAsState()
val route = remember(userState) { "User/${it.pubkeyHex}" }
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 {
"${nip19.additionalChars} "
}
@ -323,7 +325,7 @@ fun CreateClickableText(
@Composable
fun CreateTextWithEmoji(
text: String,
tags: List<List<String>>?,
tags: ImmutableList<List<String>>?,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontWeight: FontWeight? = null,
@ -382,7 +384,7 @@ fun CreateTextWithEmoji(
@Composable
fun CreateTextWithEmoji(
text: String,
emojis: Map<String, String>,
emojis: ImmutableMap<String, String>,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontWeight: FontWeight? = null,
@ -438,7 +440,7 @@ fun CreateTextWithEmoji(
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
tags: List<List<String>>?,
tags: ImmutableList<List<String>>?,
style: TextStyle,
onClick: (Int) -> Unit
) {
@ -475,7 +477,7 @@ fun CreateClickableTextWithEmoji(
fun CreateClickableTextWithEmoji(
clickablePart: String,
suffix: String,
tags: List<List<String>>?,
tags: ImmutableList<List<String>>?,
overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal,
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 {
val url = emojis[it]
if (url != null) {
@ -520,15 +522,20 @@ suspend fun assembleAnnotatedList(text: String, emojis: Map<String, String>): Li
} else {
TextType(it)
}
}
}.toImmutableList()
}
@Immutable
open class Renderable()
@Immutable
class TextType(val text: String) : Renderable()
@Immutable
class ImageUrlType(val url: String) : Renderable()
@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 ->
if (value is ImageUrlType) {
Pair(
@ -589,7 +596,7 @@ fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle
@Composable
fun InLineIconRenderer(
wordsInOrder: List<Renderable>,
wordsInOrder: ImmutableList<Renderable>,
style: SpanStyle,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,

View File

@ -702,55 +702,90 @@ fun startsWithNIP19Scheme(word: String): Boolean {
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
}
@Immutable
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)
@Composable
fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var nip19Route by remember { mutableStateOf<Nip19.Return?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
LaunchedEffect(key1 = word) {
launch(Dispatchers.IO) {
Nip19.uriToRoute(word)?.let {
var returningNote: Note? = null
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNotePair = Pair(note, it.additionalChars)
returningNote = note
}
}
nip19Route = it
loadedLink = LoadedBechLink(returningNote, it)
}
}
}
if (canPreview) {
baseNotePair?.let {
NoteCompose(
baseNote = it.first,
accountViewModel = accountViewModel,
modifier = Modifier
.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} "
)
loadedLink?.let { loadedLink ->
loadedLink.baseNote?.let {
DisplayFullNote(it, accountViewModel, backgroundColor, nav, loadedLink)
} ?: run {
ClickableRoute(loadedLink.nip19, nav)
}
} ?: nip19Route?.let {
ClickableRoute(it, nav)
} ?: Text(text = "$word ")
} ?: run {
Text(text = remember { "$word " })
}
} else {
nip19Route?.let {
ClickableRoute(it, nav)
} ?: Text(text = "$word ")
loadedLink?.let {
ClickableRoute(it.nip19, nav)
} ?: 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}"
}
val userTags = remember(innerUserState) {
innerUserState?.user?.info?.latestMetadata?.tags
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList()
}
CreateClickableTextWithEmoji(
clickablePart = displayName,
suffix = "${it.second} ",
suffix = remember { "${it.second} " },
tags = userTags,
route = route,
nav = nav

View File

@ -1,17 +1,11 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() {
var user: User? = null
fun loadUserProfile(user: User?) {
this.user = user
}
override fun feed(): List<Pair<Note, Note>> {
return UserZaps.forProfileFeed(user?.zaps)
class UserProfileZapsFeedFilter(val user: User) : FeedFilter<ZapReqResponse>() {
override fun feed(): List<ZapReqResponse> {
return UserZaps.forProfileFeed(user.zaps)
}
}

View File

@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -227,7 +228,7 @@ private fun AccountName(
}
val tags by remember(userState) {
derivedStateOf {
user.info?.latestMetadata?.tags
user.info?.latestMetadata?.tags?.toImmutableList()
}
}

View File

@ -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.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
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 bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
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}" }
Box {
@ -193,8 +194,8 @@ fun ProfileContent(
}
if (bestUserName != null) {
CreateTextWithEmoji(
text = " @$bestUserName",
tags = accountUser.info?.latestMetadata?.tags,
text = remember { " @$bestUserName" },
tags = tags,
color = Color.LightGray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -418,7 +418,7 @@ private fun RenderChangeChannelMetadataNote(
CreateTextWithEmoji(
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(
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 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 }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
Row(
verticalAlignment = Alignment.CenterVertically,
@ -468,17 +468,19 @@ private fun DrawAuthorInfo(
robot = pubkeyHex,
model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = Modifier
.width(25.dp)
.height(25.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
nav(route)
})
modifier = remember {
Modifier
.width(25.dp)
.height(25.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
nav(route)
})
}
)
CreateClickableTextWithEmoji(
clickablePart = " $userDisplayName",
clickablePart = remember { " $userDisplayName" },
suffix = "",
tags = userTags,
fontWeight = FontWeight.Bold,

View File

@ -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)) {
CreateTextWithEmoji(
text = it,
tags = remember { note.event?.tags() ?: emptyList() },
tags = remember { (note.event?.tags() ?: emptyList()).toImmutableList() },
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
}
}
val website = it.website
val website = remember(it) { it.website }
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
@ -1717,7 +1718,7 @@ fun DisplayHighlight(
val userState by userBase.live().metadata.observeAsState()
val route = remember { "User/${userBase.pubkeyHex}" }
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) {
CreateClickableTextWithEmoji(

View File

@ -20,6 +20,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -231,17 +232,14 @@ private fun ReplyInfoMention(
onUserTagClick: (User) -> Unit
) {
val innerUserState by user.live().metadata.observeAsState()
val innerUser = remember(innerUserState) {
innerUserState?.user
} ?: return
CreateClickableTextWithEmoji(
clickablePart = "$prefix${innerUser.toBestDisplayName()}",
tags = innerUser.info?.latestMetadata?.tags,
clickablePart = remember(innerUserState) { "$prefix${innerUserState?.user?.toBestDisplayName()}" },
tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList() },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(alpha = 0.52f),
fontSize = 13.sp
),
onClick = { onUserTagClick(innerUser) }
onClick = { onUserTagClick(user) }
)
}

View File

@ -12,6 +12,8 @@ import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
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 bestDisplayName = remember(userState) { userState?.user?.bestDisplayName() }
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)
}
@ -39,7 +41,7 @@ private fun UserNameDisplay(
bestUserName: String?,
bestDisplayName: String?,
npubDisplay: String,
tags: List<List<String>>?,
tags: ImmutableList<List<String>>?,
modifier: Modifier
) {
if (bestUserName != null && bestDisplayName != null) {
@ -49,7 +51,7 @@ private fun UserNameDisplay(
fontWeight = FontWeight.Bold
)
CreateTextWithEmoji(
text = "@$bestUserName",
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1,
@ -67,7 +69,7 @@ private fun UserNameDisplay(
)
} else if (bestUserName != null) {
CreateTextWithEmoji(
text = "@$bestUserName",
text = remember { "@$bestUserName" },
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1,

View File

@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent
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.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
@ -38,37 +39,44 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseNoteRequest by baseNote.first.live().metadata.observeAsState()
val noteZapRequest = remember(baseNoteRequest) { baseNoteRequest?.note } ?: return
fun ZapNoteCompose(baseReqResponse: ZapReqResponse, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseNoteRequest by baseReqResponse.request.live().metadata.observeAsState()
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) {
BlankNote()
} else {
val route = remember(baseAuthor) {
"User/${baseAuthor?.pubkeyHex}"
}
Column(
modifier =
Modifier.clickable(
onClick = { nav("User/${baseAuthor?.pubkeyHex}") }
onClick = { nav(route) }
),
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 {
RenderZapNote(it, baseNote.second, nav, accountViewModel)
RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel)
}
Divider(
@ -87,30 +95,37 @@ private fun RenderZapNote(
accountViewModel: AccountViewModel
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
),
modifier = remember {
Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
},
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseAuthor, nav, accountViewModel, 55.dp)
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
modifier = remember {
Modifier
.padding(start = 10.dp)
.weight(1f)
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseAuthor)
}
AboutDisplay(baseAuthor)
Row(verticalAlignment = Alignment.CenterVertically) {
AboutDisplay(baseAuthor)
}
}
Column(
modifier = Modifier.padding(start = 10.dp),
modifier = remember {
Modifier.padding(start = 10.dp)
},
verticalArrangement = Arrangement.Center
) {
ZapAmount(zapNote)
@ -125,13 +140,15 @@ private fun RenderZapNote(
@Composable
private fun ZapAmount(zapEventNote: Note) {
val noteState by zapEventNote.live().metadata.observeAsState()
val noteZap = remember(noteState) { noteState?.note } ?: return
var zapAmount by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteZap) {
LaunchedEffect(key1 = noteState) {
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
) {
val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState()
val isHidden by remember(accountState) {
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) {
ShowUserButton {
scope.launch(Dispatchers.IO) {
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) } }
} else {
FollowButton({ scope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } })

View File

@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
import kotlinx.collections.immutable.toImmutableList
@Composable
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)) {
CreateTextWithEmoji(
text = user.bestDisplayName() ?: user.bestUsername() ?: "",
tags = user.info?.latestMetadata?.tags,
tags = user.info?.latestMetadata?.tags?.toImmutableList(),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)

View File

@ -1,11 +1,18 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
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 {
object Loading : LnZapFeedState()
class Loaded(val feed: MutableState<List<Pair<Note, Note>>>) : LnZapFeedState()
class Loaded(val feed: MutableState<ImmutableList<ZapReqResponse>>) : LnZapFeedState()
object Empty : LnZapFeedState()
class FeedError(val errorMessage: String) : LnZapFeedState()
}

View File

@ -2,78 +2,44 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LnZapFeedView(
viewModel: LnZapFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
enablePullRefresh: Boolean = true
nav: (String) -> Unit
) {
val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) }
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
val modifier = if (enablePullRefresh) {
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()
}
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is LnZapFeedState.Empty -> {
FeedEmpty {
viewModel.invalidateData()
}
}
}
if (enablePullRefresh) {
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
is LnZapFeedState.FeedError -> {
FeedError(state.errorMessage) {
viewModel.invalidateData()
}
}
is LnZapFeedState.Loaded -> {
LnZapFeedLoaded(state, accountViewModel, nav)
}
is LnZapFeedState.Loading -> {
LoadingFeed()
}
}
}
}
@ -93,7 +59,7 @@ private fun LnZapFeedLoaded(
),
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)
}
}

View File

@ -2,13 +2,16 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
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.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -17,9 +20,15 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
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)
val feedContent = _feedContent.asStateFlow()
@ -32,12 +41,12 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
private fun refreshSuspended() {
checkNotInMainThread()
val notes = dataSource.loadTop()
val notes = dataSource.loadTop().toImmutableList()
val oldNotesState = _feedContent.value
if (oldNotesState is LnZapFeedState.Loaded) {
// Using size as a proxy for has changed.
if (notes != oldNotesState.feed.value) {
if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
updateFeed(notes)
}
} 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)
scope.launch {
val currentState = _feedContent.value

View File

@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
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.note.UserPicture
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(
baseUser = baseUser,
followsFeedViewModel,
followersFeedViewModel,
appRecommendations,
zapFeedViewModel,
accountViewModel = accountViewModel,
nav = nav
)
@ -171,12 +178,12 @@ fun ProfileScreen(
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileZapsFeedFilter.loadUserProfile(baseUser)
UserProfileReportsFeedFilter.loadUserProfile(baseUser)
UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
@ -293,10 +300,10 @@ fun ProfileScreen(
1 -> TabNotesConversations(accountViewModel, nav)
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
4 -> TabReceivedZaps(baseUser, accountViewModel, nav)
6 -> TabBookmarks(baseUser, accountViewModel, nav)
7 -> TabReports(baseUser, accountViewModel, nav)
8 -> TabRelays(baseUser, accountViewModel)
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
5 -> TabBookmarks(baseUser, accountViewModel, nav)
6 -> TabReports(baseUser, accountViewModel, nav)
7 -> TabRelays(baseUser, accountViewModel)
}
}
}
@ -579,7 +586,7 @@ private fun DrawAdditionalInfo(
) {
val userState by baseUser.live().metadata.observeAsState()
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 clipboardManager = LocalClipboardManager.current
@ -1120,16 +1127,14 @@ private fun WatchFollowChanges(
}
@Composable
fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
WatchZapsAndUpdateFeed(baseUser, feedViewModel)
fun TabReceivedZaps(baseUser: User, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel)
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
LnZapFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
LnZapFeedView(zapFeedViewModel, accountViewModel, nav)
}
}
}