Uses 1/3 of the memory per not-fully loaded user/note

BugFix for invalid Hexes in mentions, contact lists, etc.
This commit is contained in:
Vitor Pamplona
2023-02-19 11:22:01 -05:00
parent 71b86ea69c
commit 8bc5aab880
38 changed files with 315 additions and 286 deletions
@@ -5,6 +5,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
@@ -29,7 +30,8 @@ class EUrlUserTagTransformationTest {
@Test @Test
fun transformationText() { fun transformationText() {
val user = LocalCache.getOrCreateUser(decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z").toHexKey()) val user = LocalCache.getOrCreateUser(decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z").toHexKey())
user.info.displayName = "Vitor Pamplona" user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
var transformedText = buildAnnotatedStringWithUrlHighlighting( var transformedText = buildAnnotatedStringWithUrlHighlighting(
AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"), AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"),
@@ -63,7 +65,8 @@ class EUrlUserTagTransformationTest {
@Test @Test
fun transformationTextTwoKeys() { fun transformationTextTwoKeys() {
val user = LocalCache.getOrCreateUser(decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z").toHexKey()) val user = LocalCache.getOrCreateUser(decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z").toHexKey())
user.info.displayName = "Vitor Pamplona" user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
var transformedText = buildAnnotatedStringWithUrlHighlighting( var transformedText = buildAnnotatedStringWithUrlHighlighting(
AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z and @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"), AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z and @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"),
@@ -276,8 +276,8 @@ class Account(
val user = LocalCache.users[toUser] ?: return val user = LocalCache.users[toUser] ?: return
val signedEvent = PrivateDmEvent.create( val signedEvent = PrivateDmEvent.create(
recipientPubKey = user.pubkey, recipientPubKey = user.pubkey(),
publishedRecipientPubKey = user.pubkey, publishedRecipientPubKey = user.pubkey(),
msg = message, msg = message,
privateKey = loggedIn.privKey!!, privateKey = loggedIn.privKey!!,
advertiseNip18 = false advertiseNip18 = false
@@ -406,7 +406,7 @@ class Account(
} }
init { init {
userProfile().liveRelays.observeForever { userProfile().live().relays.observeForever {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
reconnectIfRelaysHaveChanged() reconnectIfRelaysHaveChanged()
} }
@@ -5,12 +5,10 @@ import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class Channel(val id: ByteArray) { class Channel(val idHex: String) {
val idHex = id.toHexKey()
val idDisplayHex = id.toShortenHex()
var creator: User? = null var creator: User? = null
var info = ChannelCreateEvent.ChannelData(null, null, null) var info = ChannelCreateEvent.ChannelData(null, null, null)
@@ -18,8 +16,12 @@ class Channel(val id: ByteArray) {
val notes = ConcurrentHashMap<HexKey, Note>() val notes = ConcurrentHashMap<HexKey, Note>()
fun id() = Hex.decode(idHex)
fun idNote() = id().toNote()
fun idDisplayNote() = idNote().toShortenHex()
fun toBestDisplayName(): String { fun toBestDisplayName(): String {
return info.name ?: idDisplayHex return info.name ?: idDisplayNote()
} }
@Synchronized @Synchronized
@@ -22,6 +22,7 @@ import java.time.format.DateTimeFormatter
import java.util.Collections import java.util.Collections
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import fr.acinq.secp256k1.Hex
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
@@ -51,6 +52,17 @@ object LocalCache {
val users = ConcurrentHashMap<HexKey, User>() val users = ConcurrentHashMap<HexKey, User>()
val notes = ConcurrentHashMap<HexKey, Note>() val notes = ConcurrentHashMap<HexKey, Note>()
val channels = ConcurrentHashMap<HexKey, Channel>() val channels = ConcurrentHashMap<HexKey, Channel>()
fun checkGetOrCreateUser(key: HexKey): User? {
return try {
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
getOrCreateUser(key)
} catch (e: IllegalArgumentException) {
Log.e("LocalCache", "Invalid Key to create user: $key", e)
null
}
}
@Synchronized @Synchronized
fun getOrCreateUser(key: HexKey): User { fun getOrCreateUser(key: HexKey): User {
return users[key] ?: run { return users[key] ?: run {
@@ -72,7 +84,7 @@ object LocalCache {
@Synchronized @Synchronized
fun getOrCreateChannel(key: String): Channel { fun getOrCreateChannel(key: String): Channel {
return channels[key] ?: run { return channels[key] ?: run {
val answer = Channel(key.toByteArray()) val answer = Channel(key)
channels.put(key, answer) channels.put(key, answer)
answer answer
} }
@@ -82,18 +94,19 @@ object LocalCache {
fun consume(event: MetadataEvent) { fun consume(event: MetadataEvent) {
// new event // new event
val oldUser = getOrCreateUser(event.pubKey.toHexKey()) val oldUser = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > oldUser.updatedMetadataAt) { if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
val newUser = try { val newUser = try {
metadataParser.readValue<UserMetadata>(ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), UserMetadata::class.java) metadataParser.readValue(
ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)),
UserMetadata::class.java
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Log.w("MT", "Content Parse Error ${e.localizedMessage} ${event.content}") Log.w("MT", "Content Parse Error ${e.localizedMessage} ${event.content}")
return return
} }
oldUser.updateUserInfo(newUser, event.createdAt) oldUser.updateUserInfo(newUser, event)
oldUser.latestMetadata = event
//Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") //Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
} else { } else {
//Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
@@ -118,7 +131,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val mentions = event.mentions.map { getOrCreateUser(it) } val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).map { getOrCreateNote(it) } val replyTo = replyToWithoutCitations(event).map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, mentions, replyTo)
@@ -227,7 +240,7 @@ object LocalCache {
//Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) } val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) }
val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateUser(it) } val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -252,7 +265,7 @@ object LocalCache {
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.originalAuthor.map { getOrCreateUser(it) } val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.boostedPost.map { getOrCreateNote(it) } val repliesTo = event.boostedPost.map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -283,7 +296,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.originalAuthor.map { getOrCreateUser(it) } val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.originalPost.map { getOrCreateNote(it) } val repliesTo = event.originalPost.map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -328,7 +341,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.reportedAuthor.map { getOrCreateUser(it) } val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.reportedPost.map { getOrCreateNote(it) } val repliesTo = event.reportedPost.map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -406,7 +419,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val mentions = event.mentions.map { getOrCreateUser(it) } val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
val replyTo = event.replyTos val replyTo = event.replyTos
.map { getOrCreateNote(it) } .map { getOrCreateNote(it) }
.filter { it.event !is ChannelCreateEvent } .filter { it.event !is ChannelCreateEvent }
@@ -447,7 +460,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.zappedAuthor.map { getOrCreateUser(it) } val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost.map { getOrCreateNote(it) } val repliesTo = event.zappedPost.map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -483,7 +496,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.zappedAuthor.map { getOrCreateUser(it) } val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost.map { getOrCreateNote(it) } val repliesTo = event.zappedPost.map { getOrCreateNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -508,9 +521,9 @@ object LocalCache {
fun findUsersStartingWith(username: String): List<User> { fun findUsersStartingWith(username: String): List<User> {
return users.values.filter { return users.values.filter {
it.info.anyNameStartsWith(username) (it.anyNameStartsWith(username))
|| it.pubkeyHex.startsWith(username, true) || it.pubkeyHex.startsWith(username, true)
|| it.pubkey.toNpub().startsWith(username, true) || it.pubkeyNpub().startsWith(username, true)
} }
} }
@@ -519,15 +532,15 @@ object LocalCache {
(it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false) (it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|| (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false) || (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false)
|| it.idHex.startsWith(text, true) || it.idHex.startsWith(text, true)
|| it.id.toNote().startsWith(text, true) || it.idNote().startsWith(text, true)
} }
} }
fun findChannelsStartingWith(text: String): List<Channel> { fun findChannelsStartingWith(text: String): List<Channel> {
return channels.values.filter { return channels.values.filter {
it.anyNameStartsWith(text) it.anyNameStartsWith(text)
|| it.idHex.startsWith(text, true) || it.idHex.startsWith(text, true)
|| it.id.toNote().startsWith(text, true) || it.idNote().startsWith(text, true)
} }
} }
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
@@ -11,7 +10,6 @@ import fr.acinq.secp256k1.Hex
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Collections
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -21,18 +19,13 @@ import com.vitorpamplona.amethyst.service.relays.Relay
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Date import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
import nostr.postr.events.Event import nostr.postr.events.Event
import nostr.postr.toNpub
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
class Note(val idHex: String) { class Note(val idHex: String) {
// These fields are always available.
// They are immutable
val id = Hex.decode(idHex)
val idDisplayNote = id.toNote().toShortenHex()
// These fields are only available after the Text Note event is received. // These fields are only available after the Text Note event is received.
// They are immutable after that. // They are immutable after that.
var event: Event? = null var event: Event? = null
@@ -59,13 +52,17 @@ class Note(val idHex: String) {
var lastReactionsDownloadTime: Long? = null var lastReactionsDownloadTime: Long? = null
fun id() = Hex.decode(idHex)
fun idNote() = id().toNote()
fun idDisplayNote() = idNote().toShortenHex()
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) { fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
this.event = event this.event = event
this.author = author this.author = author
this.mentions = mentions this.mentions = mentions
this.replyTo = replyTo this.replyTo = replyTo
live.invalidateData() liveSet?.metadata?.invalidateData()
} }
fun formattedDateTime(timestamp: Long): String { fun formattedDateTime(timestamp: Long): String {
@@ -103,31 +100,31 @@ class Note(val idHex: String) {
fun addReply(note: Note) { fun addReply(note: Note) {
if (note !in replies) { if (note !in replies) {
replies = replies + note replies = replies + note
liveReplies.invalidateData() liveSet?.replies?.invalidateData()
} }
} }
fun addBoost(note: Note) { fun addBoost(note: Note) {
if (note !in boosts) { if (note !in boosts) {
boosts = boosts + note boosts = boosts + note
liveBoosts.invalidateData() liveSet?.boosts?.invalidateData()
} }
} }
fun addZap(zapRequest: Note, zap: Note?) { fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) { if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
liveZaps.invalidateData() liveSet?.zaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) { } else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
liveZaps.invalidateData() liveSet?.zaps?.invalidateData()
} }
} }
fun addReaction(note: Note) { fun addReaction(note: Note) {
if (note !in reactions) { if (note !in reactions) {
reactions = reactions + note reactions = reactions + note
liveReactions.invalidateData() liveSet?.reactions?.invalidateData()
} }
} }
@@ -136,17 +133,17 @@ class Note(val idHex: String) {
if (author !in reports.keys) { if (author !in reports.keys) {
reports = reports + Pair(author, setOf(note)) reports = reports + Pair(author, setOf(note))
liveReports.invalidateData() liveSet?.reports?.invalidateData()
} else if (reports[author]?.contains(note) == false) { } else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveReports.invalidateData() liveSet?.reports?.invalidateData()
} }
} }
fun addRelay(relay: Relay) { fun addRelay(relay: Relay) {
if (relay.url !in relays) { if (relay.url !in relays) {
relays = relays + relay.url relays = relays + relay.url
liveRelays.invalidateData() liveSet?.relays?.invalidateData()
} }
} }
@@ -189,7 +186,7 @@ class Note(val idHex: String) {
val dayAgo = Date().time / 1000 - 24*60*60 val dayAgo = Date().time / 1000 - 24*60*60
return reports.isNotEmpty() || return reports.isNotEmpty() ||
(author?.reports?.values?.filter { (author?.reports?.values?.filter {
it.firstOrNull { it.event?.createdAt ?: 0 > dayAgo } != null it.firstOrNull { ( it.event?.createdAt ?: 0 ) > dayAgo } != null
}?.isNotEmpty() ?: false) }?.isNotEmpty() ?: false)
} }
@@ -200,7 +197,9 @@ class Note(val idHex: String) {
try { try {
val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) } val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) }
if (tag != null && tag[0] == "p") { if (tag != null && tag[0] == "p") {
returningList.add(LocalCache.getOrCreateUser(tag[1])) LocalCache.checkGetOrCreateUser(tag[1])?.let {
returningList.add(it)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -233,15 +232,42 @@ class Note(val idHex: String) {
return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
} }
// Observers line up here. var liveSet: NoteLiveSet? = null
val live: NoteLiveData = NoteLiveData(this)
val liveReactions: NoteLiveData = NoteLiveData(this) fun live(): NoteLiveSet {
val liveBoosts: NoteLiveData = NoteLiveData(this) if (liveSet == null) {
val liveReplies: NoteLiveData = NoteLiveData(this) liveSet = NoteLiveSet(this)
val liveReports: NoteLiveData = NoteLiveData(this) }
val liveRelays: NoteLiveData = NoteLiveData(this) return liveSet!!
val liveZaps: NoteLiveData = NoteLiveData(this) }
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == true) {
liveSet = null
}
}
}
class NoteLiveSet(u: Note) {
// Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u)
val reactions: NoteLiveData = NoteLiveData(u)
val boosts: NoteLiveData = NoteLiveData(u)
val replies: NoteLiveData = NoteLiveData(u)
val reports: NoteLiveData = NoteLiveData(u)
val relays: NoteLiveData = NoteLiveData(u)
val zaps: NoteLiveData = NoteLiveData(u)
fun isInUse(): Boolean {
return reactions.hasObservers()
|| boosts.hasObservers()
|| replies.hasObservers()
|| reports.hasObservers()
|| relays.hasObservers()
|| zaps.hasObservers()
}
} }
class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) { class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
@@ -22,16 +21,10 @@ import nostr.postr.events.MetadataEvent
import nostr.postr.toNpub import nostr.postr.toNpub
class User(val pubkeyHex: String) { class User(val pubkeyHex: String) {
val pubkey = Hex.decode(pubkeyHex) var info: UserMetadata? = null
val pubkeyDisplayHex = pubkey.toNpub().toShortenHex()
var info = UserMetadata()
var updatedMetadataAt: Long = 0;
var updatedFollowsAt: Long = 0; var updatedFollowsAt: Long = 0;
var latestContactList: ContactListEvent? = null var latestContactList: ContactListEvent? = null
var latestMetadata: MetadataEvent? = null
var follows = setOf<User>() var follows = setOf<User>()
private set private set
@@ -56,44 +49,44 @@ class User(val pubkeyHex: String) {
var relaysBeingUsed = mapOf<String, RelayInfo>() var relaysBeingUsed = mapOf<String, RelayInfo>()
private set private set
data class Chatroom(var roomMessages: Set<Note>)
var privateChatrooms = mapOf<User, Chatroom>() var privateChatrooms = mapOf<User, Chatroom>()
private set private set
var latestReportRequestEOSE: Long? = null fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
fun toBestDisplayName(): String { fun toBestDisplayName(): String {
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex()
} }
fun bestUsername(): String? { fun bestUsername(): String? {
return info.name?.ifBlank { null } ?: info.username?.ifBlank { null } return info?.name?.ifBlank { null } ?: info?.username?.ifBlank { null }
} }
fun bestDisplayName(): String? { fun bestDisplayName(): String? {
return info.displayName?.ifBlank { null } ?: info.display_name?.ifBlank { null } return info?.displayName?.ifBlank { null } ?: info?.display_name?.ifBlank { null }
} }
fun profilePicture(): String? { fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) info.picture = null if (info?.picture.isNullOrBlank()) info?.picture = null
return info.picture return info?.picture
} }
fun follow(user: User, followedAt: Long) { fun follow(user: User, followedAt: Long) {
follows = follows + user follows = follows + user
user.followers = user.followers + this user.followers = user.followers + this
liveFollows.invalidateData() liveSet?.follows?.invalidateData()
user.liveFollows.invalidateData() user.liveSet?.follows?.invalidateData()
} }
fun unfollow(user: User) { fun unfollow(user: User) {
follows = follows - user follows = follows - user
user.followers = user.followers - this user.followers = user.followers - this
liveFollows.invalidateData() liveSet?.follows?.invalidateData()
user.liveFollows.invalidateData() user.liveSet?.follows?.invalidateData()
} }
fun follow(users: Set<User>, followedAt: Long) { fun follow(users: Set<User>, followedAt: Long) {
@@ -101,11 +94,11 @@ class User(val pubkeyHex: String) {
users.forEach { users.forEach {
if (this !in it.followers) { if (this !in it.followers) {
it.followers = it.followers + this it.followers = it.followers + this
it.liveFollows.invalidateData() it.liveSet?.follows?.invalidateData()
} }
} }
liveFollows.invalidateData() liveSet?.follows?.invalidateData()
} }
fun unfollow(users: Set<User>) { fun unfollow(users: Set<User>) {
@@ -113,10 +106,10 @@ class User(val pubkeyHex: String) {
users.forEach { users.forEach {
if (this in it.followers) { if (this in it.followers) {
it.followers = it.followers - this it.followers = it.followers - this
it.liveFollows.invalidateData() it.liveSet?.follows?.invalidateData()
} }
} }
liveFollows.invalidateData() liveSet?.follows?.invalidateData()
} }
fun addTaggedPost(note: Note) { fun addTaggedPost(note: Note) {
@@ -138,10 +131,10 @@ class User(val pubkeyHex: String) {
if (author !in reports.keys) { if (author !in reports.keys) {
reports = reports + Pair(author, setOf(note)) reports = reports + Pair(author, setOf(note))
liveReports.invalidateData() liveSet?.reports?.invalidateData()
} else if (reports[author]?.contains(note) == false) { } else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveReports.invalidateData() liveSet?.reports?.invalidateData()
} }
val reportTime = note.event?.createdAt ?: 0 val reportTime = note.event?.createdAt ?: 0
@@ -153,10 +146,10 @@ class User(val pubkeyHex: String) {
fun addZap(zapRequest: Note, zap: Note?) { fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) { if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
liveZaps.invalidateData() liveSet?.zaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) { } else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
liveZaps.invalidateData() liveSet?.zaps?.invalidateData()
} }
} }
@@ -195,16 +188,10 @@ class User(val pubkeyHex: String) {
val privateChatroom = getOrCreatePrivateChatroom(user) val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) { if (msg !in privateChatroom.roomMessages) {
privateChatroom.roomMessages = privateChatroom.roomMessages + msg privateChatroom.roomMessages = privateChatroom.roomMessages + msg
liveMessages.invalidateData() liveSet?.messages?.invalidateData()
} }
} }
data class RelayInfo (
val url: String,
var lastEvent: Long,
var counter: Long
)
fun addRelay(relay: Relay, eventTime: Long) { fun addRelay(relay: Relay, eventTime: Long) {
val here = relaysBeingUsed[relay.url] val here = relaysBeingUsed[relay.url]
if (here == null) { if (here == null) {
@@ -216,7 +203,7 @@ class User(val pubkeyHex: String) {
here.counter++ here.counter++
} }
liveRelayInfo.invalidateData() liveSet?.relayInfo?.invalidateData()
} }
fun updateFollows(newFollows: Set<User>, updateAt: Long) { fun updateFollows(newFollows: Set<User>, updateAt: Long) {
@@ -232,14 +219,15 @@ class User(val pubkeyHex: String) {
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) { fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
// no need to test if relays are different. The Account will check for us. // no need to test if relays are different. The Account will check for us.
relays = relayUse relays = relayUse
liveRelays.invalidateData() liveSet?.relays?.invalidateData()
} }
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) { fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) {
info = newUserInfo info = newUserInfo
updatedMetadataAt = updateAt info?.latestMetadata = latestMetadata
info?.updatedMetadataAt = latestMetadata.createdAt
liveMetadata.invalidateData() liveSet?.metadata?.invalidateData()
} }
fun isFollowing(user: User): Boolean { fun isFollowing(user: User): Boolean {
@@ -258,16 +246,55 @@ class User(val pubkeyHex: String) {
} != null } != null
} }
// UI Observers line up here. fun anyNameStartsWith(username: String): Boolean {
val liveFollows: UserLiveData = UserLiveData(this) return info?.anyNameStartsWith(username) ?: false
val liveReports: UserLiveData = UserLiveData(this) }
val liveMessages: UserLiveData = UserLiveData(this)
val liveRelays: UserLiveData = UserLiveData(this) var liveSet: UserLiveSet? = null
val liveRelayInfo: UserLiveData = UserLiveData(this)
val liveMetadata: UserLiveData = UserLiveData(this) fun live(): UserLiveSet {
val liveZaps: UserLiveData = UserLiveData(this) if (liveSet == null) {
liveSet = UserLiveSet(this)
}
return liveSet!!
}
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == true) {
liveSet = null
}
}
} }
class UserLiveSet(u: User) {
// UI Observers line up here.
val follows: UserLiveData = UserLiveData(u)
val reports: UserLiveData = UserLiveData(u)
val messages: UserLiveData = UserLiveData(u)
val relays: UserLiveData = UserLiveData(u)
val relayInfo: UserLiveData = UserLiveData(u)
val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u)
fun isInUse(): Boolean {
return follows.hasObservers()
|| reports.hasObservers()
|| messages.hasObservers()
|| relays.hasObservers()
|| relayInfo.hasObservers()
|| metadata.hasObservers()
|| zaps.hasObservers()
}
}
data class RelayInfo (
val url: String,
var lastEvent: Long,
var counter: Long
)
data class Chatroom(var roomMessages: Set<Note>)
class UserMetadata { class UserMetadata {
var name: String? = null var name: String? = null
var username: String? = null var username: String? = null
@@ -287,6 +314,9 @@ class UserMetadata {
var main_relay: String? = null var main_relay: String? = null
var twitter: String? = null var twitter: String? = null
var updatedMetadataAt: Long = 0;
var latestMetadata: MetadataEvent? = null
fun anyNameStartsWith(prefix: String): Boolean { fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16) return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
.filter { it.startsWith(prefix, true) }.isNotEmpty() .filter { it.startsWith(prefix, true) }.isNotEmpty()
@@ -323,12 +353,12 @@ class UserLiveData(val user: User): LiveData<UserState>(UserState(user)) {
override fun onActive() { override fun onActive() {
super.onActive() super.onActive()
NostrSingleUserDataSource.add(user.pubkeyHex) NostrSingleUserDataSource.add(user)
} }
override fun onInactive() { override fun onInactive() {
super.onInactive() super.onInactive()
NostrSingleUserDataSource.remove(user.pubkeyHex) NostrSingleUserDataSource.remove(user)
} }
} }
@@ -26,7 +26,7 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
override fun start() { override fun start() {
if (this::account.isInitialized) { if (this::account.isInitialized) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.observeForever(cacheListener) account.userProfile().live().follows.observeForever(cacheListener)
} }
} }
super.start() super.start()
@@ -36,7 +36,7 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
super.stop() super.stop()
if (this::account.isInitialized) { if (this::account.isInitialized) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.removeObserver(cacheListener) account.userProfile().live().follows.removeObserver(cacheListener)
} }
} }
} }
@@ -45,7 +45,7 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
val follows = account.userProfile().follows val follows = account.userProfile().follows
val followKeys = follows.map { val followKeys = follows.map {
it.pubkey.toHex().substring(0, 6) it.pubkeyHex.substring(0, 6)
} }
val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6))
@@ -9,17 +9,17 @@ import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<String>() var usersToWatch = setOf<User>()
fun createUserFilter(): List<TypedFilter>? { fun createUserFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null if (usersToWatch.isEmpty()) return null
return usersToWatch.filter { LocalCache.getOrCreateUser(it).latestMetadata == null }.map { return usersToWatch.filter { it.info?.latestMetadata == null }.map {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(MetadataEvent.kind), kinds = listOf(MetadataEvent.kind),
authors = listOf(it), authors = listOf(it.pubkeyHex),
limit = 1 limit = 1
) )
) )
@@ -34,8 +34,8 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(ReportEvent.kind), kinds = listOf(ReportEvent.kind),
tags = mapOf("p" to listOf(it)), tags = mapOf("p" to listOf(it.pubkeyHex)),
since = LocalCache.users[it]?.latestReportTime since = it.latestReportTime
) )
) )
} }
@@ -54,13 +54,13 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
userChannelOnce.typedFilters = listOfNotNull(createUserReportFilter()).flatten().ifEmpty { null } userChannelOnce.typedFilters = listOfNotNull(createUserReportFilter()).flatten().ifEmpty { null }
} }
fun add(userId: String) { fun add(user: User) {
usersToWatch = usersToWatch.plus(userId) usersToWatch = usersToWatch.plus(user)
invalidateFilters() invalidateFilters()
} }
fun remove(userId: String) { fun remove(user: User) {
usersToWatch = usersToWatch.minus(userId) usersToWatch = usersToWatch.minus(user)
invalidateFilters() invalidateFilters()
} }
} }
@@ -161,7 +161,7 @@ class NewPostViewModel: ViewModel() {
userSuggestionAnchor?.let { userSuggestionAnchor?.let {
val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkey.toNpub()} " val wordToInsert = "@${item.pubkeyNpub()} "
message = TextFieldValue( message = TextFieldValue(
message.text.replaceRange(lastWordStart, it.end, wordToInsert), message.text.replaceRange(lastWordStart, it.end, wordToInsert),
@@ -29,19 +29,19 @@ class NewUserMetadataViewModel: ViewModel() {
account.userProfile().let { account.userProfile().let {
userName.value = it.bestUsername() ?: "" userName.value = it.bestUsername() ?: ""
displayName.value = it.bestDisplayName() ?: "" displayName.value = it.bestDisplayName() ?: ""
about.value = it.info.about ?: "" about.value = it.info?.about ?: ""
picture.value = it.info.picture ?: "" picture.value = it.info?.picture ?: ""
banner.value = it.info.banner ?: "" banner.value = it.info?.banner ?: ""
website.value = it.info.website ?: "" website.value = it.info?.website ?: ""
nip05.value = it.info.nip05 ?: "" nip05.value = it.info?.nip05 ?: ""
lnAddress.value = it.info.lud16 ?: "" lnAddress.value = it.info?.lud16 ?: ""
lnURL.value = it.info.lud06 ?: "" lnURL.value = it.info?.lud06 ?: ""
} }
} }
fun create() { fun create() {
// Tries to not delete any existing attribute that we do not work with. // Tries to not delete any existing attribute that we do not work with.
val latest = account.userProfile().latestMetadata val latest = account.userProfile().info?.latestMetadata
val currentJson = if (latest != null) { val currentJson = if (latest != null) {
ObjectMapper().readTree( ObjectMapper().readTree(
ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8)) ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8))
@@ -18,7 +18,7 @@ fun ClickableNoteTag(
navController: NavController navController: NavController
) { ) {
ClickableText( ClickableText(
text = AnnotatedString("@${baesNote.id.toNote().toShortenHex()} "), text = AnnotatedString("@${baesNote.idNote().toShortenHex()} "),
onClick = { navController.navigate("Note/${baesNote.idHex}") }, onClick = { navController.navigate("Note/${baesNote.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
@@ -9,13 +9,8 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
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.User
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.toNote
import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import nostr.postr.toNpub
@Composable @Composable
fun ClickableRoute( fun ClickableRoute(
@@ -25,7 +20,7 @@ fun ClickableRoute(
if (nip19.type == Nip19.Type.USER) { if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex) val userBase = LocalCache.getOrCreateUser(nip19.hex)
val userState by userBase.liveMetadata.observeAsState() val userState by userBase.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val route = "User/${nip19.hex}" val route = "User/${nip19.hex}"
@@ -38,12 +33,12 @@ fun ClickableRoute(
) )
} else { } else {
val noteBase = LocalCache.getOrCreateNote(nip19.hex) val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live.observeAsState() val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return val note = noteState?.note ?: return
if (note.event is ChannelCreateEvent) { if (note.event is ChannelCreateEvent) {
ClickableText( ClickableText(
text = AnnotatedString("@${note.idDisplayNote} "), text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Channel/${nip19.hex}") }, onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
@@ -55,7 +50,7 @@ fun ClickableRoute(
) )
} else { } else {
ClickableText( ClickableText(
text = AnnotatedString("@${note.idDisplayNote} "), text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") }, onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
@@ -15,7 +15,7 @@ fun ClickableUserTag(
user: User, user: User,
navController: NavController navController: NavController
) { ) {
val innerUserState by user.liveMetadata.observeAsState() val innerUserState by user.live().metadata.observeAsState()
ClickableText( ClickableText(
text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "), text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "),
onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") }, onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") },
@@ -6,18 +6,23 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
object ChatroomFeedFilter: FeedFilter<Note>() { object ChatroomFeedFilter: FeedFilter<Note>() {
lateinit var account: Account var account: Account? = null
lateinit var withUser: User var withUser: User? = null
fun loadMessagesBetween(accountIn: Account, userId: String) { fun loadMessagesBetween(accountIn: Account, userId: String) {
account = accountIn account = accountIn
withUser = LocalCache.getOrCreateUser(userId) withUser = LocalCache.checkGetOrCreateUser(userId)
} }
// returns the last Note of each user. // returns the last Note of each user.
override fun feed(): List<Note> { override fun feed(): List<Note> {
val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList() val myAccount = account
val myUser = withUser
return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed() if (myAccount == null || myUser == null) return emptyList()
val messages = myAccount.userProfile().privateChatrooms[myUser] ?: return emptyList()
return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
} }
} }
@@ -6,17 +6,17 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
object UserProfileNoteFeedFilter: FeedFilter<Note>() { object UserProfileNoteFeedFilter: FeedFilter<Note>() {
lateinit var account: Account var account: Account? = null
var user: User? = null var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) { fun loadUserProfile(accountLoggedIn: Account, userId: String) {
account = accountLoggedIn account = accountLoggedIn
user = LocalCache.getOrCreateUser(userId) user = LocalCache.checkGetOrCreateUser(userId)
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
return user?.notes return user?.notes
?.filter { account.isAcceptable(it) } ?.filter { account?.isAcceptable(it) == true }
?.sortedBy { it.event?.createdAt } ?.sortedBy { it.event?.createdAt }
?.reversed() ?.reversed()
?: emptyList() ?: emptyList()
@@ -9,7 +9,7 @@ object UserProfileReportsFeedFilter: FeedFilter<Note>() {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(userId: String) {
user = LocalCache.getOrCreateUser(userId) user = LocalCache.checkGetOrCreateUser(userId)
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -9,7 +9,7 @@ object UserProfileZapsFeedFilter: FeedFilter<Pair<Note,Note>>() {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(userId: String) {
user = LocalCache.getOrCreateUser(userId) user = LocalCache.checkGetOrCreateUser(userId)
} }
override fun feed(): List<Pair<Note,Note>> { override fun feed(): List<Pair<Note,Note>> {
@@ -78,7 +78,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().liveMetadata.observeAsState() val accountUserState by account.userProfile().live().metadata.observeAsState()
val accountUser = accountUserState?.user ?: return val accountUser = accountUserState?.user ?: return
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
@@ -149,7 +149,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
println("Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0)/(1024*1024)}/${(imageLoader.memoryCache?.maxSize ?: 0)/(1024*1024)} MB") println("Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0)/(1024*1024)}/${(imageLoader.memoryCache?.maxSize ?: 0)/(1024*1024)} MB")
println("Notes: " + LocalCache.notes.filter { it.value.event != null }.size +"/"+ LocalCache.notes.size) println("Notes: " + LocalCache.notes.filter { it.value.event != null }.size +"/"+ LocalCache.notes.size)
println("Users: " + LocalCache.users.filter { it.value.latestMetadata != null }.size +"/"+ LocalCache.users.size) println("Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size +"/"+ LocalCache.users.size)
} }
) { ) {
Icon( Icon(
@@ -123,16 +123,16 @@ fun DrawerContent(navController: NavHostController,
fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) { fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val accountUserState by baseAccountUser.liveMetadata.observeAsState() val accountUserState by baseAccountUser.live().metadata.observeAsState()
val accountUser = accountUserState?.user ?: return val accountUser = accountUserState?.user ?: return
val accountUserFollowsState by baseAccountUser.liveFollows.observeAsState() val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
val accountUserFollows = accountUserFollowsState?.user ?: return val accountUserFollows = accountUserFollowsState?.user ?: return
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
Box { Box {
val banner = accountUser.info.banner val banner = accountUser.info?.banner
if (banner != null && banner.isNotBlank()) { if (banner != null && banner.isNotBlank()) {
AsyncImageProxy( AsyncImageProxy(
model = ResizeImage(banner, 150.dp), model = ResizeImage(banner, 150.dp),
@@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by boostSetCard.note.live.observeAsState() val noteState by boostSetCard.note.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -1,14 +1,11 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Center
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -33,7 +30,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
@@ -41,9 +37,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -55,7 +48,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -69,7 +62,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier) BlankNote(Modifier)
} else if (note.channel != null) { } else if (note.channel != null) {
val authorState by note.author!!.liveMetadata.observeAsState() val authorState by note.author!!.live().metadata.observeAsState()
val author = authorState?.user val author = authorState?.user
val channelState by note.channel!!.live.observeAsState() val channelState by note.channel!!.live.observeAsState()
@@ -2,33 +2,27 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -47,12 +41,10 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
@@ -60,11 +52,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent 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.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -74,13 +63,13 @@ val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val noteReportsState by baseNote.liveReports.observeAsState() val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = noteReportsState?.note ?: return val noteForReports = noteReportsState?.note ?: return
val accountUser = account.userProfile() val accountUser = account.userProfile()
@@ -158,7 +147,7 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp), modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp),
) { ) {
val authorState by note.author!!.liveMetadata.observeAsState() val authorState by note.author!!.live().metadata.observeAsState()
val author = authorState?.user!! val author = authorState?.user!!
if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) { if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) {
@@ -279,7 +268,7 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
@Composable @Composable
private fun RelayBadges(baseNote: Note) { private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.liveRelays.observeAsState() val noteRelaysState by baseNote.live().relays.observeAsState()
val noteRelays = noteRelaysState?.note?.relays ?: emptySet() val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -1,15 +1,12 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -21,14 +18,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@@ -37,7 +31,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by likeSetCard.note.live.observeAsState() val noteState by likeSetCard.note.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -23,8 +22,6 @@ import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentColor
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.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -40,10 +37,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -51,33 +46,27 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Account
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.model.toNote import com.vitorpamplona.amethyst.model.toNote
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Following import com.vitorpamplona.amethyst.ui.theme.Following
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
import nostr.postr.toNpub
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -92,10 +81,10 @@ fun NoteCompose(
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val noteReportsState by baseNote.liveReports.observeAsState() val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = noteReportsState?.note ?: return val noteForReports = noteReportsState?.note ?: return
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
@@ -346,7 +335,7 @@ fun NoteCompose(
@Composable @Composable
private fun RelayBadges(baseNote: Note) { private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.liveRelays.observeAsState() val noteRelaysState by baseNote.live().relays.observeAsState()
val noteRelays = noteRelaysState?.note?.relays ?: emptySet() val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -423,7 +412,7 @@ fun NoteAuthorPicture(
pictureModifier: Modifier = Modifier, pictureModifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null onClick: ((User) -> Unit)? = null
) { ) {
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note ?: return val note = noteState?.note ?: return
val author = note.author val author = note.author
@@ -470,7 +459,7 @@ fun UserPicture(
pictureModifier: Modifier = Modifier, pictureModifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null onClick: ((User) -> Unit)? = null
) { ) {
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
@@ -499,7 +488,7 @@ fun UserPicture(
) )
val accountState by baseUserAccount.liveFollows.observeAsState() val accountState by baseUserAccount.live().follows.observeAsState()
val accountUser = accountState?.user ?: return val accountUser = accountState?.user ?: return
if (accountUser.isFollowing(user) || user == accountUser) { if (accountUser.isFollowing(user) || user == accountUser) {
@@ -545,10 +534,10 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
Text("Copy Text") Text("Copy Text")
} }
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkey?.toNpub() ?: "")); onDismiss() }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkeyNpub() ?: "")); onDismiss() }) {
Text("Copy User PubKey") Text("Copy User PubKey")
} }
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.id.toNote())); onDismiss() }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
Text("Copy Note ID") Text("Copy Note ID")
} }
Divider() Divider()
@@ -53,16 +53,16 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val reactionsState by baseNote.liveReactions.observeAsState() val reactionsState by baseNote.live().reactions.observeAsState()
val reactedNote = reactionsState?.note val reactedNote = reactionsState?.note
val boostsState by baseNote.liveBoosts.observeAsState() val boostsState by baseNote.live().boosts.observeAsState()
val boostedNote = boostsState?.note val boostedNote = boostsState?.note
val zapsState by baseNote.liveZaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note val zappedNote = zapsState?.note
val repliesState by baseNote.liveReplies.observeAsState() val repliesState by baseNote.live().replies.observeAsState()
val replies = repliesState?.note?.replies ?: emptySet() val replies = repliesState?.note?.replies ?: emptySet()
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.time.Instant import java.time.Instant
@@ -30,7 +31,7 @@ import java.time.format.DateTimeFormatter
@Composable @Composable
fun RelayCompose( fun RelayCompose(
relay: User.RelayInfo, relay: RelayInfo,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController, navController: NavController,
onAddRelay: () -> Unit, onAddRelay: () -> Unit,
@@ -44,7 +44,7 @@ fun ReplyInformation(replyTo: List<Note>?, mentions: List<User>?, prefix: String
val mentionSet = mentions.toSet() val mentionSet = mentions.toSet()
mentionSet.toSet().forEachIndexed { idx, user -> mentionSet.toSet().forEachIndexed { idx, user ->
val innerUserState by user.liveMetadata.observeAsState() val innerUserState by user.live().metadata.observeAsState()
val innerUser = innerUserState?.user val innerUser = innerUserState?.user
innerUser?.let { myUser -> innerUser?.let { myUser ->
@@ -123,7 +123,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?,
val mentionSet = mentions.toSet() val mentionSet = mentions.toSet()
mentionSet.forEachIndexed { idx, user -> mentionSet.forEachIndexed { idx, user ->
val innerUserState by user.liveMetadata.observeAsState() val innerUserState by user.live().metadata.observeAsState()
val innerUser = innerUserState?.user val innerUser = innerUserState?.user
innerUser?.let { myUser -> innerUser?.let { myUser ->
@@ -34,7 +34,7 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val userState by account.userProfile().liveFollows.observeAsState() val userState by account.userProfile().live().follows.observeAsState()
val userFollows = userState?.user ?: return val userFollows = userState?.user ?: return
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
@@ -60,11 +60,11 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
UsernameDisplay(baseUser) UsernameDisplay(baseUser)
} }
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
Text( Text(
user.info.about ?: "", user.info?.about ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -14,7 +14,7 @@ import com.vitorpamplona.amethyst.model.User
@Composable @Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) { fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note ?: return val note = noteState?.note ?: return
val author = note.author val author = note.author
@@ -26,7 +26,7 @@ fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@Composable @Composable
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) { fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
if (user.bestUsername() != null || user.bestDisplayName() != null) { if (user.bestUsername() != null || user.bestDisplayName() != null) {
@@ -53,7 +53,7 @@ fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
} }
} else { } else {
Text( Text(
user.pubkeyDisplayHex, user.pubkeyDisplayHex(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@@ -21,7 +20,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.ui.screen.FollowButton import com.vitorpamplona.amethyst.ui.screen.FollowButton
@@ -35,13 +33,13 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val userState by account.userProfile().liveFollows.observeAsState() val userState by account.userProfile().live().follows.observeAsState()
val userFollows = userState?.user ?: return val userFollows = userState?.user ?: return
val noteState by baseNote.second.live.observeAsState() val noteState by baseNote.second.live().metadata.observeAsState()
val noteZap = noteState?.note ?: return val noteZap = noteState?.note ?: return
val baseNoteRequest by baseNote.first.live.observeAsState() val baseNoteRequest by baseNote.first.live().metadata.observeAsState()
val noteZapRequest = baseNoteRequest?.note ?: return val noteZapRequest = baseNoteRequest?.note ?: return
val baseAuthor = noteZapRequest.author val baseAuthor = noteZapRequest.author
@@ -74,11 +72,11 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
UsernameDisplay(baseAuthor) UsernameDisplay(baseAuthor)
} }
val userState by baseAuthor.liveMetadata.observeAsState() val userState by baseAuthor.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
Text( Text(
user.info.about ?: "", user.info?.about ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -20,22 +20,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@Composable @Composable
fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by zapSetCard.note.live.observeAsState() val noteState by zapSetCard.note.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -112,7 +112,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 35.dp, vertical = 10.dp) modifier = Modifier.fillMaxWidth().padding(horizontal = 35.dp, vertical = 10.dp)
) { ) {
QrCodeDrawer("nostr:${user.pubkey.toNpub()}") QrCodeDrawer("nostr:${user.pubkeyNpub()}")
} }
} }
@@ -19,6 +19,7 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
@@ -35,9 +36,9 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RelayFeedViewModel: ViewModel() { class RelayFeedViewModel: ViewModel() {
val order = compareByDescending<User.RelayInfo> { it.lastEvent }.thenByDescending { it.counter }.thenBy { it.url } val order = compareByDescending<RelayInfo> { it.lastEvent }.thenByDescending { it.counter }.thenBy { it.url }
private val _feedContent = MutableStateFlow<List<User.RelayInfo>>(emptyList()) private val _feedContent = MutableStateFlow<List<RelayInfo>>(emptyList())
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
var currentUser: User? = null var currentUser: User? = null
@@ -49,7 +50,7 @@ class RelayFeedViewModel: ViewModel() {
val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull { val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull {
if (it.key !in beingUsedSet) { if (it.key !in beingUsedSet) {
User.RelayInfo(it.key, 0, 0) RelayInfo(it.key, 0, 0)
} else { } else {
null null
} }
@@ -67,14 +68,14 @@ class RelayFeedViewModel: ViewModel() {
fun subscribeTo(user: User) { fun subscribeTo(user: User) {
currentUser = user currentUser = user
user.liveRelays.observeForever(listener) user.live().relays.observeForever(listener)
user.liveRelayInfo.observeForever(listener) user.live().relayInfo.observeForever(listener)
invalidateData() invalidateData()
} }
fun unsubscribeTo(user: User) { fun unsubscribeTo(user: User) {
user.liveRelays.removeObserver(listener) user.live().relays.removeObserver(listener)
user.liveRelayInfo.removeObserver(listener) user.live().relayInfo.removeObserver(listener)
currentUser = null currentUser = null
} }
@@ -3,20 +3,15 @@ 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.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.foundation.shape.CircleShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
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
@@ -28,20 +23,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.text.style.TextDirection
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -49,8 +39,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.timeAgoLong import com.vitorpamplona.amethyst.ui.note.timeAgoLong
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -162,10 +150,10 @@ fun Modifier.drawReplyLevel(level: Int, color: Color): Modifier = this
@Composable @Composable
fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) { fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by baseNote.live.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val noteReportsState by baseNote.liveReports.observeAsState() val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = noteReportsState?.note ?: return val noteForReports = noteReportsState?.note ?: return
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -271,7 +271,7 @@ private fun NoteCopyButton(
expanded = popupExpanded, expanded = popupExpanded,
onDismissRequest = { popupExpanded = false } onDismissRequest = { popupExpanded = false }
) { ) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.id.toNote())); popupExpanded = false }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); popupExpanded = false }) {
Text("Copy Channel ID (Note) to the Clipboard") Text("Copy Channel ID (Note) to the Clipboard")
} }
} }
@@ -162,7 +162,7 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val authorState by baseUser.liveMetadata.observeAsState() val authorState by baseUser.live().metadata.observeAsState()
val author = authorState?.user!! val author = authorState?.user!!
AsyncImageProxy( AsyncImageProxy(
@@ -176,7 +176,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
selected = pagerState.currentPage == 1, selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
text = { text = {
val userState by baseUser.liveFollows.observeAsState() val userState by baseUser.live().follows.observeAsState()
val userFollows = userState?.user?.follows?.size ?: "--" val userFollows = userState?.user?.follows?.size ?: "--"
Text(text = "$userFollows Follows") Text(text = "$userFollows Follows")
@@ -187,7 +187,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
selected = pagerState.currentPage == 2, selected = pagerState.currentPage == 2,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
text = { text = {
val userState by baseUser.liveFollows.observeAsState() val userState by baseUser.live().follows.observeAsState()
val userFollows = userState?.user?.followers?.size ?: "--" val userFollows = userState?.user?.followers?.size ?: "--"
Text(text = "$userFollows Followers") Text(text = "$userFollows Followers")
@@ -198,7 +198,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
selected = pagerState.currentPage == 3, selected = pagerState.currentPage == 3,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } },
text = { text = {
val userState by baseUser.liveZaps.observeAsState() val userState by baseUser.live().zaps.observeAsState()
val userZaps = userState?.user?.zappedAmount() val userZaps = userState?.user?.zappedAmount()
Text(text = "${showAmount(userZaps)} Zaps") Text(text = "${showAmount(userZaps)} Zaps")
@@ -209,7 +209,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
selected = pagerState.currentPage == 4, selected = pagerState.currentPage == 4,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(4) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(4) } },
text = { text = {
val userState by baseUser.liveReports.observeAsState() val userState by baseUser.live().reports.observeAsState()
val userReports = userState?.user?.reports?.values?.flatten()?.count() val userReports = userState?.user?.reports?.values?.flatten()?.count()
Text(text = "${userReports} Reports") Text(text = "${userReports} Reports")
@@ -220,11 +220,11 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
selected = pagerState.currentPage == 5, selected = pagerState.currentPage == 5,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(5) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(5) } },
text = { text = {
val userState by baseUser.liveRelays.observeAsState() val userState by baseUser.live().relays.observeAsState()
val userRelaysBeingUsed = val userRelaysBeingUsed =
userState?.user?.relaysBeingUsed?.size ?: "--" userState?.user?.relaysBeingUsed?.size ?: "--"
val userStateRelayInfo by baseUser.liveRelayInfo.observeAsState() val userStateRelayInfo by baseUser.live().relayInfo.observeAsState()
val userRelays = userStateRelayInfo?.user?.relays?.size ?: "--" val userRelays = userStateRelayInfo?.user?.relays?.size ?: "--"
Text(text = "$userRelaysBeingUsed / $userRelays Relays") Text(text = "$userRelaysBeingUsed / $userRelays Relays")
@@ -263,7 +263,7 @@ private fun ProfileHeader(
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
val accountUserState by account.userProfile().liveFollows.observeAsState() val accountUserState by account.userProfile().live().follows.observeAsState()
val accountUser = accountUserState?.user ?: return val accountUser = accountUserState?.user ?: return
Box { Box {
@@ -357,24 +357,28 @@ private fun ProfileHeader(
@Composable @Composable
private fun DrawAdditionalInfo(baseUser: User, account: Account) { private fun DrawAdditionalInfo(baseUser: User, account: Account) {
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
Text( user.bestDisplayName()?.let {
user.bestDisplayName() ?: "", Text( "$it",
modifier = Modifier.padding(top = 7.dp), modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 25.sp fontSize = 25.sp
) )
Text( }
"@${user.bestUsername()}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
)
val website = user.info.website user.bestUsername()?.let {
Text(
"@$it",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
)
}
val website = user.info?.website
if (!website.isNullOrEmpty()) { if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
@@ -386,7 +390,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
ClickableText( ClickableText(
text = AnnotatedString(website.removePrefix("https://")), text = AnnotatedString(website.removePrefix("https://")),
onClick = { user.info.website?.let { runCatching { uri.openUri(it) } } }, onClick = { website.let { runCatching { uri.openUri(it) } } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary), style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp) modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
) )
@@ -395,7 +399,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
var ZapExpanded by remember { mutableStateOf(false) } var ZapExpanded by remember { mutableStateOf(false) }
val lud16 = user.info.lud16?.trim() val lud16 = user.info?.lud16?.trim()
if (!lud16.isNullOrEmpty()) { if (!lud16.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -423,19 +427,21 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
} }
} }
Text( user.info?.about?.let {
"${user.info.about}", Text(
color = MaterialTheme.colors.onSurface, "$it",
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp) color = MaterialTheme.colors.onSurface,
) modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
} }
@Composable @Composable
private fun DrawBanner(baseUser: User) { private fun DrawBanner(baseUser: User) {
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val banner = user.info.banner val banner = user.info?.banner
if (banner != null && banner.isNotBlank()) { if (banner != null && banner.isNotBlank()) {
AsyncImageProxy( AsyncImageProxy(
@@ -632,7 +638,7 @@ private fun NPubCopyButton(
expanded = popupExpanded, expanded = popupExpanded,
onDismissRequest = { popupExpanded = false } onDismissRequest = { popupExpanded = false }
) { ) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())); popupExpanded = false }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); popupExpanded = false }) {
Text("Copy Public Key (NPub) to the Clipboard") Text("Copy Public Key (NPub) to the Clipboard")
} }
} }
@@ -750,7 +756,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
expanded = popupExpanded, expanded = popupExpanded,
onDismissRequest = onDismiss onDismissRequest = onDismiss
) { ) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())); onDismiss() }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) {
Text("Copy User ID") Text("Copy User ID")
} }
@@ -296,11 +296,11 @@ fun UserLine(
UsernameDisplay(baseUser) UsernameDisplay(baseUser)
} }
val userState by baseUser.liveMetadata.observeAsState() val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
Text( Text(
user.info.about?.take(100) ?: "", user.info?.about?.take(100) ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis