mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 15:44:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e887232132 | ||
|
|
7bcf4e3750 | ||
|
|
ed0676a5f5 | ||
|
|
f731c654b0 | ||
|
|
b026bffe4a | ||
|
|
a1aaec0b7d | ||
|
|
6f59097ac0 | ||
|
|
263df0c895 | ||
|
|
1ca5713494 | ||
|
|
b706c4e871 | ||
|
|
e8574c10bb | ||
|
|
7cb87ea9c5 | ||
|
|
07e5132943 | ||
|
|
2b1e3cfc93 | ||
|
|
c17e1f0e1f | ||
|
|
e88d1d0d00 | ||
|
|
5fdff97cf8 | ||
|
|
e5328d7975 | ||
|
|
71b45b96fb | ||
|
|
dee28f3aa8 | ||
|
|
3a118bb939 | ||
|
|
971c92b27a | ||
|
|
4c70065843 | ||
|
|
eab69337ff | ||
|
|
ab772eb65e | ||
|
|
7a6720a7ab | ||
|
|
0f3dc18f7a | ||
|
|
84df86f4b6 | ||
|
|
a0289a0cb4 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 394
|
||||
versionName "0.89.8"
|
||||
versionCode 395
|
||||
versionName "0.89.9"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -30,6 +30,7 @@ import coil.decode.SvgDecoder
|
||||
import coil.size.Precision
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.Base64Fetcher
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
@@ -98,6 +99,7 @@ class ServiceManager {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
add(Base64Fetcher.Factory)
|
||||
} // .logger(DebugLogger())
|
||||
.okHttpClient { HttpClientManager.getHttpClient() }
|
||||
.precision(Precision.INEXACT)
|
||||
|
||||
@@ -39,10 +39,10 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.Constants
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
@@ -108,7 +108,6 @@ import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import com.vitorpamplona.quartz.utils.MinimumRelayListProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -121,7 +120,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
@@ -225,6 +223,7 @@ class Account(
|
||||
@Immutable
|
||||
class LiveFollowLists(
|
||||
val users: Set<String> = emptySet(),
|
||||
val usersPlusMe: Set<String>,
|
||||
val hashtags: Set<String> = emptySet(),
|
||||
val geotags: Set<String> = emptySet(),
|
||||
val communities: Set<String> = emptySet(),
|
||||
@@ -423,17 +422,29 @@ class Account(
|
||||
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
|
||||
userProfile().flow().follows.stateFlow.transformLatest {
|
||||
checkNotInMainThread()
|
||||
|
||||
// makes sure the output include only valid p tags
|
||||
val verifiedFollowingUsers = it.user.latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
||||
|
||||
emit(
|
||||
LiveFollowLists(
|
||||
it.user.cachedFollowingKeySet(),
|
||||
it.user.cachedFollowingTagSet(),
|
||||
it.user.cachedFollowingGeohashSet(),
|
||||
it.user.cachedFollowingCommunitiesSet(),
|
||||
verifiedFollowingUsers,
|
||||
verifiedFollowingUsers + keyPair.pubKeyHex,
|
||||
it.user.latestContactList
|
||||
?.unverifiedFollowTagSet()
|
||||
?.map { it.lowercase() }
|
||||
?.toSet() ?: emptySet(),
|
||||
it.user.latestContactList
|
||||
?.unverifiedFollowGeohashSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
it.user.latestContactList
|
||||
?.verifiedFollowAddressSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val liveKind3Follows = liveKind3FollowsFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
|
||||
val liveKind3Follows = liveKind3FollowsFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveHomeList: Flow<ListNameNotePair> by lazy {
|
||||
@@ -465,11 +476,11 @@ class Account(
|
||||
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
||||
emit(kind3Follows)
|
||||
} else if (peopleListFollows.event == null) {
|
||||
emit(LiveFollowLists())
|
||||
emit(LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
} else {
|
||||
val result = waitToDecrypt(peopleListFollows.event)
|
||||
if (result == null) {
|
||||
emit(LiveFollowLists())
|
||||
emit(LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
} else {
|
||||
emit(result)
|
||||
}
|
||||
@@ -481,54 +492,98 @@ class Account(
|
||||
}
|
||||
|
||||
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
liveHomeFollowListFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
|
||||
liveHomeFollowListFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
}
|
||||
|
||||
fun relaysFromPeopleListFlows(
|
||||
currentFollowList: LiveFollowLists,
|
||||
relayUrlsToIgnore: Set<String>,
|
||||
): Flow<List<MinimumRelayListProcessor.RelayRecommendation>> =
|
||||
/**
|
||||
* filter onion and local host from write relays
|
||||
* for each user pubkey, a list of valid relays.
|
||||
*/
|
||||
private fun assembleAuthorsPerWriteRelay(
|
||||
userList: Map<HexKey, List<String>>,
|
||||
hasOnionConnection: Boolean = false,
|
||||
): Map<String, List<HexKey>> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val authorsPerRelayUrl = mutableMapOf<String, MutableSet<HexKey>>()
|
||||
val relayUrlsPerAuthor = mutableMapOf<HexKey, MutableSet<String>>()
|
||||
|
||||
userList.forEach { userWriteRelayListPair ->
|
||||
userWriteRelayListPair.value.forEach { relayUrl ->
|
||||
if (!RelayUrlFormatter.isLocalHost(relayUrl) && (hasOnionConnection || !RelayUrlFormatter.isOnion(relayUrl))) {
|
||||
RelayUrlFormatter.normalizeOrNull(relayUrl)?.let { normRelayUrl ->
|
||||
val userSet = authorsPerRelayUrl[normRelayUrl]
|
||||
if (userSet != null) {
|
||||
userSet.add(userWriteRelayListPair.key)
|
||||
} else {
|
||||
authorsPerRelayUrl[normRelayUrl] = mutableSetOf(userWriteRelayListPair.key)
|
||||
}
|
||||
|
||||
val relaySet = authorsPerRelayUrl[userWriteRelayListPair.key]
|
||||
if (relaySet != null) {
|
||||
relaySet.add(normRelayUrl)
|
||||
} else {
|
||||
relayUrlsPerAuthor[userWriteRelayListPair.key] = mutableSetOf(normRelayUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for each relay, authors that only use this relay go first.
|
||||
// then keeps order by pubkey asc
|
||||
val comparator = compareByDescending<HexKey> { relayUrlsPerAuthor[it]?.size ?: 0 }.thenBy { it }
|
||||
|
||||
return authorsPerRelayUrl.mapValues {
|
||||
it.value.sortedWith(comparator)
|
||||
}
|
||||
}
|
||||
|
||||
fun authorsPerRelay(
|
||||
pubkeyList: Set<HexKey>,
|
||||
defaultRelayList: List<String>,
|
||||
): Flow<Map<String, List<String>>> =
|
||||
combine(
|
||||
currentFollowList.users.map {
|
||||
pubkeyList.map {
|
||||
getNIP65RelayListFlow(it)
|
||||
},
|
||||
) { followsNIP65RelayLists ->
|
||||
MinimumRelayListProcessor
|
||||
.reliableRelaySetFor(
|
||||
followsNIP65RelayLists.mapNotNull {
|
||||
(it.note.event as? AdvertisedRelayListEvent)
|
||||
},
|
||||
relayUrlsToIgnore,
|
||||
hasOnionConnection = proxy != null,
|
||||
).sortedByDescending { it.users.size }
|
||||
assembleAuthorsPerWriteRelay(
|
||||
followsNIP65RelayLists
|
||||
.mapNotNull {
|
||||
val author = (it.note as? AddressableNote)?.address?.pubKeyHex
|
||||
val event = (it.note.event as? AdvertisedRelayListEvent)
|
||||
|
||||
if (event != null) {
|
||||
event.pubKey to event.writeRelays()
|
||||
} else {
|
||||
if (author != null) {
|
||||
author to defaultRelayList
|
||||
} else {
|
||||
Log.e("Account", "This author should NEVER be null. Note: ${it.note.idHex}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}.toMap(),
|
||||
hasOnionConnection = proxy != null,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveHomeFollowRelayFlow: Flow<List<MinimumRelayListProcessor.RelayRecommendation>> by lazy {
|
||||
val liveHomeListAuthorsPerRelayFlow: Flow<Map<String, List<String>>?> by lazy {
|
||||
combineTransform(liveHomeFollowListFlow, connectToRelaysFlow) { followList, existing ->
|
||||
if (followList != null) {
|
||||
emit(
|
||||
relaysFromPeopleListFlows(
|
||||
followList,
|
||||
existing.mapNotNullTo(HashSet()) {
|
||||
if (it.read && FeedType.FOLLOWS in it.feedTypes) {
|
||||
it.url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
emit(authorsPerRelay(followList.usersPlusMe, existing.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }))
|
||||
} else {
|
||||
emit(MutableStateFlow(emptyList()))
|
||||
emit(MutableStateFlow(null))
|
||||
}
|
||||
}.flatMapLatest {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val liveHomeFollowRelays: StateFlow<List<MinimumRelayListProcessor.RelayRecommendation>> by lazy {
|
||||
liveHomeFollowRelayFlow.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
val liveHomeListAuthorsPerRelay: StateFlow<Map<String, List<String>>?> by lazy {
|
||||
liveHomeListAuthorsPerRelayFlow.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -540,7 +595,7 @@ class Account(
|
||||
|
||||
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -552,7 +607,24 @@ class Account(
|
||||
|
||||
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveStoriesListAuthorsPerRelayFlow: Flow<Map<String, List<String>>?> by lazy {
|
||||
combineTransform(liveStoriesFollowLists, connectToRelaysFlow) { followList, existing ->
|
||||
if (followList != null) {
|
||||
emit(authorsPerRelay(followList.usersPlusMe, existing.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }))
|
||||
} else {
|
||||
emit(MutableStateFlow(null))
|
||||
}
|
||||
}.flatMapLatest {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val liveStoriesListAuthorsPerRelay: StateFlow<Map<String, List<String>>?> by lazy {
|
||||
liveStoriesListAuthorsPerRelayFlow.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -564,7 +636,24 @@ class Account(
|
||||
|
||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
|
||||
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex)))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveDiscoveryListAuthorsPerRelayFlow: Flow<Map<String, List<String>>?> by lazy {
|
||||
combineTransform(liveDiscoveryFollowLists, connectToRelaysFlow) { followList, existing ->
|
||||
if (followList != null) {
|
||||
emit(authorsPerRelay(followList.usersPlusMe, existing.filter { it.read }.map { it.url }))
|
||||
} else {
|
||||
emit(MutableStateFlow(null))
|
||||
}
|
||||
}.flatMapLatest {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val liveDiscoveryListAuthorsPerRelay: StateFlow<Map<String, List<String>>?> by lazy {
|
||||
liveDiscoveryListAuthorsPerRelayFlow.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
}
|
||||
|
||||
private fun decryptLiveFollows(
|
||||
@@ -572,10 +661,11 @@ class Account(
|
||||
onReady: (LiveFollowLists) -> Unit,
|
||||
) {
|
||||
listEvent.privateTags(signer) { privateTagList ->
|
||||
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
||||
onReady(
|
||||
LiveFollowLists(
|
||||
users =
|
||||
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(),
|
||||
users = users,
|
||||
usersPlusMe = users + userProfile().pubkeyHex,
|
||||
hashtags =
|
||||
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||
geotags =
|
||||
@@ -759,6 +849,10 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun countFollowersOf(pubkey: HexKey): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false }
|
||||
|
||||
suspend fun followerCount(): Int = countFollowersOf(keyPair.pubKeyHex)
|
||||
|
||||
fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
picture: String? = null,
|
||||
@@ -1109,7 +1203,7 @@ class Account(
|
||||
listOf(
|
||||
TypedFilter(
|
||||
setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL),
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
ids = listOf(it.id),
|
||||
),
|
||||
),
|
||||
@@ -2813,9 +2907,7 @@ class Account(
|
||||
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
||||
flowHiddenUsers.value.spammers.contains(userHex)
|
||||
|
||||
fun followingKeySet(): Set<HexKey> = userProfile().cachedFollowingKeySet()
|
||||
|
||||
fun followingTagSet(): Set<HexKey> = userProfile().cachedFollowingTagSet()
|
||||
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.users
|
||||
|
||||
fun isAcceptable(user: User): Boolean {
|
||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||
@@ -2865,8 +2957,6 @@ class Account(
|
||||
}
|
||||
|
||||
fun getRelevantReports(note: Note): Set<Note> {
|
||||
val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet()
|
||||
|
||||
val innerReports =
|
||||
if (note.event is RepostEvent || note.event is GenericRepostEvent) {
|
||||
note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList()
|
||||
@@ -2875,8 +2965,8 @@ class Account(
|
||||
}
|
||||
|
||||
return (
|
||||
note.reportsBy(followsPlusMe) +
|
||||
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
|
||||
note.reportsBy(liveKind3Follows.value.usersPlusMe) +
|
||||
(note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) +
|
||||
innerReports
|
||||
).toSet()
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ import com.vitorpamplona.amethyst.ui.actions.relays.updated
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
|
||||
@@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
@@ -336,52 +336,24 @@ class User(
|
||||
|
||||
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
|
||||
fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false
|
||||
|
||||
fun isFollowingHashtagCached(tag: String): Boolean {
|
||||
return latestContactList?.verifiedFollowTagSet?.let {
|
||||
fun isFollowingHashtag(tag: String): Boolean {
|
||||
return latestContactList?.unverifiedFollowTagSet()?.map { it.lowercase() }?.toSet()?.let {
|
||||
return tag.lowercase() in it
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun isFollowingGeohashCached(geoTag: String): Boolean {
|
||||
return latestContactList?.verifiedFollowGeohashSet?.let {
|
||||
fun isFollowingGeohash(geoTag: String): Boolean {
|
||||
return latestContactList?.unverifiedFollowAddressSet()?.toSet()?.let {
|
||||
return geoTag.lowercase() in it
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun isFollowingCached(user: User): Boolean {
|
||||
return latestContactList?.verifiedFollowKeySet?.let {
|
||||
return user.pubkeyHex in it
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun isFollowingCached(userHex: String): Boolean {
|
||||
return latestContactList?.verifiedFollowKeySet?.let {
|
||||
return userHex in it
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
|
||||
|
||||
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun cachedFollowingKeySet(): Set<HexKey> = latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
|
||||
fun cachedFollowingTagSet(): Set<String> = latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingGeohashSet(): Set<HexKey> = latestContactList?.verifiedFollowGeohashSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> = latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
|
||||
fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size
|
||||
|
||||
suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
val messagesToUser = privateChatrooms[key] ?: return false
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern
|
||||
import java.util.Base64
|
||||
|
||||
@Stable
|
||||
class Base64Fetcher(
|
||||
private val options: Options,
|
||||
private val data: Uri,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
checkNotInMainThread()
|
||||
|
||||
val matcher = base64contentPattern.matcher(data.toString())
|
||||
|
||||
if (matcher.find()) {
|
||||
val base64String = matcher.group(2)
|
||||
|
||||
val byteArray = Base64.getDecoder().decode(base64String)
|
||||
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) ?: throw Exception("Unable to load base64 $base64String")
|
||||
|
||||
return DrawableResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
} else {
|
||||
throw Exception("Unable to load base64 $data")
|
||||
}
|
||||
}
|
||||
|
||||
object Factory : Fetcher.Factory<Uri> {
|
||||
override fun create(
|
||||
data: Uri,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
): Fetcher? {
|
||||
return if (base64contentPattern.matcher(data.toString()).find()) {
|
||||
return Base64Fetcher(options, data)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Base64Requester {
|
||||
fun imageRequest(
|
||||
context: Context,
|
||||
message: String,
|
||||
): ImageRequest =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(message)
|
||||
.fetcherFactory(Base64Fetcher.Factory)
|
||||
.build()
|
||||
}
|
||||
+12
-12
@@ -26,11 +26,11 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
@@ -81,7 +81,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -92,7 +92,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -103,7 +103,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
StatusEvent.KIND,
|
||||
@@ -124,7 +124,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
@@ -146,7 +146,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 100,
|
||||
@@ -157,7 +157,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
@@ -172,7 +172,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 400,
|
||||
),
|
||||
@@ -189,7 +189,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -222,7 +222,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
GitReplyEvent.KIND,
|
||||
@@ -244,7 +244,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(GiftWrapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
|
||||
@@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
|
||||
@@ -58,7 +58,7 @@ object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
@@ -71,7 +71,7 @@ object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
@@ -86,7 +86,7 @@ object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
@@ -96,7 +96,7 @@ object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
|
||||
+3
-3
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
|
||||
@@ -50,7 +50,7 @@ object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = myPeer.users.toList(),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
@@ -73,7 +73,7 @@ object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
tags = mapOf("p" to myPeer.users.map { it }),
|
||||
|
||||
+7
-7
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
@@ -41,7 +41,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
@@ -56,7 +56,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
@@ -71,7 +71,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
@@ -91,7 +91,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
// Metadata comes from any relay
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND),
|
||||
ids = followingEvents.toList(),
|
||||
since =
|
||||
@@ -113,7 +113,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
// Metadata comes from any relay
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMetadataEvent.KIND),
|
||||
tags = mapOf("e" to listOf(it)),
|
||||
limit = 1,
|
||||
@@ -131,7 +131,7 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
tags = mapOf("e" to listOf(it)),
|
||||
since =
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
|
||||
@@ -38,7 +38,7 @@ object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed")
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
authors =
|
||||
community
|
||||
.moderators()
|
||||
|
||||
+24
-30
@@ -24,8 +24,9 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -66,10 +67,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createMarketplaceFilter(): List<TypedFilter> {
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
@@ -81,9 +79,9 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -98,7 +96,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -120,7 +118,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -146,7 +144,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(AppDefinitionEvent.KIND),
|
||||
limit = 300,
|
||||
tags = mapOf("k" to kTags),
|
||||
@@ -165,12 +163,14 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
?.users
|
||||
?.toList()
|
||||
|
||||
val followsRelays = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
authors = follows,
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = followsRelays,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
limit = 300,
|
||||
since =
|
||||
@@ -184,7 +184,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
tags = mapOf("p" to it),
|
||||
kinds = listOf(LiveActivitiesEvent.KIND),
|
||||
limit = 100,
|
||||
@@ -200,17 +200,14 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createPublicChatFilter(): List<TypedFilter> {
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
val followChats = account.selectedChatsFollowList().toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
limit = 500,
|
||||
@@ -225,7 +222,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
ids = followChats,
|
||||
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -243,15 +240,12 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createCommunitiesFilter(): TypedFilter {
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val follows = account.liveDiscoveryListAuthorsPerRelay.value
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -275,7 +269,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -305,7 +299,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -335,7 +329,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
@@ -366,7 +360,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
@@ -397,7 +391,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -427,7 +421,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -43,7 +43,7 @@ object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
tags =
|
||||
mapOf(
|
||||
"g" to
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -43,7 +43,7 @@ object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
|
||||
@@ -24,8 +24,9 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
@@ -76,13 +77,13 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
}
|
||||
|
||||
fun createFollowAccountsFilter(): TypedFilter {
|
||||
val follows = account.liveHomeFollowLists.value?.users
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
|
||||
val follows =
|
||||
account.liveHomeListAuthorsPerRelay.value
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -99,7 +100,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
LiveActivitiesEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
),
|
||||
authors = followSet,
|
||||
authors = follows,
|
||||
limit = 400,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
@@ -111,20 +112,19 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
}
|
||||
|
||||
fun createFollowMetadataAndReleaseFilter(): TypedFilter? {
|
||||
val follows = account.liveHomeFollowLists.value?.users
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.shuffled()?.ifEmpty { null }
|
||||
val follows = account.liveHomeListAuthorsPerRelay.value
|
||||
|
||||
return if (followSet != null) {
|
||||
return if (follows != null) {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
),
|
||||
authors = followSet.take(500),
|
||||
authors = follows,
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
@@ -145,7 +145,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -184,7 +184,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -223,7 +223,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@@ -39,7 +39,7 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
return TypedFilter(
|
||||
types = feedTypes,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LnZapPaymentResponseEvent.KIND),
|
||||
authors = listOf(fromServiceHex),
|
||||
tags =
|
||||
|
||||
+7
-7
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
@@ -95,7 +95,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, aTag.kind),
|
||||
authors = listOfNotNull(aTag.pubKeyHex),
|
||||
// just to be sure
|
||||
@@ -110,7 +110,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
ids = listOfNotNull(hexToWatch),
|
||||
),
|
||||
),
|
||||
@@ -118,7 +118,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOfNotNull(hexToWatch),
|
||||
// just to be sure
|
||||
@@ -135,7 +135,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
search = mySearchString,
|
||||
limit = 1000,
|
||||
@@ -144,7 +144,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -165,7 +165,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
ChannelMetadataEvent.KIND,
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
|
||||
@@ -44,7 +44,7 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMetadataEvent.KIND),
|
||||
tags = mapOf("e" to reactionsToWatch),
|
||||
),
|
||||
@@ -65,7 +65,7 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND),
|
||||
ids = interestedEvents.toList(),
|
||||
),
|
||||
@@ -88,7 +88,7 @@ object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(aTag.kind),
|
||||
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
|
||||
+10
-10
@@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
@@ -67,7 +67,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -89,7 +89,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
@@ -117,7 +117,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(aTag.kind),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
limit = 5,
|
||||
@@ -127,7 +127,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(aTag.kind),
|
||||
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
@@ -150,7 +150,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -173,7 +173,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
@@ -200,7 +200,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
tags = mapOf("q" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
@@ -232,7 +232,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
ids = interestedEvents.toList(),
|
||||
),
|
||||
),
|
||||
|
||||
+5
-5
@@ -21,10 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
@@ -45,7 +45,7 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = firstTimers,
|
||||
),
|
||||
@@ -69,7 +69,7 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
|
||||
authors = groupIds,
|
||||
since = minEOSEs,
|
||||
@@ -78,7 +78,7 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ReportEvent.KIND),
|
||||
tags = mapOf("p" to groupIds),
|
||||
since = minEOSEs,
|
||||
|
||||
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
|
||||
object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
|
||||
private var eventToWatch: String? = null
|
||||
@@ -45,7 +45,7 @@ object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
ids = eventsToLoad.toList(),
|
||||
),
|
||||
)
|
||||
|
||||
+10
-10
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.AppRecommendationEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
@@ -56,7 +56,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -69,7 +69,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -94,7 +94,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(LnZapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
limit = 200,
|
||||
@@ -107,7 +107,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -120,7 +120,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
),
|
||||
@@ -132,7 +132,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(BadgeProfilesEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -145,7 +145,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
@@ -159,7 +159,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(ProfileGalleryEntryEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
@@ -173,7 +173,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(BadgeAwardEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
limit = 20,
|
||||
|
||||
@@ -24,8 +24,9 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
@@ -64,15 +65,12 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
}
|
||||
|
||||
fun createContextualFilter(): TypedFilter {
|
||||
val follows =
|
||||
account.liveStoriesFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val follows = account.liveStoriesListAuthorsPerRelay.value
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
SinceAuthorPerRelayFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
limit = 200,
|
||||
@@ -97,7 +95,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -128,7 +126,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
Filter(
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.relays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
|
||||
class EOSERelayList(
|
||||
var relayList: Map<String, EOSETime> = emptyMap(),
|
||||
|
||||
@@ -49,7 +49,19 @@ fun stringRes(id: Int): String = resourceCache.get(id) ?: stringResource(id).als
|
||||
@Composable
|
||||
fun stringRes(
|
||||
id: Int,
|
||||
vararg args: Any,
|
||||
vararg args: String,
|
||||
): String =
|
||||
String
|
||||
.format(
|
||||
LocalConfiguration.current.locales.get(0),
|
||||
resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun stringRes(
|
||||
id: Int,
|
||||
vararg args: Int?,
|
||||
): String =
|
||||
String
|
||||
.format(
|
||||
@@ -66,7 +78,22 @@ fun stringRes(
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
id: Int,
|
||||
vararg args: Any?,
|
||||
vararg args: String?,
|
||||
): String {
|
||||
val res = ctx.resources
|
||||
|
||||
return String
|
||||
.format(
|
||||
res.configuration.locales.get(0),
|
||||
resourceCache.get(id) ?: res.getString(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
}
|
||||
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
id: Int,
|
||||
vararg args: Int?,
|
||||
): String {
|
||||
val res = ctx.resources
|
||||
|
||||
|
||||
+4
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.relays
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -30,7 +31,7 @@ import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.ammolite.relays.RelayStats
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.utils.MinimumRelayListProcessor
|
||||
import com.vitorpamplona.quartz.utils.RelayListRecommendationProcessor
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -38,6 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class Kind3RelayListViewModel : ViewModel() {
|
||||
private lateinit var account: Account
|
||||
|
||||
@@ -138,7 +140,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
||||
private fun refreshProposals() {
|
||||
_proposedRelays.update {
|
||||
val proposed =
|
||||
MinimumRelayListProcessor
|
||||
RelayListRecommendationProcessor
|
||||
.reliableRelaySetFor(
|
||||
account.liveKind3Follows.value.users.mapNotNull {
|
||||
account.getNIP65RelayList(it)
|
||||
|
||||
+2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.relays
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class Nip65RelayListViewModel : ViewModel() {
|
||||
private lateinit var account: Account
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -29,7 +28,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
@@ -49,8 +47,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
@@ -65,10 +61,6 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.compose.SubcomposeAsyncImageContent
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Base64Segment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
||||
@@ -83,7 +75,6 @@ import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||
@@ -96,7 +87,6 @@ import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
@@ -112,7 +102,6 @@ import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun isMarkdown(content: String): Boolean =
|
||||
content.startsWith("> ") ||
|
||||
@@ -445,47 +434,7 @@ private fun RenderWordWithPreview(
|
||||
is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
is Base64Segment -> ImageFromBase64(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageFromBase64(base64String: String) {
|
||||
val context = LocalContext.current
|
||||
var imageBytes by remember { mutableStateOf<ByteArray?>(null) }
|
||||
LaunchedEffect(base64String) {
|
||||
imageBytes =
|
||||
withContext(Dispatchers.IO) {
|
||||
var base64String2 = base64String.removePrefix("data:image/jpeg;base64,")
|
||||
RichTextParser.imageExtensions.forEach {
|
||||
base64String2 = base64String2.removePrefix("data:image/$it;base64,")
|
||||
}
|
||||
runCatching { Base64.decode(base64String2, Base64.DEFAULT) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
if (imageBytes == null) {
|
||||
BlankNote()
|
||||
} else {
|
||||
val request =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(imageBytes)
|
||||
.build()
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
model = request,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
when (painter.state) {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent()
|
||||
}
|
||||
else -> BlankNote()
|
||||
}
|
||||
}
|
||||
is Base64Segment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-106
@@ -20,15 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Face
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@@ -37,22 +33,10 @@ import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.DataSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
|
||||
import java.util.Base64
|
||||
|
||||
@Composable
|
||||
fun RobohashAsyncImage(
|
||||
@@ -92,53 +76,34 @@ fun RobohashFallbackAsyncImage(
|
||||
loadRobohash: Boolean,
|
||||
) {
|
||||
if (model != null && loadProfilePicture) {
|
||||
val isBase64 = model.startsWith("data:image/jpeg;base64,")
|
||||
|
||||
if (isBase64) {
|
||||
val context = LocalContext.current
|
||||
val base64Painter =
|
||||
rememberAsyncImagePainter(
|
||||
model = Base64Requester.imageRequest(context, model),
|
||||
val painter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(
|
||||
image = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight),
|
||||
)
|
||||
} else {
|
||||
forwardingPainter(
|
||||
painter =
|
||||
rememberVectorPainter(
|
||||
image = Icons.Default.Face,
|
||||
),
|
||||
colorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = base64Painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
} else {
|
||||
val painter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(
|
||||
image = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight),
|
||||
)
|
||||
} else {
|
||||
forwardingPainter(
|
||||
painter =
|
||||
rememberVectorPainter(
|
||||
image = Icons.Default.Face,
|
||||
),
|
||||
colorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = painter,
|
||||
fallback = painter,
|
||||
error = painter,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality,
|
||||
)
|
||||
}
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = painter,
|
||||
fallback = painter,
|
||||
error = painter,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality,
|
||||
)
|
||||
} else {
|
||||
if (loadRobohash) {
|
||||
Image(
|
||||
@@ -161,48 +126,3 @@ fun RobohashFallbackAsyncImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Base64Requester {
|
||||
fun imageRequest(
|
||||
context: Context,
|
||||
message: String,
|
||||
): ImageRequest =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(message)
|
||||
.fetcherFactory(Base64Fetcher.Factory)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Base64Fetcher(
|
||||
private val options: Options,
|
||||
private val data: Uri,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
checkNotInMainThread()
|
||||
|
||||
val base64String = data.toString().removePrefix("data:image/jpeg;base64,")
|
||||
|
||||
val byteArray = Base64.getDecoder().decode(base64String)
|
||||
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||
|
||||
if (bitmap == null) {
|
||||
throw Exception("Unable to load base64 $base64String")
|
||||
}
|
||||
|
||||
return DrawableResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
}
|
||||
|
||||
object Factory : Fetcher.Factory<Uri> {
|
||||
override fun create(
|
||||
data: Uri,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
): Fetcher = Base64Fetcher(options, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxState
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart
|
||||
@@ -39,10 +40,13 @@ import androidx.compose.material3.SwipeToDismissBoxValue.Settled
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -69,6 +73,7 @@ fun SwipeToDeleteContainer(
|
||||
}
|
||||
return@rememberSwipeToDismissBoxState true
|
||||
},
|
||||
positionalThreshold = { it * .40f },
|
||||
)
|
||||
|
||||
SwipeToDismissBox(
|
||||
@@ -86,11 +91,19 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState) {
|
||||
if (dismissState.currentValue > dismissState.targetValue) {
|
||||
Color(0xFFFF1744)
|
||||
} else {
|
||||
Color.Transparent
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
},
|
||||
label = "DismissBackground",
|
||||
)
|
||||
|
||||
val haptic = LocalHapticFeedback.current
|
||||
LaunchedEffect(key1 = dismissState.currentValue > dismissState.targetValue) {
|
||||
// doesn't run for the first time or when the list is updated.
|
||||
if (dismissState.progress > 0 && dismissState.progress < 1) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
|
||||
+10
-10
@@ -36,15 +36,15 @@ class BookmarkPrivateFeedFilter(
|
||||
|
||||
val privateTags = bookmarks?.cachedPrivateTags() ?: return emptyList()
|
||||
|
||||
val notes =
|
||||
bookmarks.filterEvents(privateTags).mapNotNull { LocalCache.checkGetOrCreateNote(it) }
|
||||
|
||||
val addresses =
|
||||
bookmarks.filterAddresses(privateTags).map { LocalCache.getOrCreateAddressableNote(it) }
|
||||
|
||||
return notes
|
||||
.plus(addresses)
|
||||
.toSet()
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
return privateTags
|
||||
.mapNotNull {
|
||||
if (it.size > 1 && it[0] == "e") {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
} else if (it.size > 1 && it[0] == "a") {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it[1])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -32,14 +32,16 @@ class BookmarkPublicFeedFilter(
|
||||
override fun feed(): List<Note> {
|
||||
val bookmarks = account.userProfile().latestBookmarkList
|
||||
|
||||
val notes =
|
||||
bookmarks?.taggedEvents()?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
val addresses =
|
||||
bookmarks?.taggedAddresses()?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes
|
||||
.plus(addresses)
|
||||
.toSet()
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
return bookmarks
|
||||
?.tags
|
||||
?.mapNotNull {
|
||||
if (it.size > 1 && it[0] == "e") {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
} else if (it.size > 1 && it[0] == "a") {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it[1])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}?.reversed() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
val DefaultFeedOrder: Comparator<Note> =
|
||||
compareBy<Note>(
|
||||
compareByDescending<Note>
|
||||
{
|
||||
val noteEvent = it.event
|
||||
if (noteEvent == null) {
|
||||
@@ -36,8 +36,4 @@ val DefaultFeedOrder: Comparator<Note> =
|
||||
null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.idHex
|
||||
},
|
||||
).reversed()
|
||||
}.thenBy { it.idHex }
|
||||
|
||||
@@ -763,9 +763,11 @@ class FollowListViewModel(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
||||
account.userProfile().flow().follows.stateFlow.transformLatest {
|
||||
account.liveKind3FollowsFlow.transformLatest {
|
||||
checkNotInMainThread()
|
||||
|
||||
val communities =
|
||||
it.user.cachedFollowingCommunitiesSet().mapNotNull {
|
||||
it.communities.mapNotNull {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||
CodeName(
|
||||
"Community/${communityNote.idHex}",
|
||||
@@ -776,12 +778,12 @@ class FollowListViewModel(
|
||||
}
|
||||
|
||||
val hashtags =
|
||||
it.user.cachedFollowingTagSet().map {
|
||||
it.hashtags.map {
|
||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val geotags =
|
||||
it.user.cachedFollowingGeohashSet().map {
|
||||
it.geotags.map {
|
||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
@@ -1007,7 +1009,7 @@ fun AmethystClickableIcon() {
|
||||
fun debugState(context: Context) {
|
||||
Client
|
||||
.allSubscriptions()
|
||||
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toJson() }}") }
|
||||
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
|
||||
|
||||
NostrAccountDataSource.printCounter()
|
||||
NostrChannelDataSource.printCounter()
|
||||
|
||||
@@ -83,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView
|
||||
@@ -114,7 +115,6 @@ import com.vitorpamplona.ammolite.relays.RelayPoolStatus
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -151,7 +151,7 @@ fun DrawerContent(
|
||||
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel, drawerState)
|
||||
}
|
||||
|
||||
FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser)
|
||||
FollowingAndFollowerCounts(accountViewModel.account, onClickUser)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
@@ -380,18 +380,12 @@ fun UserStatusDeleteButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
private fun FollowingAndFollowerCounts(
|
||||
baseAccountUser: User,
|
||||
baseAccountUser: Account,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
var followingCount by remember { mutableStateOf("--") }
|
||||
val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle()
|
||||
var followerCount by remember { mutableStateOf("--") }
|
||||
|
||||
WatchFollow(baseAccountUser = baseAccountUser) { newFollowing ->
|
||||
if (followingCount != newFollowing) {
|
||||
followingCount = newFollowing
|
||||
}
|
||||
}
|
||||
|
||||
WatchFollower(baseAccountUser = baseAccountUser) { newFollower ->
|
||||
if (followerCount != newFollower) {
|
||||
followerCount = newFollower
|
||||
@@ -402,7 +396,9 @@ private fun FollowingAndFollowerCounts(
|
||||
modifier = drawerSpacing.clickable(onClick = onClick),
|
||||
) {
|
||||
Text(
|
||||
text = followingCount,
|
||||
text =
|
||||
followingCount.value.users.size
|
||||
.toString(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
@@ -419,31 +415,19 @@ private fun FollowingAndFollowerCounts(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollow(
|
||||
baseAccountUser: User,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = accountUserFollowsState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onReady(accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollower(
|
||||
baseAccountUser: User,
|
||||
baseAccountUser: Account,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val accountUserFollowersState by baseAccountUser.live().followers.observeAsState()
|
||||
val accountUserFollowersState by baseAccountUser
|
||||
.userProfile()
|
||||
.live()
|
||||
.followers
|
||||
.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = accountUserFollowersState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onReady(accountUserFollowersState?.user?.cachedFollowerCount()?.toString() ?: "--")
|
||||
}
|
||||
onReady(baseAccountUser.followerCount().toString() ?: "--")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -676,7 +676,7 @@ fun LoadModerators(
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||
|
||||
@@ -732,7 +732,7 @@ private fun LoadParticipants(
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder()
|
||||
.followsThatParticipateOn(baseNote, allFollows)
|
||||
@@ -889,7 +889,7 @@ fun RenderChannelThumb(
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
@@ -426,18 +427,6 @@ fun UpdateZapAmountDialog(
|
||||
Modifier.weight(1f),
|
||||
)
|
||||
|
||||
IconButton(onClick = {
|
||||
onClose()
|
||||
runCatching { uri.openUri("https://app.mutinywallet.com/settings/connections?name=Amethyst") }
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(R.mipmap.mutiny),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClose()
|
||||
@@ -458,7 +447,7 @@ fun UpdateZapAmountDialog(
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentPaste,
|
||||
Icons.Outlined.ContentPaste,
|
||||
contentDescription = stringRes(id = R.string.paste_from_clipboard),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
@@ -696,7 +685,7 @@ fun authenticate(
|
||||
stringRes(
|
||||
context,
|
||||
R.string.biometric_authentication_failed_explainer_with_error,
|
||||
errString,
|
||||
errString.toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -367,11 +367,10 @@ fun WatchUserFollows(
|
||||
remember {
|
||||
accountViewModel.userFollows
|
||||
.map {
|
||||
it.user.isFollowingCached(userHex) ||
|
||||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
accountViewModel.isFollowing(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged()
|
||||
}.observeAsState(
|
||||
accountViewModel.account.userProfile().isFollowingCached(userHex) ||
|
||||
accountViewModel.isFollowing(userHex) ||
|
||||
(userHex == accountViewModel.account.userProfile().pubkeyHex),
|
||||
)
|
||||
|
||||
|
||||
+3
-4
@@ -29,12 +29,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -44,12 +44,11 @@ fun DisplayFollowingHashtagsInPost(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val userFollowState by accountViewModel.userFollows.observeAsState()
|
||||
val userFollowState by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||
var firstTag by remember(baseNote) { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = userFollowState) {
|
||||
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
|
||||
val newFirstTag = baseNote.event?.firstIsTaggedHashes(followingTags)
|
||||
val newFirstTag = baseNote.event?.firstIsTaggedHashes(userFollowState.hashtags)
|
||||
|
||||
if (firstTag != newFirstTag) {
|
||||
firstTag = newFirstTag
|
||||
|
||||
+2
-2
@@ -807,10 +807,10 @@ class AccountViewModel(
|
||||
|
||||
fun isFollowing(user: User?): Boolean {
|
||||
if (user == null) return false
|
||||
return account.userProfile().isFollowingCached(user)
|
||||
return account.isFollowing(user)
|
||||
}
|
||||
|
||||
fun isFollowing(user: HexKey): Boolean = account.userProfile().isFollowingCached(user)
|
||||
fun isFollowing(user: HexKey): Boolean = account.isFollowing(user)
|
||||
|
||||
val hideDeleteRequestDialog: Boolean
|
||||
get() = account.hideDeleteRequestDialog
|
||||
|
||||
+13
-14
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys.DRAFTS
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.maxWidthWithBackground
|
||||
|
||||
@Composable
|
||||
fun DraftListScreen(
|
||||
@@ -178,24 +178,23 @@ private fun DraftFeedLoaded(
|
||||
}
|
||||
}
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
SwipeToDeleteContainer(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
onStartToEnd = { accountViewModel.delete(item) },
|
||||
onEndToStart = { accountViewModel.delete(item) },
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement(),
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
SwipeToDeleteContainer(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
onStartToEnd = { accountViewModel.delete(item) },
|
||||
onEndToStart = { accountViewModel.delete(item) },
|
||||
) {
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = MaterialTheme.colorScheme.maxWidthWithBackground,
|
||||
isBoostedNote = false,
|
||||
isHiddenFeed = state.showHidden.value,
|
||||
quotesLeft = 3,
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ fun GeoHashActionOptions(
|
||||
.observeAsState()
|
||||
val isFollowingTag by
|
||||
remember(userState) {
|
||||
derivedStateOf { userState?.user?.isFollowingGeohashCached(tag) ?: false }
|
||||
derivedStateOf { userState?.user?.isFollowingGeohash(tag) ?: false }
|
||||
}
|
||||
|
||||
if (isFollowingTag) {
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ fun HashtagActionOptions(
|
||||
.observeAsState()
|
||||
val isFollowingTag by
|
||||
remember(userState) {
|
||||
derivedStateOf { userState?.user?.isFollowingHashtagCached(tag) ?: false }
|
||||
derivedStateOf { userState?.user?.isFollowingHashtag(tag) ?: false }
|
||||
}
|
||||
|
||||
if (isFollowingTag) {
|
||||
|
||||
@@ -189,6 +189,16 @@ val LightInnerPostBorderModifier =
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkMaxWidthWithBackground =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkColorPalette.background)
|
||||
|
||||
val LightMaxWidthWithBackground =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LightColorPalette.background)
|
||||
|
||||
val DarkSelectedReactionBoxModifier =
|
||||
Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 5.dp)
|
||||
@@ -395,6 +405,9 @@ val ColorScheme.replyModifier: Modifier
|
||||
val ColorScheme.innerPostModifier: Modifier
|
||||
get() = if (isLight) LightInnerPostBorderModifier else DarkInnerPostBorderModifier
|
||||
|
||||
val ColorScheme.maxWidthWithBackground: Modifier
|
||||
get() = if (isLight) LightMaxWidthWithBackground else DarkMaxWidthWithBackground
|
||||
|
||||
val ColorScheme.channelNotePictureModifier: Modifier
|
||||
get() = if (isLight) LightChannelNotePictureModifier else DarkChannelNotePictureModifier
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -801,4 +801,6 @@
|
||||
<string name="http_status_508">Detekována smyčka - Server detekuje nekonečnou smyčku při zpracování požadavku</string>
|
||||
<string name="http_status_511">Vyžadováno ověření sítě - Klient musí být autentizován, aby získal přístup k síti</string>
|
||||
<string name="add_a_nip96_server">Přidat NIP-96 server</string>
|
||||
<string name="delete_all">Vymazat vše</string>
|
||||
<string name="delete_all_drafts_confirmation">Opravdu chcete odstranit všechny koncepty?</string>
|
||||
</resources>
|
||||
|
||||
@@ -806,4 +806,6 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="http_status_508">Schleife erkannt - Der Server erkennt eine Endlosschleife, während er die Anfrage bearbeitet</string>
|
||||
<string name="http_status_511">Netzwerk-Authentifizierung erforderlich - Der Client muss authentifiziert werden, um auf das Netzwerk zuzugreifen</string>
|
||||
<string name="add_a_nip96_server">NIP-96 Server hinzufügen</string>
|
||||
<string name="delete_all">Alles löschen</string>
|
||||
<string name="delete_all_drafts_confirmation">Möchten Sie wirklich alle Entwürfe löschen?</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="post_was_hidden">Dit bericht is verborgen omdat het je verborgen gebruikers of woorden noemt</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Bericht gemarkeerd als ongepast door</string>
|
||||
<string name="post_not_found">Bericht niet gevonden</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Kanaalafbeelding</string>
|
||||
<string name="referenced_event_not_found">Verwezen event niet gevonden</string>
|
||||
<string name="could_not_decrypt_the_message">Note versleuteld met encryptie</string>
|
||||
@@ -36,6 +37,7 @@
|
||||
<string name="report_impersonation">Meld imitatie</string>
|
||||
<string name="report_explicit_content">Rapporteer expliciete inhoud</string>
|
||||
<string name="report_illegal_behaviour">Rapporteer illegaal gedrag</string>
|
||||
<string name="report_malware">Rapporteer malware</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Je gebruikt een publieke sleutel en publieke sleutels zijn alleen-lezen. Login met een geheime sleutel om te reageren</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Je gebruikt een publieke sleutel en publieke sleutels zijn alleen-lezen. Login met een geheime sleutel om berichten te boosten</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Je gebruikt een publieke sleutel en publieke sleutels zijn alleen-lezen. Login met een geheime sleutel om berichten te liken</string>
|
||||
@@ -131,6 +133,7 @@
|
||||
<string name="conversations">Gesprekken</string>
|
||||
<string name="notes">Notes</string>
|
||||
<string name="replies">Reacties</string>
|
||||
<string name="gallery">Galerij</string>
|
||||
<string name="follows">"Volgend"</string>
|
||||
<string name="reports">"Rapporten"</string>
|
||||
<string name="more_options">Meer opties</string>
|
||||
@@ -254,6 +257,8 @@
|
||||
<string name="quick_action_delete">Verwijderen</string>
|
||||
<string name="quick_action_unfollow">Ontvolgen</string>
|
||||
<string name="quick_action_follow">Volgen</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Verwijderen uit galerij</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Verwijder deze media uit je galerij, je kunt het later lezen</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Verzoek om te verwijderen</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst zal vragen om uw note te verwijderen van de relays waarmee u momenteel verbonden bent. Er is geen garantie dat uw note permanent wordt verwijderd van deze relays, of van andere relays waar het kan worden opgeslagen.</string>
|
||||
<string name="quick_action_block_dialog_btn">Blokkeren</string>
|
||||
@@ -267,6 +272,7 @@
|
||||
<string name="report_dialog_impersonation">Kwaadwillige imitatie</string>
|
||||
<string name="report_dialog_nudity">Naaktheid of gevoelige inhoud</string>
|
||||
<string name="report_dialog_illegal">Illegaal gedrag</string>
|
||||
<string name="report_dialog_malware">Malware</string>
|
||||
<string name="report_dialog_blocking_a_user">Als u een gebruiker blokkeert, wordt zijn inhoud in uw app verborgen. Uw notes zijn nog steeds publiekelijk zichtbaar, ook voor mensen die u blokkeert. Geblokkeerde gebruikers worden vermeld in het scherm Beveiligingsfilters.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Blokkeer & verberg gebruiker]]></string>
|
||||
<string name="report_dialog_report_btn">Misbruik melden</string>
|
||||
@@ -339,6 +345,14 @@
|
||||
<string name="zap_type_nonzap_explainer">Geen spoor binnen Nostr, alleen in Lightning</string>
|
||||
<string name="file_server">File server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress of @User</string>
|
||||
<string name="media_servers">Mediaservers</string>
|
||||
<string name="set_preferred_media_servers">Stel uw gewenste media upload servers in.</string>
|
||||
<string name="no_media_server_message">Je hebt geen aangepaste mediaservers ingesteld. Je kunt de lijst van Amethyst gebruiken of er hieronder een toevoegen ↓</string>
|
||||
<string name="built_in_media_servers_title">Ingebouwde mediaservers</string>
|
||||
<string name="built_in_servers_description">Amethyst\'s standaardlijst. U kunt ze individueel toevoegen of de lijst toevoegen.</string>
|
||||
<string name="use_default_servers">Gebruik standaard lijst</string>
|
||||
<string name="add_media_server">Mediaserver toevoegen</string>
|
||||
<string name="delete_media_server">Mediaserver verwijderen</string>
|
||||
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt.</string>
|
||||
<string name="connect_via_tor_short">Orbot/Tor setup</string>
|
||||
@@ -507,6 +521,7 @@
|
||||
<string name="share_or_save">Delen of opslaan</string>
|
||||
<string name="copy_url_to_clipboard">Kopieer URL naar klembord</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Note naar klembord kopiëren</string>
|
||||
<string name="add_media_to_gallery">Media toevoegen aan galerij</string>
|
||||
<string name="created_at">Gemaakt op</string>
|
||||
<string name="rules">Regels</string>
|
||||
<string name="login_with_external_signer">Login met Amber</string>
|
||||
@@ -710,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">Plaats tussen de 1 tot 3 relays om gebeurtenissen op te slaan die niemand anders kan zien, zoals uw Concepten en/of app instellingen. Idealiter zijn deze relays lokaal of vereist authenticatie voordat de inhoud van elke gebruiker wordt gedownload.</string>
|
||||
<string name="kind_3_section">Algemene relays</string>
|
||||
<string name="kind_3_section_description">Amethyst gebruikt deze relays om berichten voor jou te downloaden.</string>
|
||||
<string name="kind_3_recommended_section">Aanbevolen relays</string>
|
||||
<string name="kind_3_recommended_section_description">Voeg de volgende relays toe aan je algemene relay-lijst om berichten van de vermelde gebruikers te ontvangen.</string>
|
||||
<string name="search_section">Relays zoeken</string>
|
||||
<string name="search_section_explainer">Lijst van relays om te gebruiken bij het zoeken naar inhoud of gebruikers. Taggen en zoeken werkt niet als er geen opties beschikbaar zijn. Zorg ervoor dat ze NIP-50 implementeren.</string>
|
||||
<string name="local_section">Lokale relays</string>
|
||||
@@ -785,4 +802,7 @@
|
||||
<string name="http_status_507">Onvoldoende opslagruimte - De server heeft niet genoeg opslagruimte om het verzoek succesvol te verwerken</string>
|
||||
<string name="http_status_508">Loop gedetecteerd - De server detecteert een oneindige loop tijdens het verwerken van het verzoek</string>
|
||||
<string name="http_status_511">Netwerk authenticatie vereist - De client moet geauthenticeerd zijn om toegang te krijgen tot het netwerk</string>
|
||||
<string name="add_a_nip96_server">Voeg een NIP-96 Server toe</string>
|
||||
<string name="delete_all">Alles verwijderen</string>
|
||||
<string name="delete_all_drafts_confirmation">Weet u zeker dat u alle concepten wilt verwijderen?</string>
|
||||
</resources>
|
||||
|
||||
@@ -801,4 +801,6 @@
|
||||
<string name="http_status_508">Loop detectado - O servidor detecta um loop infinito enquanto processa a requisição</string>
|
||||
<string name="http_status_511">Autenticação de rede necessária - O cliente deve ser autenticado para acessar a rede</string>
|
||||
<string name="add_a_nip96_server">Adicionar um servidor NIP-96</string>
|
||||
<string name="delete_all">Apagar tudo</string>
|
||||
<string name="delete_all_drafts_confirmation">Você tem certeza de que deseja excluir todos os rascunhos?</string>
|
||||
</resources>
|
||||
|
||||
@@ -800,4 +800,6 @@
|
||||
<string name="http_status_508">Loop upptäckt - Servern upptäcker en oändlig loop medan den bearbetar begäran</string>
|
||||
<string name="http_status_511">Nätverksautentisering krävs - Klienten måste autentiseras för att få tillgång till nätverket</string>
|
||||
<string name="add_a_nip96_server">Lägg till en NIP-96 Server</string>
|
||||
<string name="delete_all">Radera alla</string>
|
||||
<string name="delete_all_drafts_confirmation">Är du säker på att du vill ta bort alla utkast?</string>
|
||||
</resources>
|
||||
|
||||
@@ -800,4 +800,6 @@
|
||||
<string name="http_status_508">พบลูป - เซิร์ฟเวอร์ตรวจพบการวนลูปไม่สิ้นสุดขณะประมวลผลคำขอ</string>
|
||||
<string name="http_status_511">ต้องการการตรวจสอบเครือข่าย - ไคลเอนต์ต้องได้รับการตรวจสอบเพื่อเข้าถึงเครือข่าย</string>
|
||||
<string name="add_a_nip96_server">เพิ่ม เซิร์ฟเวอร์ NIP-96</string>
|
||||
<string name="delete_all">ลบทั้งหมด</string>
|
||||
<string name="delete_all_drafts_confirmation">คุณแน่ใจหรือไม่ว่าต้องการลบแบบร่างทั้งหมด</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
class Filter(
|
||||
val ids: List<String>? = null,
|
||||
val authors: List<String>? = null,
|
||||
val kinds: List<Int>? = null,
|
||||
val tags: Map<String, List<String>>? = null,
|
||||
val since: Map<String, EOSETime>? = null,
|
||||
val until: Long? = null,
|
||||
val limit: Int? = null,
|
||||
val search: String? = null,
|
||||
) {
|
||||
fun toJson(forRelay: String? = null): String {
|
||||
val factory = Event.mapper.nodeFactory
|
||||
val filter =
|
||||
factory.objectNode().apply {
|
||||
ids?.run {
|
||||
replace(
|
||||
"ids",
|
||||
factory.arrayNode(ids.size).apply { ids.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
authors?.run {
|
||||
replace(
|
||||
"authors",
|
||||
factory.arrayNode(authors.size).apply { authors.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
kinds?.run {
|
||||
replace(
|
||||
"kinds",
|
||||
factory.arrayNode(kinds.size).apply { kinds.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
tags?.run {
|
||||
entries.forEach { kv ->
|
||||
replace(
|
||||
"#${kv.key}",
|
||||
factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
since?.run {
|
||||
if (!isEmpty()) {
|
||||
if (forRelay != null) {
|
||||
val relaySince = get(forRelay)
|
||||
if (relaySince != null) {
|
||||
put("since", relaySince.time)
|
||||
}
|
||||
} else {
|
||||
val jsonObjectSince = factory.objectNode()
|
||||
entries.forEach { sincePairs ->
|
||||
jsonObjectSince.put(sincePairs.key, "${sincePairs.value}")
|
||||
}
|
||||
put("since", jsonObjectSince)
|
||||
}
|
||||
}
|
||||
}
|
||||
until?.run { put("until", until) }
|
||||
limit?.run { put("limit", limit) }
|
||||
search?.run { put("search", search) }
|
||||
}
|
||||
return Event.mapper.writeValueAsString(filter)
|
||||
}
|
||||
|
||||
fun match(
|
||||
event: Event,
|
||||
forRelay: String? = null,
|
||||
): Boolean {
|
||||
if (ids?.any { event.id == it } == false) return false
|
||||
if (kinds?.any { event.kind == it } == false) return false
|
||||
if (authors?.any { event.pubKey == it } == false) return false
|
||||
tags?.forEach { tag ->
|
||||
if (!event.tags.any { it.first() == tag.key && it[1] in tag.value }) return false
|
||||
}
|
||||
if (event.createdAt !in (since?.get(forRelay)?.time ?: Long.MIN_VALUE)..(until ?: Long.MAX_VALUE)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ abstract class NostrDataSource(
|
||||
}
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
private val bundler = BundledUpdate(300, Dispatchers.Default)
|
||||
|
||||
fun invalidateFilters() {
|
||||
bundler.invalidate {
|
||||
|
||||
@@ -30,14 +30,14 @@ object RelayStats {
|
||||
|
||||
fun addBytesReceived(
|
||||
url: String,
|
||||
bytesUsedInMemory: Int,
|
||||
bytesUsedInMemory: Long,
|
||||
) {
|
||||
get(url).addBytesReceived(bytesUsedInMemory)
|
||||
}
|
||||
|
||||
fun addBytesSent(
|
||||
url: String,
|
||||
bytesUsedInMemory: Int,
|
||||
bytesUsedInMemory: Long,
|
||||
) {
|
||||
get(url).addBytesSent(bytesUsedInMemory)
|
||||
}
|
||||
@@ -102,11 +102,11 @@ class RelayStat(
|
||||
messages.put(debugMessage, debugMessage)
|
||||
}
|
||||
|
||||
fun addBytesReceived(bytesUsedInMemory: Int) {
|
||||
fun addBytesReceived(bytesUsedInMemory: Long) {
|
||||
receivedBytes += bytesUsedInMemory
|
||||
}
|
||||
|
||||
fun addBytesSent(bytesUsedInMemory: Int) {
|
||||
fun addBytesSent(bytesUsedInMemory: Long) {
|
||||
sentBytes += bytesUsedInMemory
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.ammolite.relays
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import java.util.UUID
|
||||
|
||||
data class Subscription(
|
||||
@@ -42,29 +44,76 @@ data class Subscription(
|
||||
typedFilters?.forEachIndexed { index, typedFilter ->
|
||||
val otherFilter = otherFilters?.getOrNull(index) ?: return true
|
||||
|
||||
// Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed.
|
||||
// fast check
|
||||
if (typedFilter.filter.authors?.size != otherFilter.filter.authors?.size ||
|
||||
typedFilter.filter.ids?.size != otherFilter.filter.ids?.size ||
|
||||
typedFilter.filter.tags?.size != otherFilter.filter.tags?.size ||
|
||||
typedFilter.filter.kinds?.size != otherFilter.filter.kinds?.size ||
|
||||
typedFilter.filter.limit != otherFilter.filter.limit ||
|
||||
typedFilter.filter.search?.length != otherFilter.filter.search?.length ||
|
||||
typedFilter.filter.until != otherFilter.filter.until
|
||||
) {
|
||||
return true
|
||||
if (typedFilter.filter is SincePerRelayFilter && otherFilter.filter is SincePerRelayFilter) {
|
||||
return isDifferent(typedFilter.filter, otherFilter.filter)
|
||||
}
|
||||
|
||||
// deep check
|
||||
if (typedFilter.filter.ids != otherFilter.filter.ids ||
|
||||
typedFilter.filter.authors != otherFilter.filter.authors ||
|
||||
typedFilter.filter.tags != otherFilter.filter.tags ||
|
||||
typedFilter.filter.kinds != otherFilter.filter.kinds ||
|
||||
typedFilter.filter.search != otherFilter.filter.search
|
||||
) {
|
||||
return true
|
||||
if (typedFilter.filter is SinceAuthorPerRelayFilter && otherFilter.filter is SinceAuthorPerRelayFilter) {
|
||||
return isDifferent(typedFilter.filter, otherFilter.filter)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun isDifferent(
|
||||
filter1: SincePerRelayFilter,
|
||||
filter2: SincePerRelayFilter,
|
||||
): Boolean {
|
||||
// Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed.
|
||||
// fast check
|
||||
if (filter1.authors?.size != filter2.authors?.size ||
|
||||
filter1.ids?.size != filter2.ids?.size ||
|
||||
filter1.tags?.size != filter2.tags?.size ||
|
||||
filter1.kinds?.size != filter2.kinds?.size ||
|
||||
filter1.limit != filter2.limit ||
|
||||
filter1.search?.length != filter2.search?.length ||
|
||||
filter1.until != filter2.until
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// deep check
|
||||
if (filter1.ids != filter2.ids ||
|
||||
filter1.authors != filter2.authors ||
|
||||
filter1.tags != filter2.tags ||
|
||||
filter1.kinds != filter2.kinds ||
|
||||
filter1.search != filter2.search
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun isDifferent(
|
||||
filter1: SinceAuthorPerRelayFilter,
|
||||
filter2: SinceAuthorPerRelayFilter,
|
||||
): Boolean {
|
||||
// Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed.
|
||||
// fast check
|
||||
if (filter1.authors?.size != filter2.authors?.size ||
|
||||
filter1.ids?.size != filter2.ids?.size ||
|
||||
filter1.tags?.size != filter2.tags?.size ||
|
||||
filter1.kinds?.size != filter2.kinds?.size ||
|
||||
filter1.limit != filter2.limit ||
|
||||
filter1.search?.length != filter2.search?.length ||
|
||||
filter1.until != filter2.until
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// deep check
|
||||
if (filter1.ids != filter2.ids ||
|
||||
filter1.authors != filter2.authors ||
|
||||
filter1.tags != filter2.tags ||
|
||||
filter1.kinds != filter2.kinds ||
|
||||
filter1.search != filter2.search
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.ammolite.relays
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.filters.IPerRelayFilter
|
||||
|
||||
class TypedFilter(
|
||||
val types: Set<FeedType>,
|
||||
val filter: Filter,
|
||||
val filter: IPerRelayFilter,
|
||||
)
|
||||
|
||||
+4
-1
@@ -18,8 +18,11 @@
|
||||
* 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.ammolite.relays
|
||||
package com.vitorpamplona.ammolite.relays.filters
|
||||
|
||||
/*
|
||||
* Wrapper class to allow changing in EOSE without modifying the list it is included within
|
||||
*/
|
||||
class EOSETime(
|
||||
var time: Long,
|
||||
) {
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
class Filter(
|
||||
val ids: List<String>? = null,
|
||||
val authors: List<String>? = null,
|
||||
val kinds: List<Int>? = null,
|
||||
val tags: Map<String, List<String>>? = null,
|
||||
val since: Long? = null,
|
||||
val until: Long? = null,
|
||||
val limit: Int? = null,
|
||||
val search: String? = null,
|
||||
) {
|
||||
fun toJson() = FilterSerializer.toJson(ids, authors, kinds, tags, since, until, limit, search)
|
||||
|
||||
fun match(event: Event) = FilterMatcher.match(event, ids, authors, kinds, tags, since, until)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
object FilterMatcher {
|
||||
fun match(
|
||||
event: Event,
|
||||
ids: List<String>? = null,
|
||||
authors: List<String>? = null,
|
||||
kinds: List<Int>? = null,
|
||||
tags: Map<String, List<String>>? = null,
|
||||
since: Long? = null,
|
||||
until: Long? = null,
|
||||
): Boolean {
|
||||
if (ids?.contains(event.id) == false) return false
|
||||
if (kinds?.contains(event.kind) == false) return false
|
||||
if (authors?.contains(event.pubKey) == false) return false
|
||||
tags?.forEach { tag ->
|
||||
if (!event.tags.any { it.first() == tag.key && it[1] in tag.value }) return false
|
||||
}
|
||||
if (event.createdAt !in (since ?: Long.MIN_VALUE)..(until ?: Long.MAX_VALUE)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
object FilterSerializer {
|
||||
fun toJsonObject(
|
||||
ids: List<String>? = null,
|
||||
authors: List<String>? = null,
|
||||
kinds: List<Int>? = null,
|
||||
tags: Map<String, List<String>>? = null,
|
||||
since: Long? = null,
|
||||
until: Long? = null,
|
||||
limit: Int? = null,
|
||||
search: String? = null,
|
||||
): ObjectNode {
|
||||
val factory = Event.mapper.nodeFactory
|
||||
return factory.objectNode().apply {
|
||||
ids?.run {
|
||||
replace(
|
||||
"ids",
|
||||
factory.arrayNode(ids.size).apply { ids.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
authors?.run {
|
||||
replace(
|
||||
"authors",
|
||||
factory.arrayNode(authors.size).apply { authors.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
kinds?.run {
|
||||
replace(
|
||||
"kinds",
|
||||
factory.arrayNode(kinds.size).apply { kinds.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
tags?.run {
|
||||
entries.forEach { kv ->
|
||||
replace(
|
||||
"#${kv.key}",
|
||||
factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
since?.run { put("since", since) }
|
||||
until?.run { put("until", until) }
|
||||
limit?.run { put("limit", limit) }
|
||||
search?.run { put("search", search) }
|
||||
}
|
||||
}
|
||||
|
||||
fun toJson(
|
||||
ids: List<String>? = null,
|
||||
authors: List<String>? = null,
|
||||
kinds: List<Int>? = null,
|
||||
tags: Map<String, List<String>>? = null,
|
||||
since: Long? = null,
|
||||
until: Long? = null,
|
||||
limit: Int? = null,
|
||||
search: String? = null,
|
||||
): String =
|
||||
Event.mapper.writeValueAsString(
|
||||
toJsonObject(
|
||||
ids,
|
||||
authors,
|
||||
kinds,
|
||||
tags,
|
||||
since,
|
||||
until,
|
||||
limit,
|
||||
search,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
interface IPerRelayFilter {
|
||||
fun toJson(forRelay: String): String
|
||||
|
||||
fun match(
|
||||
event: Event,
|
||||
forRelay: String,
|
||||
): Boolean
|
||||
|
||||
fun toDebugJson(): String
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
/**
|
||||
* This is a nostr filter with per-relay authors list and since parameters
|
||||
*/
|
||||
class SinceAuthorPerRelayFilter(
|
||||
val ids: List<String>? = null,
|
||||
val authors: Map<String, List<String>>? = null,
|
||||
val kinds: List<Int>? = null,
|
||||
val tags: Map<String, List<String>>? = null,
|
||||
val since: Map<String, EOSETime>? = null,
|
||||
val until: Long? = null,
|
||||
val limit: Int? = null,
|
||||
val search: String? = null,
|
||||
) : IPerRelayFilter {
|
||||
override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||
|
||||
override fun match(
|
||||
event: Event,
|
||||
forRelay: String,
|
||||
) = FilterMatcher.match(event, ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until)
|
||||
|
||||
override fun toDebugJson(): String {
|
||||
val factory = Event.mapper.nodeFactory
|
||||
val obj = FilterSerializer.toJsonObject(ids, null, kinds, tags, null, until, limit, search)
|
||||
authors?.run {
|
||||
if (isNotEmpty()) {
|
||||
val jsonObjectPerRelayAuthors = factory.objectNode()
|
||||
entries.forEach { relayAuthorPairs ->
|
||||
jsonObjectPerRelayAuthors.put(relayAuthorPairs.key, factory.arrayNode(relayAuthorPairs.value.size).apply { relayAuthorPairs.value.forEach { add(it) } })
|
||||
}
|
||||
obj.put("authors", jsonObjectPerRelayAuthors)
|
||||
}
|
||||
}
|
||||
|
||||
since?.run {
|
||||
if (isNotEmpty()) {
|
||||
val jsonObjectSince = factory.objectNode()
|
||||
entries.forEach { sincePairs ->
|
||||
jsonObjectSince.put(sincePairs.key, "${sincePairs.value}")
|
||||
}
|
||||
obj.put("since", jsonObjectSince)
|
||||
}
|
||||
}
|
||||
return Event.mapper.writeValueAsString(obj)
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays.filters
|
||||
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
/**
|
||||
* This is a nostr filter with per-relay authors list and since parameters
|
||||
*/
|
||||
class SincePerRelayFilter(
|
||||
val ids: List<String>? = null,
|
||||
val authors: List<String>? = null,
|
||||
val kinds: List<Int>? = null,
|
||||
val tags: Map<String, List<String>>? = null,
|
||||
val since: Map<String, EOSETime>? = null,
|
||||
val until: Long? = null,
|
||||
val limit: Int? = null,
|
||||
val search: String? = null,
|
||||
) : IPerRelayFilter {
|
||||
override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||
|
||||
override fun match(
|
||||
event: Event,
|
||||
forRelay: String,
|
||||
) = FilterMatcher.match(event, ids, authors, kinds, tags, since?.get(forRelay)?.time, until)
|
||||
|
||||
override fun toDebugJson(): String {
|
||||
val factory = Event.mapper.nodeFactory
|
||||
val obj = FilterSerializer.toJsonObject(ids, authors, kinds, tags, null, until, limit, search)
|
||||
|
||||
since?.run {
|
||||
if (isNotEmpty()) {
|
||||
val jsonObjectSince = factory.objectNode()
|
||||
entries.forEach { sincePairs ->
|
||||
jsonObjectSince.put(sincePairs.key, "${sincePairs.value}")
|
||||
}
|
||||
obj.put("since", jsonObjectSince)
|
||||
}
|
||||
}
|
||||
return Event.mapper.writeValueAsString(obj)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.Nip01Serializer
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventFactory
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Rule
|
||||
@@ -246,4 +248,13 @@ class EventBenchmark {
|
||||
serializer.escapeStringInto(specialEncoders, Nip01Serializer.BufferedDigestWriter(MessageDigest.getInstance("SHA-256")))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eventFactoryPerformanceTest() {
|
||||
val now = TimeUtils.now()
|
||||
val tags = arrayOf(arrayOf(""))
|
||||
benchmarkRule.measureRepeated {
|
||||
EventFactory.create("id", "pubkey", now, 1, tags, "content", "sig")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-46
@@ -42,6 +42,47 @@ import java.util.regex.Pattern
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class RichTextParser {
|
||||
fun createImageContent(
|
||||
fullUrl: String,
|
||||
eventTags: ImmutableListOfLists<String>,
|
||||
description: String?,
|
||||
callbackUri: String? = null,
|
||||
): MediaUrlImage {
|
||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||
val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists)
|
||||
|
||||
return MediaUrlImage(
|
||||
url = fullUrl,
|
||||
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
|
||||
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
|
||||
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
|
||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||
uri = callbackUri,
|
||||
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||
)
|
||||
}
|
||||
|
||||
fun createVideoContent(
|
||||
fullUrl: String,
|
||||
eventTags: ImmutableListOfLists<String>,
|
||||
description: String?,
|
||||
callbackUri: String? = null,
|
||||
): MediaUrlVideo {
|
||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||
val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists)
|
||||
return MediaUrlVideo(
|
||||
url = fullUrl,
|
||||
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
|
||||
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
|
||||
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
|
||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||
uri = callbackUri,
|
||||
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||
)
|
||||
}
|
||||
|
||||
fun parseMediaUrl(
|
||||
fullUrl: String,
|
||||
eventTags: ImmutableListOfLists<String>,
|
||||
@@ -50,50 +91,17 @@ class RichTextParser {
|
||||
): MediaUrlContent? {
|
||||
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
|
||||
return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||
val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists)
|
||||
|
||||
MediaUrlImage(
|
||||
url = fullUrl,
|
||||
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
|
||||
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
|
||||
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
|
||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||
uri = callbackUri,
|
||||
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||
)
|
||||
createImageContent(fullUrl, eventTags, description, callbackUri)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||
val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists)
|
||||
MediaUrlVideo(
|
||||
url = fullUrl,
|
||||
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
|
||||
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
|
||||
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
|
||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||
uri = callbackUri,
|
||||
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||
)
|
||||
createVideoContent(fullUrl, eventTags, description, callbackUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBase64Images(content: String): LinkedHashSet<String> {
|
||||
val regex = "data:image/(${imageExtensions.joinToString(separator = "|") { it } });base64,[a-zA-Z0-9+/]+={0,2}"
|
||||
val pattern = Pattern.compile(regex)
|
||||
val matcher = pattern.matcher(content)
|
||||
|
||||
val base64Images = mutableListOf<String>()
|
||||
|
||||
// Find all matches and add them to the list
|
||||
while (matcher.find()) {
|
||||
base64Images.add(matcher.group())
|
||||
}
|
||||
|
||||
return base64Images.mapTo(LinkedHashSet(base64Images.size)) { it }
|
||||
private fun checkBase64(content: String): Boolean {
|
||||
val matcher = base64contentPattern.matcher(content)
|
||||
return matcher.find()
|
||||
}
|
||||
|
||||
fun parseValidUrls(content: String): LinkedHashSet<String> {
|
||||
@@ -129,16 +137,23 @@ class RichTextParser {
|
||||
|
||||
val imagesForPager =
|
||||
urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags, content, callbackUri) }.associateBy { it.url }
|
||||
val imageList = imagesForPager.values.toList()
|
||||
|
||||
val emojiMap = Nip30CustomEmoji.createEmojiMap(tags)
|
||||
|
||||
val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags)
|
||||
|
||||
val base64Images = segments.map { it.words.filterIsInstance<Base64Segment>() }.flatten()
|
||||
|
||||
val imagesForPagerWithBase64 =
|
||||
imagesForPager +
|
||||
base64Images
|
||||
.map { createImageContent(it.segmentText, tags, content, callbackUri) }
|
||||
.associateBy { it.url }
|
||||
|
||||
return RichTextViewerState(
|
||||
urlSet.toImmutableSet(),
|
||||
imagesForPager.toImmutableMap(),
|
||||
imageList.toImmutableList(),
|
||||
imagesForPagerWithBase64.toImmutableMap(),
|
||||
imagesForPagerWithBase64.values.toImmutableList(),
|
||||
emojiMap.toImmutableMap(),
|
||||
segments,
|
||||
)
|
||||
@@ -218,11 +233,8 @@ class RichTextParser {
|
||||
): Segment {
|
||||
if (word.isEmpty()) return RegularTextSegment(word)
|
||||
|
||||
if (word.startsWith("data:image")) {
|
||||
val base64Images = parseBase64Images(word)
|
||||
if (base64Images.isNotEmpty()) {
|
||||
return Base64Segment(word)
|
||||
}
|
||||
if (word.startsWith("data:image/")) {
|
||||
if (checkBase64(word)) return Base64Segment(word)
|
||||
}
|
||||
|
||||
if (images.contains(word)) return ImageSegment(word)
|
||||
@@ -338,6 +350,8 @@ class RichTextParser {
|
||||
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif")
|
||||
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8")
|
||||
|
||||
val base64contentPattern = Pattern.compile("data:image/(${imageExtensions.joinToString(separator = "|") { it } });base64,([a-zA-Z0-9+/]+={0,2})")
|
||||
|
||||
val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)")
|
||||
val hashTagsPattern: Pattern =
|
||||
Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.crypto
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
class KeyPair(
|
||||
@@ -29,6 +30,7 @@ class KeyPair(
|
||||
) {
|
||||
val privKey: ByteArray?
|
||||
val pubKey: ByteArray
|
||||
val pubKeyHex: HexKey
|
||||
|
||||
init {
|
||||
if (privKey == null) {
|
||||
@@ -52,6 +54,8 @@ class KeyPair(
|
||||
this.pubKey = pubKey
|
||||
}
|
||||
}
|
||||
|
||||
this.pubKeyHex = this.pubKey.toHexKey().intern()
|
||||
}
|
||||
|
||||
override fun toString(): String = "KeyPair(privateKey=${privKey?.toHexKey()}, publicKey=${pubKey.toHexKey()}"
|
||||
|
||||
@@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
data class ATag(
|
||||
@@ -30,6 +32,13 @@ data class ATag(
|
||||
val dTag: String,
|
||||
val relay: String?,
|
||||
) {
|
||||
fun countMemory(): Long =
|
||||
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
|
||||
8L + // kind
|
||||
pubKeyHex.bytesUsedInMemory() +
|
||||
dTag.bytesUsedInMemory() +
|
||||
(relay?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
|
||||
|
||||
fun toNAddr(): String =
|
||||
|
||||
@@ -27,6 +27,8 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
@Stable
|
||||
@@ -58,6 +60,28 @@ class AppMetadata {
|
||||
@Transient
|
||||
var tags: ImmutableListOfLists<String>? = null
|
||||
|
||||
fun countMemory(): Long =
|
||||
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) +
|
||||
(cashuAccepted?.bytesUsedInMemory() ?: 0L) +
|
||||
(encryptionSupported?.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) +
|
||||
(twitter?.bytesUsedInMemory() ?: 0L) +
|
||||
(tags?.lists?.sumOf { it.sumOf { it.bytesUsedInMemory() } } ?: 0L)
|
||||
|
||||
fun anyName(): String? = displayName ?: name ?: username
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
@@ -108,6 +132,8 @@ class AppDefinitionEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun countMemory(): Long = super.countMemory() + (cachedMetadata?.countMemory() ?: 8L)
|
||||
|
||||
@Transient private var cachedMetadata: AppMetadata? = null
|
||||
|
||||
fun appMetaData() =
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
@@ -37,6 +39,10 @@ class ChannelListEvent(
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateEventCache: ImmutableSet<HexKey>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
32 + (publicAndPrivateEventCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0L) // rough calculation
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun publicAndPrivateEvents(
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@@ -39,6 +40,10 @@ class CommunityListEvent(
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateEventCache: ImmutableSet<ATag>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
32 + (publicAndPrivateEventCache?.sumOf { pointerSizeInBytes + it.countMemory() } ?: 0L) // rough calculation
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun publicAndPrivateEvents(
|
||||
|
||||
@@ -46,40 +46,35 @@ class ContactListEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
// This function is only used by the user logged in
|
||||
// But it is used all the time.
|
||||
|
||||
@delegate:Transient
|
||||
val verifiedFollowKeySet: Set<HexKey> by lazy {
|
||||
tags.mapNotNullTo(HashSet()) {
|
||||
try {
|
||||
if (it.size > 1 && it[0] == "p") {
|
||||
/**
|
||||
* Returns a list of p-tags that are verified as hex keys.
|
||||
*/
|
||||
fun verifiedFollowKeySet(): Set<HexKey> =
|
||||
tags.mapNotNullTo(mutableSetOf()) {
|
||||
if (it.size > 1 && it[0] == "p") {
|
||||
try {
|
||||
decodePublicKey(it[1]).toHexKey()
|
||||
} else {
|
||||
} catch (e: Exception) {
|
||||
Log.w("ContactListEvent", "Can't parse p-tag $it in the contact list of $pubKey with id $id", e)
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
val verifiedFollowTagSet: Set<String> by lazy {
|
||||
unverifiedFollowTagSet().map { it.lowercase() }.toSet()
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
val verifiedFollowGeohashSet: Set<String> by lazy {
|
||||
unverifiedFollowGeohashSet().map { it.lowercase() }.toSet()
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
val verifiedFollowCommunitySet: Set<String> by lazy { unverifiedFollowAddressSet().toSet() }
|
||||
|
||||
@delegate:Transient
|
||||
val verifiedFollowKeySetAndMe: Set<HexKey> by lazy { verifiedFollowKeySet + pubKey }
|
||||
/**
|
||||
* Returns a list of a-tags that are verified as correct.
|
||||
*/
|
||||
fun verifiedFollowAddressSet(): Set<HexKey> =
|
||||
tags
|
||||
.mapNotNullTo(mutableSetOf()) {
|
||||
if (it.size > 1 && it[0] == "a") {
|
||||
ATag.parse(it[1], null)?.toTag()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun unverifiedFollowKeySet() = tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
@@ -105,7 +100,7 @@ class ContactListEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun followsTags() = tags.filter { it.size > 1 && it[0] == "t" }.mapNotNull { it.getOrNull(1) }
|
||||
fun followsTags() = hashtags()
|
||||
|
||||
fun relays(): Map<String, ReadWrite>? =
|
||||
try {
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class DraftEvent(
|
||||
@@ -37,6 +38,10 @@ class DraftEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
32 + (cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) })
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun isDeleted() = content == ""
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import java.math.BigDecimal
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -60,10 +61,11 @@ open class Event(
|
||||
override fun isContentEncoded() = false
|
||||
|
||||
override fun countMemory(): Long =
|
||||
12L +
|
||||
7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
|
||||
12L + // createdAt + kind
|
||||
id.bytesUsedInMemory() +
|
||||
pubKey.bytesUsedInMemory() +
|
||||
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
|
||||
tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } +
|
||||
content.bytesUsedInMemory() +
|
||||
sig.bytesUsedInMemory()
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
class EventFactory {
|
||||
companion object {
|
||||
val additionalFactories =
|
||||
mutableMapOf(
|
||||
WikiNoteEvent.KIND to ::WikiNoteEvent,
|
||||
)
|
||||
|
||||
fun create(
|
||||
id: String,
|
||||
pubKey: String,
|
||||
@@ -141,7 +146,13 @@ class EventFactory {
|
||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
else -> Event(id, pubKey, createdAt, kind, tags, content, sig)
|
||||
else -> {
|
||||
additionalFactories[kind]?.let {
|
||||
return it(id, pubKey, createdAt, tags, content, sig)
|
||||
}
|
||||
|
||||
Event(id, pubKey, createdAt, kind, tags, content, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class GalleryListEvent(
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
var tags = arrayOf(tagName, url, eventid)
|
||||
val tags = arrayOf(tagName, url, eventid)
|
||||
if (relay != null) {
|
||||
tags + relay
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import java.util.HashSet
|
||||
@@ -42,6 +44,10 @@ abstract class GeneralListEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun category() = dTag()
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class GiftWrapEvent(
|
||||
@@ -38,6 +39,10 @@ class GiftWrapEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
32 + (cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) }) // rough calculation
|
||||
|
||||
fun copyNoContent(): GiftWrapEvent {
|
||||
val copy =
|
||||
GiftWrapEvent(
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class LnZapEvent(
|
||||
@@ -38,6 +39,10 @@ class LnZapEvent(
|
||||
// This event is also kept in LocalCache (same object)
|
||||
@Transient val zapRequest: LnZapRequestEvent?
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (zapRequest?.countMemory() ?: 0) // rough calculation
|
||||
|
||||
override fun containedPost(): LnZapRequestEvent? =
|
||||
try {
|
||||
description()?.ifBlank { null }?.let { fromJson(it) } as? LnZapRequestEvent
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class LnZapPaymentRequestEvent(
|
||||
@@ -42,6 +44,10 @@ class LnZapPaymentRequestEvent(
|
||||
// Once one of an app user decrypts the payment, all users else can see it.
|
||||
@Transient private var lnInvoice: String? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (lnInvoice?.bytesUsedInMemory() ?: 0) // rough calculation
|
||||
|
||||
fun walletServicePubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) walletServicePubKey() ?: pubKey else pubKey
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class LnZapPaymentResponseEvent(
|
||||
@@ -42,6 +44,8 @@ class LnZapPaymentResponseEvent(
|
||||
// Once one of an app user decrypts the payment, all users else can see it.
|
||||
@Transient private var response: Response? = null
|
||||
|
||||
override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (response?.countMemory() ?: 0)
|
||||
|
||||
fun requestAuthor() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
fun requestId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
@@ -91,7 +95,9 @@ class LnZapPaymentResponseEvent(
|
||||
// RESPONSE OBJECTS
|
||||
abstract class Response(
|
||||
@JsonProperty("result_type") val resultType: String,
|
||||
)
|
||||
) {
|
||||
abstract fun countMemory(): Long
|
||||
}
|
||||
|
||||
// PayInvoice Call
|
||||
|
||||
@@ -100,7 +106,11 @@ class PayInvoiceSuccessResponse(
|
||||
) : Response("pay_invoice") {
|
||||
class PayInvoiceResultParams(
|
||||
val preimage: String,
|
||||
)
|
||||
) {
|
||||
fun countMemory(): Long = pointerSizeInBytes + preimage.bytesUsedInMemory()
|
||||
}
|
||||
|
||||
override fun countMemory(): Long = pointerSizeInBytes + (result?.countMemory() ?: 0)
|
||||
}
|
||||
|
||||
class PayInvoiceErrorResponse(
|
||||
@@ -109,7 +119,11 @@ class PayInvoiceErrorResponse(
|
||||
class PayInvoiceErrorParams(
|
||||
val code: ErrorType?,
|
||||
val message: String?,
|
||||
)
|
||||
) {
|
||||
fun countMemory(): Long = pointerSizeInBytes + pointerSizeInBytes + (message?.bytesUsedInMemory() ?: 0)
|
||||
}
|
||||
|
||||
override fun countMemory(): Long = pointerSizeInBytes + (error?.countMemory() ?: 0)
|
||||
|
||||
enum class ErrorType {
|
||||
@JsonProperty(value = "RATE_LIMITED")
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import java.nio.charset.Charset
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.BadPaddingException
|
||||
@@ -47,6 +48,8 @@ class LnZapRequestEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var privateZapEvent: LnZapPrivateEvent? = null
|
||||
|
||||
override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (privateZapEvent?.countMemory() ?: 0)
|
||||
|
||||
fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||
|
||||
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
@@ -39,6 +41,11 @@ class MuteListEvent(
|
||||
|
||||
@Transient var publicAndPrivateWordCache: ImmutableSet<String>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (publicAndPrivateUserCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) +
|
||||
pointerSizeInBytes + (publicAndPrivateWordCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0)
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun publicAndPrivateUsersAndWords(
|
||||
|
||||
+6
@@ -26,6 +26,8 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class NIP90ContentDiscoveryResponseEvent(
|
||||
@@ -38,6 +40,10 @@ class NIP90ContentDiscoveryResponseEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var events: List<HexKey>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (events?.sumOf { it.bytesUsedInMemory() } ?: 0)
|
||||
|
||||
fun innerTags(): List<HexKey> {
|
||||
if (content.isEmpty()) {
|
||||
return listOf()
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.ots.VerifyResult
|
||||
import com.vitorpamplona.quartz.ots.op.OpSHA256
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.util.Base64
|
||||
|
||||
@@ -48,6 +49,10 @@ class OtsEvent(
|
||||
@Transient
|
||||
var verifiedTime: Long? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + Long.SIZE_BYTES // verifiedTime
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
@@ -39,6 +41,11 @@ class PeopleListEvent(
|
||||
|
||||
@Transient var publicAndPrivateWordCache: ImmutableSet<String>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (publicAndPrivateUserCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0) +
|
||||
pointerSizeInBytes + (publicAndPrivateWordCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0)
|
||||
|
||||
fun publicAndPrivateWords(
|
||||
signer: NostrSigner,
|
||||
onReady: (ImmutableSet<String>) -> Unit,
|
||||
|
||||
@@ -27,6 +27,8 @@ import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
import com.vitorpamplona.quartz.encoders.Nip54InlineMetadata
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
@Immutable
|
||||
@@ -41,6 +43,10 @@ class PrivateDmEvent(
|
||||
ChatroomKeyable {
|
||||
@Transient private var decryptedContent: Map<HexKey, String> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() })
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class PrivateOutboxRelayListEvent(
|
||||
@@ -39,6 +41,10 @@ class PrivateOutboxRelayListEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun relays(): List<String>? =
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class SealedGossipEvent(
|
||||
@@ -39,6 +40,10 @@ class SealedGossipEvent(
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) }
|
||||
|
||||
fun copyNoContent(): SealedGossipEvent {
|
||||
val copy =
|
||||
SealedGossipEvent(
|
||||
|
||||
+12
-12
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
|
||||
class MinimumRelayListProcessor {
|
||||
class RelayListRecommendationProcessor {
|
||||
companion object {
|
||||
fun transpose(
|
||||
userList: Map<HexKey, MutableSet<String>>,
|
||||
@@ -52,7 +52,7 @@ class MinimumRelayListProcessor {
|
||||
* filter onion and local host from write relays
|
||||
* for each user pubkey, a list of valid relays.
|
||||
*/
|
||||
fun filterValidRelays(
|
||||
private fun filterValidRelays(
|
||||
userList: List<AdvertisedRelayListEvent>,
|
||||
hasOnionConnection: Boolean = false,
|
||||
): MutableMap<HexKey, MutableSet<String>> {
|
||||
@@ -76,16 +76,6 @@ class MinimumRelayListProcessor {
|
||||
return validWriteRelayUrls
|
||||
}
|
||||
|
||||
fun reliableRelaySetFor(
|
||||
userList: List<AdvertisedRelayListEvent>,
|
||||
relayUrlsToIgnore: Set<String> = emptySet(),
|
||||
hasOnionConnection: Boolean = false,
|
||||
): Set<RelayRecommendation> =
|
||||
reliableRelaySetFor(
|
||||
filterValidRelays(userList, hasOnionConnection),
|
||||
relayUrlsToIgnore,
|
||||
)
|
||||
|
||||
fun reliableRelaySetFor(
|
||||
usersAndRelays: MutableMap<HexKey, MutableSet<String>>,
|
||||
relayUrlsToIgnore: Set<String> = emptySet(),
|
||||
@@ -154,6 +144,16 @@ class MinimumRelayListProcessor {
|
||||
|
||||
return returningSet
|
||||
}
|
||||
|
||||
fun reliableRelaySetFor(
|
||||
userList: List<AdvertisedRelayListEvent>,
|
||||
relayUrlsToIgnore: Set<String> = emptySet(),
|
||||
hasOnionConnection: Boolean = false,
|
||||
): Set<RelayRecommendation> =
|
||||
reliableRelaySetFor(
|
||||
filterValidRelays(userList, hasOnionConnection),
|
||||
relayUrlsToIgnore,
|
||||
)
|
||||
}
|
||||
|
||||
class RelayRecommendation(
|
||||
@@ -22,7 +22,11 @@ package com.vitorpamplona.quartz.utils
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
fun String.bytesUsedInMemory(): Int = (8 * ((((this.length) * 2) + 45) / 8))
|
||||
val pointerSizeInBytes = 4
|
||||
|
||||
fun String.bytesUsedInMemory(): Long = (8 * (((this.length * 2L) + 45) / 8))
|
||||
|
||||
fun Boolean.bytesUsedInMemory(): Long = 8
|
||||
|
||||
fun String.containsIgnoreCase(term: String): Boolean {
|
||||
if (term.isEmpty()) return true // Empty string is contained
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class MinimumRelayListProcessorTest {
|
||||
class RelayListRecommendationProcessorTest {
|
||||
val userList =
|
||||
mutableMapOf(
|
||||
"User1" to mutableSetOf("wss://relay1.com", "wss://relay2.com", "wss://relay3.com"),
|
||||
@@ -44,13 +44,13 @@ class MinimumRelayListProcessorTest {
|
||||
"wss://relay5.com" to listOf("User2"),
|
||||
"wss://relay6.com" to listOf("User2", "User3"),
|
||||
).toString(),
|
||||
MinimumRelayListProcessor.transpose(userList).toString(),
|
||||
RelayListRecommendationProcessor.transpose(userList).toString(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProcessor() {
|
||||
val recommendations = MinimumRelayListProcessor.reliableRelaySetFor(userList).toList()
|
||||
val recommendations = RelayListRecommendationProcessor.reliableRelaySetFor(userList).toList()
|
||||
|
||||
val rec1 = recommendations[0]
|
||||
|
||||
Reference in New Issue
Block a user