- Moves away from persistent collections (slower)

- Adds a hashcode cash for spam and blocked user public keys
- Adds a cache for isHidden
- Moves isHidden composable from LiveData to Flow
This commit is contained in:
Vitor Pamplona 2024-06-12 15:05:46 -04:00
parent a716d13c69
commit 42408978c4
7 changed files with 384 additions and 486 deletions

View File

@ -107,10 +107,6 @@ import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.DualCase
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -206,7 +202,7 @@ class Account(
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()), var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope, val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) { ) {
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf() var transientHiddenUsers: Set<String> = setOf()
data class PaymentRequest( data class PaymentRequest(
val relayUrl: String, val relayUrl: String,
@ -223,13 +219,16 @@ class Account(
@Immutable @Immutable
class LiveFollowLists( class LiveFollowLists(
val users: ImmutableSet<String> = persistentSetOf(), val users: Set<String> = emptySet(),
val hashtags: ImmutableSet<String> = persistentSetOf(), val hashtags: Set<String> = emptySet(),
val geotags: ImmutableSet<String> = persistentSetOf(), val geotags: Set<String> = emptySet(),
val communities: ImmutableSet<String> = persistentSetOf(), val communities: Set<String> = emptySet(),
) )
class ListNameNotePair(val listName: String, val event: GeneralListEvent?) class ListNameNotePair(
val listName: String,
val event: GeneralListEvent?,
)
val connectToRelaysFlow = val connectToRelaysFlow =
combineTransform( combineTransform(
@ -361,10 +360,10 @@ class Account(
userProfile().flow().follows.stateFlow.transformLatest { userProfile().flow().follows.stateFlow.transformLatest {
emit( emit(
LiveFollowLists( LiveFollowLists(
it.user.cachedFollowingKeySet().toImmutableSet(), it.user.cachedFollowingKeySet(),
it.user.cachedFollowingTagSet().toImmutableSet(), it.user.cachedFollowingTagSet(),
it.user.cachedFollowingGeohashSet().toImmutableSet(), it.user.cachedFollowingGeohashSet(),
it.user.cachedFollowingCommunitiesSet().toImmutableSet(), it.user.cachedFollowingCommunitiesSet(),
), ),
) )
} }
@ -379,8 +378,8 @@ class Account(
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> { fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) { if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
val note = LocalCache.checkGetOrCreateAddressableNote(listName) val note = LocalCache.checkGetOrCreateAddressableNote(listName)
note?.flow()?.metadata?.stateFlow?.mapLatest { note?.flow()?.metadata?.stateFlow?.mapLatest {
val noteEvent = it.note.event as? GeneralListEvent val noteEvent = it.note.event as? GeneralListEvent
@ -389,13 +388,12 @@ class Account(
} else { } else {
MutableStateFlow(ListNameNotePair(listName, null)) MutableStateFlow(ListNameNotePair(listName, null))
} }
}
fun combinePeopleListFlows( fun combinePeopleListFlows(
kind3FollowsSource: Flow<LiveFollowLists>, kind3FollowsSource: Flow<LiveFollowLists>,
peopleListFollowsSource: Flow<ListNameNotePair>, peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> { ): Flow<LiveFollowLists?> =
return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows -> combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
if (peopleListFollows.listName == GLOBAL_FOLLOWS) { if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null) emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) { } else if (peopleListFollows.listName == KIND3_FOLLOWS) {
@ -411,7 +409,6 @@ class Account(
} }
} }
} }
}
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy { val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList) combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
@ -465,38 +462,41 @@ class Account(
onReady( onReady(
LiveFollowLists( LiveFollowLists(
users = users =
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(), (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(),
hashtags = hashtags =
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(), (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
geotags = geotags =
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(), (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
communities = communities =
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList)) (listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
.map { it.toTag() } .map { it.toTag() }
.toImmutableSet(), .toSet(),
), ),
) )
} }
} }
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? { suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
return withTimeoutOrNull(1000) { withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) { decryptLiveFollows(peopleListFollows) {
continuation.resume(it) continuation.resume(it)
} }
} }
} }
}
@Immutable @Immutable
data class LiveHiddenUsers( class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>, val hiddenUsers: Set<String>,
val spammers: ImmutableSet<String>, val spammers: Set<String>,
val hiddenWords: ImmutableSet<String>, val hiddenWords: Set<String>,
val hiddenWordsCase: List<DualCase>,
val showSensitiveContent: Boolean?, val showSensitiveContent: Boolean?,
) ) {
// speeds up isHidden calculations
val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }
val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() }
val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }
}
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy { val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
combineTransform( combineTransform(
@ -530,25 +530,22 @@ class Account(
emit( emit(
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(), hiddenUsers = (resultBlockList.users + resultMuteList.users),
hiddenWords = hiddenWords.toPersistentSet(), hiddenWords = hiddenWords,
hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) },
spammers = localLive.account.transientHiddenUsers, spammers = localLive.account.transientHiddenUsers,
showSensitiveContent = localLive.account.showSensitiveContent, showSensitiveContent = localLive.account.showSensitiveContent,
), ),
) )
} }.stateIn(
.stateIn( scope,
scope, SharingStarted.Eagerly,
SharingStarted.Eagerly, LiveHiddenUsers(
LiveHiddenUsers( hiddenUsers = setOf(),
hiddenUsers = persistentSetOf(), hiddenWords = setOf(),
hiddenWords = persistentSetOf(), spammers = transientHiddenUsers,
hiddenWordsCase = emptyList(), showSensitiveContent = showSensitiveContent,
spammers = transientHiddenUsers, ),
showSensitiveContent = showSensitiveContent, )
),
)
} }
val liveHiddenUsers = flowHiddenUsers.asLiveData() val liveHiddenUsers = flowHiddenUsers.asLiveData()
@ -599,24 +596,21 @@ class Account(
filterSpamFromStrangers = filterSpam filterSpamFromStrangers = filterSpam
LocalCache.antiSpam.active = filterSpamFromStrangers LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) { if (!filterSpamFromStrangers) {
transientHiddenUsers = persistentSetOf() transientHiddenUsers = setOf()
} }
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
} }
fun userProfile(): User { fun userProfile(): User =
return userProfileCache userProfileCache
?: run { ?: run {
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey()) val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
userProfileCache = myUser userProfileCache = myUser
myUser myUser
} }
}
fun isWriteable(): Boolean { fun isWriteable(): Boolean = keyPair.privKey != null || signer is NostrSignerExternal
return keyPair.privKey != null || signer is NostrSignerExternal
}
fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) { fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -689,24 +683,16 @@ class Account(
fun reactionTo( fun reactionTo(
note: Note, note: Note,
reaction: String, reaction: String,
): List<Note> { ): List<Note> = note.reactedBy(userProfile(), reaction)
return note.reactedBy(userProfile(), reaction)
}
fun hasBoosted(note: Note): Boolean { fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty()
return boostsTo(note).isNotEmpty()
}
fun boostsTo(note: Note): List<Note> { fun boostsTo(note: Note): List<Note> = note.boostedBy(userProfile())
return note.boostedBy(userProfile())
}
fun hasReacted( fun hasReacted(
note: Note, note: Note,
reaction: String, reaction: String,
): Boolean { ): Boolean = note.hasReacted(userProfile(), reaction)
return note.hasReacted(userProfile(), reaction)
}
suspend fun reactTo( suspend fun reactTo(
note: Note, note: Note,
@ -721,7 +707,12 @@ class Account(
if (note.event is ChatMessageEvent) { if (note.event is ChatMessageEvent) {
val event = note.event as ChatMessageEvent val event = note.event as ChatMessageEvent
val users = event.recipientsPubKey().plus(event.pubKey).toSet().toList() val users =
event
.recipientsPubKey()
.plus(event.pubKey)
.toSet()
.toList()
if (reaction.startsWith(":")) { if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrl.decode(reaction) val emojiUrl = EmojiUrl.decode(reaction)
@ -801,24 +792,23 @@ class Account(
} }
} }
fun getReceivingRelays(): Set<String> { fun getReceivingRelays(): Set<String> =
return getNIP65RelayList()?.readRelays()?.toSet() getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile().latestContactList?.relays()?.filter { it.value.read }?.keys?.ifEmpty { null } ?: userProfile()
.latestContactList
?.relays()
?.filter { it.value.read }
?.keys
?.ifEmpty { null }
?: localRelays.filter { it.read }.map { it.url }.toSet() ?: localRelays.filter { it.read }.map { it.url }.toSet()
}
fun hasWalletConnectSetup(): Boolean { fun hasWalletConnectSetup(): Boolean = zapPaymentRequest != null
return zapPaymentRequest != null
}
fun isNIP47Author(pubkeyHex: String?): Boolean { fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex)
return (getNIP47Signer().pubKey == pubkeyHex)
}
fun getNIP47Signer(): NostrSigner { fun getNIP47Signer(): NostrSigner =
return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
?: signer ?: signer
}
fun decryptZapPaymentResponseEvent( fun decryptZapPaymentResponseEvent(
zapResponseEvent: LnZapPaymentResponseEvent, zapResponseEvent: LnZapPaymentResponseEvent,
@ -885,7 +875,11 @@ class Account(
) { ) {
LnZapRequestEvent.create( LnZapRequestEvent.create(
userPubKeyHex, userPubKeyHex,
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } userProfile()
.latestContactList
?.relays()
?.keys
?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(), ?: localRelays.map { it.url }.toSet(),
signer, signer,
message, message,
@ -2010,7 +2004,8 @@ class Account(
if (receiver != null) { if (receiver != null) {
val relayList = val relayList =
( (
LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver)) LocalCache
.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
?.event as? ChatMessageRelayListEvent ?.event as? ChatMessageRelayListEvent
)?.relays()?.ifEmpty { null } )?.relays()?.ifEmpty { null }
@ -2201,9 +2196,7 @@ class Account(
relay: Relay, relay: Relay,
challenge: String, challenge: String,
onReady: (RelayAuthEvent) -> Unit, onReady: (RelayAuthEvent) -> Unit,
) { ) = createAuthEvent(relay.url, challenge, onReady = onReady)
return createAuthEvent(relay.url, challenge, onReady = onReady)
}
fun createAuthEvent( fun createAuthEvent(
relayUrl: String, relayUrl: String,
@ -2271,13 +2264,9 @@ class Account(
return LocalCache.getOrCreateAddressableNote(aTag) return LocalCache.getOrCreateAddressableNote(aTag)
} }
fun getBlockList(): PeopleListEvent? { fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
return getBlockListNote().event as? PeopleListEvent
}
fun getMuteList(): MuteListEvent? { fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
return getMuteListNote().event as? MuteListEvent
}
fun hideWord(word: String) { fun hideWord(word: String) {
val muteList = getMuteList() val muteList = getMuteList()
@ -2388,7 +2377,7 @@ class Account(
} }
} }
transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
} }
@ -2509,9 +2498,7 @@ class Account(
return event.cachedGossip(signer, onReady) return event.cachedGossip(signer, onReady)
} }
fun cachedDecryptContent(note: Note): String? { fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
return cachedDecryptContent(note.event)
}
fun cachedDecryptContent(event: EventInterface?): String? { fun cachedDecryptContent(event: EventInterface?): String? {
if (event == null) return null if (event == null) return null
@ -2591,9 +2578,7 @@ class Account(
fun preferenceBetween( fun preferenceBetween(
source: String, source: String,
target: String, target: String,
): String? { ): String? = languagePreferences.get("$source,$target")
return languagePreferences.get("$source,$target")
}
private fun updateContactListTo(newContactList: ContactListEvent?) { private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList == null || newContactList.tags.isEmpty()) return if (newContactList == null || newContactList.tags.isEmpty()) return
@ -2625,39 +2610,27 @@ class Account(
return usersRelayList.toTypedArray() return usersRelayList.toTypedArray()
} }
fun convertLocalRelays(): Array<RelaySetupInfo> { fun convertLocalRelays(): Array<RelaySetupInfo> = localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
return localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
}
fun activeGlobalRelays(): Array<String> { fun activeGlobalRelays(): Array<String> =
return connectToRelays.value connectToRelays.value
.filter { it.feedTypes.contains(FeedType.GLOBAL) } .filter { it.feedTypes.contains(FeedType.GLOBAL) }
.map { it.url } .map { it.url }
.toTypedArray() .toTypedArray()
}
fun activeWriteRelays(): List<RelaySetupInfo> { fun activeWriteRelays(): List<RelaySetupInfo> = connectToRelays.value.filter { it.write }
return connectToRelays.value.filter { it.write }
}
fun isAllHidden(users: Set<HexKey>): Boolean { fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
return users.all { isHidden(it) }
}
fun isHidden(user: User) = isHidden(user.pubkeyHex) fun isHidden(user: User) = isHidden(user.pubkeyHex)
fun isHidden(userHex: String): Boolean { fun isHidden(userHex: String): Boolean =
return flowHiddenUsers.value.hiddenUsers.contains(userHex) || flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
flowHiddenUsers.value.spammers.contains(userHex) flowHiddenUsers.value.spammers.contains(userHex)
}
fun followingKeySet(): Set<HexKey> { fun followingKeySet(): Set<HexKey> = userProfile().cachedFollowingKeySet()
return userProfile().cachedFollowingKeySet()
}
fun followingTagSet(): Set<HexKey> { fun followingTagSet(): Set<HexKey> = userProfile().cachedFollowingTagSet()
return userProfile().cachedFollowingTagSet()
}
fun isAcceptable(user: User): Boolean { fun isAcceptable(user: User): Boolean {
if (userProfile().pubkeyHex == user.pubkeyHex) { if (userProfile().pubkeyHex == user.pubkeyHex) {
@ -2669,11 +2642,14 @@ class Account(
} }
if (!warnAboutPostsWithReports) { if (!warnAboutPostsWithReports) {
return !isHidden(user) && // if user hasn't hided this author return !isHidden(user) &&
// if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
} }
return !isHidden(user) && // if user hasn't hided this author return !isHidden(user) &&
user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() &&
// if user has not reported this post
user.countReportAuthorsBy(followingKeySet()) < 5 user.countReportAuthorsBy(followingKeySet()) < 5
} }
@ -2681,20 +2657,18 @@ class Account(
if (!warnAboutPostsWithReports) { if (!warnAboutPostsWithReports) {
return !note.hasReportsBy(userProfile()) return !note.hasReportsBy(userProfile())
} }
return !note.hasReportsBy(userProfile()) && // if user has not reported this post return !note.hasReportsBy(userProfile()) &&
// if user has not reported this post
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
} }
fun isFollowing(user: User): Boolean { fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
return user.pubkeyHex in followingKeySet()
}
fun isFollowing(user: HexKey): Boolean { fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
return user in followingKeySet()
}
fun isAcceptable(note: Note): Boolean { fun isAcceptable(note: Note): Boolean {
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author return note.author?.let { isAcceptable(it) } ?: true &&
// if user hasn't hided this author
isAcceptableDirect(note) && isAcceptableDirect(note) &&
( (
(note.event !is RepostEvent && note.event !is GenericRepostEvent) || (note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
@ -2719,8 +2693,7 @@ class Account(
note.reportsBy(followsPlusMe) + note.reportsBy(followsPlusMe) +
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) + (note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
innerReports innerReports
) ).toSet()
.toSet()
} }
fun saveKind3RelayList(value: List<RelaySetupInfo>) { fun saveKind3RelayList(value: List<RelaySetupInfo>) {
@ -2734,19 +2707,14 @@ class Account(
} }
} }
fun getDMRelayListNote(): AddressableNote { fun getDMRelayListNote(): AddressableNote =
return LocalCache.getOrCreateAddressableNote( LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey), ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
) )
}
fun getDMRelayListFlow(): StateFlow<NoteState> { fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
return getDMRelayListNote().flow().metadata.stateFlow
}
fun getDMRelayList(): ChatMessageRelayListEvent? { fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
return getDMRelayListNote().event as? ChatMessageRelayListEvent
}
fun saveDMRelayList(dmRelays: List<String>) { fun saveDMRelayList(dmRelays: List<String>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -2772,19 +2740,14 @@ class Account(
} }
} }
fun getPrivateOutboxRelayListNote(): AddressableNote { fun getPrivateOutboxRelayListNote(): AddressableNote =
return LocalCache.getOrCreateAddressableNote( LocalCache.getOrCreateAddressableNote(
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey), PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
) )
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> { fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
}
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? { fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
}
fun savePrivateOutboxRelayList(relays: List<String>) { fun savePrivateOutboxRelayList(relays: List<String>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -2811,19 +2774,14 @@ class Account(
} }
} }
fun getSearchRelayListNote(): AddressableNote { fun getSearchRelayListNote(): AddressableNote =
return LocalCache.getOrCreateAddressableNote( LocalCache.getOrCreateAddressableNote(
SearchRelayListEvent.createAddressATag(signer.pubKey), SearchRelayListEvent.createAddressATag(signer.pubKey),
) )
}
fun getSearchRelayListFlow(): StateFlow<NoteState> { fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
return getSearchRelayListNote().flow().metadata.stateFlow
}
fun getSearchRelayList(): SearchRelayListEvent? { fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
return getSearchRelayListNote().event as? SearchRelayListEvent
}
fun saveSearchRelayList(searchRelays: List<String>) { fun saveSearchRelayList(searchRelays: List<String>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -2850,19 +2808,14 @@ class Account(
} }
} }
fun getNIP65RelayListNote(): AddressableNote { fun getNIP65RelayListNote(): AddressableNote =
return LocalCache.getOrCreateAddressableNote( LocalCache.getOrCreateAddressableNote(
AdvertisedRelayListEvent.createAddressATag(signer.pubKey), AdvertisedRelayListEvent.createAddressATag(signer.pubKey),
) )
}
fun getNIP65RelayListFlow(): StateFlow<NoteState> { fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
return getNIP65RelayListNote().flow().metadata.stateFlow
}
fun getNIP65RelayList(): AdvertisedRelayListEvent? { fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
return getNIP65RelayListNote().event as? AdvertisedRelayListEvent
}
fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) { fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -2889,17 +2842,11 @@ class Account(
} }
} }
fun getFileServersList(): FileServersEvent? { fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
return getFileServersNote().event as? FileServersEvent
}
fun getFileServersListFlow(): StateFlow<NoteState> { fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
return getFileServersNote().flow().metadata.stateFlow
}
fun getFileServersNote(): AddressableNote { fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
return LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
}
fun sendFileServersList(servers: List<String>) { fun sendFileServersList(servers: List<String>) {
if (!isWriteable()) return if (!isWriteable()) return
@ -2961,13 +2908,9 @@ class Account(
} }
} }
fun loadLastRead(route: String): Long { fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0
return lastReadPerRoute[route] ?: 0
}
fun hasDonatedInThisVersion(): Boolean { fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
}
fun markDonatedInThisVersion() { fun markDonatedInThisVersion() {
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
@ -2988,7 +2931,7 @@ class Account(
it.cache.spamMessages.snapshot().values.forEach { it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) { if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet() transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
live.invalidateData() live.invalidateData()
} }
} }
@ -3009,8 +2952,9 @@ class Account(
} }
} }
class AccountLiveData(private val account: Account) : class AccountLiveData(
LiveData<AccountState>(AccountState(account)) { private val account: Account,
) : LiveData<AccountState>(AccountState(account)) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.Default) private val bundler = BundledUpdate(300, Dispatchers.Default)
@ -3027,4 +2971,6 @@ class AccountLiveData(private val account: Account) :
} }
} }
@Immutable class AccountState(val account: Account) @Immutable class AccountState(
val account: Account,
)

View File

@ -75,7 +75,9 @@ import java.math.BigDecimal
import kotlin.coroutines.resume import kotlin.coroutines.resume
@Stable @Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) { class AddressableNote(
val address: ATag,
) : Note(address.toTag()) {
override fun idNote() = address.toNAddr() override fun idNote() = address.toNAddr()
override fun toNEvent() = address.toNAddr() override fun toNEvent() = address.toNAddr()
@ -93,9 +95,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
return minOf(publishedAt, lastCreatedAt) return minOf(publishedAt, lastCreatedAt)
} }
fun dTag(): String? { fun dTag(): String? = (event as? AddressableEvent)?.dTag()
return (event as? AddressableEvent)?.dTag()
}
override fun wasOrShouldBeDeletedBy( override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>, deletionEvents: Set<HexKey>,
@ -107,7 +107,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
} }
@Stable @Stable
open class Note(val idHex: String) { open class Note(
val idHex: String,
) {
// These fields are only available after the Text Note event is received. // These fields are only available after the Text Note event is received.
// They are immutable after that. // They are immutable after that.
var event: EventInterface? = null var event: EventInterface? = null
@ -163,14 +165,12 @@ open class Note(val idHex: String) {
} }
} }
fun toNostrUri(): String { fun toNostrUri(): String = "nostr:${toNEvent()}"
return "nostr:${toNEvent()}"
}
open fun idDisplayNote() = idNote().toShortenHex() open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? { fun channelHex(): HexKey? =
return if ( if (
event is ChannelMessageEvent || event is ChannelMessageEvent ||
event is ChannelMetadataEvent || event is ChannelMetadataEvent ||
event is ChannelCreateEvent || event is ChannelCreateEvent ||
@ -185,7 +185,6 @@ open class Note(val idHex: String) {
} else { } else {
null null
} }
}
open fun address(): ATag? = null open fun address(): ATag? = null
@ -523,39 +522,30 @@ open class Note(val idHex: String) {
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor) isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
} }
fun getReactionBy(user: User): String? { fun getReactionBy(user: User): String? =
return reactions.firstNotNullOfOrNull { reactions.firstNotNullOfOrNull {
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) { if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
it.key it.key
} else { } else {
null null
} }
} }
}
fun isBoostedBy(user: User): Boolean { fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun hasReportsBy(user: User): Boolean { fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false
return reports[user]?.isNotEmpty() ?: false
}
fun countReportAuthorsBy(users: Set<HexKey>): Int { fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
return reports.count { it.key.pubkeyHex in users }
}
fun reportsBy(users: Set<HexKey>): List<Note> { fun reportsBy(users: Set<HexKey>): List<Note> =
return reports reports
.mapNotNull { .mapNotNull {
if (it.key.pubkeyHex in users) { if (it.key.pubkeyHex in users) {
it.value it.value
} else { } else {
null null
} }
} }.flatten()
.flatten()
}
private fun updateZapTotal() { private fun updateZapTotal() {
var sumOfAmounts = BigDecimal.ZERO var sumOfAmounts = BigDecimal.ZERO
@ -635,8 +625,8 @@ open class Note(val idHex: String) {
) )
} }
fun hasPledgeBy(user: User): Boolean { fun hasPledgeBy(user: User): Boolean =
return replies replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.any { .any {
val pledgeValue = val pledgeValue =
@ -650,10 +640,9 @@ open class Note(val idHex: String) {
pledgeValue != null && it.author == user pledgeValue != null && it.author == user
} }
}
fun pledgedAmountByOthers(): BigDecimal { fun pledgedAmountByOthers(): BigDecimal =
return replies replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.mapNotNull { .mapNotNull {
try { try {
@ -663,9 +652,7 @@ open class Note(val idHex: String) {
null null
// do nothing if it can't convert to bigdecimal // do nothing if it can't convert to bigdecimal
} }
} }.sumOf { it }
.sumOf { it }
}
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = TimeUtils.oneDayAgo() val dayAgo = TimeUtils.oneDayAgo()
@ -676,8 +663,8 @@ open class Note(val idHex: String) {
) )
} }
fun isNewThread(): Boolean { fun isNewThread(): Boolean =
return ( (
event is RepostEvent || event is RepostEvent ||
event is GenericRepostEvent || event is GenericRepostEvent ||
replyTo == null || replyTo == null ||
@ -685,29 +672,20 @@ open class Note(val idHex: String) {
) && ) &&
event !is ChannelMessageEvent && event !is ChannelMessageEvent &&
event !is LiveActivitiesChatMessageEvent event !is LiveActivitiesChatMessageEvent
}
fun hasZapped(loggedIn: User): Boolean { fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
return zaps.any { it.key.author == loggedIn }
}
fun hasReacted( fun hasReacted(
loggedIn: User, loggedIn: User,
content: String, content: String,
): Boolean { ): Boolean = reactedBy(loggedIn, content).isNotEmpty()
return reactedBy(loggedIn, content).isNotEmpty()
}
fun reactedBy( fun reactedBy(
loggedIn: User, loggedIn: User,
content: String, content: String,
): List<Note> { ): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
}
fun reactedBy(loggedIn: User): List<String> { fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
}
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
return boosts.firstOrNull { return boosts.firstOrNull {
@ -715,9 +693,7 @@ open class Note(val idHex: String) {
} != null // 5 minute protection } != null // 5 minute protection
} }
fun boostedBy(loggedIn: User): List<Note> { fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
return boosts.filter { it.author == loggedIn }
}
fun moveAllReferencesTo(note: AddressableNote) { fun moveAllReferencesTo(note: AddressableNote) {
// migrates these comments to a new version // migrates these comments to a new version
@ -762,33 +738,38 @@ open class Note(val idHex: String) {
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
val thisEvent = event ?: return false val thisEvent = event ?: return false
val hash = thisEvent.pubKey().hashCode()
val isBoostedNoteHidden = // if the author is hidden by spam or blocked
if ( if (accountChoices.hiddenUsersHashCodes.contains(hash) ||
thisEvent is GenericRepostEvent || accountChoices.spammersHashCodes.contains(hash)
thisEvent is RepostEvent || ) {
thisEvent is CommunityPostApprovalEvent return true
) { }
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else { // if the post is sensitive and the user doesn't want to see sensitive content
false if (accountChoices.showSensitiveContent == false && thisEvent.isSensitive()) {
return true
}
// if this is a repost, consider the inner event.
if (
thisEvent is GenericRepostEvent ||
thisEvent is RepostEvent ||
thisEvent is CommunityPostApprovalEvent
) {
if (replyTo?.lastOrNull()?.isHiddenFor(accountChoices) == true) {
return true
} }
}
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
if (thisEvent is BaseTextNoteEvent) { if (thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
accountChoices.hiddenWords.any { return true
thisEvent.content.containsAny(accountChoices.hiddenWordsCase)
}
} else {
false
} }
}
val isSensitive = thisEvent.isSensitive() return false
return isBoostedNoteHidden ||
isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
} }
var liveSet: NoteLiveSet? = null var liveSet: NoteLiveSet? = null
@ -858,13 +839,13 @@ open class Note(val idHex: String) {
} }
@Stable @Stable
class NoteFlowSet(u: Note) { class NoteFlowSet(
u: Note,
) {
// Observers line up here. // Observers line up here.
val metadata = NoteBundledRefresherFlow(u) val metadata = NoteBundledRefresherFlow(u)
fun isInUse(): Boolean { fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0
return metadata.stateFlow.subscriptionCount.value > 0
}
fun destroy() { fun destroy() {
metadata.destroy() metadata.destroy()
@ -872,7 +853,9 @@ class NoteFlowSet(u: Note) {
} }
@Stable @Stable
class NoteLiveSet(u: Note) { class NoteLiveSet(
u: Note,
) {
// Observers line up here. // Observers line up here.
val innerMetadata = NoteBundledRefresherLiveData(u) val innerMetadata = NoteBundledRefresherLiveData(u)
val innerReactions = NoteBundledRefresherLiveData(u) val innerReactions = NoteBundledRefresherLiveData(u)
@ -901,8 +884,7 @@ class NoteLiveSet(u: Note) {
?: false || ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false || boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false reactionState?.note?.reactions?.isNotEmpty() ?: false
} }.distinctUntilChanged()
.distinctUntilChanged()
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged() val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
@ -912,8 +894,7 @@ class NoteLiveSet(u: Note) {
var total = 0 var total = 0
it.note.reactions.forEach { total += it.value.size } it.note.reactions.forEach { total += it.value.size }
total total
} }.distinctUntilChanged()
.distinctUntilChanged()
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged() val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
@ -921,8 +902,8 @@ class NoteLiveSet(u: Note) {
val content = innerMetadata.map { it.note.event?.content() ?: "" } val content = innerMetadata.map { it.note.event?.content() ?: "" }
fun isInUse(): Boolean { fun isInUse(): Boolean =
return metadata.hasObservers() || metadata.hasObservers() ||
reactions.hasObservers() || reactions.hasObservers() ||
boosts.hasObservers() || boosts.hasObservers() ||
replies.hasObservers() || replies.hasObservers() ||
@ -936,7 +917,6 @@ class NoteLiveSet(u: Note) {
boostCount.hasObservers() || boostCount.hasObservers() ||
innerOts.hasObservers() || innerOts.hasObservers() ||
innerModifications.hasObservers() innerModifications.hasObservers()
}
fun destroy() { fun destroy() {
innerMetadata.destroy() innerMetadata.destroy()
@ -952,7 +932,9 @@ class NoteLiveSet(u: Note) {
} }
@Stable @Stable
class NoteBundledRefresherFlow(val note: Note) { class NoteBundledRefresherFlow(
val note: Note,
) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO) private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(NoteState(note)) val stateFlow = MutableStateFlow(NoteState(note))
@ -973,7 +955,9 @@ class NoteBundledRefresherFlow(val note: Note) {
} }
@Stable @Stable
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) { class NoteBundledRefresherLiveData(
val note: Note,
) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO) private val bundler = BundledUpdate(500, Dispatchers.IO)
@ -1000,7 +984,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
} }
@Stable @Stable
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) { class NoteLoadingLiveData<Y>(
val note: Note,
initialValue: Y?,
) : MediatorLiveData<Y>(initialValue) {
override fun onActive() { override fun onActive() {
super.onActive() super.onActive()
if (note is AddressableNote) { if (note is AddressableNote) {
@ -1020,7 +1007,9 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
} }
} }
@Immutable class NoteState(val note: Note) @Immutable class NoteState(
val note: Note,
)
object RelayBriefInfoCache { object RelayBriefInfoCache {
val cache = LruCache<String, RelayBriefInfo?>(50) val cache = LruCache<String, RelayBriefInfo?>(50)

View File

@ -30,8 +30,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -64,12 +62,7 @@ fun WatchBlockAndReport(
nav: (String) -> Unit, nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit, normalNote: @Composable (canPreview: Boolean) -> Unit,
) { ) {
val isHiddenState by remember(note) { val isHiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle()
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val showAnyway = val showAnyway =
remember { remember {

View File

@ -436,8 +436,7 @@ fun ClickableNote(
} }
}, },
onLongClick = showPopup, onLongClick = showPopup,
) ).background(backgroundColor.value)
.background(backgroundColor.value)
} }
Column(modifier = updatedModifier) { content() } Column(modifier = updatedModifier) { content() }
@ -476,8 +475,11 @@ fun InnerNoteWithReactions(
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
val showSecondRow = val showSecondRow =
baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is RepostEvent &&
!isBoostedNote && !isQuotedNote && accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED baseNote.event !is GenericRepostEvent &&
!isBoostedNote &&
!isQuotedNote &&
accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED
NoteBody( NoteBody(
baseNote = baseNote, baseNote = baseNote,
showAuthorPicture = isQuotedNote, showAuthorPicture = isQuotedNote,
@ -843,15 +845,14 @@ fun RenderRepost(
} }
} }
fun getGradient(backgroundColor: MutableState<Color>): Brush { fun getGradient(backgroundColor: MutableState<Color>): Brush =
return Brush.verticalGradient( Brush.verticalGradient(
colors = colors =
listOf( listOf(
backgroundColor.value.copy(alpha = 0f), backgroundColor.value.copy(alpha = 0f),
backgroundColor.value, backgroundColor.value,
), ),
) )
}
@Composable @Composable
fun ReplyNoteComposition( fun ReplyNoteComposition(
@ -1126,7 +1127,10 @@ private fun ChannelNotePicture(
loadProfilePicture: Boolean, loadProfilePicture: Boolean,
) { ) {
val model by val model by
baseChannel.live.map { it.channel.profilePicture() }.distinctUntilChanged().observeAsState() baseChannel.live
.map { it.channel.profilePicture() }
.distinctUntilChanged()
.observeAsState()
Box(Size30Modifier) { Box(Size30Modifier) {
RobohashFallbackAsyncImage( RobohashFallbackAsyncImage(

View File

@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -106,7 +107,11 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -116,9 +121,12 @@ import java.util.Locale
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
@Immutable open class ToastMsg() @Immutable open class ToastMsg
@Immutable class StringToastMsg(val title: String, val msg: String) : ToastMsg() @Immutable class StringToastMsg(
val title: String,
val msg: String,
) : ToastMsg()
@Immutable class ResourceToastMsg( @Immutable class ResourceToastMsg(
val titleResId: Int, val titleResId: Int,
@ -126,16 +134,34 @@ import kotlin.time.measureTimedValue
val params: Array<out String>? = null, val params: Array<out String>? = null,
) : ToastMsg() ) : ToastMsg()
@Immutable class ThrowableToastMsg(val titleResId: Int, val msg: String? = null, val throwable: Throwable) : ToastMsg() @Immutable class ThrowableToastMsg(
val titleResId: Int,
val msg: String? = null,
val throwable: Throwable,
) : ToastMsg()
@Stable @Stable
class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao { class AccountViewModel(
val account: Account,
val settings: SettingsState,
) : ViewModel(),
Dao {
val accountLiveData: LiveData<AccountState> = account.live.map { it } val accountLiveData: LiveData<AccountState> = account.live.map { it }
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it } val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
val accountMarkAsReadUpdates = mutableIntStateOf(0) val accountMarkAsReadUpdates = mutableIntStateOf(0)
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it } val userFollows: LiveData<UserState> =
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it } account
.userProfile()
.live()
.follows
.map { it }
val userRelays: LiveData<UserState> =
account
.userProfile()
.live()
.relays
.map { it }
val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey) val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey) val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey)
@ -182,13 +208,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) } viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) }
} }
fun isWriteable(): Boolean { fun isWriteable(): Boolean = account.isWriteable()
return account.isWriteable()
}
fun userProfile(): User { fun userProfile(): User = account.userProfile()
return account.userProfile()
}
suspend fun reactTo( suspend fun reactTo(
note: Note, note: Note,
@ -200,16 +222,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
fun <T : Event> observeByETag( fun <T : Event> observeByETag(
kind: Int, kind: Int,
eTag: HexKey, eTag: HexKey,
): StateFlow<T?> { ): StateFlow<T?> = LocalCache.observeETag<T>(kind = kind, eventId = eTag, viewModelScope).latest
return LocalCache.observeETag<T>(kind = kind, eventId = eTag, viewModelScope).latest
}
fun <T : Event> observeByAuthor( fun <T : Event> observeByAuthor(
kind: Int, kind: Int,
pubkeyHex: HexKey, pubkeyHex: HexKey,
): StateFlow<T?> { ): StateFlow<T?> = LocalCache.observeAuthor<T>(kind = kind, pubkey = pubkeyHex, viewModelScope).latest
return LocalCache.observeAuthor<T>(kind = kind, pubkey = pubkeyHex, viewModelScope).latest
}
fun reactToOrDelete( fun reactToOrDelete(
note: Note, note: Note,
@ -236,16 +254,24 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
fun isNoteHidden(note: Note): Boolean { val noteIsHiddenFlows = LruCache<Note, StateFlow<Boolean>>(300)
return note.isHiddenFor(account.flowHiddenUsers.value)
} fun createIsHiddenFlow(note: Note): StateFlow<Boolean> =
noteIsHiddenFlows.get(note)
?: combineTransform(account.flowHiddenUsers, note.flow().metadata.stateFlow) { hiddenUsers, metadata ->
emit(metadata.note.isHiddenFor(hiddenUsers))
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false,
).also {
noteIsHiddenFlows.put(note, it)
}
fun hasReactedTo( fun hasReactedTo(
baseNote: Note, baseNote: Note,
reaction: String, reaction: String,
): Boolean { ): Boolean = account.hasReacted(baseNote, reaction)
return account.hasReacted(baseNote, reaction)
}
suspend fun deleteReactionTo( suspend fun deleteReactionTo(
note: Note, note: Note,
@ -254,9 +280,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.delete(account.reactionTo(note, reaction)) account.delete(account.reactionTo(note, reaction))
} }
fun hasBoosted(baseNote: Note): Boolean { fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote)
return account.hasBoosted(baseNote)
}
fun deleteBoostsTo(note: Note) { fun deleteBoostsTo(note: Note) {
viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) } viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) }
@ -332,15 +356,17 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val initialResults = val initialResults =
zaps.associate { zaps
it.request to .associate {
ZapAmountCommentNotification( it.request to
it.request.author, ZapAmountCommentNotification(
it.request.event?.content()?.ifBlank { null }, it.request.author,
showAmountAxis((it.response.event as? LnZapEvent)?.amount), it.request.event
) ?.content()
} ?.ifBlank { null },
.toMutableMap() showAmountAxis((it.response.event as? LnZapEvent)?.amount),
)
}.toMutableMap()
collectSuccessfulSigningOperations<CombinedZap, ZapAmountCommentNotification>( collectSuccessfulSigningOperations<CombinedZap, ZapAmountCommentNotification>(
operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
@ -359,8 +385,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
fun cachedDecryptAmountMessageInGroup(zapNotes: List<CombinedZap>): ImmutableList<ZapAmountCommentNotification> { fun cachedDecryptAmountMessageInGroup(zapNotes: List<CombinedZap>): ImmutableList<ZapAmountCommentNotification> =
return zapNotes zapNotes
.map { .map {
val request = it.request.event as? LnZapRequestEvent val request = it.request.event as? LnZapRequestEvent
if (request?.isPrivateZap() == true) { if (request?.isPrivateZap() == true) {
@ -374,20 +400,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
it.request.author, it.request.author,
it.request.event?.content()?.ifBlank { null }, it.request.event
?.content()
?.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountAxis((it.response.event as? LnZapEvent)?.amount),
) )
} }
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
it.request.author, it.request.author,
it.request.event?.content()?.ifBlank { null }, it.request.event
?.content()
?.ifBlank { null },
showAmountAxis((it.response.event as? LnZapEvent)?.amount), showAmountAxis((it.response.event as? LnZapEvent)?.amount),
) )
} }
} }.toImmutableList()
.toImmutableList()
}
fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList<ZapAmountCommentNotification> { fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList<ZapAmountCommentNotification> {
val myList = baseNote.zaps.toList() val myList = baseNote.zaps.toList()
@ -406,19 +434,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
it.first.author, it.first.author,
it.first.event?.content()?.ifBlank { null }, it.first.event
?.content()
?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
) )
} }
} else { } else {
ZapAmountCommentNotification( ZapAmountCommentNotification(
it.first.author, it.first.author,
it.first.event?.content()?.ifBlank { null }, it.first.event
?.content()
?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
) )
} }
} }.toImmutableList()
.toImmutableList()
} }
fun decryptAmountMessageInGroup( fun decryptAmountMessageInGroup(
@ -434,11 +465,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
it.first to it.first to
ZapAmountCommentNotification( ZapAmountCommentNotification(
it.first.author, it.first.author,
it.first.event?.content()?.ifBlank { null }, it.first.event
?.content()
?.ifBlank { null },
showAmountAxis((it.second?.event as? LnZapEvent)?.amount), showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
) )
} }.toMutableMap()
.toMutableMap()
collectSuccessfulSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>( collectSuccessfulSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
operationsInput = myList, operationsInput = myList,
@ -588,9 +620,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.isInPrivateBookmarks(note, onReady) account.isInPrivateBookmarks(note, onReady)
} }
fun isInPublicBookmarks(note: Note): Boolean { fun isInPublicBookmarks(note: Note): Boolean = account.isInPublicBookmarks(note)
return account.isInPublicBookmarks(note)
}
fun broadcast(note: Note) { fun broadcast(note: Note) {
viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) } viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) }
@ -615,13 +645,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { account.delete(note) } viewModelScope.launch(Dispatchers.IO) { account.delete(note) }
} }
fun cachedDecrypt(note: Note): String? { fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note)
return account.cachedDecryptContent(note)
}
fun cachedDecrypt(event: EventInterface?): String? { fun cachedDecrypt(event: EventInterface?): String? = account.cachedDecryptContent(event)
return account.cachedDecryptContent(event)
}
fun decrypt( fun decrypt(
note: Note, note: Note,
@ -685,18 +711,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) } viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) }
} }
fun isLoggedUser(user: User?): Boolean { fun isLoggedUser(user: User?): Boolean = account.userProfile().pubkeyHex == user?.pubkeyHex
return account.userProfile().pubkeyHex == user?.pubkeyHex
}
fun isFollowing(user: User?): Boolean { fun isFollowing(user: User?): Boolean {
if (user == null) return false if (user == null) return false
return account.userProfile().isFollowingCached(user) return account.userProfile().isFollowingCached(user)
} }
fun isFollowing(user: HexKey): Boolean { fun isFollowing(user: HexKey): Boolean = account.userProfile().isFollowingCached(user)
return account.userProfile().isFollowingCached(user)
}
val hideDeleteRequestDialog: Boolean val hideDeleteRequestDialog: Boolean
get() = account.hideDeleteRequestDialog get() = account.hideDeleteRequestDialog
@ -737,9 +759,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
fun defaultZapType(): LnZapEvent.ZapType { fun defaultZapType(): LnZapEvent.ZapType = account.defaultZapType
return account.defaultZapType
}
@Immutable @Immutable
data class NoteComposeReportState( data class NoteComposeReportState(
@ -898,13 +918,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { runOnIO() } viewModelScope.launch(Dispatchers.IO) { runOnIO() }
} }
suspend fun checkGetOrCreateUser(key: HexKey): User? { suspend fun checkGetOrCreateUser(key: HexKey): User? = LocalCache.checkGetOrCreateUser(key)
return LocalCache.checkGetOrCreateUser(key)
}
override suspend fun getOrCreateUser(key: HexKey): User { override suspend fun getOrCreateUser(key: HexKey): User = LocalCache.getOrCreateUser(key)
return LocalCache.getOrCreateUser(key)
}
fun checkGetOrCreateUser( fun checkGetOrCreateUser(
key: HexKey, key: HexKey,
@ -913,17 +929,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) } viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) }
} }
fun getUserIfExists(hex: HexKey): User? { fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex)
return LocalCache.getUserIfExists(hex)
}
private suspend fun checkGetOrCreateNote(key: HexKey): Note? { private suspend fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key)
return LocalCache.checkGetOrCreateNote(key)
}
override suspend fun getOrCreateNote(key: HexKey): Note { override suspend fun getOrCreateNote(key: HexKey): Note = LocalCache.getOrCreateNote(key)
return LocalCache.getOrCreateNote(key)
}
fun checkGetOrCreateNote( fun checkGetOrCreateNote(
key: HexKey, key: HexKey,
@ -948,13 +958,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
fun getNoteIfExists(hex: HexKey): Note? { fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
return LocalCache.getNoteIfExists(hex)
}
override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? { override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? = LocalCache.checkGetOrCreateAddressableNote(key)
return LocalCache.checkGetOrCreateAddressableNote(key)
}
fun checkGetOrCreateAddressableNote( fun checkGetOrCreateAddressableNote(
key: HexKey, key: HexKey,
@ -963,9 +969,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) } viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
} }
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? { suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? = LocalCache.getOrCreateAddressableNote(key)
return LocalCache.getOrCreateAddressableNote(key)
}
fun getOrCreateAddressableNote( fun getOrCreateAddressableNote(
key: ATag, key: ATag,
@ -974,9 +978,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) } viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) }
} }
fun getAddressableNoteIfExists(key: String): AddressableNote? { fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
return LocalCache.getAddressableNoteIfExists(key)
}
suspend fun findStatusesForUser( suspend fun findStatusesForUser(
myUser: User, myUser: User,
@ -1007,9 +1009,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? { private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key)
return LocalCache.checkGetOrCreateChannel(key)
}
fun checkGetOrCreateChannel( fun checkGetOrCreateChannel(
key: HexKey, key: HexKey,
@ -1018,9 +1018,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) } viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) }
} }
fun getChannelIfExists(hex: HexKey): Channel? { fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex)
return LocalCache.getChannelIfExists(hex)
}
fun loadParticipants( fun loadParticipants(
participants: List<Participant>, participants: List<Participant>,
@ -1036,8 +1034,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
it, it,
) )
} }
} }.toImmutableList()
.toImmutableList()
onReady(participantUsers) onReady(participantUsers)
} }
@ -1150,10 +1147,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory { class Factory(
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel { val account: Account,
return AccountViewModel(account, settings) as AccountViewModel val settings: SettingsState,
} ) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel = AccountViewModel(account, settings) as AccountViewModel
} }
private var collectorJob: Job? = null private var collectorJob: Job? = null
@ -1404,19 +1402,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
fun getRelayListFor(user: User): AdvertisedRelayListEvent? { fun getRelayListFor(user: User): AdvertisedRelayListEvent? = (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?)
return (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?)
}
fun getRelayListNoteFor(user: User): AddressableNote? { fun getRelayListNoteFor(user: User): AddressableNote? =
return LocalCache.getAddressableNoteIfExists( LocalCache.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex), AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex),
) )
}
val draftNoteCache = CachedDraftNotes(this) val draftNoteCache = CachedDraftNotes(this)
class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync<DraftEvent, Note>(20) { class CachedDraftNotes(
val accountViewModel: AccountViewModel,
) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
override suspend fun compute( override suspend fun compute(
key: DraftEvent, key: DraftEvent,
onReady: (Note?) -> Unit, onReady: (Note?) -> Unit,
@ -1431,9 +1428,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
val bechLinkCache = CachedLoadedBechLink(this) val bechLinkCache = CachedLoadedBechLink(this)
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) { class CachedLoadedBechLink(
override suspend fun compute(key: String): LoadedBechLink? { val accountViewModel: AccountViewModel,
return Nip19Bech32.uriToRoute(key)?.let { ) : GenericBaseCache<String, LoadedBechLink>(20) {
override suspend fun compute(key: String): LoadedBechLink? =
Nip19Bech32.uriToRoute(key)?.let {
var returningNote: Note? = null var returningNote: Note? = null
when (val parsed = it.entity) { when (val parsed = it.entity) {
@ -1460,11 +1459,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
LoadedBechLink(returningNote, it) LoadedBechLink(returningNote, it)
} }
}
} }
} }
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) { class HasNotificationDot(
bottomNavigationItems: ImmutableList<Route>,
) {
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) } val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
fun update( fun update(
@ -1489,7 +1489,10 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
} }
} }
@Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19Bech32.ParseReturn) @Immutable data class LoadedBechLink(
val baseNote: Note?,
val nip19: Nip19Bech32.ParseReturn,
)
public suspend fun <T, K> collectSuccessfulSigningOperations( public suspend fun <T, K> collectSuccessfulSigningOperations(
operationsInput: List<T>, operationsInput: List<T>,

View File

@ -59,14 +59,13 @@ open class Event(
) : EventInterface { ) : EventInterface {
override fun isContentEncoded() = false override fun isContentEncoded() = false
override fun countMemory(): Long { override fun countMemory(): Long =
return 12L + 12L +
id.bytesUsedInMemory() + id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() + pubKey.bytesUsedInMemory() +
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } + tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() + content.bytesUsedInMemory() +
sig.bytesUsedInMemory() sig.bytesUsedInMemory()
}
override fun id(): HexKey = id override fun id(): HexKey = id
@ -152,8 +151,8 @@ open class Event(
override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" } override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" }
override fun zapSplitSetup(): List<ZapSplitSetup> { override fun zapSplitSetup(): List<ZapSplitSetup> =
return tags tags
.filter { it.size > 1 && it[0] == "zap" } .filter { it.size > 1 && it[0] == "zap" }
.mapNotNull { .mapNotNull {
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true) val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
@ -170,7 +169,6 @@ open class Event(
null null
} }
} }
}
override fun taggedAddresses() = override fun taggedAddresses() =
tags tags
@ -272,29 +270,23 @@ open class Event(
return rank return rank
} }
override fun getGeoHash(): String? { override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getReward(): BigDecimal? { override fun getReward(): BigDecimal? =
return try { try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) } tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
}
open fun toNIP19(): String { open fun toNIP19(): String =
return if (this is AddressableEvent) { if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr() ATag(kind, pubKey, dTag(), null).toNAddr()
} else { } else {
Nip19Bech32.createNEvent(id, pubKey, kind, null) Nip19Bech32.createNEvent(id, pubKey, kind, null)
} }
}
fun toNostrUri(): String { fun toNostrUri(): String = "nostr:${toNIP19()}"
return "nostr:${toNIP19()}"
}
fun hasCorrectIDHash(): Boolean { fun hasCorrectIDHash(): Boolean {
if (id.isEmpty()) return false if (id.isEmpty()) return false
@ -320,8 +312,7 @@ open class Event(
| Event: ${toJson()} | Event: ${toJson()}
| Actual ID: $id | Actual ID: $id
| Generated: ${generateId()} | Generated: ${generateId()}
""" """.trimIndent(),
.trimIndent(),
) )
} }
if (!hasVerifiedSignature()) { if (!hasVerifiedSignature()) {
@ -329,22 +320,17 @@ open class Event(
} }
} }
override fun hasValidSignature(): Boolean { override fun hasValidSignature(): Boolean =
return try { try {
hasCorrectIDHash() && hasVerifiedSignature() hasCorrectIDHash() && hasVerifiedSignature()
} catch (e: Exception) { } catch (e: Exception) {
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e) Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false false
} }
}
fun makeJsonForId(): String { fun makeJsonForId(): String = makeJsonForId(pubKey, createdAt, kind, tags, content)
return makeJsonForId(pubKey, createdAt, kind, tags, content)
}
fun generateId(): String { fun generateId(): String = CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
}
fun generateId2(): String { fun generateId2(): String {
val sha256 = MessageDigest.getInstance("SHA-256") val sha256 = MessageDigest.getInstance("SHA-256")
@ -356,9 +342,7 @@ open class Event(
override fun deserialize( override fun deserialize(
jp: JsonParser, jp: JsonParser,
ctxt: DeserializationContext, ctxt: DeserializationContext,
): Event { ): Event = fromJson(jp.codec.readTree(jp))
return fromJson(jp.codec.readTree(jp))
}
} }
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) { private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
@ -460,8 +444,8 @@ open class Event(
.addDeserializer(Request::class.java, RequestDeserializer()), .addDeserializer(Request::class.java, RequestDeserializer()),
) )
fun fromJson(jsonObject: JsonNode): Event { fun fromJson(jsonObject: JsonNode): Event =
return EventFactory.create( EventFactory.create(
id = jsonObject.get("id").asText().intern(), id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(), pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(), createdAt = jsonObject.get("created_at").asLong(),
@ -473,11 +457,8 @@ open class Event(
content = jsonObject.get("content").asText(), content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText(), sig = jsonObject.get("sig").asText(),
) )
}
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> { private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> = Array(size()) { transform(get(it)) }
return Array(size()) { transform(get(it)) }
}
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
@ -518,9 +499,7 @@ open class Event(
kind: Int, kind: Int,
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
): ByteArray { ): ByteArray = CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
}
fun create( fun create(
signer: NostrSigner, signer: NostrSigner,
@ -529,9 +508,7 @@ open class Event(
content: String = "", content: String = "",
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (Event) -> Unit, onReady: (Event) -> Unit,
) { ) = signer.sign(createdAt, kind, tags, content, onReady)
return signer.sign(createdAt, kind, tags, content, onReady)
}
} }
} }
@ -572,7 +549,8 @@ open class BaseAddressableEvent(
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
sig: HexKey, sig: HexKey,
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { ) : Event(id, pubKey, createdAt, kind, tags, content, sig),
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null) override fun address() = ATag(kind, pubKey, dTag(), null)

View File

@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
@Immutable @Immutable
class PeopleListEvent( class PeopleListEvent(
@ -71,9 +70,9 @@ class PeopleListEvent(
} }
@Immutable @Immutable
data class UsersAndWords( class UsersAndWords(
val users: ImmutableSet<String> = persistentSetOf(), val users: Set<String> = setOf(),
val words: ImmutableSet<String> = persistentSetOf(), val words: Set<String> = setOf(),
) )
fun publicAndPrivateUsersAndWords( fun publicAndPrivateUsersAndWords(
@ -120,9 +119,7 @@ class PeopleListEvent(
const val BLOCK_LIST_D_TAG = "mute" const val BLOCK_LIST_D_TAG = "mute"
const val ALT = "List of people" const val ALT = "List of people"
fun blockListFor(pubKeyHex: HexKey): String { fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
return "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
}
fun createListWithTag( fun createListWithTag(
name: String, name: String,
@ -161,9 +158,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
return createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
fun createListWithWord( fun createListWithWord(
name: String, name: String,
@ -172,9 +167,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
return createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
}
fun addUsers( fun addUsers(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,
@ -223,9 +216,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
}
fun addUser( fun addUser(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,
@ -234,9 +225,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
fun addTag( fun addTag(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,
@ -284,9 +273,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
}
fun removeUser( fun removeUser(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,
@ -295,9 +282,7 @@ class PeopleListEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit, onReady: (PeopleListEvent) -> Unit,
) { ) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
fun removeTag( fun removeTag(
earlierVersion: PeopleListEvent, earlierVersion: PeopleListEvent,