Merge branch 'vitorpamplona:main' into labeled-bookmarks

This commit is contained in:
KotlinGeekDev
2025-11-26 21:45:35 +00:00
committed by GitHub
13 changed files with 486 additions and 37 deletions
@@ -121,6 +121,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.dimension
import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
import com.vitorpamplona.quartz.experimental.profileGallery.hash
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -209,12 +210,17 @@ import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
@@ -1721,6 +1727,25 @@ class Account(
).toSet()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun loadUserCardFlow(target: HexKey): Flow<Int?> =
trustProviderList.liveUserRankProvider
.transformLatest { provider ->
if (provider != null) {
emitAll(
cache
.getOrCreateAddressableNote(
ContactCardEvent.createAddress(provider.pubkey, target),
).flow()
.metadata.stateFlow,
)
} else {
emit(null)
}
}.map {
(it?.note?.event as? ContactCardEvent)?.rank()
}.flowOn(Dispatchers.IO)
suspend fun saveDMRelayList(dmRelays: List<NormalizedRelayUrl>) = sendLiterallyEverywhere(dmRelayList.saveRelayList(dmRelays))
suspend fun savePrivateOutboxRelayList(relays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(privateStorageRelayList.saveRelayList(relays))
@@ -29,8 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@@ -68,6 +66,8 @@ class User(
var relaysBeingUsed = mapOf<NormalizedRelayUrl, RelayInfo>()
private set
var flowSet: UserFlowSet? = null
fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
@@ -251,10 +251,6 @@ class User(
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
fun isFollowingHashtag(tag: String) = latestContactList?.isTaggedHash(tag) ?: false
fun isFollowingGeohash(geoTag: String) = latestContactList?.isTaggedGeoHash(geoTag) ?: false
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
@@ -303,8 +299,6 @@ class User(
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var flowSet: UserFlowSet? = null
@Synchronized
fun createOrDestroyFlowSync(create: Boolean) {
if (create) {
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ProviderTypes
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.utils.Log
@@ -36,6 +37,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -76,6 +78,40 @@ class TrustProviderListState(
emptySet(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val liveUserRankProvider: StateFlow<ServiceProviderTag?> =
liveTrustProviderList
.map {
it.firstOrNull { it.service == ProviderTypes.rank }
}.onStart {
emit(
liveTrustProviderList.value.firstOrNull {
it.service == ProviderTypes.rank
},
)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
null,
)
@OptIn(ExperimentalCoroutinesApi::class)
val liveUserFollowerCount: StateFlow<ServiceProviderTag?> =
liveTrustProviderList
.map { tagList ->
tagList.firstOrNull { it.service == ProviderTypes.followerCount }
}.onStart {
emit(
liveTrustProviderList.value.firstOrNull { it.service == ProviderTypes.followerCount },
)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
null,
)
init {
settings.backupTrustProviderList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserOutboxFinderSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserCardsSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
@@ -46,6 +47,7 @@ class UserFinderFilterAssembler(
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
UserWatcherSubAssembler(client, cache, ::allKeys),
UserReportsSubAssembler(client, ::allKeys),
UserCardsSubAssembler(client, ::allKeys),
)
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
@@ -0,0 +1,55 @@
/**
* 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.service.relayClient.reqCommand.user.watchers
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
val ContactCardKindList = listOf(ContactCardEvent.KIND)
fun filterContactCardsToKeysFromTrusted(
targets: Set<HexKey>,
trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>>,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
if (targets.isEmpty() || trustedAccounts.isEmpty()) return emptyList()
val sortedTargets = mapOf("d" to targets.sorted())
return trustedAccounts.mapNotNull { relayAuthors ->
if (relayAuthors.value.isNotEmpty()) {
RelayBasedFilter(
relay = relayAuthors.key,
filter =
Filter(
kinds = ContactCardKindList,
authors = relayAuthors.value.sorted(),
tags = sortedTargets,
since = since?.get(relayAuthors.key)?.time,
),
)
} else {
null
}
}
}
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -38,7 +37,6 @@ val UserMetadataForKeyKinds =
listOf(
MetadataEvent.KIND,
StatusEvent.KIND,
ContactCardEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
)
@@ -0,0 +1,147 @@
/**
* 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.service.relayClient.reqCommand.user.watchers
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.ammolite.relays.filters.MutableTime
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
class UserCardsSubAssembler(
client: INostrClient,
allKeys: () -> Set<UserFinderQueryState>,
) : SingleSubEoseManager<UserFinderQueryState>(client, allKeys) {
var lastUsersOnFilter: Set<User> = emptySet()
/**
* This assembler saves the EOSE per user key. That EOSE includes their metadata, etc
* and reports, but only from trusted accounts (follows of all logged in users).
*/
var latestEOSEs: EOSEAccountFast<User> = EOSEAccountFast<User>(2000)
override fun newEose(
relay: NormalizedRelayUrl,
time: Long,
filters: List<Filter>?,
) {
lastUsersOnFilter.forEach {
latestEOSEs.newEose(it, relay, time)
}
super.newEose(relay, time, filters)
}
override fun updateFilter(
keys: List<UserFinderQueryState>,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
if (keys.isEmpty()) return null
lastUsersOnFilter = keys.mapTo(mutableSetOf()) { it.user }
if (lastUsersOnFilter.isEmpty()) return null
val accounts = keys.mapTo(mutableSetOf()) { it.account }
val trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>> =
mapOfSet {
accounts.forEach { account ->
account.outboxRelays.flow.value.map {
add(it, account.userProfile().pubkeyHex)
}
}
accounts.map { it.trustProviderList.liveUserRankProvider.value }.forEach { account ->
if (account != null) {
add(account.relayUrl, account.pubkey)
}
}
}
return groupByRelayPresence(lastUsersOnFilter, latestEOSEs, trustedAccounts.keys)
.map { group ->
val groupIds = group.map { it.pubkeyHex }.toSet()
if (groupIds.isNotEmpty()) {
val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs)
filterContactCardsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs)
} else {
emptyList()
}
}.flatten()
}
fun groupByRelayPresence(
users: Iterable<User>,
eoseCache: EOSEAccountFast<User>,
inRelays: Set<NormalizedRelayUrl>,
): Collection<List<User>> {
if (users.none()) return emptyList()
val relaySnapshot = inRelays.toSet()
return users
.groupBy { user ->
val relaysForUser = eoseCache.sinceRelaySet(user)
if (relaysForUser.isNullOrEmpty() || relaySnapshot.isEmpty()) {
null
} else {
val intersection = relaysForUser.filter { it in relaySnapshot }.sorted()
if (intersection.isEmpty()) {
null
} else {
intersection.hashCode()
}
}
}.values
.map {
// important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
it.sortedBy { it.pubkeyHex }
}
}
fun findMinimumEOSEsForUsers(
users: List<User>,
eoseCache: EOSEAccountFast<User>,
): SincePerRelayMap {
val minLatestEOSEs = mutableMapOf<NormalizedRelayUrl, MutableTime>()
users.forEach {
eoseCache.since(it)?.forEach {
val minEose = minLatestEOSEs[it.key]
if (minEose == null) {
minLatestEOSEs.put(it.key, it.value.copy())
} else {
minEose.updateIfOlder(it.value.time)
}
}
}
return minLatestEOSEs
}
override fun distinct(key: UserFinderQueryState) = key.user
}
@@ -21,19 +21,27 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
@@ -45,7 +53,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font10SP
import com.vitorpamplona.amethyst.ui.theme.Font6SP
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
@@ -354,6 +367,12 @@ fun BaseUserPicture(
FollowingIcon(Modifier.size(size.div(3.5f)))
}
}
WatchUserCards(baseUser.pubkeyHex, accountViewModel) { score ->
if (score != null) {
ScoreTag(score, size, Modifier.align(Alignment.BottomCenter))
}
}
}
}
@@ -382,9 +401,109 @@ fun BaseUserPicture(
FollowingIcon(Modifier.size(size.div(3.5f)))
}
}
WatchUserCards(baseUserHex, accountViewModel) { score ->
if (score != null) {
ScoreTag(score, size, Modifier.align(Alignment.BottomCenter))
}
}
}
}
@Preview
@Composable
fun ScoreTag55Preview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
Row {
var size = 75.dp
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
InnerUserPicture(
userHex = "AABBCC",
userPicture = "http://null",
userName = "vitor",
size = size,
modifier = Modifier,
accountViewModel = accountViewModel,
)
FollowingIcon(Modifier.size(size.div(3.5f)))
ScoreTag(100, size, Modifier.align(Alignment.BottomCenter))
}
size = 55.dp
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
InnerUserPicture(
userHex = "AABBCC",
userPicture = "http://null",
userName = "vitor",
size = size,
modifier = Modifier,
accountViewModel = accountViewModel,
)
FollowingIcon(Modifier.size(size.div(3.5f)))
ScoreTag(100, size, Modifier.align(Alignment.BottomCenter))
}
size = 35.dp
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
InnerUserPicture(
userHex = "AABBCC",
userPicture = "http://null",
userName = "vitor",
size = size,
modifier = Modifier,
accountViewModel = accountViewModel,
)
FollowingIcon(Modifier.size(size.div(3.5f)))
ScoreTag(100, size, Modifier.align(Alignment.BottomCenter))
}
size = 25.dp
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
InnerUserPicture(
userHex = "AABBCC",
userPicture = "http://null",
userName = "vitor",
size = size,
modifier = Modifier,
accountViewModel = accountViewModel,
)
FollowingIcon(Modifier.size(size.div(3.5f)))
ScoreTag(100, size, Modifier.align(Alignment.BottomCenter))
}
size = 18.dp
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
InnerUserPicture(
userHex = "AABBCC",
userPicture = "http://null",
userName = "vitor",
size = size,
modifier = Modifier,
accountViewModel = accountViewModel,
)
FollowingIcon(Modifier.size(size.div(3.5f)))
ScoreTag(100, size, Modifier.align(Alignment.BottomCenter))
}
}
}
}
@Composable
fun ScoreTag(
score: Int,
size: Dp,
modifier: Modifier,
) {
Text(
text = score.toString(),
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = if (size > 34.dp) Font10SP else Font6SP,
modifier =
modifier
.clip(SmallBorder)
.background(Color.Black)
.padding(horizontal = if (size > 34.dp) 4.dp else 3.dp, vertical = 0.dp),
)
}
@Composable
fun LoadUserProfilePicture(
baseUser: User,
@@ -458,3 +577,19 @@ fun WatchUserFollows(
onFollowChanges(state.authors.contains(userHex))
}
}
@Composable
fun WatchUserCards(
userHex: String,
accountViewModel: AccountViewModel,
onScoreChanges: @Composable (Int?) -> Unit,
) {
val flow =
remember(userHex) {
accountViewModel.account.loadUserCardFlow(userHex)
}
val score by flow.collectAsStateWithLifecycle(null)
onScoreChanges(score)
}
@@ -52,6 +52,9 @@ val Typography =
*/
)
val Font4SP = 4.sp
val Font6SP = 6.sp
val Font8SP = 8.sp
val Font10SP = 10.sp
val Font12SP = 12.sp
val Font14SP = 14.sp
@@ -24,10 +24,12 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetNameTag
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.tagArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
@@ -49,10 +51,20 @@ class ContactCardEvent(
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
companion object Companion {
companion object {
const val KIND = 30382
const val ALT = "Contact Card"
fun createAddress(
owner: HexKey,
target: HexKey,
): Address = Address(KIND, owner, target)
fun createAddressTag(
owner: HexKey,
target: HexKey,
): ATag = ATag(KIND, owner, target, null)
suspend fun create(
targetUser: HexKey,
petName: String? = null,
@@ -61,28 +61,3 @@ data class ServiceProviderTag(
fun assemble(id: ServiceProviderTag) = arrayOf(id.service, id.pubkey, id.relayUrl)
}
}
data class ServiceType(
val kind: Int,
val type: String,
) {
fun toValue() = assemble(kind, type)
companion object {
fun assemble(
kind: Int,
type: String,
) = "$kind:$type"
fun parse(serviceType: String): ServiceType? {
val (kindStr, type) = serviceType.split(":", limit = 2)
val kind = kindStr.toIntOrNull() ?: return null
return ServiceType(kind, type)
}
fun isOfKind(
serviceType: String,
kind: String,
) = serviceType.startsWith(kind) && serviceType[kind.length] == ':'
}
}
@@ -0,0 +1,67 @@
/**
* 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.quartz.experimental.trustedAssertions.list.tags
data class ServiceType(
val kind: Int,
val type: String,
) {
fun toValue() = assemble(kind, type)
companion object {
fun assemble(
kind: Int,
type: String,
) = "$kind:$type"
fun parse(serviceType: String): ServiceType? {
val (kindStr, type) = serviceType.split(":", limit = 2)
val kind = kindStr.toIntOrNull() ?: return null
return ServiceType(kind, type)
}
fun isOfKind(
serviceType: String,
kind: String,
) = serviceType.startsWith(kind) && serviceType[kind.length] == ':'
}
}
object ProviderTypes {
val rank = ServiceType(30382, "rank")
val followerCount = ServiceType(30382, "followers")
val firstPost = ServiceType(30382, "first_created_at")
val postCount = ServiceType(30382, "post_cnt")
val replyCount = ServiceType(30382, "reply_cnt")
val reactionsCount = ServiceType(30382, "reactions_cnt")
val zapReceivedAmount = ServiceType(30382, "zap_amt_recd")
val zapSentAmount = ServiceType(30382, "zap_amt_sent")
val zapReceivedCount = ServiceType(30382, "zap_cnt_recd")
val zapSentCount = ServiceType(30382, "zap_cnt_sent")
val averageZapAmountReceivedPerDay = ServiceType(30382, "zap_avg_amt_day_recd")
val averageZapAmountSentPerDay = ServiceType(30382, "zap_avg_amt_day_sent")
val reportsReceivedCount = ServiceType(30382, "reports_cnt_recd")
val reportsSentCount = ServiceType(30382, "reports_cnt_sent")
val topics = ServiceType(30382, "t")
val activeFrom = ServiceType(30382, "active_hours_start")
val activeTo = ServiceType(30382, "active_hours_end")
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.utils.sha256
import java.io.InputStream
val pool = Sha256Pool(5) // max parallel operations
val pool = Sha256Pool(10) // max parallel operations
actual fun sha256(data: ByteArray) = pool.hash(data)