diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt index 397366c46..706bae212 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt @@ -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"), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a235212f3..78bdccd04 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 11e5d89f1..c8d03634d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -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() + 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index edd8002da..2e130292d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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() val notes = ConcurrentHashMap() val channels = ConcurrentHashMap() + + 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(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 { 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,15 +532,15 @@ 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) } } fun findChannelsStartingWith(text: String): List { return channels.values.filter { - it.anyNameStartsWith(text) + it.anyNameStartsWith(text) || it.idHex.startsWith(text, true) - || it.id.toNote().startsWith(text, true) + || it.idNote().startsWith(text, true) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index cea6e6134..d46573d0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -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, replyTo: List) { 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(note)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index e6cc99f63..c51044d93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -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() private set @@ -56,44 +49,44 @@ class User(val pubkeyHex: String) { var relaysBeingUsed = mapOf() private set - data class Chatroom(var roomMessages: Set) - var privateChatrooms = mapOf() 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, 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) { @@ -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, updateAt: Long) { @@ -232,14 +219,15 @@ class User(val pubkeyHex: String) { fun updateRelays(relayUse: Map) { // 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) + 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(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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 0ef892de8..108da074f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -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)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index c5d2fc364..59fab426c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -9,17 +9,17 @@ import nostr.postr.JsonFilter import nostr.postr.events.MetadataEvent object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { - var usersToWatch = setOf() + var usersToWatch = setOf() fun createUserFilter(): List? { 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 82762eab6..218926b1d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 0e353bb98..370829413 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -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)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt index 3d0c48636..a987e1d76 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt @@ -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) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index be2b099f7..534548c84 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -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) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt index 5e356091f..8cd690d40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt @@ -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}") }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 807537f8a..fe58f8031 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -6,18 +6,23 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User object ChatroomFeedFilter: FeedFilter() { - 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 { - 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt index dccff52b1..7285f0725 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt @@ -6,17 +6,17 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User object UserProfileNoteFeedFilter: FeedFilter() { - 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 { return user?.notes - ?.filter { account.isAcceptable(it) } + ?.filter { account?.isAcceptable(it) == true } ?.sortedBy { it.event?.createdAt } ?.reversed() ?: emptyList() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt index 3e0cc30b9..8ed3df6b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt @@ -9,7 +9,7 @@ object UserProfileReportsFeedFilter: FeedFilter() { var user: User? = null fun loadUserProfile(userId: String) { - user = LocalCache.getOrCreateUser(userId) + user = LocalCache.checkGetOrCreateUser(userId) } override fun feed(): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt index 58ff6c0bb..146abdcae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt @@ -9,7 +9,7 @@ object UserProfileZapsFeedFilter: FeedFilter>() { var user: User? = null fun loadUserProfile(userId: String) { - user = LocalCache.getOrCreateUser(userId) + user = LocalCache.checkGetOrCreateUser(userId) } override fun feed(): List> { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 9eb61f3bf..a4456d2c1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 881df306a..63e681606 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index c669c9d62..60a009fa1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 5ce6c1bf6..08b2896d5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index c71436933..1d8469e51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index 9b6ca8b23..ef852ea33 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b7d880c53..f681f1403 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 3818bc761..75a30b8d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 2361e53e4..5b5f735a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -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, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 500fc9cb2..f6815f38c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -44,7 +44,7 @@ fun ReplyInformation(replyTo: List?, mentions: List?, 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?, 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 -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 203d617b3..4b5223842 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index ab4ab6779..1f530b123 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -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, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 4b442023c..42d6293c7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -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, 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, 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt index e1b02d83d..9376dc8e5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index e3f39bb3a..bb252e5f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -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()}") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 1a79e0084..86dbc46eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -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 { it.lastEvent }.thenByDescending { it.counter }.thenBy { it.url } + val order = compareByDescending { it.lastEvent }.thenByDescending { it.counter }.thenBy { it.url } - private val _feedContent = MutableStateFlow>(emptyList()) + private val _feedContent = MutableStateFlow>(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 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index b132b5221..921be72b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 5144cf674..80cac64a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -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") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 12b93b6bc..5b456bbbc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 5ad6a01af..dacb021ce 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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() ?: "", - modifier = Modifier.padding(top = 7.dp), - fontWeight = FontWeight.Bold, - 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) - ) + user.bestDisplayName()?.let { + Text( "$it", + modifier = Modifier.padding(top = 7.dp), + fontWeight = FontWeight.Bold, + fontSize = 25.sp + ) + } - 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()) { 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) { } } - Text( - "${user.info.about}", - color = MaterialTheme.colors.onSurface, - modifier = Modifier.padding(top = 5.dp, bottom = 5.dp) - ) + user.info?.about?.let { + Text( + "$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") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index bf2487f81..a3e831117 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -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