mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-29 00:10:45 +00: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.NostrSignerExternal
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
import com.vitorpamplona.quartz.utils.DualCase
|
import com.vitorpamplona.quartz.utils.DualCase
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -206,7 +202,7 @@ class Account(
|
|||||||
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||||
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
||||||
) {
|
) {
|
||||||
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
var transientHiddenUsers: Set<String> = setOf()
|
||||||
|
|
||||||
data class PaymentRequest(
|
data class PaymentRequest(
|
||||||
val relayUrl: String,
|
val relayUrl: String,
|
||||||
@ -223,13 +219,16 @@ class Account(
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class LiveFollowLists(
|
class LiveFollowLists(
|
||||||
val users: ImmutableSet<String> = persistentSetOf(),
|
val users: Set<String> = emptySet(),
|
||||||
val hashtags: ImmutableSet<String> = persistentSetOf(),
|
val hashtags: Set<String> = emptySet(),
|
||||||
val geotags: ImmutableSet<String> = persistentSetOf(),
|
val geotags: Set<String> = emptySet(),
|
||||||
val communities: ImmutableSet<String> = persistentSetOf(),
|
val communities: Set<String> = emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class ListNameNotePair(val listName: String, val event: GeneralListEvent?)
|
class ListNameNotePair(
|
||||||
|
val listName: String,
|
||||||
|
val event: GeneralListEvent?,
|
||||||
|
)
|
||||||
|
|
||||||
val connectToRelaysFlow =
|
val connectToRelaysFlow =
|
||||||
combineTransform(
|
combineTransform(
|
||||||
@ -361,10 +360,10 @@ class Account(
|
|||||||
userProfile().flow().follows.stateFlow.transformLatest {
|
userProfile().flow().follows.stateFlow.transformLatest {
|
||||||
emit(
|
emit(
|
||||||
LiveFollowLists(
|
LiveFollowLists(
|
||||||
it.user.cachedFollowingKeySet().toImmutableSet(),
|
it.user.cachedFollowingKeySet(),
|
||||||
it.user.cachedFollowingTagSet().toImmutableSet(),
|
it.user.cachedFollowingTagSet(),
|
||||||
it.user.cachedFollowingGeohashSet().toImmutableSet(),
|
it.user.cachedFollowingGeohashSet(),
|
||||||
it.user.cachedFollowingCommunitiesSet().toImmutableSet(),
|
it.user.cachedFollowingCommunitiesSet(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -379,8 +378,8 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> {
|
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
|
||||||
return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||||
note?.flow()?.metadata?.stateFlow?.mapLatest {
|
note?.flow()?.metadata?.stateFlow?.mapLatest {
|
||||||
val noteEvent = it.note.event as? GeneralListEvent
|
val noteEvent = it.note.event as? GeneralListEvent
|
||||||
@ -389,13 +388,12 @@ class Account(
|
|||||||
} else {
|
} else {
|
||||||
MutableStateFlow(ListNameNotePair(listName, null))
|
MutableStateFlow(ListNameNotePair(listName, null))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun combinePeopleListFlows(
|
fun combinePeopleListFlows(
|
||||||
kind3FollowsSource: Flow<LiveFollowLists>,
|
kind3FollowsSource: Flow<LiveFollowLists>,
|
||||||
peopleListFollowsSource: Flow<ListNameNotePair>,
|
peopleListFollowsSource: Flow<ListNameNotePair>,
|
||||||
): Flow<LiveFollowLists?> {
|
): Flow<LiveFollowLists?> =
|
||||||
return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
||||||
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
|
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
|
||||||
emit(null)
|
emit(null)
|
||||||
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
||||||
@ -411,7 +409,6 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
|
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
|
||||||
@ -465,38 +462,41 @@ class Account(
|
|||||||
onReady(
|
onReady(
|
||||||
LiveFollowLists(
|
LiveFollowLists(
|
||||||
users =
|
users =
|
||||||
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(),
|
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(),
|
||||||
hashtags =
|
hashtags =
|
||||||
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(),
|
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||||
geotags =
|
geotags =
|
||||||
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(),
|
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
||||||
communities =
|
communities =
|
||||||
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
||||||
.map { it.toTag() }
|
.map { it.toTag() }
|
||||||
.toImmutableSet(),
|
.toSet(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? {
|
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
|
||||||
return withTimeoutOrNull(1000) {
|
withTimeoutOrNull(1000) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
decryptLiveFollows(peopleListFollows) {
|
decryptLiveFollows(peopleListFollows) {
|
||||||
continuation.resume(it)
|
continuation.resume(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class LiveHiddenUsers(
|
class LiveHiddenUsers(
|
||||||
val hiddenUsers: ImmutableSet<String>,
|
val hiddenUsers: Set<String>,
|
||||||
val spammers: ImmutableSet<String>,
|
val spammers: Set<String>,
|
||||||
val hiddenWords: ImmutableSet<String>,
|
val hiddenWords: Set<String>,
|
||||||
val hiddenWordsCase: List<DualCase>,
|
|
||||||
val showSensitiveContent: Boolean?,
|
val showSensitiveContent: Boolean?,
|
||||||
)
|
) {
|
||||||
|
// speeds up isHidden calculations
|
||||||
|
val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }
|
||||||
|
val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() }
|
||||||
|
val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }
|
||||||
|
}
|
||||||
|
|
||||||
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
|
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
|
||||||
combineTransform(
|
combineTransform(
|
||||||
@ -530,25 +530,22 @@ class Account(
|
|||||||
|
|
||||||
emit(
|
emit(
|
||||||
LiveHiddenUsers(
|
LiveHiddenUsers(
|
||||||
hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(),
|
hiddenUsers = (resultBlockList.users + resultMuteList.users),
|
||||||
hiddenWords = hiddenWords.toPersistentSet(),
|
hiddenWords = hiddenWords,
|
||||||
hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) },
|
|
||||||
spammers = localLive.account.transientHiddenUsers,
|
spammers = localLive.account.transientHiddenUsers,
|
||||||
showSensitiveContent = localLive.account.showSensitiveContent,
|
showSensitiveContent = localLive.account.showSensitiveContent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}.stateIn(
|
||||||
.stateIn(
|
scope,
|
||||||
scope,
|
SharingStarted.Eagerly,
|
||||||
SharingStarted.Eagerly,
|
LiveHiddenUsers(
|
||||||
LiveHiddenUsers(
|
hiddenUsers = setOf(),
|
||||||
hiddenUsers = persistentSetOf(),
|
hiddenWords = setOf(),
|
||||||
hiddenWords = persistentSetOf(),
|
spammers = transientHiddenUsers,
|
||||||
hiddenWordsCase = emptyList(),
|
showSensitiveContent = showSensitiveContent,
|
||||||
spammers = transientHiddenUsers,
|
),
|
||||||
showSensitiveContent = showSensitiveContent,
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val liveHiddenUsers = flowHiddenUsers.asLiveData()
|
val liveHiddenUsers = flowHiddenUsers.asLiveData()
|
||||||
@ -599,24 +596,21 @@ class Account(
|
|||||||
filterSpamFromStrangers = filterSpam
|
filterSpamFromStrangers = filterSpam
|
||||||
LocalCache.antiSpam.active = filterSpamFromStrangers
|
LocalCache.antiSpam.active = filterSpamFromStrangers
|
||||||
if (!filterSpamFromStrangers) {
|
if (!filterSpamFromStrangers) {
|
||||||
transientHiddenUsers = persistentSetOf()
|
transientHiddenUsers = setOf()
|
||||||
}
|
}
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun userProfile(): User {
|
fun userProfile(): User =
|
||||||
return userProfileCache
|
userProfileCache
|
||||||
?: run {
|
?: run {
|
||||||
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
|
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
|
||||||
userProfileCache = myUser
|
userProfileCache = myUser
|
||||||
myUser
|
myUser
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun isWriteable(): Boolean {
|
fun isWriteable(): Boolean = keyPair.privKey != null || signer is NostrSignerExternal
|
||||||
return keyPair.privKey != null || signer is NostrSignerExternal
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
|
fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -689,24 +683,16 @@ class Account(
|
|||||||
fun reactionTo(
|
fun reactionTo(
|
||||||
note: Note,
|
note: Note,
|
||||||
reaction: String,
|
reaction: String,
|
||||||
): List<Note> {
|
): List<Note> = note.reactedBy(userProfile(), reaction)
|
||||||
return note.reactedBy(userProfile(), reaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasBoosted(note: Note): Boolean {
|
fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty()
|
||||||
return boostsTo(note).isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun boostsTo(note: Note): List<Note> {
|
fun boostsTo(note: Note): List<Note> = note.boostedBy(userProfile())
|
||||||
return note.boostedBy(userProfile())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasReacted(
|
fun hasReacted(
|
||||||
note: Note,
|
note: Note,
|
||||||
reaction: String,
|
reaction: String,
|
||||||
): Boolean {
|
): Boolean = note.hasReacted(userProfile(), reaction)
|
||||||
return note.hasReacted(userProfile(), reaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reactTo(
|
suspend fun reactTo(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -721,7 +707,12 @@ class Account(
|
|||||||
|
|
||||||
if (note.event is ChatMessageEvent) {
|
if (note.event is ChatMessageEvent) {
|
||||||
val event = note.event as ChatMessageEvent
|
val event = note.event as ChatMessageEvent
|
||||||
val users = event.recipientsPubKey().plus(event.pubKey).toSet().toList()
|
val users =
|
||||||
|
event
|
||||||
|
.recipientsPubKey()
|
||||||
|
.plus(event.pubKey)
|
||||||
|
.toSet()
|
||||||
|
.toList()
|
||||||
|
|
||||||
if (reaction.startsWith(":")) {
|
if (reaction.startsWith(":")) {
|
||||||
val emojiUrl = EmojiUrl.decode(reaction)
|
val emojiUrl = EmojiUrl.decode(reaction)
|
||||||
@ -801,24 +792,23 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getReceivingRelays(): Set<String> {
|
fun getReceivingRelays(): Set<String> =
|
||||||
return getNIP65RelayList()?.readRelays()?.toSet()
|
getNIP65RelayList()?.readRelays()?.toSet()
|
||||||
?: userProfile().latestContactList?.relays()?.filter { it.value.read }?.keys?.ifEmpty { null }
|
?: userProfile()
|
||||||
|
.latestContactList
|
||||||
|
?.relays()
|
||||||
|
?.filter { it.value.read }
|
||||||
|
?.keys
|
||||||
|
?.ifEmpty { null }
|
||||||
?: localRelays.filter { it.read }.map { it.url }.toSet()
|
?: localRelays.filter { it.read }.map { it.url }.toSet()
|
||||||
}
|
|
||||||
|
|
||||||
fun hasWalletConnectSetup(): Boolean {
|
fun hasWalletConnectSetup(): Boolean = zapPaymentRequest != null
|
||||||
return zapPaymentRequest != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNIP47Author(pubkeyHex: String?): Boolean {
|
fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex)
|
||||||
return (getNIP47Signer().pubKey == pubkeyHex)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNIP47Signer(): NostrSigner {
|
fun getNIP47Signer(): NostrSigner =
|
||||||
return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
|
zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
|
||||||
?: signer
|
?: signer
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptZapPaymentResponseEvent(
|
fun decryptZapPaymentResponseEvent(
|
||||||
zapResponseEvent: LnZapPaymentResponseEvent,
|
zapResponseEvent: LnZapPaymentResponseEvent,
|
||||||
@ -885,7 +875,11 @@ class Account(
|
|||||||
) {
|
) {
|
||||||
LnZapRequestEvent.create(
|
LnZapRequestEvent.create(
|
||||||
userPubKeyHex,
|
userPubKeyHex,
|
||||||
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
|
userProfile()
|
||||||
|
.latestContactList
|
||||||
|
?.relays()
|
||||||
|
?.keys
|
||||||
|
?.ifEmpty { null }
|
||||||
?: localRelays.map { it.url }.toSet(),
|
?: localRelays.map { it.url }.toSet(),
|
||||||
signer,
|
signer,
|
||||||
message,
|
message,
|
||||||
@ -2010,7 +2004,8 @@ class Account(
|
|||||||
if (receiver != null) {
|
if (receiver != null) {
|
||||||
val relayList =
|
val relayList =
|
||||||
(
|
(
|
||||||
LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
|
LocalCache
|
||||||
|
.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
|
||||||
?.event as? ChatMessageRelayListEvent
|
?.event as? ChatMessageRelayListEvent
|
||||||
)?.relays()?.ifEmpty { null }
|
)?.relays()?.ifEmpty { null }
|
||||||
|
|
||||||
@ -2201,9 +2196,7 @@ class Account(
|
|||||||
relay: Relay,
|
relay: Relay,
|
||||||
challenge: String,
|
challenge: String,
|
||||||
onReady: (RelayAuthEvent) -> Unit,
|
onReady: (RelayAuthEvent) -> Unit,
|
||||||
) {
|
) = createAuthEvent(relay.url, challenge, onReady = onReady)
|
||||||
return createAuthEvent(relay.url, challenge, onReady = onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createAuthEvent(
|
fun createAuthEvent(
|
||||||
relayUrl: String,
|
relayUrl: String,
|
||||||
@ -2271,13 +2264,9 @@ class Account(
|
|||||||
return LocalCache.getOrCreateAddressableNote(aTag)
|
return LocalCache.getOrCreateAddressableNote(aTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBlockList(): PeopleListEvent? {
|
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
|
||||||
return getBlockListNote().event as? PeopleListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMuteList(): MuteListEvent? {
|
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
|
||||||
return getMuteListNote().event as? MuteListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideWord(word: String) {
|
fun hideWord(word: String) {
|
||||||
val muteList = getMuteList()
|
val muteList = getMuteList()
|
||||||
@ -2388,7 +2377,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet()
|
transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
@ -2509,9 +2498,7 @@ class Account(
|
|||||||
return event.cachedGossip(signer, onReady)
|
return event.cachedGossip(signer, onReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedDecryptContent(note: Note): String? {
|
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
|
||||||
return cachedDecryptContent(note.event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cachedDecryptContent(event: EventInterface?): String? {
|
fun cachedDecryptContent(event: EventInterface?): String? {
|
||||||
if (event == null) return null
|
if (event == null) return null
|
||||||
@ -2591,9 +2578,7 @@ class Account(
|
|||||||
fun preferenceBetween(
|
fun preferenceBetween(
|
||||||
source: String,
|
source: String,
|
||||||
target: String,
|
target: String,
|
||||||
): String? {
|
): String? = languagePreferences.get("$source,$target")
|
||||||
return languagePreferences.get("$source,$target")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateContactListTo(newContactList: ContactListEvent?) {
|
private fun updateContactListTo(newContactList: ContactListEvent?) {
|
||||||
if (newContactList == null || newContactList.tags.isEmpty()) return
|
if (newContactList == null || newContactList.tags.isEmpty()) return
|
||||||
@ -2625,39 +2610,27 @@ class Account(
|
|||||||
return usersRelayList.toTypedArray()
|
return usersRelayList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertLocalRelays(): Array<RelaySetupInfo> {
|
fun convertLocalRelays(): Array<RelaySetupInfo> = localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
|
||||||
return localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun activeGlobalRelays(): Array<String> {
|
fun activeGlobalRelays(): Array<String> =
|
||||||
return connectToRelays.value
|
connectToRelays.value
|
||||||
.filter { it.feedTypes.contains(FeedType.GLOBAL) }
|
.filter { it.feedTypes.contains(FeedType.GLOBAL) }
|
||||||
.map { it.url }
|
.map { it.url }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
}
|
|
||||||
|
|
||||||
fun activeWriteRelays(): List<RelaySetupInfo> {
|
fun activeWriteRelays(): List<RelaySetupInfo> = connectToRelays.value.filter { it.write }
|
||||||
return connectToRelays.value.filter { it.write }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAllHidden(users: Set<HexKey>): Boolean {
|
fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
|
||||||
return users.all { isHidden(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isHidden(user: User) = isHidden(user.pubkeyHex)
|
fun isHidden(user: User) = isHidden(user.pubkeyHex)
|
||||||
|
|
||||||
fun isHidden(userHex: String): Boolean {
|
fun isHidden(userHex: String): Boolean =
|
||||||
return flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
||||||
flowHiddenUsers.value.spammers.contains(userHex)
|
flowHiddenUsers.value.spammers.contains(userHex)
|
||||||
}
|
|
||||||
|
|
||||||
fun followingKeySet(): Set<HexKey> {
|
fun followingKeySet(): Set<HexKey> = userProfile().cachedFollowingKeySet()
|
||||||
return userProfile().cachedFollowingKeySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun followingTagSet(): Set<HexKey> {
|
fun followingTagSet(): Set<HexKey> = userProfile().cachedFollowingTagSet()
|
||||||
return userProfile().cachedFollowingTagSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAcceptable(user: User): Boolean {
|
fun isAcceptable(user: User): Boolean {
|
||||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||||
@ -2669,11 +2642,14 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!warnAboutPostsWithReports) {
|
if (!warnAboutPostsWithReports) {
|
||||||
return !isHidden(user) && // if user hasn't hided this author
|
return !isHidden(user) &&
|
||||||
|
// if user hasn't hided this author
|
||||||
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
|
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
|
||||||
}
|
}
|
||||||
return !isHidden(user) && // if user hasn't hided this author
|
return !isHidden(user) &&
|
||||||
user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
|
// if user hasn't hided this author
|
||||||
|
user.reportsBy(userProfile()).isEmpty() &&
|
||||||
|
// if user has not reported this post
|
||||||
user.countReportAuthorsBy(followingKeySet()) < 5
|
user.countReportAuthorsBy(followingKeySet()) < 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2681,20 +2657,18 @@ class Account(
|
|||||||
if (!warnAboutPostsWithReports) {
|
if (!warnAboutPostsWithReports) {
|
||||||
return !note.hasReportsBy(userProfile())
|
return !note.hasReportsBy(userProfile())
|
||||||
}
|
}
|
||||||
return !note.hasReportsBy(userProfile()) && // if user has not reported this post
|
return !note.hasReportsBy(userProfile()) &&
|
||||||
|
// if user has not reported this post
|
||||||
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
|
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFollowing(user: User): Boolean {
|
fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
|
||||||
return user.pubkeyHex in followingKeySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isFollowing(user: HexKey): Boolean {
|
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
||||||
return user in followingKeySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAcceptable(note: Note): Boolean {
|
fun isAcceptable(note: Note): Boolean {
|
||||||
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
|
return note.author?.let { isAcceptable(it) } ?: true &&
|
||||||
|
// if user hasn't hided this author
|
||||||
isAcceptableDirect(note) &&
|
isAcceptableDirect(note) &&
|
||||||
(
|
(
|
||||||
(note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
|
(note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
|
||||||
@ -2719,8 +2693,7 @@ class Account(
|
|||||||
note.reportsBy(followsPlusMe) +
|
note.reportsBy(followsPlusMe) +
|
||||||
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
|
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
|
||||||
innerReports
|
innerReports
|
||||||
)
|
).toSet()
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveKind3RelayList(value: List<RelaySetupInfo>) {
|
fun saveKind3RelayList(value: List<RelaySetupInfo>) {
|
||||||
@ -2734,19 +2707,14 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDMRelayListNote(): AddressableNote {
|
fun getDMRelayListNote(): AddressableNote =
|
||||||
return LocalCache.getOrCreateAddressableNote(
|
LocalCache.getOrCreateAddressableNote(
|
||||||
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
|
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun getDMRelayListFlow(): StateFlow<NoteState> {
|
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
|
||||||
return getDMRelayListNote().flow().metadata.stateFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDMRelayList(): ChatMessageRelayListEvent? {
|
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
|
||||||
return getDMRelayListNote().event as? ChatMessageRelayListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveDMRelayList(dmRelays: List<String>) {
|
fun saveDMRelayList(dmRelays: List<String>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -2772,19 +2740,14 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPrivateOutboxRelayListNote(): AddressableNote {
|
fun getPrivateOutboxRelayListNote(): AddressableNote =
|
||||||
return LocalCache.getOrCreateAddressableNote(
|
LocalCache.getOrCreateAddressableNote(
|
||||||
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
|
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> {
|
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
|
||||||
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? {
|
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
||||||
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePrivateOutboxRelayList(relays: List<String>) {
|
fun savePrivateOutboxRelayList(relays: List<String>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -2811,19 +2774,14 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSearchRelayListNote(): AddressableNote {
|
fun getSearchRelayListNote(): AddressableNote =
|
||||||
return LocalCache.getOrCreateAddressableNote(
|
LocalCache.getOrCreateAddressableNote(
|
||||||
SearchRelayListEvent.createAddressATag(signer.pubKey),
|
SearchRelayListEvent.createAddressATag(signer.pubKey),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchRelayListFlow(): StateFlow<NoteState> {
|
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
|
||||||
return getSearchRelayListNote().flow().metadata.stateFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchRelayList(): SearchRelayListEvent? {
|
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
|
||||||
return getSearchRelayListNote().event as? SearchRelayListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveSearchRelayList(searchRelays: List<String>) {
|
fun saveSearchRelayList(searchRelays: List<String>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -2850,19 +2808,14 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNIP65RelayListNote(): AddressableNote {
|
fun getNIP65RelayListNote(): AddressableNote =
|
||||||
return LocalCache.getOrCreateAddressableNote(
|
LocalCache.getOrCreateAddressableNote(
|
||||||
AdvertisedRelayListEvent.createAddressATag(signer.pubKey),
|
AdvertisedRelayListEvent.createAddressATag(signer.pubKey),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun getNIP65RelayListFlow(): StateFlow<NoteState> {
|
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
|
||||||
return getNIP65RelayListNote().flow().metadata.stateFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNIP65RelayList(): AdvertisedRelayListEvent? {
|
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
|
||||||
return getNIP65RelayListNote().event as? AdvertisedRelayListEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) {
|
fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -2889,17 +2842,11 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileServersList(): FileServersEvent? {
|
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
|
||||||
return getFileServersNote().event as? FileServersEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFileServersListFlow(): StateFlow<NoteState> {
|
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
|
||||||
return getFileServersNote().flow().metadata.stateFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFileServersNote(): AddressableNote {
|
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
|
||||||
return LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendFileServersList(servers: List<String>) {
|
fun sendFileServersList(servers: List<String>) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
@ -2961,13 +2908,9 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLastRead(route: String): Long {
|
fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0
|
||||||
return lastReadPerRoute[route] ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasDonatedInThisVersion(): Boolean {
|
fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
|
||||||
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markDonatedInThisVersion() {
|
fun markDonatedInThisVersion() {
|
||||||
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
|
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
|
||||||
@ -2988,7 +2931,7 @@ class Account(
|
|||||||
it.cache.spamMessages.snapshot().values.forEach {
|
it.cache.spamMessages.snapshot().values.forEach {
|
||||||
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
|
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
|
||||||
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
|
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
|
||||||
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet()
|
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3009,8 +2952,9 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountLiveData(private val account: Account) :
|
class AccountLiveData(
|
||||||
LiveData<AccountState>(AccountState(account)) {
|
private val account: Account,
|
||||||
|
) : LiveData<AccountState>(AccountState(account)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Default)
|
private val bundler = BundledUpdate(300, Dispatchers.Default)
|
||||||
|
|
||||||
@ -3027,4 +2971,6 @@ class AccountLiveData(private val account: Account) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable class AccountState(val account: Account)
|
@Immutable class AccountState(
|
||||||
|
val account: Account,
|
||||||
|
)
|
||||||
|
@ -75,7 +75,9 @@ import java.math.BigDecimal
|
|||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
class AddressableNote(
|
||||||
|
val address: ATag,
|
||||||
|
) : Note(address.toTag()) {
|
||||||
override fun idNote() = address.toNAddr()
|
override fun idNote() = address.toNAddr()
|
||||||
|
|
||||||
override fun toNEvent() = address.toNAddr()
|
override fun toNEvent() = address.toNAddr()
|
||||||
@ -93,9 +95,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
|||||||
return minOf(publishedAt, lastCreatedAt)
|
return minOf(publishedAt, lastCreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dTag(): String? {
|
fun dTag(): String? = (event as? AddressableEvent)?.dTag()
|
||||||
return (event as? AddressableEvent)?.dTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun wasOrShouldBeDeletedBy(
|
override fun wasOrShouldBeDeletedBy(
|
||||||
deletionEvents: Set<HexKey>,
|
deletionEvents: Set<HexKey>,
|
||||||
@ -107,7 +107,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
open class Note(val idHex: String) {
|
open class Note(
|
||||||
|
val idHex: String,
|
||||||
|
) {
|
||||||
// These fields are only available after the Text Note event is received.
|
// These fields are only available after the Text Note event is received.
|
||||||
// They are immutable after that.
|
// They are immutable after that.
|
||||||
var event: EventInterface? = null
|
var event: EventInterface? = null
|
||||||
@ -163,14 +165,12 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toNostrUri(): String {
|
fun toNostrUri(): String = "nostr:${toNEvent()}"
|
||||||
return "nostr:${toNEvent()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun idDisplayNote() = idNote().toShortenHex()
|
open fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
|
||||||
fun channelHex(): HexKey? {
|
fun channelHex(): HexKey? =
|
||||||
return if (
|
if (
|
||||||
event is ChannelMessageEvent ||
|
event is ChannelMessageEvent ||
|
||||||
event is ChannelMetadataEvent ||
|
event is ChannelMetadataEvent ||
|
||||||
event is ChannelCreateEvent ||
|
event is ChannelCreateEvent ||
|
||||||
@ -185,7 +185,6 @@ open class Note(val idHex: String) {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
open fun address(): ATag? = null
|
open fun address(): ATag? = null
|
||||||
|
|
||||||
@ -523,39 +522,30 @@ open class Note(val idHex: String) {
|
|||||||
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
|
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getReactionBy(user: User): String? {
|
fun getReactionBy(user: User): String? =
|
||||||
return reactions.firstNotNullOfOrNull {
|
reactions.firstNotNullOfOrNull {
|
||||||
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
|
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
|
||||||
it.key
|
it.key
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun isBoostedBy(user: User): Boolean {
|
fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||||
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasReportsBy(user: User): Boolean {
|
fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false
|
||||||
return reports[user]?.isNotEmpty() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
|
||||||
return reports.count { it.key.pubkeyHex in users }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
fun reportsBy(users: Set<HexKey>): List<Note> =
|
||||||
return reports
|
reports
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
if (it.key.pubkeyHex in users) {
|
if (it.key.pubkeyHex in users) {
|
||||||
it.value
|
it.value
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}.flatten()
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateZapTotal() {
|
private fun updateZapTotal() {
|
||||||
var sumOfAmounts = BigDecimal.ZERO
|
var sumOfAmounts = BigDecimal.ZERO
|
||||||
@ -635,8 +625,8 @@ open class Note(val idHex: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasPledgeBy(user: User): Boolean {
|
fun hasPledgeBy(user: User): Boolean =
|
||||||
return replies
|
replies
|
||||||
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||||
.any {
|
.any {
|
||||||
val pledgeValue =
|
val pledgeValue =
|
||||||
@ -650,10 +640,9 @@ open class Note(val idHex: String) {
|
|||||||
|
|
||||||
pledgeValue != null && it.author == user
|
pledgeValue != null && it.author == user
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun pledgedAmountByOthers(): BigDecimal {
|
fun pledgedAmountByOthers(): BigDecimal =
|
||||||
return replies
|
replies
|
||||||
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
try {
|
try {
|
||||||
@ -663,9 +652,7 @@ open class Note(val idHex: String) {
|
|||||||
null
|
null
|
||||||
// do nothing if it can't convert to bigdecimal
|
// do nothing if it can't convert to bigdecimal
|
||||||
}
|
}
|
||||||
}
|
}.sumOf { it }
|
||||||
.sumOf { it }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasAnyReports(): Boolean {
|
fun hasAnyReports(): Boolean {
|
||||||
val dayAgo = TimeUtils.oneDayAgo()
|
val dayAgo = TimeUtils.oneDayAgo()
|
||||||
@ -676,8 +663,8 @@ open class Note(val idHex: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNewThread(): Boolean {
|
fun isNewThread(): Boolean =
|
||||||
return (
|
(
|
||||||
event is RepostEvent ||
|
event is RepostEvent ||
|
||||||
event is GenericRepostEvent ||
|
event is GenericRepostEvent ||
|
||||||
replyTo == null ||
|
replyTo == null ||
|
||||||
@ -685,29 +672,20 @@ open class Note(val idHex: String) {
|
|||||||
) &&
|
) &&
|
||||||
event !is ChannelMessageEvent &&
|
event !is ChannelMessageEvent &&
|
||||||
event !is LiveActivitiesChatMessageEvent
|
event !is LiveActivitiesChatMessageEvent
|
||||||
}
|
|
||||||
|
|
||||||
fun hasZapped(loggedIn: User): Boolean {
|
fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
|
||||||
return zaps.any { it.key.author == loggedIn }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasReacted(
|
fun hasReacted(
|
||||||
loggedIn: User,
|
loggedIn: User,
|
||||||
content: String,
|
content: String,
|
||||||
): Boolean {
|
): Boolean = reactedBy(loggedIn, content).isNotEmpty()
|
||||||
return reactedBy(loggedIn, content).isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reactedBy(
|
fun reactedBy(
|
||||||
loggedIn: User,
|
loggedIn: User,
|
||||||
content: String,
|
content: String,
|
||||||
): List<Note> {
|
): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
|
||||||
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reactedBy(loggedIn: User): List<String> {
|
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||||
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||||
return boosts.firstOrNull {
|
return boosts.firstOrNull {
|
||||||
@ -715,9 +693,7 @@ open class Note(val idHex: String) {
|
|||||||
} != null // 5 minute protection
|
} != null // 5 minute protection
|
||||||
}
|
}
|
||||||
|
|
||||||
fun boostedBy(loggedIn: User): List<Note> {
|
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
|
||||||
return boosts.filter { it.author == loggedIn }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun moveAllReferencesTo(note: AddressableNote) {
|
fun moveAllReferencesTo(note: AddressableNote) {
|
||||||
// migrates these comments to a new version
|
// migrates these comments to a new version
|
||||||
@ -762,33 +738,38 @@ open class Note(val idHex: String) {
|
|||||||
|
|
||||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||||
val thisEvent = event ?: return false
|
val thisEvent = event ?: return false
|
||||||
|
val hash = thisEvent.pubKey().hashCode()
|
||||||
|
|
||||||
val isBoostedNoteHidden =
|
// if the author is hidden by spam or blocked
|
||||||
if (
|
if (accountChoices.hiddenUsersHashCodes.contains(hash) ||
|
||||||
thisEvent is GenericRepostEvent ||
|
accountChoices.spammersHashCodes.contains(hash)
|
||||||
thisEvent is RepostEvent ||
|
) {
|
||||||
thisEvent is CommunityPostApprovalEvent
|
return true
|
||||||
) {
|
}
|
||||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
|
||||||
} else {
|
// if the post is sensitive and the user doesn't want to see sensitive content
|
||||||
false
|
if (accountChoices.showSensitiveContent == false && thisEvent.isSensitive()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a repost, consider the inner event.
|
||||||
|
if (
|
||||||
|
thisEvent is GenericRepostEvent ||
|
||||||
|
thisEvent is RepostEvent ||
|
||||||
|
thisEvent is CommunityPostApprovalEvent
|
||||||
|
) {
|
||||||
|
if (replyTo?.lastOrNull()?.isHiddenFor(accountChoices) == true) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isHiddenByWord =
|
if (thisEvent is BaseTextNoteEvent) {
|
||||||
if (thisEvent is BaseTextNoteEvent) {
|
if (thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
|
||||||
accountChoices.hiddenWords.any {
|
return true
|
||||||
thisEvent.content.containsAny(accountChoices.hiddenWordsCase)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isSensitive = thisEvent.isSensitive()
|
return false
|
||||||
return isBoostedNoteHidden ||
|
|
||||||
isHiddenByWord ||
|
|
||||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
|
||||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
|
||||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveSet: NoteLiveSet? = null
|
var liveSet: NoteLiveSet? = null
|
||||||
@ -858,13 +839,13 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NoteFlowSet(u: Note) {
|
class NoteFlowSet(
|
||||||
|
u: Note,
|
||||||
|
) {
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val metadata = NoteBundledRefresherFlow(u)
|
val metadata = NoteBundledRefresherFlow(u)
|
||||||
|
|
||||||
fun isInUse(): Boolean {
|
fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0
|
||||||
return metadata.stateFlow.subscriptionCount.value > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
metadata.destroy()
|
metadata.destroy()
|
||||||
@ -872,7 +853,9 @@ class NoteFlowSet(u: Note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NoteLiveSet(u: Note) {
|
class NoteLiveSet(
|
||||||
|
u: Note,
|
||||||
|
) {
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val innerMetadata = NoteBundledRefresherLiveData(u)
|
val innerMetadata = NoteBundledRefresherLiveData(u)
|
||||||
val innerReactions = NoteBundledRefresherLiveData(u)
|
val innerReactions = NoteBundledRefresherLiveData(u)
|
||||||
@ -901,8 +884,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
?: false ||
|
?: false ||
|
||||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||||
}
|
}.distinctUntilChanged()
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
|
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
|
||||||
|
|
||||||
@ -912,8 +894,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
var total = 0
|
var total = 0
|
||||||
it.note.reactions.forEach { total += it.value.size }
|
it.note.reactions.forEach { total += it.value.size }
|
||||||
total
|
total
|
||||||
}
|
}.distinctUntilChanged()
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
|
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
|
||||||
|
|
||||||
@ -921,8 +902,8 @@ class NoteLiveSet(u: Note) {
|
|||||||
|
|
||||||
val content = innerMetadata.map { it.note.event?.content() ?: "" }
|
val content = innerMetadata.map { it.note.event?.content() ?: "" }
|
||||||
|
|
||||||
fun isInUse(): Boolean {
|
fun isInUse(): Boolean =
|
||||||
return metadata.hasObservers() ||
|
metadata.hasObservers() ||
|
||||||
reactions.hasObservers() ||
|
reactions.hasObservers() ||
|
||||||
boosts.hasObservers() ||
|
boosts.hasObservers() ||
|
||||||
replies.hasObservers() ||
|
replies.hasObservers() ||
|
||||||
@ -936,7 +917,6 @@ class NoteLiveSet(u: Note) {
|
|||||||
boostCount.hasObservers() ||
|
boostCount.hasObservers() ||
|
||||||
innerOts.hasObservers() ||
|
innerOts.hasObservers() ||
|
||||||
innerModifications.hasObservers()
|
innerModifications.hasObservers()
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
innerMetadata.destroy()
|
innerMetadata.destroy()
|
||||||
@ -952,7 +932,9 @@ class NoteLiveSet(u: Note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NoteBundledRefresherFlow(val note: Note) {
|
class NoteBundledRefresherFlow(
|
||||||
|
val note: Note,
|
||||||
|
) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||||
val stateFlow = MutableStateFlow(NoteState(note))
|
val stateFlow = MutableStateFlow(NoteState(note))
|
||||||
@ -973,7 +955,9 @@ class NoteBundledRefresherFlow(val note: Note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
class NoteBundledRefresherLiveData(
|
||||||
|
val note: Note,
|
||||||
|
) : LiveData<NoteState>(NoteState(note)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||||
|
|
||||||
@ -1000,7 +984,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
class NoteLoadingLiveData<Y>(
|
||||||
|
val note: Note,
|
||||||
|
initialValue: Y?,
|
||||||
|
) : MediatorLiveData<Y>(initialValue) {
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
if (note is AddressableNote) {
|
if (note is AddressableNote) {
|
||||||
@ -1020,7 +1007,9 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable class NoteState(val note: Note)
|
@Immutable class NoteState(
|
||||||
|
val note: Note,
|
||||||
|
)
|
||||||
|
|
||||||
object RelayBriefInfoCache {
|
object RelayBriefInfoCache {
|
||||||
val cache = LruCache<String, RelayBriefInfo?>(50)
|
val cache = LruCache<String, RelayBriefInfo?>(50)
|
||||||
|
@ -30,8 +30,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
|
||||||
import androidx.lifecycle.map
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@ -64,12 +62,7 @@ fun WatchBlockAndReport(
|
|||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isHiddenState by remember(note) {
|
val isHiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle()
|
||||||
accountViewModel.account.liveHiddenUsers
|
|
||||||
.map { note.isHiddenFor(it) }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
|
||||||
.observeAsState(accountViewModel.isNoteHidden(note))
|
|
||||||
|
|
||||||
val showAnyway =
|
val showAnyway =
|
||||||
remember {
|
remember {
|
||||||
|
@ -436,8 +436,7 @@ fun ClickableNote(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = showPopup,
|
onLongClick = showPopup,
|
||||||
)
|
).background(backgroundColor.value)
|
||||||
.background(backgroundColor.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = updatedModifier) { content() }
|
Column(modifier = updatedModifier) { content() }
|
||||||
@ -476,8 +475,11 @@ fun InnerNoteWithReactions(
|
|||||||
|
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
val showSecondRow =
|
val showSecondRow =
|
||||||
baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent &&
|
baseNote.event !is RepostEvent &&
|
||||||
!isBoostedNote && !isQuotedNote && accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED
|
baseNote.event !is GenericRepostEvent &&
|
||||||
|
!isBoostedNote &&
|
||||||
|
!isQuotedNote &&
|
||||||
|
accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED
|
||||||
NoteBody(
|
NoteBody(
|
||||||
baseNote = baseNote,
|
baseNote = baseNote,
|
||||||
showAuthorPicture = isQuotedNote,
|
showAuthorPicture = isQuotedNote,
|
||||||
@ -843,15 +845,14 @@ fun RenderRepost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGradient(backgroundColor: MutableState<Color>): Brush {
|
fun getGradient(backgroundColor: MutableState<Color>): Brush =
|
||||||
return Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors =
|
colors =
|
||||||
listOf(
|
listOf(
|
||||||
backgroundColor.value.copy(alpha = 0f),
|
backgroundColor.value.copy(alpha = 0f),
|
||||||
backgroundColor.value,
|
backgroundColor.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReplyNoteComposition(
|
fun ReplyNoteComposition(
|
||||||
@ -1126,7 +1127,10 @@ private fun ChannelNotePicture(
|
|||||||
loadProfilePicture: Boolean,
|
loadProfilePicture: Boolean,
|
||||||
) {
|
) {
|
||||||
val model by
|
val model by
|
||||||
baseChannel.live.map { it.channel.profilePicture() }.distinctUntilChanged().observeAsState()
|
baseChannel.live
|
||||||
|
.map { it.channel.profilePicture() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeAsState()
|
||||||
|
|
||||||
Box(Size30Modifier) {
|
Box(Size30Modifier) {
|
||||||
RobohashFallbackAsyncImage(
|
RobohashFallbackAsyncImage(
|
||||||
|
@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@ -106,7 +107,11 @@ import kotlinx.coroutines.channels.BufferOverflow
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@ -116,9 +121,12 @@ import java.util.Locale
|
|||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.time.measureTimedValue
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
@Immutable open class ToastMsg()
|
@Immutable open class ToastMsg
|
||||||
|
|
||||||
@Immutable class StringToastMsg(val title: String, val msg: String) : ToastMsg()
|
@Immutable class StringToastMsg(
|
||||||
|
val title: String,
|
||||||
|
val msg: String,
|
||||||
|
) : ToastMsg()
|
||||||
|
|
||||||
@Immutable class ResourceToastMsg(
|
@Immutable class ResourceToastMsg(
|
||||||
val titleResId: Int,
|
val titleResId: Int,
|
||||||
@ -126,16 +134,34 @@ import kotlin.time.measureTimedValue
|
|||||||
val params: Array<out String>? = null,
|
val params: Array<out String>? = null,
|
||||||
) : ToastMsg()
|
) : ToastMsg()
|
||||||
|
|
||||||
@Immutable class ThrowableToastMsg(val titleResId: Int, val msg: String? = null, val throwable: Throwable) : ToastMsg()
|
@Immutable class ThrowableToastMsg(
|
||||||
|
val titleResId: Int,
|
||||||
|
val msg: String? = null,
|
||||||
|
val throwable: Throwable,
|
||||||
|
) : ToastMsg()
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao {
|
class AccountViewModel(
|
||||||
|
val account: Account,
|
||||||
|
val settings: SettingsState,
|
||||||
|
) : ViewModel(),
|
||||||
|
Dao {
|
||||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||||
val accountMarkAsReadUpdates = mutableIntStateOf(0)
|
val accountMarkAsReadUpdates = mutableIntStateOf(0)
|
||||||
|
|
||||||
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
|
val userFollows: LiveData<UserState> =
|
||||||
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
|
account
|
||||||
|
.userProfile()
|
||||||
|
.live()
|
||||||
|
.follows
|
||||||
|
.map { it }
|
||||||
|
val userRelays: LiveData<UserState> =
|
||||||
|
account
|
||||||
|
.userProfile()
|
||||||
|
.live()
|
||||||
|
.relays
|
||||||
|
.map { it }
|
||||||
|
|
||||||
val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
|
val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
|
||||||
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey)
|
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey)
|
||||||
@ -182,13 +208,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) }
|
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isWriteable(): Boolean {
|
fun isWriteable(): Boolean = account.isWriteable()
|
||||||
return account.isWriteable()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun userProfile(): User {
|
fun userProfile(): User = account.userProfile()
|
||||||
return account.userProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reactTo(
|
suspend fun reactTo(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -200,16 +222,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
fun <T : Event> observeByETag(
|
fun <T : Event> observeByETag(
|
||||||
kind: Int,
|
kind: Int,
|
||||||
eTag: HexKey,
|
eTag: HexKey,
|
||||||
): StateFlow<T?> {
|
): StateFlow<T?> = LocalCache.observeETag<T>(kind = kind, eventId = eTag, viewModelScope).latest
|
||||||
return LocalCache.observeETag<T>(kind = kind, eventId = eTag, viewModelScope).latest
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Event> observeByAuthor(
|
fun <T : Event> observeByAuthor(
|
||||||
kind: Int,
|
kind: Int,
|
||||||
pubkeyHex: HexKey,
|
pubkeyHex: HexKey,
|
||||||
): StateFlow<T?> {
|
): StateFlow<T?> = LocalCache.observeAuthor<T>(kind = kind, pubkey = pubkeyHex, viewModelScope).latest
|
||||||
return LocalCache.observeAuthor<T>(kind = kind, pubkey = pubkeyHex, viewModelScope).latest
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reactToOrDelete(
|
fun reactToOrDelete(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -236,16 +254,24 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNoteHidden(note: Note): Boolean {
|
val noteIsHiddenFlows = LruCache<Note, StateFlow<Boolean>>(300)
|
||||||
return note.isHiddenFor(account.flowHiddenUsers.value)
|
|
||||||
}
|
fun createIsHiddenFlow(note: Note): StateFlow<Boolean> =
|
||||||
|
noteIsHiddenFlows.get(note)
|
||||||
|
?: combineTransform(account.flowHiddenUsers, note.flow().metadata.stateFlow) { hiddenUsers, metadata ->
|
||||||
|
emit(metadata.note.isHiddenFor(hiddenUsers))
|
||||||
|
}.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
false,
|
||||||
|
).also {
|
||||||
|
noteIsHiddenFlows.put(note, it)
|
||||||
|
}
|
||||||
|
|
||||||
fun hasReactedTo(
|
fun hasReactedTo(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
reaction: String,
|
reaction: String,
|
||||||
): Boolean {
|
): Boolean = account.hasReacted(baseNote, reaction)
|
||||||
return account.hasReacted(baseNote, reaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteReactionTo(
|
suspend fun deleteReactionTo(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -254,9 +280,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
account.delete(account.reactionTo(note, reaction))
|
account.delete(account.reactionTo(note, reaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasBoosted(baseNote: Note): Boolean {
|
fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote)
|
||||||
return account.hasBoosted(baseNote)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteBoostsTo(note: Note) {
|
fun deleteBoostsTo(note: Note) {
|
||||||
viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) }
|
viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) }
|
||||||
@ -332,15 +356,17 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val initialResults =
|
val initialResults =
|
||||||
zaps.associate {
|
zaps
|
||||||
it.request to
|
.associate {
|
||||||
ZapAmountCommentNotification(
|
it.request to
|
||||||
it.request.author,
|
ZapAmountCommentNotification(
|
||||||
it.request.event?.content()?.ifBlank { null },
|
it.request.author,
|
||||||
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
it.request.event
|
||||||
)
|
?.content()
|
||||||
}
|
?.ifBlank { null },
|
||||||
.toMutableMap()
|
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
||||||
|
)
|
||||||
|
}.toMutableMap()
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<CombinedZap, ZapAmountCommentNotification>(
|
collectSuccessfulSigningOperations<CombinedZap, ZapAmountCommentNotification>(
|
||||||
operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
|
operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
|
||||||
@ -359,8 +385,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedDecryptAmountMessageInGroup(zapNotes: List<CombinedZap>): ImmutableList<ZapAmountCommentNotification> {
|
fun cachedDecryptAmountMessageInGroup(zapNotes: List<CombinedZap>): ImmutableList<ZapAmountCommentNotification> =
|
||||||
return zapNotes
|
zapNotes
|
||||||
.map {
|
.map {
|
||||||
val request = it.request.event as? LnZapRequestEvent
|
val request = it.request.event as? LnZapRequestEvent
|
||||||
if (request?.isPrivateZap() == true) {
|
if (request?.isPrivateZap() == true) {
|
||||||
@ -374,20 +400,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
} else {
|
} else {
|
||||||
ZapAmountCommentNotification(
|
ZapAmountCommentNotification(
|
||||||
it.request.author,
|
it.request.author,
|
||||||
it.request.event?.content()?.ifBlank { null },
|
it.request.event
|
||||||
|
?.content()
|
||||||
|
?.ifBlank { null },
|
||||||
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZapAmountCommentNotification(
|
ZapAmountCommentNotification(
|
||||||
it.request.author,
|
it.request.author,
|
||||||
it.request.event?.content()?.ifBlank { null },
|
it.request.event
|
||||||
|
?.content()
|
||||||
|
?.ifBlank { null },
|
||||||
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
showAmountAxis((it.response.event as? LnZapEvent)?.amount),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}.toImmutableList()
|
||||||
.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList<ZapAmountCommentNotification> {
|
fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList<ZapAmountCommentNotification> {
|
||||||
val myList = baseNote.zaps.toList()
|
val myList = baseNote.zaps.toList()
|
||||||
@ -406,19 +434,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
} else {
|
} else {
|
||||||
ZapAmountCommentNotification(
|
ZapAmountCommentNotification(
|
||||||
it.first.author,
|
it.first.author,
|
||||||
it.first.event?.content()?.ifBlank { null },
|
it.first.event
|
||||||
|
?.content()
|
||||||
|
?.ifBlank { null },
|
||||||
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZapAmountCommentNotification(
|
ZapAmountCommentNotification(
|
||||||
it.first.author,
|
it.first.author,
|
||||||
it.first.event?.content()?.ifBlank { null },
|
it.first.event
|
||||||
|
?.content()
|
||||||
|
?.ifBlank { null },
|
||||||
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}.toImmutableList()
|
||||||
.toImmutableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptAmountMessageInGroup(
|
fun decryptAmountMessageInGroup(
|
||||||
@ -434,11 +465,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
it.first to
|
it.first to
|
||||||
ZapAmountCommentNotification(
|
ZapAmountCommentNotification(
|
||||||
it.first.author,
|
it.first.author,
|
||||||
it.first.event?.content()?.ifBlank { null },
|
it.first.event
|
||||||
|
?.content()
|
||||||
|
?.ifBlank { null },
|
||||||
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
showAmountAxis((it.second?.event as? LnZapEvent)?.amount),
|
||||||
)
|
)
|
||||||
}
|
}.toMutableMap()
|
||||||
.toMutableMap()
|
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
|
collectSuccessfulSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
|
||||||
operationsInput = myList,
|
operationsInput = myList,
|
||||||
@ -588,9 +620,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
account.isInPrivateBookmarks(note, onReady)
|
account.isInPrivateBookmarks(note, onReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInPublicBookmarks(note: Note): Boolean {
|
fun isInPublicBookmarks(note: Note): Boolean = account.isInPublicBookmarks(note)
|
||||||
return account.isInPublicBookmarks(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun broadcast(note: Note) {
|
fun broadcast(note: Note) {
|
||||||
viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) }
|
viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) }
|
||||||
@ -615,13 +645,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { account.delete(note) }
|
viewModelScope.launch(Dispatchers.IO) { account.delete(note) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedDecrypt(note: Note): String? {
|
fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note)
|
||||||
return account.cachedDecryptContent(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cachedDecrypt(event: EventInterface?): String? {
|
fun cachedDecrypt(event: EventInterface?): String? = account.cachedDecryptContent(event)
|
||||||
return account.cachedDecryptContent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decrypt(
|
fun decrypt(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -685,18 +711,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) }
|
viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLoggedUser(user: User?): Boolean {
|
fun isLoggedUser(user: User?): Boolean = account.userProfile().pubkeyHex == user?.pubkeyHex
|
||||||
return account.userProfile().pubkeyHex == user?.pubkeyHex
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isFollowing(user: User?): Boolean {
|
fun isFollowing(user: User?): Boolean {
|
||||||
if (user == null) return false
|
if (user == null) return false
|
||||||
return account.userProfile().isFollowingCached(user)
|
return account.userProfile().isFollowingCached(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFollowing(user: HexKey): Boolean {
|
fun isFollowing(user: HexKey): Boolean = account.userProfile().isFollowingCached(user)
|
||||||
return account.userProfile().isFollowingCached(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hideDeleteRequestDialog: Boolean
|
val hideDeleteRequestDialog: Boolean
|
||||||
get() = account.hideDeleteRequestDialog
|
get() = account.hideDeleteRequestDialog
|
||||||
@ -737,9 +759,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultZapType(): LnZapEvent.ZapType {
|
fun defaultZapType(): LnZapEvent.ZapType = account.defaultZapType
|
||||||
return account.defaultZapType
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class NoteComposeReportState(
|
data class NoteComposeReportState(
|
||||||
@ -898,13 +918,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkGetOrCreateUser(key: HexKey): User? {
|
suspend fun checkGetOrCreateUser(key: HexKey): User? = LocalCache.checkGetOrCreateUser(key)
|
||||||
return LocalCache.checkGetOrCreateUser(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getOrCreateUser(key: HexKey): User {
|
override suspend fun getOrCreateUser(key: HexKey): User = LocalCache.getOrCreateUser(key)
|
||||||
return LocalCache.getOrCreateUser(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkGetOrCreateUser(
|
fun checkGetOrCreateUser(
|
||||||
key: HexKey,
|
key: HexKey,
|
||||||
@ -913,17 +929,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) }
|
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserIfExists(hex: HexKey): User? {
|
fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex)
|
||||||
return LocalCache.getUserIfExists(hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkGetOrCreateNote(key: HexKey): Note? {
|
private suspend fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key)
|
||||||
return LocalCache.checkGetOrCreateNote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getOrCreateNote(key: HexKey): Note {
|
override suspend fun getOrCreateNote(key: HexKey): Note = LocalCache.getOrCreateNote(key)
|
||||||
return LocalCache.getOrCreateNote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkGetOrCreateNote(
|
fun checkGetOrCreateNote(
|
||||||
key: HexKey,
|
key: HexKey,
|
||||||
@ -948,13 +958,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNoteIfExists(hex: HexKey): Note? {
|
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
|
||||||
return LocalCache.getNoteIfExists(hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? {
|
override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? = LocalCache.checkGetOrCreateAddressableNote(key)
|
||||||
return LocalCache.checkGetOrCreateAddressableNote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkGetOrCreateAddressableNote(
|
fun checkGetOrCreateAddressableNote(
|
||||||
key: HexKey,
|
key: HexKey,
|
||||||
@ -963,9 +969,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
|
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
|
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? = LocalCache.getOrCreateAddressableNote(key)
|
||||||
return LocalCache.getOrCreateAddressableNote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOrCreateAddressableNote(
|
fun getOrCreateAddressableNote(
|
||||||
key: ATag,
|
key: ATag,
|
||||||
@ -974,9 +978,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) }
|
viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAddressableNoteIfExists(key: String): AddressableNote? {
|
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
||||||
return LocalCache.getAddressableNoteIfExists(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findStatusesForUser(
|
suspend fun findStatusesForUser(
|
||||||
myUser: User,
|
myUser: User,
|
||||||
@ -1007,9 +1009,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? {
|
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key)
|
||||||
return LocalCache.checkGetOrCreateChannel(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkGetOrCreateChannel(
|
fun checkGetOrCreateChannel(
|
||||||
key: HexKey,
|
key: HexKey,
|
||||||
@ -1018,9 +1018,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) }
|
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelIfExists(hex: HexKey): Channel? {
|
fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex)
|
||||||
return LocalCache.getChannelIfExists(hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadParticipants(
|
fun loadParticipants(
|
||||||
participants: List<Participant>,
|
participants: List<Participant>,
|
||||||
@ -1036,8 +1034,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
it,
|
it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}.toImmutableList()
|
||||||
.toImmutableList()
|
|
||||||
|
|
||||||
onReady(participantUsers)
|
onReady(participantUsers)
|
||||||
}
|
}
|
||||||
@ -1150,10 +1147,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory {
|
class Factory(
|
||||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
val account: Account,
|
||||||
return AccountViewModel(account, settings) as AccountViewModel
|
val settings: SettingsState,
|
||||||
}
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel = AccountViewModel(account, settings) as AccountViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
private var collectorJob: Job? = null
|
private var collectorJob: Job? = null
|
||||||
@ -1404,19 +1402,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRelayListFor(user: User): AdvertisedRelayListEvent? {
|
fun getRelayListFor(user: User): AdvertisedRelayListEvent? = (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?)
|
||||||
return (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRelayListNoteFor(user: User): AddressableNote? {
|
fun getRelayListNoteFor(user: User): AddressableNote? =
|
||||||
return LocalCache.getAddressableNoteIfExists(
|
LocalCache.getAddressableNoteIfExists(
|
||||||
AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex),
|
AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val draftNoteCache = CachedDraftNotes(this)
|
val draftNoteCache = CachedDraftNotes(this)
|
||||||
|
|
||||||
class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
|
class CachedDraftNotes(
|
||||||
|
val accountViewModel: AccountViewModel,
|
||||||
|
) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
|
||||||
override suspend fun compute(
|
override suspend fun compute(
|
||||||
key: DraftEvent,
|
key: DraftEvent,
|
||||||
onReady: (Note?) -> Unit,
|
onReady: (Note?) -> Unit,
|
||||||
@ -1431,9 +1428,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
|
|
||||||
val bechLinkCache = CachedLoadedBechLink(this)
|
val bechLinkCache = CachedLoadedBechLink(this)
|
||||||
|
|
||||||
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
|
class CachedLoadedBechLink(
|
||||||
override suspend fun compute(key: String): LoadedBechLink? {
|
val accountViewModel: AccountViewModel,
|
||||||
return Nip19Bech32.uriToRoute(key)?.let {
|
) : GenericBaseCache<String, LoadedBechLink>(20) {
|
||||||
|
override suspend fun compute(key: String): LoadedBechLink? =
|
||||||
|
Nip19Bech32.uriToRoute(key)?.let {
|
||||||
var returningNote: Note? = null
|
var returningNote: Note? = null
|
||||||
|
|
||||||
when (val parsed = it.entity) {
|
when (val parsed = it.entity) {
|
||||||
@ -1460,11 +1459,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
|
|
||||||
LoadedBechLink(returningNote, it)
|
LoadedBechLink(returningNote, it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
|
class HasNotificationDot(
|
||||||
|
bottomNavigationItems: ImmutableList<Route>,
|
||||||
|
) {
|
||||||
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
|
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
@ -1489,7 +1489,10 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19Bech32.ParseReturn)
|
@Immutable data class LoadedBechLink(
|
||||||
|
val baseNote: Note?,
|
||||||
|
val nip19: Nip19Bech32.ParseReturn,
|
||||||
|
)
|
||||||
|
|
||||||
public suspend fun <T, K> collectSuccessfulSigningOperations(
|
public suspend fun <T, K> collectSuccessfulSigningOperations(
|
||||||
operationsInput: List<T>,
|
operationsInput: List<T>,
|
||||||
|
@ -59,14 +59,13 @@ open class Event(
|
|||||||
) : EventInterface {
|
) : EventInterface {
|
||||||
override fun isContentEncoded() = false
|
override fun isContentEncoded() = false
|
||||||
|
|
||||||
override fun countMemory(): Long {
|
override fun countMemory(): Long =
|
||||||
return 12L +
|
12L +
|
||||||
id.bytesUsedInMemory() +
|
id.bytesUsedInMemory() +
|
||||||
pubKey.bytesUsedInMemory() +
|
pubKey.bytesUsedInMemory() +
|
||||||
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
|
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
|
||||||
content.bytesUsedInMemory() +
|
content.bytesUsedInMemory() +
|
||||||
sig.bytesUsedInMemory()
|
sig.bytesUsedInMemory()
|
||||||
}
|
|
||||||
|
|
||||||
override fun id(): HexKey = id
|
override fun id(): HexKey = id
|
||||||
|
|
||||||
@ -152,8 +151,8 @@ open class Event(
|
|||||||
|
|
||||||
override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" }
|
override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" }
|
||||||
|
|
||||||
override fun zapSplitSetup(): List<ZapSplitSetup> {
|
override fun zapSplitSetup(): List<ZapSplitSetup> =
|
||||||
return tags
|
tags
|
||||||
.filter { it.size > 1 && it[0] == "zap" }
|
.filter { it.size > 1 && it[0] == "zap" }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
|
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
|
||||||
@ -170,7 +169,6 @@ open class Event(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun taggedAddresses() =
|
override fun taggedAddresses() =
|
||||||
tags
|
tags
|
||||||
@ -272,29 +270,23 @@ open class Event(
|
|||||||
return rank
|
return rank
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGeoHash(): String? {
|
override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
|
||||||
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getReward(): BigDecimal? {
|
override fun getReward(): BigDecimal? =
|
||||||
return try {
|
try {
|
||||||
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
|
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
open fun toNIP19(): String {
|
open fun toNIP19(): String =
|
||||||
return if (this is AddressableEvent) {
|
if (this is AddressableEvent) {
|
||||||
ATag(kind, pubKey, dTag(), null).toNAddr()
|
ATag(kind, pubKey, dTag(), null).toNAddr()
|
||||||
} else {
|
} else {
|
||||||
Nip19Bech32.createNEvent(id, pubKey, kind, null)
|
Nip19Bech32.createNEvent(id, pubKey, kind, null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun toNostrUri(): String {
|
fun toNostrUri(): String = "nostr:${toNIP19()}"
|
||||||
return "nostr:${toNIP19()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCorrectIDHash(): Boolean {
|
fun hasCorrectIDHash(): Boolean {
|
||||||
if (id.isEmpty()) return false
|
if (id.isEmpty()) return false
|
||||||
@ -320,8 +312,7 @@ open class Event(
|
|||||||
| Event: ${toJson()}
|
| Event: ${toJson()}
|
||||||
| Actual ID: $id
|
| Actual ID: $id
|
||||||
| Generated: ${generateId()}
|
| Generated: ${generateId()}
|
||||||
"""
|
""".trimIndent(),
|
||||||
.trimIndent(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!hasVerifiedSignature()) {
|
if (!hasVerifiedSignature()) {
|
||||||
@ -329,22 +320,17 @@ open class Event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasValidSignature(): Boolean {
|
override fun hasValidSignature(): Boolean =
|
||||||
return try {
|
try {
|
||||||
hasCorrectIDHash() && hasVerifiedSignature()
|
hasCorrectIDHash() && hasVerifiedSignature()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
|
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun makeJsonForId(): String {
|
fun makeJsonForId(): String = makeJsonForId(pubKey, createdAt, kind, tags, content)
|
||||||
return makeJsonForId(pubKey, createdAt, kind, tags, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateId(): String {
|
fun generateId(): String = CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
|
||||||
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateId2(): String {
|
fun generateId2(): String {
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
@ -356,9 +342,7 @@ open class Event(
|
|||||||
override fun deserialize(
|
override fun deserialize(
|
||||||
jp: JsonParser,
|
jp: JsonParser,
|
||||||
ctxt: DeserializationContext,
|
ctxt: DeserializationContext,
|
||||||
): Event {
|
): Event = fromJson(jp.codec.readTree(jp))
|
||||||
return fromJson(jp.codec.readTree(jp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
|
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
|
||||||
@ -460,8 +444,8 @@ open class Event(
|
|||||||
.addDeserializer(Request::class.java, RequestDeserializer()),
|
.addDeserializer(Request::class.java, RequestDeserializer()),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fromJson(jsonObject: JsonNode): Event {
|
fun fromJson(jsonObject: JsonNode): Event =
|
||||||
return EventFactory.create(
|
EventFactory.create(
|
||||||
id = jsonObject.get("id").asText().intern(),
|
id = jsonObject.get("id").asText().intern(),
|
||||||
pubKey = jsonObject.get("pubkey").asText().intern(),
|
pubKey = jsonObject.get("pubkey").asText().intern(),
|
||||||
createdAt = jsonObject.get("created_at").asLong(),
|
createdAt = jsonObject.get("created_at").asLong(),
|
||||||
@ -473,11 +457,8 @@ open class Event(
|
|||||||
content = jsonObject.get("content").asText(),
|
content = jsonObject.get("content").asText(),
|
||||||
sig = jsonObject.get("sig").asText(),
|
sig = jsonObject.get("sig").asText(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> {
|
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> = Array(size()) { transform(get(it)) }
|
||||||
return Array(size()) { transform(get(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
|
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
|
||||||
|
|
||||||
@ -518,9 +499,7 @@ open class Event(
|
|||||||
kind: Int,
|
kind: Int,
|
||||||
tags: Array<Array<String>>,
|
tags: Array<Array<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
): ByteArray {
|
): ByteArray = CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
|
||||||
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
@ -529,9 +508,7 @@ open class Event(
|
|||||||
content: String = "",
|
content: String = "",
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (Event) -> Unit,
|
onReady: (Event) -> Unit,
|
||||||
) {
|
) = signer.sign(createdAt, kind, tags, content, onReady)
|
||||||
return signer.sign(createdAt, kind, tags, content, onReady)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,7 +549,8 @@ open class BaseAddressableEvent(
|
|||||||
tags: Array<Array<String>>,
|
tags: Array<Array<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig),
|
||||||
|
AddressableEvent {
|
||||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||||
|
|
||||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
|||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class PeopleListEvent(
|
class PeopleListEvent(
|
||||||
@ -71,9 +70,9 @@ class PeopleListEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class UsersAndWords(
|
class UsersAndWords(
|
||||||
val users: ImmutableSet<String> = persistentSetOf(),
|
val users: Set<String> = setOf(),
|
||||||
val words: ImmutableSet<String> = persistentSetOf(),
|
val words: Set<String> = setOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun publicAndPrivateUsersAndWords(
|
fun publicAndPrivateUsersAndWords(
|
||||||
@ -120,9 +119,7 @@ class PeopleListEvent(
|
|||||||
const val BLOCK_LIST_D_TAG = "mute"
|
const val BLOCK_LIST_D_TAG = "mute"
|
||||||
const val ALT = "List of people"
|
const val ALT = "List of people"
|
||||||
|
|
||||||
fun blockListFor(pubKeyHex: HexKey): String {
|
fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
|
||||||
return "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createListWithTag(
|
fun createListWithTag(
|
||||||
name: String,
|
name: String,
|
||||||
@ -161,9 +158,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||||
return createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createListWithWord(
|
fun createListWithWord(
|
||||||
name: String,
|
name: String,
|
||||||
@ -172,9 +167,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
|
||||||
return createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addUsers(
|
fun addUsers(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
@ -223,9 +216,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
||||||
return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addUser(
|
fun addUser(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
@ -234,9 +225,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||||
return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addTag(
|
fun addTag(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
@ -284,9 +273,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
||||||
return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeUser(
|
fun removeUser(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
@ -295,9 +282,7 @@ class PeopleListEvent(
|
|||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (PeopleListEvent) -> Unit,
|
onReady: (PeopleListEvent) -> Unit,
|
||||||
) {
|
) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||||
return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeTag(
|
fun removeTag(
|
||||||
earlierVersion: PeopleListEvent,
|
earlierVersion: PeopleListEvent,
|
||||||
|
Loading…
Reference in New Issue
Block a user