Merge branch 'vitorpamplona:main' into profiles-list-management

This commit is contained in:
KotlinGeekDev
2025-09-12 22:52:03 +00:00
committed by GitHub
77 changed files with 527 additions and 168 deletions
@@ -69,7 +69,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
import kotlin.run
class AppModules(
val appContext: Context,
@@ -178,9 +177,6 @@ class AppModules(
// Caches all events in Memory
val cache: LocalCache = LocalCache
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
// Provides a relay pool
val client: INostrClient = NostrClient(websocketBuilder, applicationDefaultScope)
@@ -215,6 +211,9 @@ class AppModules(
client = client,
)
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
// and reconnects relays if the configuration has been changed.
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationDefaultScope)
@@ -283,7 +282,8 @@ class AppModules(
fun trim() {
applicationDefaultScope.launch {
trimmingService.run(null, LocalPreferences.allSavedAccounts())
val loggedIn = accountsCache.accounts.value.values
trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts())
}
}
}
@@ -106,6 +106,42 @@ fun debugState(context: Context) {
" / " +
LocalCache.users.size(),
)
Log.d(
"STATE DUMP",
"Public Chat Channels: " +
LocalCache.publicChatChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.publicChatChannels.size() +
" / " +
LocalCache.publicChatChannels.values().sumOf { it.notes.size() },
)
Log.d(
"STATE DUMP",
"Live Chat Channels: " +
LocalCache.liveChatChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.liveChatChannels.size() +
" / " +
LocalCache.liveChatChannels.values().sumOf { it.notes.size() },
)
Log.d(
"STATE DUMP",
"Ephemeral Chat Channels: " +
LocalCache.ephemeralChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.ephemeralChannels.size() +
" / " +
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
)
LocalCache.chatroomList.forEach { key, room ->
Log.d(
"STATE DUMP",
"Private Chats $key: " +
room.rooms.size() +
" / " +
room.rooms.sumOf { key, value -> value.messages.size },
)
}
Log.d(
"STATE DUMP",
"Deletion Events: " +
@@ -128,7 +164,7 @@ fun debugState(context: Context) {
Log.d(
"STATE DUMP",
"Memory used by Events: " +
LocalCache.notes.sumOfLong { _, note -> note.event?.countMemory() ?: 0L } / (1024 * 1024) +
LocalCache.notes.sumOf { _, note -> note.event?.countMemory() ?: 0 } / (1024 * 1024) +
" MB",
)
@@ -137,10 +173,10 @@ fun debugState(context: Context) {
val bytesNotes =
LocalCache.notes
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
val bytesAddressables =
LocalCache.addressables
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
@@ -97,6 +97,9 @@ val GLOBAL_FOLLOWS = " Global "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_FOLLOWS = " All Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_USER_FOLLOWS = " All User Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val AROUND_ME = " Around Me "
@@ -43,7 +43,7 @@ abstract class Channel : NotesGatherer {
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
val current = changesFlow.get()
if (current != null) return current
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
val new = MutableSharedFlow<ListChange<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
changesFlow = WeakReference(new)
return new
}
@@ -96,7 +96,7 @@ abstract class Channel : NotesGatherer {
notes.put(note.idHex, note)
note.addGatherer(this)
if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) {
if ((note.createdAt() ?: 0L) > (lastNote?.createdAt() ?: 0L)) {
lastNote = note
}
@@ -700,7 +700,7 @@ object LocalCache : ILocalCache {
if (isVerified || justVerify(event)) {
val replyTo = computeReplyTo(event)
if (event.createdAt > (note.createdAt() ?: 0)) {
if (event.createdAt > (note.createdAt() ?: 0L)) {
note.loadEvent(event, author, replyTo)
refreshNewNoteObservers(note)
@@ -743,7 +743,7 @@ object LocalCache : ILocalCache {
}
if (isVerified || justVerify(event)) {
if (event.createdAt > (note.createdAt() ?: 0)) {
if (event.createdAt > (note.createdAt() ?: 0L)) {
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
@@ -845,7 +845,7 @@ object LocalCache : ILocalCache {
if (note.event?.id == event.id) return false
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
note.loadEvent(event, author, emptyList())
val channel = getOrCreateLiveChannel(note.address)
@@ -1073,7 +1073,7 @@ object LocalCache : ILocalCache {
// Already processed this event.
if (note.event?.id == event.id) return false
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
note.loadEvent(event, author, emptyList())
author.flowSet?.statuses?.invalidateData()
@@ -1196,7 +1196,7 @@ object LocalCache : ILocalCache {
// Already processed this event.
if (replaceableNote.event?.id == event.id) return isVerified
if (event.createdAt > (replaceableNote.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (replaceableNote.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
// clear index from previous tags
replaceableNote.replyTo?.forEach {
it.removeNote(replaceableNote)
@@ -1257,7 +1257,7 @@ object LocalCache : ILocalCache {
val deleteNoteEvent = deleteNote.event
if (deleteNoteEvent is AddressableEvent) {
val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag())
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) {
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0L) <= event.createdAt) {
// Counts the replies
deleteNote(addressableNote)
@@ -1279,7 +1279,7 @@ object LocalCache : ILocalCache {
.mapNotNull { getAddressableNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0L) <= event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
@@ -746,7 +746,7 @@ open class Note(
val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() ||
(
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null }
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0L) > dayAgo } != null }
?: false
)
}
@@ -778,14 +778,14 @@ open class Note(
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
return boosts.any {
it.author == loggedIn && (it.createdAt() ?: 0) > fiveMinsAgo
it.author == loggedIn && (it.createdAt() ?: 0L) > fiveMinsAgo
}
}
fun hasBoostedInTheLast5Minutes(loggedIn: HexKey): Boolean {
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
return boosts.any {
(it.createdAt() ?: 0) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
(it.createdAt() ?: 0L) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
}
}
@@ -70,14 +70,14 @@ class Chatroom : NotesGatherer {
}
}
val createdAt = msg.createdAt() ?: 0
if (createdAt > (newestMessage?.createdAt() ?: 0)) {
val createdAt = msg.createdAt() ?: 0L
if (createdAt > (newestMessage?.createdAt() ?: 0L)) {
newestMessage = msg
}
val newSubject = msg.event?.subject()
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
if (newSubject != null && (msg.createdAt() ?: 0L) > (subjectCreatedAt ?: 0)) {
subject.tryEmit(newSubject)
subjectCreatedAt = msg.createdAt()
}
@@ -96,7 +96,7 @@ class Chatroom : NotesGatherer {
msg.removeGatherer(this)
if (msg == newestMessage) {
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0 }
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0L }
}
if (msg.event?.subject() == subject.value) {
@@ -127,7 +127,7 @@ class Chatroom : NotesGatherer {
val sorted = messages.sortedWith(DefaultFeedOrder)
val toKeep =
if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
if ((sorted.firstOrNull()?.createdAt() ?: 0L) > TimeUtils.oneWeekAgo()) {
// Recent messages, keep last 100
sorted.take(100).toSet()
} else {
@@ -21,11 +21,13 @@
package com.vitorpamplona.amethyst.model.topNavFeeds
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS
import com.vitorpamplona.amethyst.model.AROUND_ME
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow
@@ -60,6 +62,7 @@ class FeedTopNavFilterState(
when (listName) {
GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays, proxyRelays)
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
ALL_USER_FOLLOWS -> AllUserFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays)
else -> {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
/**
* This is a big OR filter on all fields.
*/
@Immutable
class AllUserFollowsByOutboxTopNavFilter(
val authors: Set<String>,
val defaultRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : IFeedTopNavFilter {
override fun matchAuthor(pubkey: HexKey): Boolean = pubkey in authors
override fun match(noteEvent: Event): Boolean =
when (noteEvent) {
is LiveActivitiesEvent -> {
noteEvent.participantsIntersect(authors)
}
is CommentEvent -> {
// ignore follows and checks only the root scope
noteEvent.pubKey in authors
}
else -> {
noteEvent.pubKey in authors
}
}
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsTopNavPerRelayFilterSet> {
val authorsPerRelay = OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it }
return combine(authorsPerRelay, defaultRelays, blockedRelays) { perRelayAuthors, default, blockedRelays ->
val allRelays = perRelayAuthors.keys.filter { it !in blockedRelays }.ifEmpty { default }
AuthorsTopNavPerRelayFilterSet(
allRelays.associateWith {
AuthorsTopNavPerRelayFilter(
authors = perRelayAuthors[it] ?: emptySet(),
)
},
)
}
}
override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet {
val authorsPerRelay = OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it }
val allRelays = authorsPerRelay.keys.filter { it !in blockedRelays.value }.ifEmpty { defaultRelays.value }
return AuthorsTopNavPerRelayFilterSet(
allRelays.associateWith {
AuthorsTopNavPerRelayFilter(
authors = authorsPerRelay[it] ?: emptySet(),
)
},
)
}
}
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/**
* This is a big OR filter on all fields.
*/
@Immutable
class AllUserFollowsByProxyTopNavFilter(
val authors: Set<String>,
val proxyRelays: Set<NormalizedRelayUrl>,
) : IFeedTopNavFilter {
override fun matchAuthor(pubkey: HexKey): Boolean = pubkey in authors
override fun match(noteEvent: Event): Boolean =
when (noteEvent) {
is LiveActivitiesEvent -> {
noteEvent.participantsIntersect(authors)
}
is CommentEvent -> {
// ignore follows and checks only the root scope
noteEvent.pubKey in authors
}
else -> {
noteEvent.pubKey in authors
}
}
// forces the use of the Proxy on all connections, replacing the outbox model.
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsTopNavPerRelayFilterSet> =
MutableStateFlow(
AuthorsTopNavPerRelayFilterSet(
proxyRelays.associateWith {
AuthorsTopNavPerRelayFilter(
authors = authors,
)
},
),
)
override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet {
// forces the use of the Proxy on all connections, replacing the outbox model.
return AuthorsTopNavPerRelayFilterSet(
proxyRelays.associateWith {
AuthorsTopNavPerRelayFilter(
authors = authors,
)
},
)
}
}
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
class AllUserFollowsFeedFlow(
val allFollows: StateFlow<FollowListState.Kind3Follows?>,
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : IFeedFlowsType {
fun convert(
kind3: FollowListState.Kind3Follows?,
proxyRelays: Set<NormalizedRelayUrl>,
): IFeedTopNavFilter =
if (kind3 != null) {
if (proxyRelays.isEmpty()) {
AllUserFollowsByOutboxTopNavFilter(
authors = kind3.authors,
defaultRelays = followsRelays,
blockedRelays = blockedRelays,
)
} else {
AllUserFollowsByProxyTopNavFilter(
authors = kind3.authors,
proxyRelays = proxyRelays,
)
}
} else {
AllUserFollowsByOutboxTopNavFilter(
authors = emptySet(),
defaultRelays = followsRelays,
blockedRelays = blockedRelays,
)
}
override fun flow() = combine(allFollows, proxyRelays, ::convert)
override fun startValue(): IFeedTopNavFilter = convert(allFollows.value, proxyRelays.value)
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
collector.emit(startValue())
}
}
@@ -33,13 +33,13 @@ class MemoryTrimmingService(
var isTrimmingMemoryMutex = AtomicBoolean(false)
private suspend fun doTrim(
account: Account? = null,
account: Collection<Account>,
otherAccounts: List<AccountInfo>,
) {
cache.cleanMemory()
cache.cleanObservers()
account?.let {
account.forEach {
cache.pruneHiddenEvents(it)
cache.pruneHiddenMessages(it)
}
@@ -53,7 +53,7 @@ class MemoryTrimmingService(
}
suspend fun run(
account: Account?,
account: Collection<Account>,
otherAccounts: List<AccountInfo>,
) {
if (isTrimmingMemoryMutex.compareAndSet(false, true)) {
@@ -72,7 +72,7 @@ class OkHttpWebSocket(
webSocket: okhttp3.WebSocket,
response: Response,
) = out.onOpen(
response.receivedResponseAtMillis - response.sentRequestAtMillis,
(response.receivedResponseAtMillis - response.sentRequestAtMillis).toInt(),
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
)
@@ -76,10 +76,6 @@ fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBase
potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, key.note.address)
}
key.account.searchRelayList.flow.value.forEach { relayUrl ->
add(relayUrl, key.note.address)
}
}
// loads threading that is event-based
@@ -35,7 +35,7 @@ class FrameStat {
kind: Int,
subId: String,
relayUrl: NormalizedRelayUrl,
memory: Long,
memory: Int,
) {
eventCount.incrementAndGet()
@@ -24,22 +24,21 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.utils.LargeCache
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.atomics.ExperimentalAtomicApi
@OptIn(ExperimentalAtomicApi::class)
class KindGroup(
var count: AtomicInteger = AtomicInteger(0),
var memory: AtomicLong = AtomicLong(0L),
var memory: AtomicInteger = AtomicInteger(0),
val subs: LargeCache<String, AtomicInteger> = LargeCache<String, AtomicInteger>(),
val relays: LargeCache<NormalizedRelayUrl, AtomicInteger> = LargeCache<NormalizedRelayUrl, AtomicInteger>(),
) {
companion object {
const val MB: Long = 1024
const val MB: Int = 1024
}
fun increment(
mem: Long,
mem: Int,
subId: String,
relayUrl: NormalizedRelayUrl,
) {
@@ -63,7 +62,7 @@ class KindGroup(
fun reset() {
count.set(0)
memory.set(0L)
memory.set(0)
subs.forEach { key, value -> value.set(0) }
relays.forEach { key, value -> value.set(0) }
}
@@ -195,7 +195,7 @@ fun RenderRelayIcon(
iconUrl: String?,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
pingInMs: Long,
pingInMs: Int,
iconModifier: Modifier = MaterialTheme.colorScheme.relayIconModifier,
) {
val green = MaterialTheme.colorScheme.allGoodColor
@@ -63,7 +63,7 @@ fun NormalTimeAgo(
val nowStr = stringRes(id = R.string.now)
val time by
remember(baseNote) { derivedStateOf { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } }
remember(baseNote) { derivedStateOf { timeAgoShort(baseNote.createdAt() ?: 0L, nowStr) } }
Text(
text = time,
@@ -104,7 +104,6 @@ fun RenderTextEvent(
note,
accountViewModel,
) { body ->
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
val newBody =
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded)
@@ -118,10 +117,13 @@ fun RenderTextEvent(
}
val eventContent =
if (!subject.isNullOrBlank() && !newBody.startsWith(subject)) {
"### $subject\n$newBody"
} else {
newBody
remember(newBody) {
val subject = (note.event as? TextNoteEvent)?.subject()?.ifBlank { null }
if (!subject.isNullOrBlank() && !newBody.contains(subject, ignoreCase = true)) {
"$subject\n\n$newBody"
} else {
newBody
}
}
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS
import com.vitorpamplona.amethyst.model.AROUND_ME
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
@@ -82,6 +83,15 @@ class FollowListState(
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
)
val kind3FollowUsers =
PeopleListOutBoxFeedDefinition(
code = ALL_USER_FOLLOWS,
name = ResourceName(R.string.follow_list_kind3follows_users_only),
type = CodeNameType.HARDCODED,
kinds = DEFAULT_FEED_KINDS,
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
)
val globalFollow =
GlobalFeedDefinition(
code = GLOBAL_FOLLOWS,
@@ -107,7 +117,7 @@ class FollowListState(
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
)
val defaultLists = persistentListOf(kind3Follow, aroundMe, globalFollow, muteListFollow)
val defaultLists = persistentListOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow, muteListFollow)
fun getPeopleLists(): List<FeedDefinition> =
account
@@ -218,7 +228,7 @@ class FollowListState(
checkNotInMainThread()
emit(
listOf(
listOf(kind3Follow, aroundMe, globalFollow),
listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow),
myLivePeopleListsFlow,
myLiveKind3FollowsFlow,
listOf(muteListFollow),
@@ -234,7 +244,7 @@ class FollowListState(
checkNotInMainThread()
emit(
listOf(
listOf(kind3Follow, aroundMe, globalFollow),
listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow),
myLivePeopleListsFlow,
listOf(muteListFollow),
).flatten().toImmutableList(),
@@ -1084,6 +1084,13 @@ class AccountViewModel(
account.markAsRead("Channel/${noteEvent.channelId()}", noteEvent.createdAt)
} else if (noteEvent is ChatroomKeyable) {
account.markAsRead("Room/${noteEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
} else if (noteEvent is DraftWrapEvent) {
val innerEvent = account.draftsDecryptionCache.preCachedDraft(noteEvent)
if (innerEvent is IsInPublicChatChannel) {
account.markAsRead("Channel/${innerEvent.channelId()}", noteEvent.createdAt)
} else if (innerEvent is ChatroomKeyable) {
account.markAsRead("Room/${innerEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
}
}
}
}
@@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun ChatTimeAgo(baseNote: Note) {
val nowStr = stringRes(id = R.string.now)
val time = remember(baseNote) { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) }
val time = remember(baseNote) { timeAgoShort(baseNote.createdAt() ?: 0L, nowStr) }
Text(
text = time,
@@ -105,7 +105,7 @@ class ChatroomListKnownFeedFilter(
val channelId = (oldNote.event as? ChannelMessageEvent)?.channelId()
if (newNotePair.key == channelId) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) {
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
@@ -121,7 +121,7 @@ class ChatroomListKnownFeedFilter(
val noteEvent = (oldNote.event as? EphemeralChatEvent)?.roomId()
if (newNotePair.key == noteEvent) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) {
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
@@ -138,7 +138,7 @@ class ChatroomListKnownFeedFilter(
if (newNotePair.key == oldRoom) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) {
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
@@ -179,7 +179,7 @@ class ChatroomListKnownFeedFilter(
if (channelId in followingChannels && account.isAcceptable(newNote)) {
val lastNote = newRelevantPublicMessages.get(channelId)
if (lastNote != null) {
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
if ((newNote.createdAt() ?: 0L) > (lastNote.createdAt() ?: 0L)) {
newRelevantPublicMessages.put(channelId, newNote)
}
} else {
@@ -205,7 +205,7 @@ class ChatroomListKnownFeedFilter(
if (room != null && room in followingEphemeralChats && account.isAcceptable(newNote)) {
val lastNote = newRelevantEphemeralChats.get(room)
if (lastNote != null) {
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
if ((newNote.createdAt() ?: 0L) > (lastNote.createdAt() ?: 0L)) {
newRelevantEphemeralChats.put(room, newNote)
}
} else {
@@ -241,7 +241,7 @@ class ChatroomListKnownFeedFilter(
) {
val lastNote = newRelevantPrivateMessages.get(roomKey)
if (lastNote != null) {
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
if ((newNote.createdAt() ?: 0L) > (lastNote.createdAt() ?: 0L)) {
newRelevantPrivateMessages.put(roomKey, newNote)
}
} else {
@@ -72,7 +72,7 @@ class ChatroomListNewFeedFilter(
if (newNotePair.key == oldRoom) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) {
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
@@ -120,7 +120,7 @@ class ChatroomListNewFeedFilter(
) {
val lastNote = newRelevantPrivateMessages.get(roomKey)
if (lastNote != null) {
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
if ((newNote.createdAt() ?: 0L) > (lastNote.createdAt() ?: 0L)) {
newRelevantPrivateMessages.put(roomKey, newNote)
}
} else {
@@ -106,12 +106,12 @@ open class DiscoverChatFeedFilter(
// precache to avoid breaking the contract
val lastNote =
items.associateWith { note ->
LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0
LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0L
}
val createdNote =
items.associateWith { note ->
note.createdAt() ?: 0
note.createdAt() ?: 0L
}
val comparator: Comparator<Note> =
@@ -127,7 +127,7 @@ open class DiscoverCommunityFeedFilter(
val createdNote =
items.associateWith { note ->
note.createdAt() ?: 0
note.createdAt() ?: 0L
}
val comparator: Comparator<Note> =
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.utils.TimeUtils
fun filterCommunitiesGlobal(
@@ -41,8 +42,8 @@ fun filterCommunitiesGlobal(
relay = it.key,
filter =
Filter(
kinds = CommunityPostApprovalEvent.KIND_LIST,
limit = 100,
kinds = listOf(CommunityDefinitionEvent.KIND),
limit = 500,
since = since,
),
),
@@ -52,7 +53,7 @@ fun filterCommunitiesGlobal(
Filter(
kinds = CommunityPostApprovalEvent.KIND_LIST,
limit = 100,
since = since ?: TimeUtils.oneWeekAgo(),
since = since ?: TimeUtils.oneMonthAgo(),
),
),
)
@@ -80,9 +80,9 @@ open class NIP90ContentDiscoveryResponseFilter(
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
// val params = buildFilterParams(account)
val maxNote = collection.filter { acceptableEvent(it) }.maxByOrNull { it.createdAt() ?: 0 } ?: return emptySet()
val maxNote = collection.filter { acceptableEvent(it) }.maxByOrNull { it.createdAt() ?: 0L } ?: return emptySet()
if ((maxNote.createdAt() ?: 0) > (latestNote?.createdAt() ?: 0)) {
if ((maxNote.createdAt() ?: 0L) > (latestNote?.createdAt() ?: 0L)) {
latestNote = maxNote
}
@@ -115,7 +115,7 @@ class HomeLiveFilter(
oldList.filter { channel ->
val channelTime = (channel as? LiveActivitiesChannel)?.info?.createdAt
(channelTime == null || channelTime > fifteenMinsAgo) ||
(channel.lastNote?.createdAt() ?: 0) > fifteenMinsAgo
(channel.lastNote?.createdAt() ?: 0L) > fifteenMinsAgo
}
val newItemsToBeAdded = applyFilter(newItems)
@@ -194,7 +194,7 @@ class HomeLiveFilter(
return collection.sortedWith(
compareByDescending<Channel> { followCounts[it] }
.thenByDescending<Channel> { it.lastNote?.createdAt() ?: 0 }
.thenByDescending<Channel> { it.lastNote?.createdAt() ?: 0L }
.thenBy { it.hashCode() },
)
}
@@ -239,7 +239,7 @@ class CardFeedContentState(
(boostsInCard + zapsInCard.map { it.response } + reactionsInCard).groupBy {
sdf.format(
Instant
.ofEpochSecond(it.createdAt() ?: 0)
.ofEpochSecond(it.createdAt() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalDateTime(),
)
@@ -273,7 +273,7 @@ class CardFeedContentState(
user.value.groupBy {
sdf.format(
Instant
.ofEpochSecond(it.createdAt() ?: 0)
.ofEpochSecond(it.createdAt() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalDateTime(),
)
@@ -43,7 +43,7 @@ abstract class Card {
class BadgeCard(
val note: Note,
) : Card() {
override fun createdAt(): Long = note.createdAt() ?: 0
override fun createdAt(): Long = note.createdAt() ?: 0L
override fun id() = note.idHex
}
@@ -52,7 +52,7 @@ class BadgeCard(
class NoteCard(
val note: Note,
) : Card() {
override fun createdAt(): Long = note.createdAt() ?: 0
override fun createdAt(): Long = note.createdAt() ?: 0L
override fun id() = note.idHex
}
@@ -62,7 +62,7 @@ class ZapUserSetCard(
val user: User,
val zapEvents: ImmutableList<CombinedZap>,
) : Card() {
val createdAt = zapEvents.maxOf { it.createdAt() ?: 0 }
val createdAt = zapEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L
override fun createdAt(): Long = createdAt
@@ -78,9 +78,9 @@ class MultiSetCard(
) : Card() {
val maxCreatedAt =
maxOf(
zapEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
zapEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L,
likeEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L,
boostEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L,
)
val minCreatedAt =
@@ -109,7 +109,7 @@ class MultiSetCard(
class MessageSetCard(
val note: Note,
) : Card() {
override fun createdAt(): Long = note.createdAt() ?: 0
override fun createdAt(): Long = note.createdAt() ?: 0L
override fun id() = note.idHex
}
+1
View File
@@ -502,6 +502,7 @@
<string name="follow_list_selection">Follow List</string>
<string name="follow_list_kind3follows">All Follows</string>
<string name="follow_list_kind3follows_users_only">All User Follows</string>
<string name="follow_list_kind3follows_proxy">Follows via Proxy</string>
<string name="follow_list_aroundme">Around Me</string>
<string name="follow_list_global">Global</string>
@@ -215,10 +215,8 @@ private fun TranslationMessage(
}
},
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.account.toggleDontTranslateFrom(source)
langSettingsPopupExpanded = false
}
accountViewModel.toggleDontTranslateFrom(source)
langSettingsPopupExpanded = false
},
)
HorizontalDivider(thickness = DividerThickness)
@@ -48,9 +48,9 @@ data class RootSceneTag(
this.relay = relayHint
}
fun countMemory(): Long =
fun countMemory(): Int =
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
8L + // kind
8 + // kind
pubKeyHex.bytesUsedInMemory() +
dTag.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0)
@@ -47,9 +47,9 @@ open class Event(
open fun extraIndexableTagNames() = emptySet<String>()
open fun countMemory(): Long =
open fun countMemory(): Int =
7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
12L + // createdAt + kind
12 + // createdAt + kind
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } +
@@ -45,7 +45,7 @@ data class EventHintBundle<T : Event>(
this.authorHomeRelay = authorHomeRelay
}
fun countMemory(): Long =
fun countMemory(): Int =
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
event.countMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0)
@@ -157,7 +157,7 @@ open class BasicRelayClient(
val onConnected: () -> Unit,
) : WebSocketListener {
override fun onOpen(
pingMillis: Long,
pingMillis: Int,
compression: Boolean,
) {
Log.d(logTag, "OnOpen (ping: ${pingMillis}ms${if (compression) ", using compression" else ""})")
@@ -260,7 +260,7 @@ open class BasicRelayClient(
}
fun markConnectionAsReady(
pingInMs: Long,
pingInMs: Int,
usingCompression: Boolean,
) {
this.resetEOSEStatuses()
@@ -459,7 +459,7 @@ open class BasicRelayClient(
)
}
socket?.let {
// Log.d(logTag, "Sending: $str")
Log.d(logTag, "Sending: $str")
val result = it.send(str)
listener.onSend(this@BasicRelayClient, str, result)
stats.addBytesSent(str.bytesUsedInMemory())
@@ -24,11 +24,11 @@ import androidx.collection.LruCache
import com.vitorpamplona.quartz.utils.TimeUtils
class RelayStat(
var receivedBytes: Long = 0L,
var sentBytes: Long = 0L,
var spamCounter: Long = 0L,
var errorCounter: Long = 0L,
var pingInMs: Long = 0L,
var receivedBytes: Int = 0,
var sentBytes: Int = 0,
var spamCounter: Int = 0,
var errorCounter: Int = 0,
var pingInMs: Int = 0,
) {
val messages = LruCache<RelayDebugMessage, RelayDebugMessage>(100)
@@ -54,11 +54,11 @@ class RelayStat(
messages.put(debugMessage, debugMessage)
}
fun addBytesReceived(bytesUsedInMemory: Long) {
fun addBytesReceived(bytesUsedInMemory: Int) {
receivedBytes += bytesUsedInMemory
}
fun addBytesSent(bytesUsedInMemory: Long) {
fun addBytesSent(bytesUsedInMemory: Int) {
sentBytes += bytesUsedInMemory
}
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
object RelayStats {
private val innerCache =
object : LruCache<NormalizedRelayUrl, RelayStat>(1000) {
object : LruCache<NormalizedRelayUrl, RelayStat>(100) {
override fun create(key: NormalizedRelayUrl?) = RelayStat()
}
@@ -33,14 +33,14 @@ object RelayStats {
fun addBytesReceived(
url: NormalizedRelayUrl,
bytesUsedInMemory: Long,
bytesUsedInMemory: Int,
) {
get(url).addBytesReceived(bytesUsedInMemory)
}
fun addBytesSent(
url: NormalizedRelayUrl,
bytesUsedInMemory: Long,
bytesUsedInMemory: Int,
) {
get(url).addBytesSent(bytesUsedInMemory)
}
@@ -61,7 +61,7 @@ object RelayStats {
fun setPing(
url: NormalizedRelayUrl,
pingInMs: Long,
pingInMs: Int,
) {
get(url).pingInMs = pingInMs
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.sockets
interface WebSocketListener {
fun onOpen(
pingMillis: Long,
pingMillis: Int,
compression: Boolean,
)
@@ -46,7 +46,7 @@ class BasicOkHttpWebSocket(
webSocket: okhttp3.WebSocket,
response: Response,
) = out.onOpen(
response.receivedResponseAtMillis - response.sentRequestAtMillis,
(response.receivedResponseAtMillis - response.sentRequestAtMillis).toInt(),
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
)
@@ -40,9 +40,9 @@ data class ATag(
) {
constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint)
fun countMemory(): Long =
fun countMemory(): Int =
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
8L + // kind
8 + // kind
pubKeyHex.bytesUsedInMemory() +
dTag.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0)
@@ -39,9 +39,9 @@ data class Address(
Parcelable {
fun toValue() = assemble(kind, pubKeyHex, dTag)
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes +
8L + // kind
8 + // kind
pubKeyHex.bytesUsedInMemory() +
dTag.bytesUsedInMemory()
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
class DTag(
val dId: String,
) {
fun countMemory(): Long = 1 * pointerSizeInBytes + dId.bytesUsedInMemory()
fun countMemory(): Int = 1 * pointerSizeInBytes + dId.bytesUsedInMemory()
fun toTagArray() = assemble(dId)
@@ -44,7 +44,7 @@ data class ETag(
this.author = authorPubKeyHex
}
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
eventId.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0) +
@@ -40,7 +40,7 @@ data class PTag(
override val pubKey: HexKey,
override val relayHint: NormalizedRelayUrl? = null,
) : PubKeyReferenceTag {
fun countMemory(): Long =
fun countMemory(): Int =
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
pubKey.bytesUsedInMemory() +
(relayHint?.url?.bytesUsedInMemory() ?: 0)
@@ -50,7 +50,7 @@ data class ContactTag(
this.petname = petname
}
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes +
pubKey.bytesUsedInMemory() +
(relayUri?.url?.bytesUsedInMemory() ?: 0) +
@@ -30,7 +30,7 @@ class PoWTag(
val nonce: String,
val commitment: Int?,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + nonce.bytesUsedInMemory() + (commitment?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + nonce.bytesUsedInMemory() + (commitment?.bytesUsedInMemory() ?: 0)
fun toTagArray() = assemble(nonce, commitment)
@@ -53,7 +53,7 @@ data class QAddressableTag(
this.relay = relayHint
}
fun countMemory(): Long =
fun countMemory(): Int =
2 * pointerSizeInBytes +
address.countMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0)
@@ -42,7 +42,7 @@ data class QEventTag(
this.author = authorPubKeyHex
}
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
eventId.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0) +
@@ -132,7 +132,18 @@ class LongTextNoteEvent(
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
if (publishedAt == null) return null
// removes posts in the future.
return if (publishedAt <= createdAt) {
publishedAt
} else {
null
}
}
companion object {
const val KIND = 30023
@@ -38,7 +38,7 @@ class ChannelTag(
val relay: NormalizedRelayUrl? = null,
val author: HexKey? = null,
) {
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
eventId.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0) +
@@ -26,7 +26,7 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
abstract class BunkerMessage {
abstract fun countMemory(): Long
abstract fun countMemory(): Int
class BunkerMessageDeserializer : StdDeserializer<BunkerMessage>(BunkerMessage::class.java) {
override fun deserialize(
@@ -36,7 +36,7 @@ open class BunkerRequest(
val method: String,
val params: Array<String> = emptyArray(),
) : BunkerMessage() {
override fun countMemory(): Long =
override fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
id.bytesUsedInMemory() +
method.bytesUsedInMemory() +
@@ -36,7 +36,7 @@ open class BunkerResponse(
val result: String?,
val error: String?,
) : BunkerMessage() {
override fun countMemory(): Long =
override fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
id.bytesUsedInMemory() +
(result?.bytesUsedInMemory() ?: 0) +
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
abstract class Response(
@field:JsonProperty("result_type") val resultType: String,
) {
abstract fun countMemory(): Long
abstract fun countMemory(): Int
}
// PayInvoice Call
@@ -39,10 +39,10 @@ class PayInvoiceSuccessResponse(
class PayInvoiceResultParams(
val preimage: String? = null,
) {
fun countMemory(): Long = pointerSizeInBytes + (preimage?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = pointerSizeInBytes + (preimage?.bytesUsedInMemory() ?: 0)
}
override fun countMemory(): Long = pointerSizeInBytes + (result?.countMemory() ?: 0)
override fun countMemory(): Int = pointerSizeInBytes + (result?.countMemory() ?: 0)
}
class PayInvoiceErrorResponse(
@@ -52,10 +52,10 @@ class PayInvoiceErrorResponse(
val code: ErrorType? = null,
val message: String? = null,
) {
fun countMemory(): Long = pointerSizeInBytes + pointerSizeInBytes + (message?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = pointerSizeInBytes + pointerSizeInBytes + (message?.bytesUsedInMemory() ?: 0)
}
override fun countMemory(): Long = pointerSizeInBytes + (error?.countMemory() ?: 0)
override fun countMemory(): Int = pointerSizeInBytes + (error?.countMemory() ?: 0)
enum class ErrorType {
@JsonProperty(value = "RATE_LIMITED")
@@ -69,6 +69,10 @@ class PayInvoiceErrorResponse(
@JsonProperty(value = "INSUFFICIENT_BALANCE")
INSUFFICIENT_BALANCE,
// The command is not known or is intentionally not implemented.
@JsonProperty(value = "PAYMENT_FAILED")
PAYMENT_FAILED,
// The wallet does not have enough funds to cover a fee reserve or the payment amount.
@JsonProperty(value = "QUOTA_EXCEEDED")
QUOTA_EXCEEDED,
@@ -31,7 +31,7 @@ data class ProxyTag(
val id: String,
val protocol: String,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + id.bytesUsedInMemory() + protocol.bytesUsedInMemory()
fun countMemory(): Int = 2 * pointerSizeInBytes + id.bytesUsedInMemory() + protocol.bytesUsedInMemory()
fun toTagArray() = assemble(id, protocol)
@@ -35,7 +35,7 @@ class AddressBookmark(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) : BookmarkIdTag {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
@@ -38,7 +38,7 @@ class EventBookmark(
val relay: NormalizedRelayUrl? = null,
val author: HexKey? = null,
) : BookmarkIdTag {
fun countMemory(): Long =
fun countMemory(): Int =
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
eventId.bytesUsedInMemory() +
(relay?.url?.bytesUsedInMemory() ?: 0) +
@@ -38,7 +38,7 @@ class UserTag(
val pubKey: HexKey,
val relayHint: NormalizedRelayUrl? = null,
) : MuteTag {
fun countMemory(): Long =
fun countMemory(): Int =
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
pubKey.bytesUsedInMemory() +
(relayHint?.url?.bytesUsedInMemory() ?: 0)
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
class WordTag(
val word: String,
) : MuteTag {
fun countMemory(): Long =
fun countMemory(): Int =
1 * pointerSizeInBytes + // 1 fields, 4 bytes each reference (32bit)
word.bytesUsedInMemory()
@@ -35,7 +35,7 @@ class MeetingSpaceTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
@@ -35,7 +35,7 @@ class MeetingRoomTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
@@ -46,6 +46,8 @@ import com.vitorpamplona.quartz.nip19Bech32.eventHints
import com.vitorpamplona.quartz.nip19Bech32.eventIds
import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints
import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag.Companion.parse
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -129,12 +131,18 @@ class WikiNoteEvent(
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
override fun publishedAt() =
try {
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
if (publishedAt == null) return null
// removes posts in the future.
return if (publishedAt <= createdAt) {
publishedAt
} else {
null
}
}
companion object {
const val KIND = 30818
@@ -72,7 +72,7 @@ class LnZapEvent(
}
}
override fun countMemory(): Long =
override fun countMemory(): Int =
super.countMemory() +
pointerSizeInBytes + (zapRequest?.countMemory() ?: 0) + // rough calculation
pointerSizeInBytes + 36 // bigdecimal size
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
class ZapRaiserTag(
val amountInSats: Long,
) {
fun countMemory(): Long = 1 * pointerSizeInBytes + amountInSats.bytesUsedInMemory()
fun countMemory(): Int = 1 * pointerSizeInBytes + amountInSats.bytesUsedInMemory()
fun toTagArray() = assemble(amountInSats)
@@ -44,7 +44,7 @@ class SealedRumorEvent(
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var innerEventId: HexKey? = null
override fun countMemory(): Long =
override fun countMemory(): Int =
super.countMemory() +
pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0)
@@ -48,7 +48,7 @@ class GiftWrapEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var innerEventId: HexKey? = null
override fun countMemory(): Long =
override fun countMemory(): Int =
super.countMemory() +
pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0)
@@ -53,7 +53,18 @@ abstract class ReplaceableVideoEvent(
override fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
if (publishedAt == null) return null
// removes posts in the future.
return if (publishedAt <= createdAt) {
publishedAt
} else {
null
}
}
override fun duration() = tags.firstNotNullOfOrNull(DurationTag::parse)
@@ -31,7 +31,7 @@ data class TextTrackTag(
val eventId: HexKey,
var relay: String? = null,
) {
fun countMemory(): Long =
fun countMemory(): Int =
2 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
eventId.bytesUsedInMemory() +
(relay?.bytesUsedInMemory() ?: 0)
@@ -36,7 +36,7 @@ class ApprovedAddressTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
@@ -36,7 +36,7 @@ class CommunityTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
@@ -49,7 +49,7 @@ class AppDefinitionEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PublishedAtProvider {
override fun countMemory(): Long = super.countMemory() + (cachedMetadata?.countMemory() ?: 8L)
override fun countMemory(): Int = super.countMemory() + (cachedMetadata?.countMemory() ?: 8)
@Transient private var cachedMetadata: AppMetadata? = null
@@ -79,7 +79,18 @@ class AppDefinitionEvent(
fun includeKind(kind: Int) = tags.isTaggedKind(kind)
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
if (publishedAt == null) return null
// removes posts in the future.
return if (publishedAt <= createdAt) {
publishedAt
} else {
null
}
}
companion object {
const val KIND = 31990
@@ -50,25 +50,25 @@ class AppMetadata {
var lud06: String? = null
var lud16: String? = null
fun countMemory(): Long =
fun countMemory(): Int =
20 * pointerSizeInBytes + // 20 fields, 4 bytes for each reference
(name?.bytesUsedInMemory() ?: 0L) +
(username?.bytesUsedInMemory() ?: 0L) +
(displayName?.bytesUsedInMemory() ?: 0L) +
(picture?.bytesUsedInMemory() ?: 0L) +
(banner?.bytesUsedInMemory() ?: 0L) +
(image?.bytesUsedInMemory() ?: 0L) +
(website?.bytesUsedInMemory() ?: 0L) +
(about?.bytesUsedInMemory() ?: 0L) +
(subscription?.bytesUsedInMemory() ?: 0L) +
(acceptsNutZaps?.bytesUsedInMemory() ?: 0L) +
(supportsEncryption?.bytesUsedInMemory() ?: 0L) +
(personalized?.bytesUsedInMemory() ?: 0L) + // A Boolean has 8 bytes of header, plus 1 byte of payload, for a total of 9 bytes of information. The JVM then rounds it up to the next multiple of 8. so the one instance of java.lang.Boolean takes up 16 bytes of memory.
(amount?.bytesUsedInMemory() ?: 0L) +
(nip05?.bytesUsedInMemory() ?: 0L) +
(domain?.bytesUsedInMemory() ?: 0L) +
(lud06?.bytesUsedInMemory() ?: 0L) +
(lud16?.bytesUsedInMemory() ?: 0L)
(name?.bytesUsedInMemory() ?: 0) +
(username?.bytesUsedInMemory() ?: 0) +
(displayName?.bytesUsedInMemory() ?: 0) +
(picture?.bytesUsedInMemory() ?: 0) +
(banner?.bytesUsedInMemory() ?: 0) +
(image?.bytesUsedInMemory() ?: 0) +
(website?.bytesUsedInMemory() ?: 0) +
(about?.bytesUsedInMemory() ?: 0) +
(subscription?.bytesUsedInMemory() ?: 0) +
(acceptsNutZaps?.bytesUsedInMemory() ?: 0) +
(supportsEncryption?.bytesUsedInMemory() ?: 0) +
(personalized?.bytesUsedInMemory() ?: 0) + // A Boolean has 8 bytes of header, plus 1 byte of payload, for a total of 9 bytes of information. The JVM then rounds it up to the next multiple of 8. so the one instance of java.lang.Boolean takes up 16 bytes of memory.
(amount?.bytesUsedInMemory() ?: 0) +
(nip05?.bytesUsedInMemory() ?: 0) +
(domain?.bytesUsedInMemory() ?: 0) +
(lud06?.bytesUsedInMemory() ?: 0) +
(lud16?.bytesUsedInMemory() ?: 0)
fun anyName(): String? = displayName ?: name ?: username
@@ -43,7 +43,7 @@ class NIP90ContentDiscoveryResponseEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var events: List<HexKey>? = null
override fun countMemory(): Long =
override fun countMemory(): Int =
super.countMemory() +
pointerSizeInBytes + (events?.sumOf { it.bytesUsedInMemory() } ?: 0)
@@ -70,7 +70,18 @@ class ClassifiedsEvent(
fun location() = tags.firstNotNullOfOrNull(LocationTag::parse)
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
if (publishedAt == null) return null
// removes posts in the future.
return if (publishedAt <= createdAt) {
publishedAt
} else {
null
}
}
fun categories() = tags.hashtags()
@@ -24,13 +24,13 @@ import kotlin.math.min
val pointerSizeInBytes = 4
fun String.bytesUsedInMemory(): Long = (8 * (((this.length * 2L) + 45) / 8))
fun String.bytesUsedInMemory(): Int = (8 * (((this.length * 2) + 45) / 8))
fun Long.bytesUsedInMemory(): Long = 8
fun Long.bytesUsedInMemory(): Int = 8
fun Int.bytesUsedInMemory(): Long = 4
fun Int.bytesUsedInMemory(): Int = 4
fun Boolean.bytesUsedInMemory(): Long = 8
fun Boolean.bytesUsedInMemory(): Int = 8
fun String.containsIgnoreCase(term: String): Boolean {
if (term.isEmpty()) return true // Empty string is contained