- 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.NostrSignerInternal
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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@ -206,7 +202,7 @@ class Account(
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) {
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
var transientHiddenUsers: Set<String> = setOf()
data class PaymentRequest(
val relayUrl: String,
@ -223,13 +219,16 @@ class Account(
@Immutable
class LiveFollowLists(
val users: ImmutableSet<String> = persistentSetOf(),
val hashtags: ImmutableSet<String> = persistentSetOf(),
val geotags: ImmutableSet<String> = persistentSetOf(),
val communities: ImmutableSet<String> = persistentSetOf(),
val users: Set<String> = emptySet(),
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
)
class ListNameNotePair(val listName: String, val event: GeneralListEvent?)
class ListNameNotePair(
val listName: String,
val event: GeneralListEvent?,
)
val connectToRelaysFlow =
combineTransform(
@ -361,10 +360,10 @@ class Account(
userProfile().flow().follows.stateFlow.transformLatest {
emit(
LiveFollowLists(
it.user.cachedFollowingKeySet().toImmutableSet(),
it.user.cachedFollowingTagSet().toImmutableSet(),
it.user.cachedFollowingGeohashSet().toImmutableSet(),
it.user.cachedFollowingCommunitiesSet().toImmutableSet(),
it.user.cachedFollowingKeySet(),
it.user.cachedFollowingTagSet(),
it.user.cachedFollowingGeohashSet(),
it.user.cachedFollowingCommunitiesSet(),
),
)
}
@ -379,8 +378,8 @@ class Account(
}
@OptIn(ExperimentalCoroutinesApi::class)
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> {
return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
note?.flow()?.metadata?.stateFlow?.mapLatest {
val noteEvent = it.note.event as? GeneralListEvent
@ -389,13 +388,12 @@ class Account(
} else {
MutableStateFlow(ListNameNotePair(listName, null))
}
}
fun combinePeopleListFlows(
kind3FollowsSource: Flow<LiveFollowLists>,
peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> {
return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
): Flow<LiveFollowLists?> =
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
@ -411,7 +409,6 @@ class Account(
}
}
}
}
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
@ -465,38 +462,41 @@ class Account(
onReady(
LiveFollowLists(
users =
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(),
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(),
hashtags =
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(),
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
geotags =
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(),
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
communities =
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
.map { it.toTag() }
.toImmutableSet(),
.toSet(),
),
)
}
}
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? {
return withTimeoutOrNull(1000) {
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) {
continuation.resume(it)
}
}
}
}
@Immutable
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>,
val hiddenWords: ImmutableSet<String>,
val hiddenWordsCase: List<DualCase>,
class LiveHiddenUsers(
val hiddenUsers: Set<String>,
val spammers: Set<String>,
val hiddenWords: Set<String>,
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 {
combineTransform(
@ -530,25 +530,22 @@ class Account(
emit(
LiveHiddenUsers(
hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(),
hiddenWords = hiddenWords.toPersistentSet(),
hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) },
hiddenUsers = (resultBlockList.users + resultMuteList.users),
hiddenWords = hiddenWords,
spammers = localLive.account.transientHiddenUsers,
showSensitiveContent = localLive.account.showSensitiveContent,
),
)
}
.stateIn(
scope,
SharingStarted.Eagerly,
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
hiddenWords = persistentSetOf(),
hiddenWordsCase = emptyList(),
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
),
)
}.stateIn(
scope,
SharingStarted.Eagerly,
LiveHiddenUsers(
hiddenUsers = setOf(),
hiddenWords = setOf(),
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
),
)
}
val liveHiddenUsers = flowHiddenUsers.asLiveData()
@ -599,24 +596,21 @@ class Account(
filterSpamFromStrangers = filterSpam
LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) {
transientHiddenUsers = persistentSetOf()
transientHiddenUsers = setOf()
}
live.invalidateData()
saveable.invalidateData()
}
fun userProfile(): User {
return userProfileCache
fun userProfile(): User =
userProfileCache
?: run {
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
userProfileCache = myUser
myUser
}
}
fun isWriteable(): Boolean {
return keyPair.privKey != null || signer is NostrSignerExternal
}
fun isWriteable(): Boolean = keyPair.privKey != null || signer is NostrSignerExternal
fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
if (!isWriteable()) return
@ -689,24 +683,16 @@ class Account(
fun reactionTo(
note: Note,
reaction: String,
): List<Note> {
return note.reactedBy(userProfile(), reaction)
}
): List<Note> = note.reactedBy(userProfile(), reaction)
fun hasBoosted(note: Note): Boolean {
return boostsTo(note).isNotEmpty()
}
fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty()
fun boostsTo(note: Note): List<Note> {
return note.boostedBy(userProfile())
}
fun boostsTo(note: Note): List<Note> = note.boostedBy(userProfile())
fun hasReacted(
note: Note,
reaction: String,
): Boolean {
return note.hasReacted(userProfile(), reaction)
}
): Boolean = note.hasReacted(userProfile(), reaction)
suspend fun reactTo(
note: Note,
@ -721,7 +707,12 @@ class Account(
if (note.event is 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(":")) {
val emojiUrl = EmojiUrl.decode(reaction)
@ -801,24 +792,23 @@ class Account(
}
}
fun getReceivingRelays(): Set<String> {
return getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile().latestContactList?.relays()?.filter { it.value.read }?.keys?.ifEmpty { null }
fun getReceivingRelays(): Set<String> =
getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile()
.latestContactList
?.relays()
?.filter { it.value.read }
?.keys
?.ifEmpty { null }
?: localRelays.filter { it.read }.map { it.url }.toSet()
}
fun hasWalletConnectSetup(): Boolean {
return zapPaymentRequest != null
}
fun hasWalletConnectSetup(): Boolean = zapPaymentRequest != null
fun isNIP47Author(pubkeyHex: String?): Boolean {
return (getNIP47Signer().pubKey == pubkeyHex)
}
fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex)
fun getNIP47Signer(): NostrSigner {
return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
fun getNIP47Signer(): NostrSigner =
zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
?: signer
}
fun decryptZapPaymentResponseEvent(
zapResponseEvent: LnZapPaymentResponseEvent,
@ -885,7 +875,11 @@ class Account(
) {
LnZapRequestEvent.create(
userPubKeyHex,
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
userProfile()
.latestContactList
?.relays()
?.keys
?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(),
signer,
message,
@ -2010,7 +2004,8 @@ class Account(
if (receiver != null) {
val relayList =
(
LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
LocalCache
.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
?.event as? ChatMessageRelayListEvent
)?.relays()?.ifEmpty { null }
@ -2201,9 +2196,7 @@ class Account(
relay: Relay,
challenge: String,
onReady: (RelayAuthEvent) -> Unit,
) {
return createAuthEvent(relay.url, challenge, onReady = onReady)
}
) = createAuthEvent(relay.url, challenge, onReady = onReady)
fun createAuthEvent(
relayUrl: String,
@ -2271,13 +2264,9 @@ class Account(
return LocalCache.getOrCreateAddressableNote(aTag)
}
fun getBlockList(): PeopleListEvent? {
return getBlockListNote().event as? PeopleListEvent
}
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
fun getMuteList(): MuteListEvent? {
return getMuteListNote().event as? MuteListEvent
}
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
fun hideWord(word: String) {
val muteList = getMuteList()
@ -2388,7 +2377,7 @@ class Account(
}
}
transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet()
transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
live.invalidateData()
saveable.invalidateData()
}
@ -2509,9 +2498,7 @@ class Account(
return event.cachedGossip(signer, onReady)
}
fun cachedDecryptContent(note: Note): String? {
return cachedDecryptContent(note.event)
}
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
fun cachedDecryptContent(event: EventInterface?): String? {
if (event == null) return null
@ -2591,9 +2578,7 @@ class Account(
fun preferenceBetween(
source: String,
target: String,
): String? {
return languagePreferences.get("$source,$target")
}
): String? = languagePreferences.get("$source,$target")
private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList == null || newContactList.tags.isEmpty()) return
@ -2625,39 +2610,27 @@ class Account(
return usersRelayList.toTypedArray()
}
fun convertLocalRelays(): Array<RelaySetupInfo> {
return localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
}
fun convertLocalRelays(): Array<RelaySetupInfo> = localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
fun activeGlobalRelays(): Array<String> {
return connectToRelays.value
fun activeGlobalRelays(): Array<String> =
connectToRelays.value
.filter { it.feedTypes.contains(FeedType.GLOBAL) }
.map { it.url }
.toTypedArray()
}
fun activeWriteRelays(): List<RelaySetupInfo> {
return connectToRelays.value.filter { it.write }
}
fun activeWriteRelays(): List<RelaySetupInfo> = connectToRelays.value.filter { it.write }
fun isAllHidden(users: Set<HexKey>): Boolean {
return users.all { isHidden(it) }
}
fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
fun isHidden(user: User) = isHidden(user.pubkeyHex)
fun isHidden(userHex: String): Boolean {
return flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
fun isHidden(userHex: String): Boolean =
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
flowHiddenUsers.value.spammers.contains(userHex)
}
fun followingKeySet(): Set<HexKey> {
return userProfile().cachedFollowingKeySet()
}
fun followingKeySet(): Set<HexKey> = userProfile().cachedFollowingKeySet()
fun followingTagSet(): Set<HexKey> {
return userProfile().cachedFollowingTagSet()
}
fun followingTagSet(): Set<HexKey> = userProfile().cachedFollowingTagSet()
fun isAcceptable(user: User): Boolean {
if (userProfile().pubkeyHex == user.pubkeyHex) {
@ -2669,11 +2642,14 @@ class Account(
}
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
}
return !isHidden(user) && // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
return !isHidden(user) &&
// if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() &&
// if user has not reported this post
user.countReportAuthorsBy(followingKeySet()) < 5
}
@ -2681,20 +2657,18 @@ class Account(
if (!warnAboutPostsWithReports) {
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
}
fun isFollowing(user: User): Boolean {
return user.pubkeyHex in followingKeySet()
}
fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
fun isFollowing(user: HexKey): Boolean {
return user in followingKeySet()
}
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
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) &&
(
(note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
@ -2719,8 +2693,7 @@ class Account(
note.reportsBy(followsPlusMe) +
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
innerReports
)
.toSet()
).toSet()
}
fun saveKind3RelayList(value: List<RelaySetupInfo>) {
@ -2734,19 +2707,14 @@ class Account(
}
}
fun getDMRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getDMRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getDMRelayListFlow(): StateFlow<NoteState> {
return getDMRelayListNote().flow().metadata.stateFlow
}
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
fun getDMRelayList(): ChatMessageRelayListEvent? {
return getDMRelayListNote().event as? ChatMessageRelayListEvent
}
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
fun saveDMRelayList(dmRelays: List<String>) {
if (!isWriteable()) return
@ -2772,19 +2740,14 @@ class Account(
}
}
fun getPrivateOutboxRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getPrivateOutboxRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> {
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? {
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
}
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
fun savePrivateOutboxRelayList(relays: List<String>) {
if (!isWriteable()) return
@ -2811,19 +2774,14 @@ class Account(
}
}
fun getSearchRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getSearchRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
SearchRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getSearchRelayListFlow(): StateFlow<NoteState> {
return getSearchRelayListNote().flow().metadata.stateFlow
}
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
fun getSearchRelayList(): SearchRelayListEvent? {
return getSearchRelayListNote().event as? SearchRelayListEvent
}
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
fun saveSearchRelayList(searchRelays: List<String>) {
if (!isWriteable()) return
@ -2850,19 +2808,14 @@ class Account(
}
}
fun getNIP65RelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getNIP65RelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
AdvertisedRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getNIP65RelayListFlow(): StateFlow<NoteState> {
return getNIP65RelayListNote().flow().metadata.stateFlow
}
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
fun getNIP65RelayList(): AdvertisedRelayListEvent? {
return getNIP65RelayListNote().event as? AdvertisedRelayListEvent
}
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) {
if (!isWriteable()) return
@ -2889,17 +2842,11 @@ class Account(
}
}
fun getFileServersList(): FileServersEvent? {
return getFileServersNote().event as? FileServersEvent
}
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
fun getFileServersListFlow(): StateFlow<NoteState> {
return getFileServersNote().flow().metadata.stateFlow
}
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
fun getFileServersNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
}
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
fun sendFileServersList(servers: List<String>) {
if (!isWriteable()) return
@ -2961,13 +2908,9 @@ class Account(
}
}
fun loadLastRead(route: String): Long {
return lastReadPerRoute[route] ?: 0
}
fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0
fun hasDonatedInThisVersion(): Boolean {
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
}
fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
fun markDonatedInThisVersion() {
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
@ -2988,7 +2931,7 @@ class Account(
it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet()
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
live.invalidateData()
}
}
@ -3009,8 +2952,9 @@ class Account(
}
}
class AccountLiveData(private val account: Account) :
LiveData<AccountState>(AccountState(account)) {
class AccountLiveData(
private val account: Account,
) : LiveData<AccountState>(AccountState(account)) {
// Refreshes observers in batches.
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
@Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) {
class AddressableNote(
val address: ATag,
) : Note(address.toTag()) {
override fun idNote() = address.toNAddr()
override fun toNEvent() = address.toNAddr()
@ -93,9 +95,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
return minOf(publishedAt, lastCreatedAt)
}
fun dTag(): String? {
return (event as? AddressableEvent)?.dTag()
}
fun dTag(): String? = (event as? AddressableEvent)?.dTag()
override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
@ -107,7 +107,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
}
@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.
// They are immutable after that.
var event: EventInterface? = null
@ -163,14 +165,12 @@ open class Note(val idHex: String) {
}
}
fun toNostrUri(): String {
return "nostr:${toNEvent()}"
}
fun toNostrUri(): String = "nostr:${toNEvent()}"
open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? {
return if (
fun channelHex(): HexKey? =
if (
event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
@ -185,7 +185,6 @@ open class Note(val idHex: String) {
} else {
null
}
}
open fun address(): ATag? = null
@ -523,39 +522,30 @@ open class Note(val idHex: String) {
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
}
fun getReactionBy(user: User): String? {
return reactions.firstNotNullOfOrNull {
fun getReactionBy(user: User): String? =
reactions.firstNotNullOfOrNull {
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
it.key
} else {
null
}
}
}
fun isBoostedBy(user: User): Boolean {
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
fun hasReportsBy(user: User): Boolean {
return reports[user]?.isNotEmpty() ?: false
}
fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false
fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.count { it.key.pubkeyHex in users }
}
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
fun reportsBy(users: Set<HexKey>): List<Note> {
return reports
fun reportsBy(users: Set<HexKey>): List<Note> =
reports
.mapNotNull {
if (it.key.pubkeyHex in users) {
it.value
} else {
null
}
}
.flatten()
}
}.flatten()
private fun updateZapTotal() {
var sumOfAmounts = BigDecimal.ZERO
@ -635,8 +625,8 @@ open class Note(val idHex: String) {
)
}
fun hasPledgeBy(user: User): Boolean {
return replies
fun hasPledgeBy(user: User): Boolean =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.any {
val pledgeValue =
@ -650,10 +640,9 @@ open class Note(val idHex: String) {
pledgeValue != null && it.author == user
}
}
fun pledgedAmountByOthers(): BigDecimal {
return replies
fun pledgedAmountByOthers(): BigDecimal =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.mapNotNull {
try {
@ -663,9 +652,7 @@ open class Note(val idHex: String) {
null
// do nothing if it can't convert to bigdecimal
}
}
.sumOf { it }
}
}.sumOf { it }
fun hasAnyReports(): Boolean {
val dayAgo = TimeUtils.oneDayAgo()
@ -676,8 +663,8 @@ open class Note(val idHex: String) {
)
}
fun isNewThread(): Boolean {
return (
fun isNewThread(): Boolean =
(
event is RepostEvent ||
event is GenericRepostEvent ||
replyTo == null ||
@ -685,29 +672,20 @@ open class Note(val idHex: String) {
) &&
event !is ChannelMessageEvent &&
event !is LiveActivitiesChatMessageEvent
}
fun hasZapped(loggedIn: User): Boolean {
return zaps.any { it.key.author == loggedIn }
}
fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
fun hasReacted(
loggedIn: User,
content: String,
): Boolean {
return reactedBy(loggedIn, content).isNotEmpty()
}
): Boolean = reactedBy(loggedIn, content).isNotEmpty()
fun reactedBy(
loggedIn: User,
content: String,
): List<Note> {
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
}
): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
fun reactedBy(loggedIn: User): List<String> {
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
}
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
return boosts.firstOrNull {
@ -715,9 +693,7 @@ open class Note(val idHex: String) {
} != null // 5 minute protection
}
fun boostedBy(loggedIn: User): List<Note> {
return boosts.filter { it.author == loggedIn }
}
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
fun moveAllReferencesTo(note: AddressableNote) {
// migrates these comments to a new version
@ -762,33 +738,38 @@ open class Note(val idHex: String) {
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
val thisEvent = event ?: return false
val hash = thisEvent.pubKey().hashCode()
val isBoostedNoteHidden =
if (
thisEvent is GenericRepostEvent ||
thisEvent is RepostEvent ||
thisEvent is CommunityPostApprovalEvent
) {
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else {
false
// if the author is hidden by spam or blocked
if (accountChoices.hiddenUsersHashCodes.contains(hash) ||
accountChoices.spammersHashCodes.contains(hash)
) {
return true
}
// if the post is sensitive and the user doesn't want to see sensitive content
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) {
accountChoices.hiddenWords.any {
thisEvent.content.containsAny(accountChoices.hiddenWordsCase)
}
} else {
false
if (thisEvent is BaseTextNoteEvent) {
if (thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
return true
}
}
val isSensitive = thisEvent.isSensitive()
return isBoostedNoteHidden ||
isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
return false
}
var liveSet: NoteLiveSet? = null
@ -858,13 +839,13 @@ open class Note(val idHex: String) {
}
@Stable
class NoteFlowSet(u: Note) {
class NoteFlowSet(
u: Note,
) {
// Observers line up here.
val metadata = NoteBundledRefresherFlow(u)
fun isInUse(): Boolean {
return metadata.stateFlow.subscriptionCount.value > 0
}
fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0
fun destroy() {
metadata.destroy()
@ -872,7 +853,9 @@ class NoteFlowSet(u: Note) {
}
@Stable
class NoteLiveSet(u: Note) {
class NoteLiveSet(
u: Note,
) {
// Observers line up here.
val innerMetadata = NoteBundledRefresherLiveData(u)
val innerReactions = NoteBundledRefresherLiveData(u)
@ -901,8 +884,7 @@ class NoteLiveSet(u: Note) {
?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}
.distinctUntilChanged()
}.distinctUntilChanged()
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
@ -912,8 +894,7 @@ class NoteLiveSet(u: Note) {
var total = 0
it.note.reactions.forEach { total += it.value.size }
total
}
.distinctUntilChanged()
}.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() ?: "" }
fun isInUse(): Boolean {
return metadata.hasObservers() ||
fun isInUse(): Boolean =
metadata.hasObservers() ||
reactions.hasObservers() ||
boosts.hasObservers() ||
replies.hasObservers() ||
@ -936,7 +917,6 @@ class NoteLiveSet(u: Note) {
boostCount.hasObservers() ||
innerOts.hasObservers() ||
innerModifications.hasObservers()
}
fun destroy() {
innerMetadata.destroy()
@ -952,7 +932,9 @@ class NoteLiveSet(u: Note) {
}
@Stable
class NoteBundledRefresherFlow(val note: Note) {
class NoteBundledRefresherFlow(
val note: Note,
) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(NoteState(note))
@ -973,7 +955,9 @@ class NoteBundledRefresherFlow(val note: Note) {
}
@Stable
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
class NoteBundledRefresherLiveData(
val note: Note,
) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
@ -1000,7 +984,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
}
@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() {
super.onActive()
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 {
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.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -64,12 +62,7 @@ fun WatchBlockAndReport(
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by remember(note) {
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val isHiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle()
val showAnyway =
remember {

View File

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

View File

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

View File

@ -59,14 +59,13 @@ open class Event(
) : EventInterface {
override fun isContentEncoded() = false
override fun countMemory(): Long {
return 12L +
override fun countMemory(): Long =
12L +
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() +
sig.bytesUsedInMemory()
}
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 zapSplitSetup(): List<ZapSplitSetup> {
return tags
override fun zapSplitSetup(): List<ZapSplitSetup> =
tags
.filter { it.size > 1 && it[0] == "zap" }
.mapNotNull {
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
@ -170,7 +169,6 @@ open class Event(
null
}
}
}
override fun taggedAddresses() =
tags
@ -272,29 +270,23 @@ open class Event(
return rank
}
override fun getGeoHash(): String? {
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
override fun getReward(): BigDecimal? {
return try {
override fun getReward(): BigDecimal? =
try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
} catch (e: Exception) {
null
}
}
open fun toNIP19(): String {
return if (this is AddressableEvent) {
open fun toNIP19(): String =
if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr()
} else {
Nip19Bech32.createNEvent(id, pubKey, kind, null)
}
}
fun toNostrUri(): String {
return "nostr:${toNIP19()}"
}
fun toNostrUri(): String = "nostr:${toNIP19()}"
fun hasCorrectIDHash(): Boolean {
if (id.isEmpty()) return false
@ -320,8 +312,7 @@ open class Event(
| Event: ${toJson()}
| Actual ID: $id
| Generated: ${generateId()}
"""
.trimIndent(),
""".trimIndent(),
)
}
if (!hasVerifiedSignature()) {
@ -329,22 +320,17 @@ open class Event(
}
}
override fun hasValidSignature(): Boolean {
return try {
override fun hasValidSignature(): Boolean =
try {
hasCorrectIDHash() && hasVerifiedSignature()
} catch (e: Exception) {
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false
}
}
fun makeJsonForId(): String {
return makeJsonForId(pubKey, createdAt, kind, tags, content)
}
fun makeJsonForId(): String = makeJsonForId(pubKey, createdAt, kind, tags, content)
fun generateId(): String {
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
}
fun generateId(): String = CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
fun generateId2(): String {
val sha256 = MessageDigest.getInstance("SHA-256")
@ -356,9 +342,7 @@ open class Event(
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Event {
return fromJson(jp.codec.readTree(jp))
}
): Event = fromJson(jp.codec.readTree(jp))
}
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
@ -460,8 +444,8 @@ open class Event(
.addDeserializer(Request::class.java, RequestDeserializer()),
)
fun fromJson(jsonObject: JsonNode): Event {
return EventFactory.create(
fun fromJson(jsonObject: JsonNode): Event =
EventFactory.create(
id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(),
@ -473,11 +457,8 @@ open class Event(
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText(),
)
}
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> {
return Array(size()) { transform(get(it)) }
}
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> = Array(size()) { transform(get(it)) }
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
@ -518,9 +499,7 @@ open class Event(
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray {
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
}
): ByteArray = CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
fun create(
signer: NostrSigner,
@ -529,9 +508,7 @@ open class Event(
content: String = "",
createdAt: Long = TimeUtils.now(),
onReady: (Event) -> Unit,
) {
return signer.sign(createdAt, kind, tags, content, onReady)
}
) = signer.sign(createdAt, kind, tags, content, onReady)
}
}
@ -572,7 +549,8 @@ open class BaseAddressableEvent(
tags: Array<Array<String>>,
content: String,
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 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.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
@Immutable
class PeopleListEvent(
@ -71,9 +70,9 @@ class PeopleListEvent(
}
@Immutable
data class UsersAndWords(
val users: ImmutableSet<String> = persistentSetOf(),
val words: ImmutableSet<String> = persistentSetOf(),
class UsersAndWords(
val users: Set<String> = setOf(),
val words: Set<String> = setOf(),
)
fun publicAndPrivateUsersAndWords(
@ -120,9 +119,7 @@ class PeopleListEvent(
const val BLOCK_LIST_D_TAG = "mute"
const val ALT = "List of people"
fun blockListFor(pubKeyHex: HexKey): String {
return "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
}
fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
fun createListWithTag(
name: String,
@ -161,9 +158,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
fun createListWithWord(
name: String,
@ -172,9 +167,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
}
) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
fun addUsers(
earlierVersion: PeopleListEvent,
@ -223,9 +216,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
}
) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
fun addUser(
earlierVersion: PeopleListEvent,
@ -234,9 +225,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
fun addTag(
earlierVersion: PeopleListEvent,
@ -284,9 +273,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
}
) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
fun removeUser(
earlierVersion: PeopleListEvent,
@ -295,9 +282,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
}
) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
fun removeTag(
earlierVersion: PeopleListEvent,