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