Loading Users and Notes in Launched Effects only because of @Synchronized in the getOrCreate methods in LocalCache

This commit is contained in:
Vitor Pamplona
2023-05-07 12:51:12 -04:00
parent 1c0fcc4ab8
commit f44549de41
22 changed files with 322 additions and 123 deletions
@@ -820,7 +820,7 @@ class Account(
return if (event is PrivateDmEvent && loggedIn.privKey != null) { return if (event is PrivateDmEvent && loggedIn.privKey != null) {
var pubkeyToUse = event.pubKey var pubkeyToUse = event.pubKey
val recepientPK = event.recipientPubKey() val recepientPK = event.verifiedRecipientPubKey()
if (note.author == userProfile() && recepientPK != null) { if (note.author == userProfile() && recepientPK != null) {
pubkeyToUse = recepientPK pubkeyToUse = recepientPK
@@ -359,7 +359,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
@@ -407,7 +407,7 @@ object LocalCache {
if (deleteNote.event is PrivateDmEvent) { if (deleteNote.event is PrivateDmEvent) {
val author = deleteNote.author val author = deleteNote.author
val recipient = (deleteNote.event as? PrivateDmEvent)?.recipientPubKey()?.let { checkGetOrCreateUser(it) } val recipient = (deleteNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()?.let { checkGetOrCreateUser(it) }
if (recipient != null && author != null) { if (recipient != null && author != null) {
author.removeMessage(recipient, deleteNote) author.removeMessage(recipient, deleteNote)
@@ -6,7 +6,7 @@ import kotlin.time.measureTimedValue
class ThreadAssembler { class ThreadAssembler {
fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? { private fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
testedNotes.add(note) testedNotes.add(note)
@@ -181,7 +181,7 @@ class User(val pubkeyHex: String) {
} }
@Synchronized @Synchronized
fun getOrCreatePrivateChatroom(user: User): Chatroom { private fun getOrCreatePrivateChatroom(user: User): Chatroom {
return privateChatrooms[user] ?: run { return privateChatrooms[user] ?: run {
val privateChatroom = Chatroom(setOf<Note>()) val privateChatroom = Chatroom(setOf<Note>())
privateChatrooms = privateChatrooms + Pair(user, privateChatroom) privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
@@ -10,13 +9,8 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String?) { fun loadUserProfile(user: User?) {
if (userId != null) { this.user = user
user = LocalCache.getOrCreateUser(userId)
} else {
user = null
}
resetFilters() resetFilters()
} }
@@ -21,7 +21,11 @@ class PrivateDmEvent(
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
* for initial messages. * for initial messages.
*/ */
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this[1]) }?.getOrNull()
fun verifiedRecipientPubKey() = recipientPubKey()?.runCatching { Hex.decode(this[1]).toHexKey() }?.getOrNull() // makes sure its a valid one
/** /**
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
@@ -5,15 +5,23 @@ 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.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.navigation.NavController import androidx.navigation.NavController
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.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable @Composable
fun ClickableRoute( fun ClickableRoute(
@@ -21,56 +29,190 @@ fun ClickableRoute(
navController: NavController navController: NavController
) { ) {
if (nip19.type == Nip19.Type.USER) { if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex) DisplayUser(nip19, navController)
val userState by userBase.live().metadata.observeAsState()
val user = userState?.user ?: return
CreateClickableText(user.toBestDisplayName(), nip19.additionalChars, "User/${nip19.hex}", navController)
} else if (nip19.type == Nip19.Type.ADDRESS) { } else if (nip19.type == Nip19.Type.ADDRESS) {
val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex) DisplayAddress(nip19, navController)
} else if (nip19.type == Nip19.Type.NOTE) {
DisplayNote(nip19, navController)
} else if (nip19.type == Nip19.Type.EVENT) {
DisplayEvent(nip19, navController)
} else {
Text(
"@${nip19.hex}${nip19.additionalChars} "
)
}
}
if (noteBase == null) { @Composable
Text( private fun DisplayEvent(
"@${nip19.hex}${nip19.additionalChars} " nip19: Nip19.Return,
navController: NavController
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
}
}
noteBase?.let {
val noteState by it.live().metadata.observeAsState()
val note = noteState?.note ?: return
val channel = note.channel()
if (note.event is ChannelCreateEvent) {
CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Channel/${nip19.hex}",
navController
)
} else if (note.event is PrivateDmEvent) {
CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Room/${note.author?.pubkeyHex}",
navController
)
} else if (channel != null) {
CreateClickableText(
channel.toBestDisplayName(),
nip19.additionalChars,
"Channel/${note.channel()?.idHex}",
navController
) )
} else { } else {
val noteState by noteBase.live().metadata.observeAsState() CreateClickableText(
val note = noteState?.note ?: return note.idDisplayNote(),
nip19.additionalChars,
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Note/${nip19.hex}", navController) "Event/${nip19.hex}",
navController
)
} }
} else if (nip19.type == Nip19.Type.NOTE) { }
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState() if (noteBase == null) {
Text(
"@${nip19.hex}${nip19.additionalChars} "
)
}
}
@Composable
private fun DisplayNote(
nip19: Nip19.Return,
navController: NavController
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
}
}
noteBase?.let {
val noteState by it.live().metadata.observeAsState()
val note = noteState?.note ?: return val note = noteState?.note ?: return
val channel = note.channel() val channel = note.channel()
if (note.event is ChannelCreateEvent) { if (note.event is ChannelCreateEvent) {
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController) CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Channel/${nip19.hex}",
navController
)
} else if (note.event is PrivateDmEvent) { } else if (note.event is PrivateDmEvent) {
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Room/${note.author?.pubkeyHex}", navController) CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Room/${note.author?.pubkeyHex}",
navController
)
} else if (channel != null) { } else if (channel != null) {
CreateClickableText(channel.toBestDisplayName(), nip19.additionalChars, "Channel/${note.channel()?.idHex}", navController) CreateClickableText(
channel.toBestDisplayName(),
nip19.additionalChars,
"Channel/${note.channel()?.idHex}",
navController
)
} else { } else {
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Note/${nip19.hex}", navController) CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Note/${nip19.hex}",
navController
)
} }
} else if (nip19.type == Nip19.Type.EVENT) { }
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
val channel = note.channel()
if (note.event is ChannelCreateEvent) { if (noteBase == null) {
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController) Text(
} else if (note.event is PrivateDmEvent) { "@${nip19.hex}${nip19.additionalChars} "
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Room/${note.author?.pubkeyHex}", navController) )
} else if (channel != null) { }
CreateClickableText(channel.toBestDisplayName(), nip19.additionalChars, "Channel/${note.channel()?.idHex}", navController) }
} else {
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Event/${nip19.hex}", navController) @Composable
private fun DisplayAddress(
nip19: Nip19.Return,
navController: NavController
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
} }
} else { }
noteBase?.let {
val noteState by it.live().metadata.observeAsState()
val note = noteState?.note ?: return
CreateClickableText(
note.idDisplayNote(),
nip19.additionalChars,
"Note/${nip19.hex}",
navController
)
}
if (noteBase == null) {
Text(
"@${nip19.hex}${nip19.additionalChars} "
)
}
}
@Composable
private fun DisplayUser(
nip19: Nip19.Return,
navController: NavController
) {
var userBase by remember { mutableStateOf<User?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
}
}
userBase?.let {
val userState by it.live().metadata.observeAsState()
val user = userState?.user ?: return
CreateClickableText(
user.toBestDisplayName(),
nip19.additionalChars,
"User/${nip19.hex}",
navController
)
}
if (userBase == null) {
Text( Text(
"@${nip19.hex}${nip19.additionalChars} " "@${nip19.hex}${nip19.additionalChars} "
) )
@@ -60,8 +60,10 @@ import java.io.File
public var muted = mutableStateOf(true) public var muted = mutableStateOf(true)
@Composable @Composable
fun VideoView(localFile: File, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) { fun VideoView(localFile: File?, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
VideoView(localFile.toUri(), description, onDialog) if (localFile != null) {
VideoView(localFile.toUri(), description, onDialog)
}
} }
@Composable @Composable
@@ -111,7 +111,7 @@ class ZoomableUrlVideo(
) : ZoomableUrlContent(url, description, hash, dim, uri) ) : ZoomableUrlContent(url, description, hash, dim, uri)
abstract class ZoomablePreloadedContent( abstract class ZoomablePreloadedContent(
val localFile: File, val localFile: File?,
description: String? = null, description: String? = null,
val mimeType: String? = null, val mimeType: String? = null,
val isVerified: Boolean? = null, val isVerified: Boolean? = null,
@@ -120,7 +120,7 @@ abstract class ZoomablePreloadedContent(
) : ZoomableContent(description, dim) ) : ZoomableContent(description, dim)
class ZoomableLocalImage( class ZoomableLocalImage(
localFile: File, localFile: File?,
mimeType: String? = null, mimeType: String? = null,
description: String? = null, description: String? = null,
val blurhash: String? = null, val blurhash: String? = null,
@@ -130,7 +130,7 @@ class ZoomableLocalImage(
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri) ) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
class ZoomableLocalVideo( class ZoomableLocalVideo(
localFile: File, localFile: File?,
mimeType: String? = null, mimeType: String? = null,
description: String? = null, description: String? = null,
dim: String? = null, dim: String? = null,
@@ -219,7 +219,7 @@ private fun LocalImageView(
} }
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
if (content.localFile.exists()) { if (content.localFile != null && content.localFile.exists()) {
AsyncImage( AsyncImage(
model = content.localFile, model = content.localFile,
contentDescription = content.description, contentDescription = content.description,
@@ -246,7 +246,7 @@ private fun LocalImageView(
} }
} }
if (imageState is AsyncImagePainter.State.Error || !content.localFile.exists()) { if (imageState is AsyncImagePainter.State.Error || content.localFile == null || !content.localFile.exists()) {
BlankNote() BlankNote()
} }
} }
@@ -9,9 +9,9 @@ object UserProfileBookmarksFeedFilter : FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.users[userId] this.user = user
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
@@ -9,9 +8,9 @@ object UserProfileConversationsFeedFilter : FeedFilter<Note>() {
var account: Account? = null var account: Account? = null
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.checkGetOrCreateUser(userId) this.user = user
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -8,9 +8,9 @@ object UserProfileFollowersFeedFilter : FeedFilter<User>() {
lateinit var account: Account lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.users[userId] this.user = user
} }
override fun feed(): List<User> { override fun feed(): List<User> {
@@ -8,9 +8,9 @@ object UserProfileFollowsFeedFilter : FeedFilter<User>() {
lateinit var account: Account lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.users[userId] this.user = user
} }
override fun feed(): List<User> { override fun feed(): List<User> {
@@ -9,9 +9,9 @@ object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
var account: Account? = null var account: Account? = null
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, user: User) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.checkGetOrCreateUser(userId) this.user = user
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -1,14 +1,13 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
object UserProfileReportsFeedFilter : FeedFilter<Note>() { object UserProfileReportsFeedFilter : FeedFilter<Note>() {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(user: User?) {
user = LocalCache.checkGetOrCreateUser(userId) this.user = user
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.LocalCache
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.service.model.zaps.UserZaps import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
@@ -8,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() { object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(user: User?) {
user = LocalCache.checkGetOrCreateUser(userId) this.user = user
} }
override fun feed(): List<Pair<Note, Note>> { override fun feed(): List<Pair<Note, Note>> {
@@ -134,7 +134,7 @@ fun ChatroomCompose(
} else { } else {
val replyAuthorBase = val replyAuthorBase =
(note.event as? PrivateDmEvent) (note.event as? PrivateDmEvent)
?.recipientPubKey() ?.verifiedRecipientPubKey()
?.let { LocalCache.getOrCreateUser(it) } ?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!! var userToComposeOn = note.author!!
@@ -89,7 +89,6 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.service.model.EventInterface
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
import com.vitorpamplona.amethyst.service.model.FileStorageEvent
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
@@ -122,6 +121,7 @@ import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.toNpub
import java.io.File import java.io.File
import java.math.BigDecimal import java.math.BigDecimal
import java.net.URL import java.net.URL
@@ -387,7 +387,7 @@ fun routeFor(note: Note, loggedIn: User): String? {
} else if (noteEvent is PrivateDmEvent) { } else if (noteEvent is PrivateDmEvent) {
val replyAuthorBase = val replyAuthorBase =
(note.event as? PrivateDmEvent) (note.event as? PrivateDmEvent)
?.recipientPubKey() ?.verifiedRecipientPubKey()
?.let { LocalCache.getOrCreateUser(it) } ?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!! var userToComposeOn = note.author!!
@@ -573,13 +573,13 @@ private fun RenderPrivateMessage(
} }
} }
} else { } else {
val recipient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) } val recipient = noteEvent.recipientPubKeyBytes()?.toNpub() ?: "Someone"
TranslatableRichTextViewer( TranslatableRichTextViewer(
stringResource( stringResource(
id = R.string.private_conversation_notification, id = R.string.private_conversation_notification,
"@${note.author?.pubkeyNpub()}", "@${note.author?.pubkeyNpub()}",
"@${recipient?.pubkeyNpub()}" "@$recipient"
), ),
canPreview = !makeItShort, canPreview = !makeItShort,
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -797,13 +797,8 @@ private fun ReplyRow(
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) { } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
val sortedMentions = noteEvent.mentions()
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.isFollowing(it) }
note.channel()?.let { note.channel()?.let {
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, account, navController)
} }
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
@@ -1016,11 +1011,19 @@ fun DisplayHighlight(
navController navController
) )
var userBase by remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
if (authorHex != null) {
userBase = LocalCache.checkGetOrCreateUser(authorHex)
}
}
}
FlowRow() { FlowRow() {
authorHex?.let { authorHex -> authorHex?.let { authorHex ->
val userBase = LocalCache.checkGetOrCreateUser(authorHex) userBase?.let { userBase ->
if (userBase != null) {
val userState by userBase.live().metadata.observeAsState() val userState by userBase.live().metadata.observeAsState()
val user = userState?.user val user = userState?.user
@@ -1307,20 +1310,23 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
val appContext = LocalContext.current.applicationContext val appContext = LocalContext.current.applicationContext
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
val fileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return var fileNote by remember { mutableStateOf<Note?>(null) }
val noteState by fileNote.live().metadata.observeAsState() LaunchedEffect(key1 = eventHeader.id) {
val note = noteState?.note withContext(Dispatchers.IO) {
fileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) }
}
}
val eventBytes = (note?.event as? FileStorageEvent) val noteState = fileNote?.live()?.metadata?.observeAsState()
val note = noteState?.value?.note
var content by remember { mutableStateOf<ZoomableContent?>(null) } var content by remember { mutableStateOf<ZoomableContent?>(null) }
LaunchedEffect(key1 = eventHeader.id, key2 = noteState) { LaunchedEffect(key1 = eventHeader.id, key2 = noteState, key3 = note?.event) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val uri = "nostr:" + baseNote.toNEvent() val uri = "nostr:" + baseNote.toNEvent()
val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex) val localDir = note?.idHex?.let { File(File(appContext.externalCacheDir, "NIP95"), it) }
val bytes = eventBytes?.decode()
val blurHash = eventHeader.blurhash() val blurHash = eventHeader.blurhash()
val dimensions = eventHeader.dimensions() val dimensions = eventHeader.dimensions()
val description = eventHeader.content val description = eventHeader.content
@@ -1337,18 +1343,14 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
uri = uri uri = uri
) )
} else { } else {
if (bytes != null) { ZoomableLocalVideo(
ZoomableLocalVideo( localFile = localDir,
localFile = localDir, mimeType = mimeType,
mimeType = mimeType, description = description,
description = description, dim = dimensions,
dim = dimensions, isVerified = true,
isVerified = true, uri = uri
uri = uri )
)
} else {
null
}
} }
} }
} }
@@ -5,6 +5,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.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -17,13 +18,23 @@ import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable @Composable
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, navController: NavController) { fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, navController: NavController) {
val dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) } var dupMentions by remember { mutableStateOf<List<User>?>(null) }
ReplyInformation(replyTo, dupMentions, account) { LaunchedEffect(Unit) {
navController.navigate("User/${it.pubkeyHex}") withContext(Dispatchers.IO) {
dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
}
}
if (dupMentions != null) {
ReplyInformation(replyTo, dupMentions, account) {
navController.navigate("User/${it.pubkeyHex}")
}
} }
} }
@@ -102,6 +113,34 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
} }
} }
@Composable
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<String>, channel: Channel, account: Account, navController: NavController) {
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
sortedMentions = mentions
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.isFollowing(it) }
}
}
if (sortedMentions != null) {
ReplyInformationChannel(
replyTo,
sortedMentions,
channel,
onUserTagClick = {
navController.navigate("User/${it.pubkeyHex}")
},
onChannelTagClick = {
navController.navigate("Channel/${it.idHex}")
}
)
}
}
@Composable @Composable
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) { fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) {
ReplyInformationChannel( ReplyInformationChannel(
@@ -175,7 +175,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
} }
} }
fun refreshFromOldState(newItems: Set<Note>) { private fun refreshFromOldState(newItems: Set<Note>) {
val oldNotesState = _feedContent.value val oldNotesState = _feedContent.value
val thisAccount = (localFilter as? NotificationFeedFilter)?.account val thisAccount = (localFilter as? NotificationFeedFilter)?.account
@@ -104,7 +104,7 @@ private fun FeedLoaded(
} else { } else {
val replyAuthorBase = val replyAuthorBase =
(note.event as? PrivateDmEvent) (note.event as? PrivateDmEvent)
?.recipientPubKey() ?.verifiedRecipientPubKey()
?.let { LocalCache.getOrCreateUser(it) } ?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!! var userToComposeOn = note.author!!
@@ -100,23 +100,42 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.math.BigDecimal import java.math.BigDecimal
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
if (userId == null) return
var userBase by remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(userId)
}
}
userBase?.let {
ProfileScreen(
user = it,
accountViewModel = accountViewModel,
navController = navController
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
if (userId == null) return UserProfileNewThreadFeedFilter.loadUserProfile(account, user)
UserProfileConversationsFeedFilter.loadUserProfile(account, user)
UserProfileFollowersFeedFilter.loadUserProfile(account, user)
UserProfileFollowsFeedFilter.loadUserProfile(account, user)
UserProfileZapsFeedFilter.loadUserProfile(user)
UserProfileReportsFeedFilter.loadUserProfile(user)
UserProfileBookmarksFeedFilter.loadUserProfile(account, user)
UserProfileNewThreadFeedFilter.loadUserProfile(account, userId) NostrUserProfileDataSource.loadUserProfile(user)
UserProfileConversationsFeedFilter.loadUserProfile(account, userId)
UserProfileFollowersFeedFilter.loadUserProfile(account, userId)
UserProfileFollowsFeedFilter.loadUserProfile(account, userId)
UserProfileZapsFeedFilter.loadUserProfile(userId)
UserProfileReportsFeedFilter.loadUserProfile(userId)
UserProfileBookmarksFeedFilter.loadUserProfile(account, userId)
NostrUserProfileDataSource.loadUserProfile(userId)
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -124,7 +143,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Profidle Start") println("Profidle Start")
NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileDataSource.loadUserProfile(user)
NostrUserProfileDataSource.start() NostrUserProfileDataSource.start()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {