mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-10 04:49:25 +02:00
- 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:
parent
a716d13c69
commit
42408978c4
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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>,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user