Merge branch 'main' of https://github.com/vitorpamplona/amethyst into vitorpamplona-main

merging upstream changes.
This commit is contained in:
KotlinGeekDev
2024-10-31 19:36:20 +01:00
56 changed files with 1092 additions and 550 deletions
+3 -1
View File
@@ -30,4 +30,6 @@ If applicable, add a video and/or screenshots to help explain your problem.
- Amber Version (if using it to sign):
**Bounty (in Bitcoin sats) offered for a solution**
Incentivize developers to work on your issue. Describe clear milestones to claim payment.
The size of the bounty is proportional to how much this matters to you. If no bounty is offered,
not even a small one, this bug will not be worked on because it doesn't matter to you. We prioritize
bug fixing to issues that have bounties, even small ones.
+3 -7
View File
@@ -7,14 +7,10 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Bounty (in Bitcoin sats) offered for the implementation**
Incentivize developers to work on your feature. Describe clear milestones to claim payment.
**Additional context**
Add any other context, video, or screenshots about the feature request here.
The size of the bounty is proportional to how much this matters to you. If no bounty is offered,
not even a small one, this feature will not be coded because it doesn't actually matter to you.
We prioritize feature development by its bounty size compared to how much work is required to get it done.
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.0" />
<option name="version" value="2.0.20" />
</component>
</project>
@@ -32,6 +32,7 @@ import androidx.security.crypto.EncryptedSharedPreferences
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.playback.VideoCache
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CoroutineScope
@@ -50,6 +51,7 @@ class Amethyst : Application() {
// Service Manager is only active when the activity is active.
val serviceManager = ServiceManager(applicationIOScope)
val locationManager = LocationState(this, applicationIOScope)
override fun onTerminate() {
super.onTerminate()
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import android.location.Location
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -28,6 +29,8 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.fasterxml.jackson.module.kotlin.readValue
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
@@ -120,10 +123,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
@@ -160,17 +161,19 @@ class Account(
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
@Immutable
class LiveFollowLists(
val users: Set<String> = emptySet(),
val usersPlusMe: Set<String>,
class LiveFollowList(
val authors: Set<String> = emptySet(),
val authorsPlusMe: Set<String>,
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
val addresses: Set<String> = emptySet(),
)
class ListNameNotePair(
class FeedsBaseFlows(
val listName: String,
val event: GeneralListEvent?,
val peopleList: StateFlow<NoteState> = MutableStateFlow(NoteState(Note(" "))),
val kind3: StateFlow<Account.LiveFollowList?> = MutableStateFlow(null),
val location: StateFlow<Location?> = MutableStateFlow(null),
)
val connectToRelaysFlow =
@@ -218,7 +221,7 @@ class Account(
localRelayList: Set<String>,
): List<RelaySetupInfo> {
val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
val searchRelaySet = (searchRelayEvent?.relays() ?: Constants.defaultSearchRelaySet).map { RelayUrlFormatter.normalize(it) }.toSet()
val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet()
val nip65RelaySet =
nip65RelayEvent?.relays()?.map {
AdvertisedRelayListEvent.AdvertisedRelayInfo(
@@ -465,20 +468,23 @@ class Account(
}.toTypedArray(),
)
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowLists {
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
// makes sure the output include only valid p tags
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
return LiveFollowLists(
verifiedFollowingUsers,
verifiedFollowingUsers + signer.pubKey,
return LiveFollowList(
authors = verifiedFollowingUsers,
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
hashtags =
latestContactList
?.unverifiedFollowTagSet()
?.map { it.lowercase() }
?.toSet() ?: emptySet(),
geotags =
latestContactList
?.unverifiedFollowGeohashSet()
?.toSet() ?: emptySet(),
addresses =
latestContactList
?.verifiedFollowAddressSet()
?.toSet() ?: emptySet(),
@@ -516,7 +522,7 @@ class Account(
)
@OptIn(ExperimentalCoroutinesApi::class)
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
val liveKind3FollowsFlow: Flow<LiveFollowList> =
userProfile().flow().follows.stateFlow.transformLatest {
checkNotInMainThread()
emit(buildFollowLists(it.user.latestContactList))
@@ -531,80 +537,102 @@ class Account(
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
)
@OptIn(ExperimentalCoroutinesApi::class)
private val liveHomeList: Flow<ListNameNotePair> =
settings.defaultHomeFollowList.flatMapLatest { listName ->
loadPeopleListFlowFromListName(listName)
}
fun peopleListFromListNameStarter(listName: String): ListNameNotePair =
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
fun loadFlowsFor(listName: String): FeedsBaseFlows =
when (listName) {
GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
AROUND_ME ->
FeedsBaseFlows(
listName,
location = Amethyst.instance.locationManager.locationStateFlow,
)
else -> {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
val noteEvent = note?.event as? GeneralListEvent
ListNameNotePair(listName, noteEvent)
if (note != null) {
FeedsBaseFlows(
listName,
peopleList =
note
.flow()
.metadata.stateFlow,
)
} else {
ListNameNotePair(listName, null)
FeedsBaseFlows(listName)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
note?.flow()?.metadata?.stateFlow?.mapLatest {
val noteEvent = it.note.event as? GeneralListEvent
ListNameNotePair(listName, noteEvent)
} ?: MutableStateFlow(ListNameNotePair(listName, null))
} else {
MutableStateFlow(ListNameNotePair(listName, null))
}
suspend fun combinePeopleList(
kind3Follows: LiveFollowLists,
peopleListFollows: ListNameNotePair,
): LiveFollowLists? =
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
suspend fun mapIntoFollowLists(
listName: String,
kind3: LiveFollowList?,
noteState: NoteState,
location: Location?,
): LiveFollowList? =
if (listName == GLOBAL_FOLLOWS) {
null
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
kind3Follows
} else if (peopleListFollows.event == null) {
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
} else if (listName == KIND3_FOLLOWS) {
kind3
} else if (listName == AROUND_ME) {
val hash = location?.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits)
if (hash != null) {
// 2 neighbors deep = 25x25km
val hashes =
listOf(hash.toString()) +
hash.adjacent
.map { listOf(it.toString()) + it.adjacent.map { it.toString() } }
.flatten()
.distinct()
LiveFollowList(
authorsPlusMe = setOf(signer.pubKey),
geotags = hashes.toSet(),
)
} else {
val result = waitToDecrypt(peopleListFollows.event)
if (result == null) {
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
}
} else {
result
val peopleList = noteState.note.event as? GeneralListEvent
if (peopleList != null) {
waitToDecrypt(peopleList) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
} else {
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
}
}
fun combinePeopleListFlows(
kind3FollowsSource: Flow<LiveFollowLists>,
peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> =
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
checkNotInMainThread()
emit(combinePeopleList(kind3Follows, peopleListFollows))
@OptIn(ExperimentalCoroutinesApi::class)
fun combinePeopleListFlows(peopleListFollowsSource: Flow<String>): Flow<LiveFollowList?> =
peopleListFollowsSource
.transformLatest { listName ->
val followList = loadFlowsFor(listName)
emitAll(
combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location ->
mapIntoFollowLists(followList.listName, kind3, peopleList, location)
},
)
}
val liveHomeFollowListFlow: Flow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3Follows, liveHomeList)
}
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
liveHomeFollowListFlow
val liveHomeFollowLists: StateFlow<LiveFollowList?> by lazy {
combinePeopleListFlows(settings.defaultHomeFollowList)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking {
combinePeopleList(
liveKind3Follows.value,
peopleListFromListNameStarter(settings.defaultHomeFollowList.value),
)
loadAndCombineFlows(settings.defaultHomeFollowList.value)
},
)
}
suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
val flows = loadFlowsFor(listName)
return mapIntoFollowLists(
flows.listName,
flows.kind3.value,
flows.peopleList.value,
flows.location.value,
)
}
/**
* filter onion and local host from write relays
* for each user pubkey, a list of valid relays.
@@ -701,7 +729,7 @@ class Account(
liveHomeFollowLists
.transformLatest { followList ->
if (followList != null) {
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
} else {
emit(null)
}
@@ -728,53 +756,33 @@ class Account(
scope,
SharingStarted.Eagerly,
authorsPerRelay(
liveHomeFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
settings.torSettings.torType.value,
).ifEmpty { null },
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
settings.defaultNotificationFollowList.flatMapLatest { listName ->
loadPeopleListFlowFromListName(listName)
}
}
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
val liveNotificationFollowLists: StateFlow<LiveFollowList?> by lazy {
combinePeopleListFlows(settings.defaultNotificationFollowList)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking {
combinePeopleList(
liveKind3Follows.value,
peopleListFromListNameStarter(settings.defaultNotificationFollowList.value),
)
loadAndCombineFlows(settings.defaultNotificationFollowList.value)
},
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
settings.defaultStoriesFollowList.flatMapLatest { listName ->
loadPeopleListFlowFromListName(listName)
}
}
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
val liveStoriesFollowLists: StateFlow<LiveFollowList?> by lazy {
combinePeopleListFlows(settings.defaultStoriesFollowList)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking {
combinePeopleList(
liveKind3Follows.value,
peopleListFromListNameStarter(settings.defaultStoriesFollowList.value),
)
loadAndCombineFlows(settings.defaultStoriesFollowList.value)
},
)
}
@@ -784,7 +792,7 @@ class Account(
liveStoriesFollowLists
.transformLatest { followList ->
if (followList != null) {
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
} else {
emit(null)
}
@@ -811,31 +819,21 @@ class Account(
scope,
SharingStarted.Eagerly,
authorsPerRelay(
liveStoriesFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
liveStoriesFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
settings.torSettings.torType.value,
).ifEmpty { null },
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
settings.defaultDiscoveryFollowList.flatMapLatest { listName ->
loadPeopleListFlowFromListName(listName)
}
}
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
val liveDiscoveryFollowLists: StateFlow<LiveFollowList?> by lazy {
combinePeopleListFlows(settings.defaultDiscoveryFollowList)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking {
combinePeopleList(
liveKind3Follows.value,
peopleListFromListNameStarter(settings.defaultDiscoveryFollowList.value),
)
loadAndCombineFlows(settings.defaultDiscoveryFollowList.value)
},
)
}
@@ -845,7 +843,7 @@ class Account(
liveDiscoveryFollowLists
.transformLatest { followList ->
if (followList != null) {
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
} else {
emit(null)
}
@@ -872,7 +870,7 @@ class Account(
scope,
SharingStarted.Eagerly,
authorsPerRelay(
liveDiscoveryFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
liveDiscoveryFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
connectToRelays.value.filter { it.read }.map { it.url },
settings.torSettings.torType.value,
).ifEmpty { null },
@@ -881,19 +879,17 @@ class Account(
private fun decryptLiveFollows(
listEvent: GeneralListEvent,
onReady: (LiveFollowLists) -> Unit,
onReady: (LiveFollowList) -> Unit,
) {
listEvent.privateTags(signer) { privateTagList ->
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
onReady(
LiveFollowLists(
users = users,
usersPlusMe = users + userProfile().pubkeyHex,
hashtags =
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
geotags =
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
communities =
LiveFollowList(
authors = users,
authorsPlusMe = users + userProfile().pubkeyHex,
hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
addresses =
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
.map { it.toTag() }
.toSet(),
@@ -902,7 +898,7 @@ class Account(
}
}
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) {
@@ -3210,7 +3206,7 @@ class Account(
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
flowHiddenUsers.value.spammers.contains(userHex)
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.users
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.authors
fun isAcceptable(user: User): Boolean {
if (userProfile().pubkeyHex == user.pubkeyHex) {
@@ -3268,8 +3264,8 @@ class Account(
}
return (
note.reportsBy(liveKind3Follows.value.usersPlusMe) +
(note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) +
note.reportsBy(liveKind3Follows.value.authorsPlusMe) +
(note.author?.reportsBy(liveKind3Follows.value.authorsPlusMe) ?: emptyList()) +
innerReports
).toSet()
}
@@ -67,9 +67,9 @@ val DefaultNIP65List =
val DefaultDMRelayList =
listOf(
RelayUrlFormatter.normalize("wss://auth.nostr1.com/"),
RelayUrlFormatter.normalize("wss://nostr.mom/"),
RelayUrlFormatter.normalize("wss://nos.lol/"),
RelayUrlFormatter.normalize("wss://auth.nostr1.com"),
RelayUrlFormatter.normalize("wss://relay.0xchat.com"),
RelayUrlFormatter.normalize("wss://nos.lol"),
)
val DefaultSearchRelayList =
@@ -77,6 +77,7 @@ val DefaultSearchRelayList =
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
RelayUrlFormatter.normalize("wss://nostr.wine"),
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
RelayUrlFormatter.normalize("wss://search.nos.today"),
)
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
@@ -85,6 +86,9 @@ val GLOBAL_FOLLOWS = " Global "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val KIND3_FOLLOWS = " All Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val AROUND_ME = " Around Me "
@Stable
class AccountSettings(
val keyPair: KeyPair,
@@ -357,7 +361,6 @@ class AccountSettings(
// Events might be different objects, we have to compare their ids.
if (backupAppSpecificData?.id != appSettings.id) {
println("AABBCC Update App Specific Data")
backupAppSpecificData = appSettings
syncedSettings.updateFrom(newSyncedSettings)
@@ -2043,7 +2043,10 @@ object LocalCache {
}
}
fun findUsersStartingWith(username: String): List<User> {
fun findUsersStartingWith(
username: String,
forAccount: Account?,
): List<User> {
checkNotInMainThread()
val key = decodePublicKeyAsHexOrNull(username)
@@ -2056,26 +2059,19 @@ object LocalCache {
}
return users.filter { _, user: User ->
(
(user.anyNameStartsWith(username)) ||
user.pubkeyHex.startsWith(username, true) ||
user.pubkeyNpub().startsWith(username, true)
) &&
(forAccount == null || (!forAccount.isHidden(user) && !user.containsAny(forAccount.flowHiddenUsers.value.hiddenWordsCase)))
}
}
fun getFollowSetsFor(user: User): List<AddressableNote> {
checkNotInMainThread()
return addressables
.filter { _, note ->
val listEvent = note.event
(
listEvent is PeopleListEvent &&
user.pubkeyHex == listEvent.pubKey
)
}
}
fun findNotesStartingWith(text: String): List<Note> {
fun findNotesStartingWith(
text: String,
forAccount: Account,
): List<Note> {
checkNotInMainThread()
val key = decodeEventIdAsHexOrNull(text)
@@ -2102,11 +2098,19 @@ object LocalCache {
note.idHex.startsWith(text, true) ||
note.idNote().startsWith(text, true)
) {
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true
} else {
return@filter false
}
}
if (note.event?.isContentEncoded() == false) {
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter note.event?.content()?.contains(text, true) ?: false
} else {
return@filter false
}
}
return@filter false
@@ -2125,11 +2129,19 @@ object LocalCache {
if (addressable.event?.matchTag1With(text) == true ||
addressable.idHex.startsWith(text, true)
) {
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true
} else {
return@filter false
}
}
if (addressable.event?.isContentEncoded() == false) {
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter addressable.event?.content()?.contains(text, true) ?: false
} else {
return@filter false
}
}
return@filter false
@@ -771,29 +771,11 @@ open class Note(
return true
}
if (author?.toBestDisplayName()?.containsAny(accountChoices.hiddenWordsCase) == true) {
if (thisEvent.anyHashTag { it.containsAny(accountChoices.hiddenWordsCase) }) {
return true
}
if (author?.profilePicture()?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.banner?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.about?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.lud06?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.lud16?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.containsAny(accountChoices.hiddenWordsCase) == true) return true
}
return false
@@ -45,6 +45,8 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import com.vitorpamplona.quartz.utils.DualCase
import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -371,6 +373,36 @@ class User(
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
} != null
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
if (hiddenWordsCase.isEmpty()) return false
if (toBestDisplayName().containsAny(hiddenWordsCase)) {
return true
}
if (profilePicture()?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.banner?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.about?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud06?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud16?.containsAny(hiddenWordsCase) == true) {
return true
}
return false
}
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var liveSet: UserLiveSet? = null
@@ -26,73 +26,91 @@ import android.location.Geocoder
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.HandlerThread
import android.os.Looper
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class LocationUtil(
context: Context,
class LocationFlow(
private val context: Context,
) {
companion object {
const val MIN_TIME: Long = 1000L
const val MIN_DISTANCE: Float = 0.0f
}
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var locationListener: LocationListener? = null
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
val providerState = mutableStateOf(false)
val isStart: MutableState<Boolean> = mutableStateOf(false)
private val locHandlerThread = HandlerThread("LocationUtil Thread")
init {
locHandlerThread.start()
}
@SuppressLint("MissingPermission")
fun start(
fun get(
minTimeMs: Long = MIN_TIME,
minDistanceM: Float = MIN_DISTANCE,
) {
locationListener().let {
locationListener = it
): Flow<Location> =
callbackFlow {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val locationCallback =
object : LocationListener {
override fun onLocationChanged(location: Location) {
launch { send(location) }
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
Log.d("Location Service", "LocationState Start")
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
minTimeMs,
minDistanceM,
it,
locHandlerThread.looper,
locationCallback,
Looper.getMainLooper(),
)
awaitClose {
locationManager.removeUpdates(locationCallback)
Log.d("Location Service", "LocationState Stop")
}
}
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isStart.value = true
}
fun stop() {
locationListener?.let { locationManager.removeUpdates(it) }
isStart.value = false
class LocationState(
context: Context,
scope: CoroutineScope,
) {
companion object {
const val MIN_TIME: Long = 10000L
const val MIN_DISTANCE: Float = 100.0f
}
private fun locationListener() =
object : LocationListener {
override fun onLocationChanged(location: Location) {
locationStateFlow.value = location
}
private var latestLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
override fun onProviderEnabled(provider: String) {
providerState.value = true
}
val locationStateFlow =
LocationFlow(context)
.get(MIN_TIME, MIN_DISTANCE)
.onEach {
latestLocation = it
}.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
latestLocation,
)
override fun onProviderDisabled(provider: String) {
providerState.value = false
}
}
val geohashStateFlow =
locationStateFlow
.map { it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits).toString() }
.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
"",
)
}
object CachedGeoLocations {
@@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
fun createLiveStreamFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.authors
?.toList()
?.ifEmpty { null }
@@ -211,7 +211,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.map { listOf(it.lowercase()) }
.flatten(),
),
limit = 100,
@@ -225,7 +225,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
}
fun createFollowCommunitiesFilter(): TypedFilter? {
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
val communitiesToLoad = account.liveHomeFollowLists.value?.addresses ?: return null
if (communitiesToLoad.isEmpty()) return null
@@ -274,7 +274,7 @@ open class EditPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
@@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationUtil
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
@@ -78,7 +78,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
@@ -163,8 +166,7 @@ open class NewPostViewModel : ViewModel() {
// GeoHash
var wantsToAddGeoHash by mutableStateOf(false)
var locUtil: LocationUtil? = null
var location: Flow<String>? = null
var location: StateFlow<String?>? = null
// ZapRaiser
var canAddZapRaiser by mutableStateOf(false)
@@ -530,14 +532,7 @@ open class NewPostViewModel : ViewModel() {
null
}
val geoLocation = locUtil?.locationStateFlow?.value
val geoHash =
if (wantsToAddGeoHash && geoLocation != null) {
geoLocation.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
} else {
null
}
val geoHash = location?.value
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
nip95attachments.forEach {
@@ -1002,7 +997,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}
@@ -1031,7 +1026,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}
@@ -1059,7 +1054,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(
compareBy(
{ account?.isFollowing(it) },
@@ -1262,28 +1257,20 @@ open class NewPostViewModel : ViewModel() {
}
@OptIn(ExperimentalCoroutinesApi::class)
fun startLocation(context: Context) {
locUtil = LocationUtil(context)
locUtil?.let {
fun locationFlow(): Flow<String?> {
if (location == null) {
location =
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
saveDraft()
}
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
Amethyst.instance.locationManager.locationStateFlow
.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}
fun stopLocation() {
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
location = null
locUtil = null
return location!!
}
override fun onCleared() {
super.onCleared()
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
location = null
locUtil = null
}
fun toggleNIP04And24() {
@@ -1386,7 +1373,7 @@ open class NewPostViewModel : ViewModel() {
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
}
}
elementList.removeLast()
elementList.removeAt(elementList.size - 1)
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
this.clear()
this.putAll(newEntries)
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -42,6 +43,8 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
@@ -50,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.ammolite.relays.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -66,6 +70,7 @@ fun AddDMRelayListDialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
SetDialogToEdgeToEdge()
Scaffold(
topBar = {
TopAppBar(
@@ -114,7 +119,7 @@ fun AddDMRelayListDialog(
),
verticalArrangement = Arrangement.SpaceAround,
) {
Explanation()
Explanation(postViewModel)
DMRelayList(postViewModel, accountViewModel, onClose, nav)
}
@@ -123,7 +128,7 @@ fun AddDMRelayListDialog(
}
@Composable
private fun Explanation() {
private fun Explanation(postViewModel: DMRelayListViewModel) {
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
@@ -133,8 +138,25 @@ private fun Explanation() {
Spacer(modifier = StdVertSpacer)
Text(
text = stringRes(id = R.string.dm_relays_not_found_examples),
text = stringRes(id = R.string.dm_relays_not_found_examples2),
)
Spacer(modifier = StdVertSpacer)
ResetDMRelaysLonger(postViewModel)
}
}
}
@Composable
fun ResetDMRelaysLonger(postViewModel: DMRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultDMRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
Text(stringRes(R.string.default_relays_longer))
}
}
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -42,6 +43,8 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
@@ -50,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.ammolite.relays.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -66,6 +70,7 @@ fun AddSearchRelayListDialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
SetDialogToEdgeToEdge()
Scaffold(
topBar = {
TopAppBar(
@@ -114,7 +119,7 @@ fun AddSearchRelayListDialog(
),
verticalArrangement = Arrangement.SpaceAround,
) {
Explanation()
Explanation(postViewModel)
SearchRelayList(postViewModel, accountViewModel, onClose, nav)
}
@@ -123,7 +128,7 @@ fun AddSearchRelayListDialog(
}
@Composable
private fun Explanation() {
private fun Explanation(postViewModel: SearchRelayListViewModel) {
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
@@ -135,6 +140,23 @@ private fun Explanation() {
Text(
text = stringRes(id = R.string.search_relays_not_found_examples),
)
Spacer(modifier = StdVertSpacer)
ResetSearchRelaysLonger(postViewModel)
}
}
}
@Composable
fun ResetSearchRelaysLonger(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultSearchRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
Text(stringRes(R.string.default_relays_longer))
}
}
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -49,6 +50,8 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -171,19 +174,18 @@ fun MappedAllRelayListView(
)
},
) { pad ->
Column(
LazyColumn(
contentPadding = FeedPadding,
modifier =
Modifier
.fillMaxSize()
.padding(
16.dp,
pad.calculateTopPadding(),
16.dp,
pad.calculateBottomPadding(),
),
verticalArrangement = Arrangement.SpaceAround,
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad),
) {
LazyColumn(contentPadding = FeedPadding) {
item {
SettingsCategory(
stringRes(R.string.public_home_section),
@@ -202,9 +204,12 @@ fun MappedAllRelayListView(
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav)
item {
SettingsCategory(
SettingsCategoryWithButton(
stringRes(R.string.private_inbox_section),
stringRes(R.string.private_inbox_section_explainer),
action = {
ResetDMRelays(dmViewModel)
},
)
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav)
@@ -260,7 +265,6 @@ fun MappedAllRelayListView(
}
}
}
}
@Composable
fun ResetKind3Relays(postViewModel: Kind3RelayListViewModel) {
@@ -280,7 +284,20 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
DefaultSearchRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
Text(stringRes(R.string.default_relays))
}
}
@Composable
fun ResetDMRelays(postViewModel: DMRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultDMRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
@@ -143,7 +143,7 @@ class Kind3RelayListViewModel : ViewModel() {
val proposed =
RelayListRecommendationProcessor
.reliableRelaySetFor(
account.liveKind3Follows.value.users.mapNotNull {
account.liveKind3Follows.value.authors.mapNotNull {
account.getNIP65RelayList(it)
},
relayUrlsToIgnore =
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
@@ -104,6 +105,7 @@ fun RelayInformationDialog(
dismissOnClickOutside = false,
),
) {
SetDialogToEdgeToEdge()
Surface {
val color =
remember {
@@ -67,7 +67,7 @@ open class DiscoverLiveFeedFilter(
override fun sort(collection: Set<Note>): List<Note> {
val followingKeySet =
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors
val counter = ParticipantListBuilder()
val participantCounts =
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
class FilterByListParams(
val isGlobal: Boolean,
val isHiddenList: Boolean,
val followLists: Account.LiveFollowLists?,
val followLists: Account.LiveFollowList?,
val hiddenLists: Account.LiveHiddenUsers,
val now: Long = TimeUtils.oneMinuteFromNow(),
) {
@@ -45,22 +45,22 @@ class FilterByListParams(
if (followLists == null) return false
return if (noteEvent is LiveActivitiesEvent) {
noteEvent.participantsIntersect(followLists.users) ||
noteEvent.participantsIntersect(followLists.authors) ||
noteEvent.isTaggedHashes(followLists.hashtags) ||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
noteEvent.isTaggedAddressableNotes(followLists.communities)
noteEvent.isTaggedAddressableNotes(followLists.addresses)
} else {
noteEvent.pubKey in followLists.users ||
noteEvent.pubKey in followLists.authors ||
noteEvent.isTaggedHashes(followLists.hashtags) ||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
noteEvent.isTaggedAddressableNotes(followLists.communities)
noteEvent.isTaggedAddressableNotes(followLists.addresses)
}
}
fun isATagInList(aTag: ATag): Boolean {
if (followLists == null) return false
return aTag.pubKeyHex in followLists.users
return aTag.pubKeyHex in followLists.authors
}
fun match(
@@ -89,7 +89,7 @@ class FilterByListParams(
fun create(
userHex: String,
selectedListName: String,
followLists: Account.LiveFollowLists?,
followLists: Account.LiveFollowList?,
hiddenUsers: Account.LiveHiddenUsers,
): FilterByListParams =
FilterByListParams(
@@ -64,6 +64,7 @@ class GeoHashFeedFilter(
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedGeoHash(geoTag) == true &&
!it.isHiddenFor(account.flowHiddenUsers.value) &&
account.isAcceptable(it)
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
@@ -68,6 +68,7 @@ class HashtagFeedFilter(
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedHash(hashTag) == true &&
!it.isHiddenFor(account.flowHiddenUsers.value) &&
account.isAcceptable(it)
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
@@ -115,7 +115,7 @@ class NotificationFeedFilter(
it.event !is NIP90ContentDiscoveryRequestEvent &&
it.event !is GiftWrapEvent &&
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
(filterParams.isGlobal || filterParams.followLists?.authors?.contains(notifAuthor) == true) &&
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
tagsAnEventByUser(it, loggedInUserHex)
@@ -37,7 +37,7 @@ class ThreadFeedFilter(
override fun feed(): List<Note> {
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
val followingKeySet = account.liveKind3Follows.value.users
val followingKeySet = account.liveKind3Follows.value.authors
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
val now = TimeUtils.now()
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.note.SearchIcon
import com.vitorpamplona.amethyst.ui.screen.CodeName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.screen.FollowListState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -112,7 +112,7 @@ private fun LoggedInUserPictureDrawer(
fun FollowListWithRoutes(
followListsModel: FollowListState,
listName: String,
onChange: (CodeName) -> Unit,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
@@ -128,7 +128,7 @@ fun FollowListWithRoutes(
fun FollowListWithoutRoutes(
followListsModel: FollowListState,
listName: String,
onChange: (CodeName) -> Unit,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
@@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts(
) {
Text(
text =
followingCount.value.users.size
followingCount.value.authors.size
.toString(),
fontWeight = FontWeight.Bold,
)
@@ -20,10 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.navigation
import android.Manifest
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -33,6 +35,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -41,12 +45,20 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.map
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.screen.CodeName
import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition
import com.vitorpamplona.amethyst.ui.screen.CommunityName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
import com.vitorpamplona.amethyst.ui.screen.HashtagName
import com.vitorpamplona.amethyst.ui.screen.Name
@@ -55,15 +67,18 @@ import com.vitorpamplona.amethyst.ui.screen.ResourceName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun FeedFilterSpinner(
placeholderCode: String,
explainer: String,
options: ImmutableList<CodeName>,
options: ImmutableList<FeedDefinition>,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -75,20 +90,61 @@ fun FeedFilterSpinner(
id = R.string.select_an_option,
)
var currentText by
var selected by
remember(placeholderCode, options) {
mutableStateOf(
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption,
options.firstOrNull { it.code == placeholderCode },
)
}
val currentText by
remember(placeholderCode, options) {
derivedStateOf {
selected?.name?.name(context) ?: selectAnOption
}
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Size20Modifier)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(currentText)
if (selected is AroundMeFeedDefinition) {
val locationPermissionState =
rememberPermissionState(
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (!locationPermissionState.status.isGranted) {
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
} else {
val location by Amethyst.instance.locationManager.geohashStateFlow
.collectAsStateWithLifecycle(null)
location?.let {
LoadCityName(
geohashStr = it,
onLoading = {
Spacer(modifier = StdHorzSpacer)
LoadingAnimation()
},
) { cityName ->
Text(
text = "($cityName)",
fontSize = 12.sp,
lineHeight = 12.sp,
)
}
}
}
}
}
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = explainer,
@@ -115,7 +171,7 @@ fun FeedFilterSpinner(
options = options,
onDismiss = { optionsShowing = false },
onSelect = {
currentText = options[it].name.name(context)
selected = options[it]
optionsShowing = false
onSelect(it)
},
@@ -671,13 +671,13 @@ fun LoadModerators(
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
?.authors
val allParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.users
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
@@ -724,7 +724,7 @@ private fun LoadParticipants(
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
?.authors
val allParticipants =
ParticipantListBuilder()
@@ -733,7 +733,7 @@ private fun LoadParticipants(
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.users
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, allFollows)
@@ -882,7 +882,7 @@ fun RenderChannelThumb(
launch(Dispatchers.IO) {
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.users
?.authors
val allParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, followingKeySet)
@@ -890,7 +890,7 @@ fun RenderChannelThumb(
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.users
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -31,6 +32,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -42,7 +44,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -59,7 +60,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
@@ -375,16 +375,7 @@ private fun RenderOptionAfterVote(
QuoteBorder,
),
) {
LinearProgressIndicator(
modifier = Modifier.matchParentSize(),
color = color,
gapSize = 0.dp,
strokeCap = StrokeCap.Square,
drawStopIndicator = {},
progress = {
poolOption.tally.value.toFloat()
},
)
DisplayProgress(poolOption, color, modifier = Modifier.matchParentSize())
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -398,10 +389,7 @@ private fun RenderOptionAfterVote(
.width(45.dp)
},
) {
Text(
text = "${(poolOption.tally.value.toFloat() * 100).roundToInt()}%",
fontWeight = FontWeight.Bold,
)
TallyText(poolOption)
}
Column(
@@ -429,6 +417,43 @@ private fun RenderOptionAfterVote(
}
}
@Composable
fun DisplayProgress(
poolOption: PollOption,
color: Color,
modifier: Modifier,
) {
val progress by poolOption.tally
// The LinearProgressIndicator has some weird update issues and renders inaccurate percentages.
Box(modifier = modifier) {
Box(
modifier =
Modifier
.fillMaxWidth(progress)
.fillMaxHeight()
.background(color = color),
) {
}
}
}
@Composable
private fun TallyText(poolOption: PollOption) {
val state = poolOption.tally
val progressTxt by
remember {
derivedStateOf {
"${(state.value * 100).roundToInt()}%"
}
}
Text(
text = progressTxt,
fontWeight = FontWeight.Bold,
)
}
@Composable
private fun RenderOptionBeforeVote(
baseNote: Note,
@@ -46,7 +46,7 @@ data class PollOption(
val option: Int,
val descriptor: String,
var zappedValue: MutableState<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
var tally: MutableState<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
var tally: MutableState<Float> = mutableStateOf(0f),
var consensusThreadhold: MutableState<Boolean> = mutableStateOf(false),
var zappedByLoggedIn: MutableState<Boolean> = mutableStateOf(false),
)
@@ -126,7 +126,7 @@ class PollNoteViewModel : ViewModel() {
}
it.zappedValue.value = zappedValue
it.tally.value = tallyValue
it.tally.value = tallyValue.toFloat()
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
}
@@ -261,7 +261,7 @@ fun UpdateZapAmountDialog(
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -373,6 +373,6 @@ fun WatchUserFollows(
} else {
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
onFollowChanges(state.users.contains(userHex))
onFollowChanges(state.authors.contains(userHex))
}
}
@@ -144,12 +144,6 @@ fun AddInboxRelayForDMCard(
Spacer(modifier = StdVertSpacer)
Text(
text = stringRes(id = R.string.dm_relays_not_found_examples),
)
Spacer(modifier = StdVertSpacer)
var wantsToEditRelays by remember { mutableStateOf(false) }
if (wantsToEditRelays) {
AddDMRelayListDialog({ wantsToEditRelays = false }, accountViewModel, nav = nav)
@@ -355,5 +355,5 @@ fun WatchAddressableNoteFollows(
) {
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
onFollowChanges(state.communities.contains(note.idHex))
onFollowChanges(state.addresses.contains(note.idHex))
}
@@ -21,7 +21,11 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -280,6 +284,8 @@ abstract class FeedViewModel(
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
private var collectorJob: Job? = null
init {
@@ -25,6 +25,7 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AROUND_ME
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
@@ -33,10 +34,24 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
@@ -56,33 +71,56 @@ class FollowListState(
val viewModelScope: CoroutineScope,
) {
val kind3Follow =
CodeName(
KIND3_FOLLOWS,
ResourceName(R.string.follow_list_kind3follows),
CodeNameType.HARDCODED,
PeopleListOutBoxFeedDefinition(
code = KIND3_FOLLOWS,
name = ResourceName(R.string.follow_list_kind3follows),
type = CodeNameType.HARDCODED,
kinds = DEFAULT_FEED_KINDS,
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
)
val globalFollow =
CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
val muteListFollow =
CodeName(
MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
ResourceName(R.string.follow_list_mute_list),
CodeNameType.HARDCODED,
)
val defaultLists = persistentListOf(kind3Follow, globalFollow, muteListFollow)
fun getPeopleLists(): List<CodeName> =
val globalFollow =
GlobalFeedDefinition(
code = GLOBAL_FOLLOWS,
name = ResourceName(R.string.follow_list_global),
type = CodeNameType.HARDCODED,
kinds = DEFAULT_FEED_KINDS,
relays = account.activeGlobalRelays().toList(),
)
val aroundMe =
AroundMeFeedDefinition(
code = AROUND_ME,
name = ResourceName(R.string.follow_list_aroundme),
type = CodeNameType.HARDCODED,
kinds = DEFAULT_FEED_KINDS,
)
val muteListFollow =
PeopleListOutBoxFeedDefinition(
code = MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
name = ResourceName(R.string.follow_list_mute_list),
type = CodeNameType.HARDCODED,
kinds = DEFAULT_FEED_KINDS,
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
)
val defaultLists = persistentListOf(kind3Follow, globalFollow, aroundMe, muteListFollow)
fun getPeopleLists(): List<FeedDefinition> =
account
.getAllPeopleLists()
.map {
CodeName(
PeopleListOutBoxFeedDefinition(
it.idHex,
PeopleListName(it),
CodeNameType.PEOPLE_LIST,
kinds = DEFAULT_FEED_KINDS,
listOf(it.idHex),
)
}.sortedBy { it.name.name() }
val livePeopleListsFlow = MutableStateFlow(emptyList<CodeName>())
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
fun updateFeedWith(newNotes: Set<Note>) {
checkNotInMainThread()
@@ -118,29 +156,46 @@ class FollowListState(
}
@OptIn(ExperimentalCoroutinesApi::class)
val liveKind3FollowsFlow: Flow<List<CodeName>> =
val liveKind3FollowsFlow: Flow<List<FeedDefinition>> =
account.liveKind3Follows.transformLatest {
checkNotInMainThread()
val communities =
it.communities.mapNotNull {
it.addresses.mapNotNull {
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
CodeName(
TagFeedDefinition(
"Community/${communityNote.idHex}",
CommunityName(communityNote),
CodeNameType.ROUTE,
kinds = DEFAULT_COMMUNITY_FEEDS,
aTags = listOf(communityNote.idHex),
relays = account.activeGlobalRelays().toList(),
)
}
}
val hashtags =
it.hashtags.map {
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
TagFeedDefinition(
"Hashtag/$it",
HashtagName(it),
CodeNameType.ROUTE,
kinds = DEFAULT_FEED_KINDS,
tTags = listOf(it),
relays = account.activeGlobalRelays().toList(),
)
}
val geotags =
it.geotags.map {
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
TagFeedDefinition(
"Geohash/$it",
GeoHashName(it),
CodeNameType.ROUTE,
kinds = DEFAULT_FEED_KINDS,
gTags = listOf(it),
relays = account.activeGlobalRelays().toList(),
)
}
emit(
@@ -156,7 +211,7 @@ class FollowListState(
checkNotInMainThread()
emit(
listOf(
listOf(kind3Follow, globalFollow),
listOf(kind3Follow, aroundMe, globalFollow),
myLivePeopleListsFlow,
myLiveKind3FollowsFlow,
listOf(muteListFollow),
@@ -172,7 +227,7 @@ class FollowListState(
checkNotInMainThread()
emit(
listOf(
listOf(kind3Follow, globalFollow),
listOf(kind3Follow, aroundMe, globalFollow),
myLivePeopleListsFlow,
listOf(muteListFollow),
).flatten().toImmutableList(),
@@ -238,8 +293,78 @@ class CommunityName(
}
@Immutable
data class CodeName(
abstract class FeedDefinition(
val code: String,
val name: Name,
val type: CodeNameType,
)
@Immutable
class GlobalFeedDefinition(
code: String,
name: Name,
type: CodeNameType,
val kinds: List<Int>,
val relays: List<String>,
) : FeedDefinition(code, name, type)
@Immutable
class TagFeedDefinition(
code: String,
name: Name,
type: CodeNameType,
val kinds: List<Int>,
val relays: List<String>,
val pTags: List<String>? = null,
val eTags: List<String>? = null,
val aTags: List<String>? = null,
val tTags: List<String>? = null,
val gTags: List<String>? = null,
) : FeedDefinition(code, name, type)
@Immutable
class AroundMeFeedDefinition(
code: String,
name: Name,
type: CodeNameType,
val kinds: List<Int>,
) : FeedDefinition(code, name, type)
@Immutable
class PeopleListOutBoxFeedDefinition(
code: String,
name: Name,
type: CodeNameType,
val kinds: List<Int>,
val unpackList: List<String>,
) : FeedDefinition(code, name, type)
val DEFAULT_FEED_KINDS =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
PinListEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesEvent.KIND,
WikiNoteEvent.KIND,
)
val DEFAULT_COMMUNITY_FEEDS =
listOf(
TextNoteEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
WikiNoteEvent.KIND,
CommunityPostApprovalEvent.KIND,
)
@@ -120,7 +120,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -396,7 +395,7 @@ class AccountViewModel(
note.flow().metadata.stateFlow,
note.flow().reports.stateFlow,
) { hiddenUsers, followingUsers, autor, metadata, reports ->
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
@@ -188,7 +188,6 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.receiveAsFlow
@@ -1335,22 +1334,10 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
@Composable
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
val context = LocalContext.current
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
DisposableEffect(key1 = context) {
postViewModel.startLocation(context = context)
locationDescriptionFlow = postViewModel.location
onDispose { postViewModel.stopLocation() }
}
locationDescriptionFlow?.let {
val location by it.collectAsStateWithLifecycle(null)
val location by postViewModel.locationFlow().collectAsStateWithLifecycle(null)
location?.let { DisplayLocationInTitle(geohash = it) }
}
}
@Composable
fun DisplayLocationInTitle(geohash: String) {
@@ -75,7 +75,7 @@ class SearchBarViewModel(
_hashtagResults.emit(findHashtags(searchValue))
_searchResultsUsers.emit(
LocalCache
.findUsersStartingWith(searchValue)
.findUsersStartingWith(searchValue, account)
.sortedWith(
compareBy(
{ it.toBestDisplayName().startsWith(searchValue, true) },
@@ -86,7 +86,7 @@ class SearchBarViewModel(
)
_searchResultsNotes.emit(
LocalCache
.findNotesStartingWith(searchValue)
.findNotesStartingWith(searchValue, account)
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed(),
)
@@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -178,7 +177,7 @@ fun WatchAccountForSearchScreen(accountViewModel: AccountViewModel) {
}
}
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
@OptIn(FlowPreview::class)
@Composable
private fun SearchBar(
searchBarViewModel: SearchBarViewModel,
@@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -197,7 +196,6 @@ import com.vitorpamplona.quartz.events.TorrentCommentEvent
import com.vitorpamplona.quartz.events.TorrentEvent
import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -211,17 +209,15 @@ fun ThreadFeedView(
accountViewModel: AccountViewModel,
nav: INav,
) {
val listState = rememberLazyListState()
RefresheableBox(viewModel) {
RenderFeedState(
viewModel = viewModel,
accountViewModel = accountViewModel,
listState = listState,
listState = viewModel.llState,
nav = nav,
routeForLastRead = null,
onLoaded = {
RenderThreadFeed(noteId, it, listState, accountViewModel, nav)
RenderThreadFeed(noteId, it, viewModel.llState, accountViewModel, nav)
},
)
}
@@ -236,7 +232,6 @@ fun RenderThreadFeed(
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
val firstTimeScrolled = remember { TimeUtils.now() }
LaunchedEffect(noteId, items.list) {
// hack to allow multiple scrolls to Item while posts on the screen load.
@@ -252,13 +247,18 @@ fun RenderThreadFeed(
// records before setting up the position on the feed.
//
// It jumps around, but it is the best we can do.
if (TimeUtils.now() - firstTimeScrolled < 1000) {
if (listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 && items.list.size > 3) {
val position = items.list.indexOfFirst { it.idHex == noteId }
if (position >= 0) {
if (position > items.list.size - 3) {
listState.scrollToItem(position, 0)
} else {
listState.scrollToItem(position, -200)
}
}
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
+196
View File
@@ -3,18 +3,23 @@
<string name="point_to_the_qr_code">کد QR را نشانه بگیرید</string>
<string name="show_qr">کد QR را نشان بده</string>
<string name="profile_image">تصویر نمایه</string>
<string name="your_profile_image">تصویر نمایه شما</string>
<string name="scan_qr">اسکن کد QR</string>
<string name="show_anyway">بهر حال نشان بده</string>
<string name="post_was_hidden">این پست پنهان شده چون به کلمات یا کاربران پنهان شده شما اشاره دارد</string>
<string name="post_was_flagged_as_inappropriate_by">این یادداشت توسط این افراد گزارش شده:</string>
<string name="post_not_found">این یادداشت یافت نشد</string>
<string name="post_not_found_short">👀</string>
<string name="channel_image">تصویر کانال</string>
<string name="referenced_event_not_found">رویداد مورد نظر یافت نشد</string>
<string name="could_not_decrypt_the_message">پیام رمزگشایی نشد</string>
<string name="group_picture">تصویر گروه</string>
<string name="explicit_content">محتوای نامناسب</string>
<string name="spam">اسپم</string>
<string name="spam_description">تعداد رویدادهای اسپم که از این رله می آید</string>
<string name="impersonation">جعل هویت</string>
<string name="illegal_behavior">رفتار ناهنجار</string>
<string name="other">سایر</string>
<string name="unknown">ناشناس</string>
<string name="relay_icon">آیکن رله</string>
<string name="unknown_author">نویسنده ناشناس</string>
@@ -22,6 +27,9 @@
<string name="copy_user_pubkey">کپی شناسه نویسنده</string>
<string name="copy_note_id">کپی شناسه یادداشت</string>
<string name="broadcast">انتشار</string>
<string name="timestamp_it">مهر زمان و تاریخ بزن</string>
<string name="timestamp_pending">مهر زمان: در انتظار تایید</string>
<string name="timestamp_pending_short">OTS: در حال انتظار</string>
<string name="request_deletion">درخواست حذف/string&gt;</string>
<string name="block_report">مسدود/گزارش</string>
<string name="block_hide_user"><![CDATA[بلاک/پنهان کردن کاربر]]></string>
@@ -29,6 +37,10 @@
<string name="report_impersonation">گزارش جعل هویت</string>
<string name="report_explicit_content">گزارش محتوای نامناسب</string>
<string name="report_illegal_behaviour">گزارش رفتار ناهنجار</string>
<string name="report_malware">گزارش بدافزار</string>
<string name="report_mod">گزارش مود</string>
<string name="malware">بدافزار</string>
<string name="mod">مود</string>
<string name="login_with_a_private_key_to_be_able_to_reply">با کلید خصوصی وارد شوید تا بتوانید پاسخ دهید</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را بازنشر کنید</string>
<string name="login_with_a_private_key_to_like_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را لایک کنید</string>
@@ -42,7 +54,12 @@
<string name="view_count">دیدن تعداد</string>
<string name="boost">بازنشر</string>
<string name="boosted">بازنشر شده</string>
<string name="edited">ویرایش شده</string>
<string name="edited_number">ویرایش#%1$s</string>
<string name="original">اورجینال</string>
<string name="quote">نقل قول</string>
<string name="fork">فورک</string>
<string name="propose_an_edit">پیشنهاد ویرایش</string>
<string name="new_amount_in_sats">مبلغ جدید به ساتوشی</string>
<string name="add">افزودن</string>
<string name="replying_to">" پاسخ به"</string>
@@ -82,6 +99,7 @@
<string name="posts">یادداشت ها</string>
<string name="bytes">بایت</string>
<string name="errors">خطاها</string>
<string name="errors_description">تعداد خطاهای اتصال در این نوبت</string>
<string name="home_feed">خبرنامه اصلی</string>
<string name="private_message_feed">پیام خصوصی</string>
<string name="public_chat_feed">گفتگوی عمومی</string>
@@ -100,8 +118,11 @@
<string name="website_url">آدری وبسایت</string>
<string name="ln_address">آدرس لایتنینگ</string>
<string name="ln_url_outdated">آدرس لایتنینگ(نقل شده)</string>
<string name="save_to_gallery">در گالری ذخیره کن</string>
<string name="image_saved_to_the_gallery">تصویر در گالری ذخیره شد</string>
<string name="failed_to_save_the_image">تصویر ذخیره نشد</string>
<string name="video_saved_to_the_gallery">ویدئو در گالری گوشی ذخیره شد</string>
<string name="failed_to_save_the_video">ویدئو ذخیره نشد</string>
<string name="upload_image">بارگذاری تصویر</string>
<string name="uploading">...در حال بارگذاری</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">این کاربر آدرس لایتنینگ برای دریافت ساتوشی تنظیم نکرده است</string>
@@ -117,6 +138,7 @@
<string name="conversations">گفتگوها</string>
<string name="notes">یادداشت ها</string>
<string name="replies">پاسخ ها</string>
<string name="gallery">گالری</string>
<string name="follows">"دنبال شوندگان"</string>
<string name="reports">"گزارش ها"</string>
<string name="more_options">انتخاب های بیشتر</string>
@@ -138,12 +160,15 @@
<string name="clear">پاک کردن</string>
<string name="app_logo">لوگوی اپ</string>
<string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string>
<string name="ncryptsec_password">پسورد باز کردن کلید</string>
<string name="show_password">نشان دادن گذرواژه</string>
<string name="hide_password">پنهان کردن گذرواژه</string>
<string name="invalid_key">کلید نامعتبر</string>
<string name="invalid_key_with_message">کلید نامعتبر: %1$s</string>
<string name="i_accept_the">"می پذیرم "</string>
<string name="terms_of_use">شرایط استفاده</string>
<string name="acceptance_of_terms_is_required">پذیرش شرایط الازمیست</string>
<string name="password_is_required">رمز عبور لازم است</string>
<string name="key_is_required">کلید الزامیست</string>
<string name="name_is_required">نام الزامی است</string>
<string name="login">ورود</string>
@@ -194,8 +219,18 @@
<string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string>
<string name="mark_all_as_read">همه را خوانده شده کن</string>
<string name="backup_keys">بازیابی کلیدها</string>
<string name="account_backup_tips2_md" tools:ignore="Typos">نکات ایمنی و ذخیره کلید
حساب کاربری شما با یک کلید خصوصی محافظت می شود. این کلید دنباله ای بلند از کاراکترها است که با **nsec1** آغاز می گردد. هر کس به این کلید خصوصی دسترسی داشته باشد می تواند بجای شما یادداشت پست کرده یا هویت شما را تغییر دهد.
هرگز کلید خصوصی خود را در هیچ وبسایت با نرم افزاری که به آن اطمینان ندارید **وارد نکنید**.
سازندگان امتیست **هرگز** کلیدتان را از شما نمی خواهند.
یک پشتیبان یدکی مخفی از کلید خود به منظور بازیابی **نگه دارید**. ما استفاده از نرم افزار مدیریت پسورد را توصیه می کنیم.</string>
<string name="account_backup_tips3_md" tools:ignore="Typos">برای ایمنی بیشتر، می توانید کلید خود را با یک رمزعبور رمزنگاری کنید. این نوع کلید با **ncryptsec1** آغاز می شود و نمی تواند بدون رمز عبور شما استفاده شود.
اگر پسوردتان را گم کنید، نخواهید توانست کلیدتان را بازیابی کنید.</string>
<string name="failed_to_encrypt_key">کلید خصوصی شما رمزنگاری نشد</string>
<string name="secret_key_copied_to_clipboard">کلید خصوصی در کلیپبورد کپی شد</string>
<string name="copy_my_secret_key">کلید خصوصیم را کپی کن</string>
<string name="encrypt_and_copy_my_secret_key">رمزنگاری و کپی کلید من</string>
<string name="biometric_authentication_failed">احراز هویت انجام نشد</string>
<string name="biometric_authentication_failed_explainer">مشخصات بیومتریک نتوانست مالک این تلفن را احراز هویت کند</string>
<string name="biometric_authentication_failed_explainer_with_error">مشخصات بیومتریک نتوانست مالک این تلفن را احراز هویت کند. خطا: %1$s</string>
@@ -226,6 +261,8 @@
<string name="quick_action_delete">حذف</string>
<string name="quick_action_unfollow">دنبال نکردن</string>
<string name="quick_action_follow">دنبال کردن</string>
<string name="quick_action_request_deletion_gallery_title">حذف از گالری</string>
<string name="quick_action_request_deletion_gallery_alert_body">حذف این رسانه از گالری، بعدا می توانید آن را بخوانید</string>
<string name="quick_action_request_deletion_alert_title">درخواست حذف</string>
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
<string name="quick_action_block_dialog_btn">بلاک</string>
@@ -239,6 +276,7 @@
<string name="report_dialog_impersonation">جعل هویت بدخواهانه</string>
<string name="report_dialog_nudity">محتوای برهنگی یا نمایان</string>
<string name="report_dialog_illegal">رفتار ناهنجار</string>
<string name="report_dialog_malware">نرم‌افزار خرابکار</string>
<string name="report_dialog_blocking_a_user">بلاک کردن کاربر محتوای ایشان را در اپ شما پنهان می کند. یادداشت های شما همچنان بطور عمومی دیده می شوند که شامل کاربران بلاک شده توسط شما نیز هست. کاربران بلاک شده در صفحه فیلترهای ایمنی فهرست می شوند.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[مسدود و پنهان کردن کاربر]]></string>
<string name="report_dialog_report_btn">گزارش سواستفاده</string>
@@ -251,6 +289,7 @@
<string name="report_dialog_title">بلاک و گزارش</string>
<string name="block_only">بلاک</string>
<string name="bookmarks">علامت گذاشته ها</string>
<string name="drafts">پیش‌نویس‌</string>
<string name="private_bookmarks">علامت گاشته های خصوصی</string>
<string name="public_bookmarks">علامت گذاشته های عمومی</string>
<string name="add_to_private_bookmarks">افزودن به علامت گذاشته های خصوصی</string>
@@ -275,6 +314,7 @@
<string name="poll_zap_value_min">حداقل زپ</string>
<string name="poll_zap_value_max">حداکثر زپ</string>
<string name="poll_consensus_threshold">توافق نظر</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">بسته شدن پس از</string>
<string name="poll_closing_time_days">روز</string>
<string name="poll_unable_to_vote">نمی توان رای داد</string>
@@ -309,18 +349,31 @@
<string name="zap_type_nonzap_explainer">هیچ ردی در نوستر نمی ماند، فقط در شبکه لایتنینگ انجام می شود</string>
<string name="file_server">سرور فایل</string>
<string name="zap_forward_lnAddress">آدرس لایتنینگ یا @User</string>
<string name="media_servers">سرورهای رسانه</string>
<string name="set_preferred_media_servers">سرورهای مورد علاقه خود برای بارگزاری رسانه را انتخاب کنید.</string>
<string name="no_media_server_message">هیچ مجموعه سرور سفارشی ندارید. می توانید از لیست امتیست استفاده کنید، یا از لیست زیر اضافه کنید↓</string>
<string name="built_in_media_servers_title">سرورهای داخلی رسانه</string>
<string name="built_in_servers_description">لیست پیش فرض امتیست. می توانید تک به تک یا تمام لیست را اضافه کنید.</string>
<string name="use_default_servers">استفاده از لیست پیش فرض</string>
<string name="add_media_server">افزودن سرور رسانه</string>
<string name="delete_media_server">حذف سرور رسانه</string>
<string name="upload_server_relays_nip95">رله های شما (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">فایل ها در رله های شما میزبانی می شوند. NIPجدید: بررسی کنید آیا پشتیبانی می کنند یا خیر </string>
<string name="privacy_options">گزینه های حریم خصوصی</string>
<string name="connect_via_tor_short">Tor/Orbot تنظیمات</string>
<string name="connect_via_tor"> از طریق تنظیم Orbot متصل شوید</string>
<string name="connect_via_tor1">تنظیم</string>
<string name="connect_via_tor2">تنظیمات تور</string>
<string name="do_you_really_want_to_disable_tor_title">قطع اتصال از Orbot/Tor?</string>
<string name="do_you_really_want_to_disable_tor_text">داده های شما بلافاصله در شبکه معمولی منتقل می شوند</string>
<string name="yes">بله</string>
<string name="no">خیر</string>
<string name="follow_list_selection">لیست دنبال ها</string>
<string name="follow_list_kind3follows">همه دنبال ها</string>
<string name="follow_list_aroundme">اطراف من</string>
<string name="follow_list_global">همگانی</string>
<string name="follow_list_mute_list">لیست خموش</string>
<string name="connect_through_your_orbot_setup_short">درگاه پیش فرض 9050 است</string>
<string name="connect_through_your_orbot_setup_markdown"> ## از طریق Tor با Orbot متصل شوید
\n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. شروع Orbot
@@ -330,6 +383,45 @@
\n6. دکمه Activate را بفشارید تا از Orbot به عنوان پراکسی استفده نمایید
</string>
<string name="orbot_socks_port">درگاه Orbot Socks</string>
<string name="use_internal_tor">موتور فعال تور</string>
<string name="use_internal_tor_explainer">استفاده از نسخه درونی یا Orbot</string>
<string name="tor_preset">تنظیمات پیش فرض حریم خصوصی/ تور</string>
<string name="tor_preset_explainer">تغییر سریع تمام تنظیمات زیر</string>
<string name="tor_use_onion_address">رله ها / آدرس های Onion </string>
<string name="tor_use_onion_address_explainer">از تور برای هر آدرس اونیون استفاده کن</string>
<string name="tor_use_dm_relays">رله های پیام خصوصی</string>
<string name="tor_use_dm_relays_explainer">تور را برای ارسال و دریافت پیام خصوصی الزامی کن</string>
<string name="tor_use_new_relays">رله های غیر معتمد</string>
<string name="tor_use_new_relays_explainer">تور را برای رله های ارسال / دریافت الزامی کن</string>
<string name="tor_use_trusted_relays">رله های معتمد</string>
<string name="tor_use_trusted_relays_explainer">تور را برای تمام رله های لیست الزامی کن</string>
<string name="tor_use_profile_pictures">تصاویر نمایه</string>
<string name="tor_use_profile_pictures_explainer">تور را برای بارگیری تصاویر نمایه اجباری کن</string>
<string name="tor_use_url_previews">پیش نمایش URL</string>
<string name="tor_use_url_previews_explainer">تور را برای بارگیری پیش نمایش آدرس الزامی کن</string>
<string name="tor_use_images">تصاویر</string>
<string name="tor_use_images_explainer">تور را برای بارگیری تصاویر الزامی کن</string>
<string name="tor_use_videos">ویدئوها</string>
<string name="tor_use_videos_explainer">تور را برای بارگیری ویدئوها الزامی کن</string>
<string name="tor_use_money_operations">عملیات پولی</string>
<string name="tor_use_money_operations_explainer">تور را برای زپ، لایتنینگ و انتقال کَشو الزامی کن</string>
<string name="tor_use_nip05_verification">تایید آدرس ناستر</string>
<string name="tor_use_nip05_verification_explainer">تور را برای تایید آدرس های NIP-05 الزامی کن</string>
<string name="tor_use_nip96_uploads">بارگذاری رسانه</string>
<string name="tor_use_nip96_uploads_explainer">تور را برای بارگذاری محتوا الزامی کن</string>
<string name="tor_internal">داخلی</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">خاموش</string>
<string name="tor_when_needed">مقدماتی</string>
<string name="tor_default">پیش‌فرض</string>
<string name="tor_small_payloads">همه بجز رسانه</string>
<string name="tor_full_privacy">حریم خصوصی کامل</string>
<string name="tor_custom">سفارشی</string>
<string name="tor_when_needed_explainer">وقتی توسط سرور الزام شده از تور استفاده کن</string>
<string name="tor_default_explainer">IP خود را از رله های غریبه پنهان کن</string>
<string name="tor_small_payloads_explainer">IP خود را از همه چیز بجز تصاویر و ویدئوها پنهان کن</string>
<string name="tor_full_privacy_explainer">IP خود را در تمام ارتباطات پنهان کن</string>
<string name="tor_custom_explainer">خودت را بساز</string>
<string name="invalid_port_number">شماره پورت نامعتبر</string>
<string name="use_orbot">استفاده از Orbot</string>
<string name="disconnect_from_your_orbot_setup">قطع اتصال Tor/Orbot</string>
@@ -363,6 +455,8 @@
<string name="sats_to_complete">زپ گیری تا %1$s. %2$s مانده تا هدف</string>
<string name="read_from_relay">خواندن از رله</string>
<string name="write_to_relay">نوشتن به رله</string>
<string name="write_to_relay_description">مقدار بایتی که به این رله فرستاده شده، شامل فیلترها و رویدادها</string>
<string name="read_from_relay_description">مقدار بایتی که از این رله فرستاده شده، شامل فیلترها و رویدادها</string>
<string name="an_error_occurred_trying_to_get_relay_information">در تلاش برای گرفتن اطلاعات رله از %1$s خطایی رخ داد</string>
<string name="owner">مالک</string>
<string name="version">نگارش</string>
@@ -376,6 +470,7 @@
<string name="languages">زبان‌ها</string>
<string name="tags">برچسب‌ها</string>
<string name="posting_policy">سیاست ارسال محتوا</string>
<string name="relay_error_messages">خطاها و هشدارهای از این رله</string>
<string name="message_length">طول پیام</string>
<string name="subscriptions">اشتراک</string>
<string name="filters">فیلترها</string>
@@ -384,6 +479,7 @@
<string name="maximum_event_tags">حداکثر تعداد برچسب رویداد</string>
<string name="content_length">طول محتوا</string>
<string name="minimum_pow">حداقل PoW</string>
<string name="auth">احراز هویت</string>
<string name="payment">پرداخت</string>
<string name="cashu">توکن Cashu</string>
<string name="cashu_redeem">بازپرداخت</string>
@@ -401,6 +497,7 @@
<string name="are_you_sure_you_want_to_log_out">خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟</string>
<string name="followed_tags">برچسب های دنبال شده</string>
<string name="relay_setup">رله ها</string>
<string name="discover_content">اکتشاف یادداشت</string>
<string name="discover_marketplace">بازار</string>
<string name="discover_live">زنده</string>
<string name="discover_community">انجمن</string>
@@ -410,14 +507,20 @@
<string name="community_no_descriptor">این انجمن هیچ توضیح و قوانینی ندارد. با مالک گروه برای افزودن آن صحبت کنید.</string>
<string name="add_sensitive_content_label">محتوای حساس</string>
<string name="add_sensitive_content_description">هشدار محتوای حساس پیش از نمایش محتوا اضافه می کند</string>
<string name="preferences">ترجیحات اپ</string>
<string name="settings">تنظیمات</string>
<string name="connectivity_type_always">همیشه</string>
<string name="connectivity_type_wifi_only">فقط Wifi</string>
<string name="connectivity_type_unmetered_wifi_only">وای فای بی اندازه</string>
<string name="connectivity_type_never">هرگز</string>
<string name="ui_feature_set_type_complete">کامل</string>
<string name="ui_feature_set_type_simplified">ساده</string>
<string name="ui_feature_set_type_performance">عملکرد</string>
<string name="system">سیستم</string>
<string name="light">روشن</string>
<string name="dark">تاریک</string>
<string name="application_preferences">ترجیحات اپلیکیشن</string>
<string name="wallet_connect">اتصال کیف پول</string>
<string name="language">زبان</string>
<string name="theme">پوسته</string>
<string name="automatically_load_images_gifs">پیش نمایش تصویر</string>
@@ -425,6 +528,8 @@
<string name="automatically_show_url_preview">پیش نمایش URL</string>
<string name="automatically_hide_nav_bars">مرور ژرف</string>
<string name="automatically_hide_nav_bars_description">پنهان کردن نوار پیمایش هنگام مرور</string>
<string name="ui_style">حالت رابط کاربر</string>
<string name="ui_style_description">قیافه پست را انتخاب کن</string>
<string name="load_image">بارگیری تصویر</string>
<string name="spamming_users">اسپمر</string>
<string name="muted_button">بیصدا شده. برای لغو کلیک کنید</string>
@@ -435,6 +540,7 @@
<string name="nip05_checking">بررسی آدرس ناستر</string>
<string name="select_deselect_all">انتخاب یا لغو انتخاب همه</string>
<string name="default_relays">پیش‌فرض</string>
<string name="default_relays_longer">بازگشت به حالت پیش فرض</string>
<string name="select_a_relay_to_continue">برای ادامه یک رله انتخاب کنید</string>
<string name="zap_forward_title">باز ارسال زپ ها به:</string>
<string name="zap_forward_explainer">کلاینت هایی که این قابلیت را دارند زپ ها را به جای شما به LNAddress یا نمایه کاربر زیر می فرستند</string>
@@ -445,6 +551,7 @@
<string name="new_feature_nip17_might_not_be_available_description">برای فعال سازی این حالت لازم است اماتیست یک پیغام NIP-17 بفرستد (پیغام های GiftWrapped, Sealed Direct and Group). مطمئن شوید که گیرنده از کلاینتی سازگاز استفاده می کند.</string>
<string name="new_feature_nip17_activate">فعال‌سازی</string>
<string name="messages_create_public_chat">عمومی</string>
<string name="messages_create_public_private_chat_description">گروه خصوصی یا عمومی جدید</string>
<string name="messages_new_message">خصوصی</string>
<string name="messages_new_message_to">به</string>
<string name="messages_new_message_subject">موضوع</string>
@@ -460,9 +567,13 @@
<string name="automatically_play_videos_description">پخش خودکار ویدئوها و جیف ها</string>
<string name="automatically_show_url_preview_description">نشان دادن پیش نمایش URL</string>
<string name="load_image_description">هنگام بارگیری تصاویر</string>
<string name="copy_stack_to_clipboard">کپی استک</string>
<string name="copy_to_clipboard">کپی به کلیپ‌بورد</string>
<string name="copy_npub_to_clipboard">کپی کلید عمومی در کلیپبورد</string>
<string name="share_or_save">اشتراک گذاری یا ذخیره‌سازی</string>
<string name="copy_url_to_clipboard">کپی URL به کلیپبورد</string>
<string name="copy_the_note_id_to_the_clipboard">کپی شناسه یادداشت به کلیپبورد</string>
<string name="add_media_to_gallery">افزودن رسانه به گالری</string>
<string name="created_at">ایجاد شده در</string>
<string name="rules">قوانین</string>
<string name="login_with_external_signer">ورود با Amber</string>
@@ -473,6 +584,10 @@
<string name="error_dialog_talk_to_user">پیام به کاربر</string>
<string name="error_dialog_button_ok">قبول</string>
<string name="relay_information_document_error_assemble_url">به %1$s نرسید: %2$s</string>
<string name="relay_information_document_error_failed_to_assemble_url">آدرس NIP-11 ساخته نشد به دلیل %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_reach_server">به %1$s نتوانست برسد: %2$s</string>
<string name="relay_information_document_error_failed_to_parse_response">پاسخ %1$s تفسیر نشد: %2$s</string>
<string name="relay_information_document_error_failed_with_http">رله درخواست %1$s را رد کرد: %2$s</string>
<string name="relay_information_document_error_reach_server">به %1$s نرسید: %2$s</string>
<string name="relay_information_document_error_parse_result">نتایج %1$s تفسیر نشد: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s کد %2$s را انجام نداد</string>
@@ -527,11 +642,16 @@
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">مشکل %1$s حل نشد. بررسی کنید که به اینترنت متصل باشید، سرور کار کند و آدرس لایتنینگ %2$s صحیح باشد. \n\nخطای %3$s</string>
<string name="could_not_fetch_invoice_from">صورتحساب از %1$s گرفته نشد</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">خطا در تفسیر JSON از آدرس لایتنینگ. تنظیمات لایتنینگ کاربر را بررسی کنید.</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">خطا در تفسیر JSON از %1$s. تنظیمات لایتنینگ کاربر را بررسی کنید</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">URL بازخوانی در پیکربندی سرور آدرس لایتنینگ یافت نشد</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">فراخوانی URL از پاسخ %1$s یافت نشد</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup">خطا در تفسیر JSON از فراخوان صورتحساب آدرس لایتنینگ. تنظیمات لایتنینگ کاربر را بررسی کنید.</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">خطا در تفسیر JSON از دریافت صورتحساب %1$s. تنظیمات لایتنینگ کاربر را بررسی کنید.</string>
<string name="incorrect_invoice_amount_sats_from_it_should_have_been">مبلغ نادرست صورتحساب (%1$s ساتوشی). می بایست %3$s ساتوشی باشد.</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error">نمی توان صورتحساب لایتنینگ را پیش از زپ زدن ارسال کرد. کیف پول لایتنینگی گیرنده خطای روبرو را داد: %1$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">صورت حساب لایتنینگ ساخته نشد. پیغام از %1$s: %2$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json">نمی توان صورتحساب لایتنینگ را پیش از زپ زدن ارسال کرد. المنت pr در JSON بدست آمده یافت نشد.</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">نمی توان صورتحساب لایتنینگ از %1$s ساخت. المنت pr در JSON بدست آمده یافت نشد.</string>
<string name="read_only_user">کاربر با امکان خواندن فقط</string>
<string name="no_reactions_setup">هیچ واکنشی تنظیم نشده است</string>
<string name="select_push_server">یک اپلیکیشن UnifiedPush انتخاب کنید</string>
@@ -555,6 +675,7 @@
<string name="classifieds_condition">شرایط</string>
<string name="classifieds_category">دسته بندی</string>
<string name="classifieds_price">قیمت (به ساتوشی)</string>
<string name="classifieds_price_placeholder">1000</string>
<string name="classifieds_location">موقعیت مکانی</string>
<string name="classifieds_location_placeholder">شهر، استان، کشور</string>
<string name="classifieds_condition_new">جدید</string>
@@ -588,4 +709,79 @@
<string name="server_did_not_provide_a_url_after_uploading">سرور پس از بارگذاری URL نداد</string>
<string name="could_not_download_from_the_server">فایل بارگذاری شده از سرور بارگیری نشد</string>
<string name="could_not_prepare_local_file_to_upload">فایل محلی برای بارگذاری آماده نشد: %1$s</string>
<string name="failed_to_upload_with_message">بارگذاری نشد: %1$s</string>
<string name="failed_to_delete_with_message">حذف نشد: %1$s</string>
<string name="media_too_big_for_nip95">رسانه برای NIP-95 بسیار بزرگ است</string>
<string name="unable_to_load_thumbnail">ریزتصویر بارگیری نشد</string>
<string name="could_not_prepare_header">اطلاعات سربرگ آماده نشد: %1$s</string>
<string name="compression_cancelled">فشرده سازی لغو شد</string>
<string name="compression_returned_null">فشرده سازی فایل خروجی نساخت</string>
<string name="media_compression_quality_label">کیفیت رسانه</string>
<string name="media_compression_quality_explainer">کیفیت کم را انتخاب کنید تا رسانه را به شکل فایل کوچکتری ذخیره کند، کیفیت بالا برای ذخیره به شکل فایل بزرگتر و نافشرده برای بارگذاری رسانه بدون فشرده سازی است.</string>
<string name="media_compression_quality_low">پایین</string>
<string name="media_compression_quality_medium">متوسط</string>
<string name="media_compression_quality_high">بالا</string>
<string name="media_compression_quality_uncompressed">نافشرده</string>
<string name="edit_draft">ویرایش پیش‌نویس</string>
<string name="login_with_qr_code">ورود با کد QR</string>
<string name="route">مسیر</string>
<string name="route_home">خانه</string>
<string name="route_search">جستجو</string>
<string name="route_discover">کاوش</string>
<string name="route_messages">پیغام‌ها</string>
<string name="route_notifications">اعلان‌ها</string>
<string name="route_global">سراسری</string>
<string name="route_video">کوتاه</string>
<string name="route_security_filters">فیلترهای ایمنی</string>
<string name="new_post">نوشته جدید</string>
<string name="new_short">رسانه های کوتاه جدید: تصاویر یا ویدئوها</string>
<string name="new_community_note">یادداشت جدید در انجمن</string>
<string name="open_all_reactions_to_this_post">باز کردن تمام واکنش ها به این پست</string>
<string name="close_all_reactions_to_this_post">بستن تمام واکنش ها به این پست</string>
<string name="reply_description">پاسخ</string>
<string name="boost_or_quote_description">بازنشر یا نقل قول</string>
<string name="like_description">پسند</string>
<string name="zap_description">زَپ</string>
<string name="change_reaction">تغییر واکنش های سریع</string>
<string name="profile_image_of_user">تصویر نمایه %1$s</string>
<string name="relay_info">رله %1$s</string>
<string name="expand_relay_list">گسترش لیست رله</string>
<string name="note_options">گزینه های یادداشت</string>
<string name="relay_list_selector">انتخابگر لیست رله</string>
<string name="poll">نظرسنجی</string>
<string name="disable_poll">غیرفعال کردن نظرسنجی</string>
<string name="add_bitcoin_invoice">صورت حساب بیتکوین</string>
<string name="cancel_bitcoin_invoice">لغو صورت حساب بیتکوین</string>
<string name="cancel_classifieds">لغو فروش</string>
<string name="add_zapraiser">زپ ریزون</string>
<string name="cancel_zapraiser">لغو زپ ریزون</string>
<string name="add_location">موقعیت مکانی</string>
<string name="remove_location">حذف موقعیت مکانی</string>
<string name="add_zap_split">تقسیم زپ</string>
<string name="cancel_zap_split">لغو تقسیم زپ</string>
<string name="add_content_warning">افزودن هشدار محتوا</string>
<string name="remove_content_warning">حذف هشدار محتوا</string>
<string name="show_npub_as_a_qr_code">کلید عمومی را به شکل کد QR نشان بده</string>
<string name="invalid_nip19_uri">آدرس نامعتبر</string>
<string name="invalid_nip19_uri_description">امتیست آدرسی برای باز کردن دریافت کرد ولی آدرس نامعتبر بود: %1$s</string>
<string name="dm_relays_title">رله های صندوق پیام خصوصی</string>
<string name="dm_relays_through">رله ها: %1$s</string>
<string name="dm_relays_regular">استفاده از رله های معمولی</string>
<string name="dm_relays_not_found">تنظیم رله های صندوق ورودی خصوصی</string>
<string name="dm_relays_not_found_description">با این تنظیمات همه می توانند بدانند که هنگام ارسال پیام به شما از کدام رله ها استفاده کنند. بدون این تنظیمات ممکن است برخی پیام ها را از دست بدهید.</string>
<string name="dm_relays_not_found_examples">گزینه های خوب:
- inbox.nostr.wine (پولی)
- auth.nostr1.com (رایگان)
- you.nostr1.com (رله شخصی - پولی)</string>
<string name="dm_relays_not_found_examples2">گزینه های خوب:
- inbox.nostr.wine (پولی)
- auth.nostr1.com (رایگان)
- you.nostr1.com (رله شخصی - پولی)</string>
<string name="dm_relays_not_found_create_now">هم اکنون تنظیم شود</string>
<string name="search_relays_title">رله های جستجو</string>
<string name="search_relays_not_found">تنظیم رله های جستجو</string>
<string name="zap_the_devs_title">به توسعه دهندگان رپ دهید!</string>
<string name="accessibility_lyrics_off">متن خاموش</string>
<string name="accessibility_send">ارسال</string>
<string name="draft_note">پیش نویس یادداشت</string>
</resources>
@@ -540,6 +540,7 @@
<string name="nip05_checking">Vérification de l\'adresse Nostr</string>
<string name="select_deselect_all">Tout sélectionner/désélectionner</string>
<string name="default_relays">Défaut</string>
<string name="default_relays_longer">Rétablir les valeurs par défaut</string>
<string name="select_a_relay_to_continue">Sélectionner un relay pour continuer</string>
<string name="zap_forward_title">Transférer les Zaps à:</string>
<string name="zap_forward_explainer">Les clients compatibles transmettront des zaps sur l\'adresse LN ou le profil Utilisateur ci-dessous au lieu du vôtre.</string>
@@ -769,6 +770,7 @@
<string name="dm_relays_not_found">Configurer vos relais de messagerie privée</string>
<string name="dm_relays_not_found_description">Ce paramètre informe tout le monde les relais à utiliser pour vous envoyer des messages. Sans eux, vous risquez de manquer certains messages.</string>
<string name="dm_relays_not_found_examples">Les bonnes options sont:\n - inbox.nostr.wine (payant)\n - auth.nostr1.com (gratuit)\n - you.nostr1.com (relais personnels - payant)</string>
<string name="dm_relays_not_found_examples2">De bonnes options sont:\n - auth.nostr1.com (gratuit)\n - inbox.nostr.wine (payant)\n - relay.0xchat.com (gratuit)</string>
<string name="dm_relays_not_found_editing">Insérez entre 1-3 relais pour utiliser de boîte de réception privée. Les relais de boîte de réception MP doivent accepter les messages de tout le monde, mais ne vous permet que de les télécharger.</string>
<string name="dm_relays_not_found_create_now">Configurer maintenant</string>
<string name="search_relays_title">Relais de recherche</string>
@@ -539,6 +539,7 @@
<string name="nip05_checking">A Nostr cím ellenőrzése</string>
<string name="select_deselect_all">Mind kijelölése/kijelölés visszavonása</string>
<string name="default_relays">Alapértelmezett</string>
<string name="default_relays_longer">Visszaállítás alaphelyzetbe</string>
<string name="select_a_relay_to_continue">A folytatáshoz válassz egy csomópontot</string>
<string name="zap_forward_title">Zap-ek továbbítása:</string>
<string name="zap_forward_explainer">A funkciót támogató kliensek a Zap-eket az Ön tárcája helyett, az alábbi LN-címre vagy felhasználói profilra továbbítják</string>
@@ -768,6 +769,7 @@
<string name="dm_relays_not_found">Állítsd be a Privát postafiókod közvetítőit</string>
<string name="dm_relays_not_found_description">Ez a beállítás mindenkit tájékoztat, hogy melyik közvetítőt használod, amikor üzeneteket küldenek neked. Nélkülük néhány üzenetről lemaradhatsz.</string>
<string name="dm_relays_not_found_examples">Jó lehetőségek a következők:\n - inbox.nostr.wine (fizetős)\n - you.nostr1.com (személyes csomópontok - fizetős)</string>
<string name="dm_relays_not_found_examples2">Jó lehetőségek:\n - auth.nostr1.com (ingyenes)\n - inbox.nostr.wine (fizetős)\n - relay.0xchat.com (ingyenes)</string>
<string name="dm_relays_not_found_editing">Adj hozzá 13 csomópontot, hogy privát postafiókodként szolgáljon. A PÜ Bejővő csomópontoknak el kell fogadniuk bármely üzenetet bárkitől, de azok letöltését csak Te általad teszik lehetővé.</string>
<string name="dm_relays_not_found_create_now">Állítsd be most</string>
<string name="search_relays_title">Kereső Csomópontok</string>
@@ -27,10 +27,16 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string>
<string name="cashu_failed_redemption">Cashu-ja ni bilo mogoče unovčiti</string>
<string name="cashu_no_wallet_found">Na sistemu ni najdene združljive Cashu denarnice</string>
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Prejemnikova \"lightning\" storitev na %1$s ni na voljo. Izračunana je bila iz \"lightning\" naslova \'%2$s\'. Napaka: %3$s. Preverite, ali strežnik deluje in ali je \"lightning\" naslov pravilen</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Ni bilo mogoče razrešiti %1$s. Preverite, ali ste povezani, ali je strežnik dosegljiv in ali je lightning naslov %2$s pravilen.\n\nIzjema je bila: %3$s</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">Napaka pri razčlenjevanju JSON med pridobivanjem fakture iz %1$s. Preverite uporabnikove nastavitve za \"Lightning\"</string>
<string name="select_push_server">Izberi \"UnifiedPush\" aplikacijo</string>
<string name="push_server_install_app_description"> Za prejemanje potisnih obvestil namestite aplikacijo, ki podpira [Unified Push](https://unifiedpush.org/), na primer [Ntfy](https://ntfy.sh/).
Po namestitvi izberite aplikacijo, ki jo želite uporabljati, v nastavitvah.
</string>
<string name="classifieds_condition_fair_explainer">V zadovoljivem stanju</string>
<string name="classifieds_category_clothing">Oblačila</string>
<string name="classifieds_category_crafts">Rokodelstvo</string>
<string name="classifieds_category_other">Drugo</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Osnutkom zapisa ni mogoče pošiljati mikro plačil (zap)</string>
<string name="http_status_416">Razpon ni zadovoljiv Strežnik ne more izpolniti vrednosti, navedene v \"requests Range header\" polju. </string>
</resources>
+3
View File
@@ -416,6 +416,7 @@
<string name="follow_list_selection">Follow List</string>
<string name="follow_list_kind3follows">All Follows</string>
<string name="follow_list_aroundme">Around Me</string>
<string name="follow_list_global">Global</string>
<string name="follow_list_mute_list">Mute List</string>
@@ -635,6 +636,7 @@
<string name="nip05_checking">Checking Nostr address</string>
<string name="select_deselect_all">Select/Deselect all</string>
<string name="default_relays">Default</string>
<string name="default_relays_longer">Reset to Defaults</string>
<string name="select_a_relay_to_continue">Select a relay to continue</string>
<string name="zap_forward_title">Forward Zaps to:</string>
@@ -929,6 +931,7 @@
<string name="dm_relays_not_found">Set up your Private Inbox relays</string>
<string name="dm_relays_not_found_description">This setting lets everybody know which relays to use when sending messages to you. Without them you might miss some messages.</string>
<string name="dm_relays_not_found_examples">Good options are:\n - inbox.nostr.wine (paid)\n - auth.nostr1.com (free)\n - you.nostr1.com (personal relays - paid)</string>
<string name="dm_relays_not_found_examples2">Good options are:\n - auth.nostr1.com (free)\n - inbox.nostr.wine (paid)\n - relay.0xchat.com (free)</string>
<string name="dm_relays_not_found_editing">Insert between 13 relays to serve as your private inbox. DM Inbox relays should accept any message from anyone, but only allow you to download them.</string>
<string name="dm_relays_not_found_create_now">Set up now</string>
+7 -7
View File
@@ -14,23 +14,23 @@ benchmarkJunit4 = "1.3.3"
biometricKtx = "1.2.0-alpha05"
blurhash = "1.0.0"
coil = "2.7.0"
composeBom = "2024.10.00"
coreKtx = "1.13.1"
composeBom = "2024.10.01"
coreKtx = "1.15.0"
espressoCore = "3.6.1"
firebaseBom = "33.4.0"
fragmentKtx = "1.8.4"
firebaseBom = "33.5.1"
fragmentKtx = "1.8.5"
gms = "4.4.2"
jacksonModuleKotlin = "2.17.2"
jna = "5.14.0"
jtorctl = "0.4.5.7"
junit = "4.13.2"
kotlin = "2.0.0"
kotlin = "2.0.20"
kotlinxCollectionsImmutable = "0.3.7"
kotlinxSerialization = "1.7.2"
kotlinxSerializationPlugin = "2.0.0"
languageId = "17.0.6"
lazysodiumAndroid = "5.1.0"
lifecycleRuntimeKtx = "2.8.6"
lifecycleRuntimeKtx = "2.8.7"
lightcompressor = "1.3.2"
markdown = "077a2cde64"
media3 = "1.4.1"
@@ -53,7 +53,7 @@ zoomable = "1.6.2"
zxing = "3.5.3"
zxingAndroidEmbedded = "4.3.0"
windowCoreAndroid = "1.3.0"
androidxCamera = "1.3.4"
androidxCamera = "1.4.0"
[libraries]
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+3
View File
@@ -61,6 +61,9 @@ dependencies {
implementation "com.goterl:lazysodium-android:5.1.0@aar"
implementation 'net.java.dev.jna:jna:5.14.0@aar'
//implementation (libs.lazysodium.android) { artifact { type = "aar" } }
//implementation (libs.jna) { artifact { type = "aar" } }
// Performant Parser of JSONs into Events
api libs.jackson.module.kotlin
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.crypto.nip44
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.crypto.nip01.Nip01
@@ -191,74 +190,3 @@ class NIP44v2Test {
return MessageDigest.getInstance("SHA-256").digest(data).toHexKey()
}
}
data class VectorFile(
val v2: V2? = V2(),
)
data class V2(
val valid: Valid? = Valid(),
val invalid: Invalid? = Invalid(),
)
data class Valid(
@JsonProperty("get_conversation_key")
val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@JsonProperty("get_message_keys") val getMessageKeys: GetMessageKeys? = GetMessageKeys(),
@JsonProperty("calc_padded_len") val calcPaddedLen: ArrayList<ArrayList<Int>> = arrayListOf(),
@JsonProperty("encrypt_decrypt") val encryptDecrypt: ArrayList<EncryptDecrypt> = arrayListOf(),
@JsonProperty("encrypt_decrypt_long_msg")
val encryptDecryptLongMsg: ArrayList<EncryptDecryptLongMsg> = arrayListOf(),
)
data class Invalid(
@JsonProperty("encrypt_msg_lengths") val encryptMsgLengths: ArrayList<Int> = arrayListOf(),
@JsonProperty("get_conversation_key")
val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@JsonProperty("decrypt") val decrypt: ArrayList<Decrypt> = arrayListOf(),
)
data class GetConversationKey(
val sec1: String? = null,
val pub2: String? = null,
val note: String? = null,
@JsonProperty("conversation_key") val conversationKey: String? = null,
)
data class GetMessageKeys(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val keys: ArrayList<Keys> = arrayListOf(),
)
data class Keys(
@JsonProperty("nonce") val nonce: String? = null,
@JsonProperty("chacha_key") val chachaKey: String? = null,
@JsonProperty("chacha_nonce") val chachaNonce: String? = null,
@JsonProperty("hmac_key") val hmacKey: String? = null,
)
data class EncryptDecrypt(
val sec1: String? = null,
val sec2: String? = null,
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
)
data class EncryptDecryptLongMsg(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val pattern: String? = null,
val repeat: Int? = null,
@JsonProperty("plaintext_sha256") val plaintextSha256: String? = null,
@JsonProperty("payload_sha256") val payloadSha256: String? = null,
)
data class Decrypt(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
val note: String? = null,
)
@@ -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.quartz.crypto.nip44
import com.fasterxml.jackson.annotation.JsonProperty
data class VectorFile(
val v2: V2? = V2(),
)
data class V2(
val valid: Valid? = Valid(),
val invalid: Invalid? = Invalid(),
)
data class Valid(
@JsonProperty("get_conversation_key")
val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@JsonProperty("get_message_keys") val getMessageKeys: GetMessageKeys? = GetMessageKeys(),
@JsonProperty("calc_padded_len") val calcPaddedLen: ArrayList<ArrayList<Int>> = arrayListOf(),
@JsonProperty("encrypt_decrypt") val encryptDecrypt: ArrayList<EncryptDecrypt> = arrayListOf(),
@JsonProperty("encrypt_decrypt_long_msg")
val encryptDecryptLongMsg: ArrayList<EncryptDecryptLongMsg> = arrayListOf(),
)
data class Invalid(
@JsonProperty("encrypt_msg_lengths") val encryptMsgLengths: ArrayList<Int> = arrayListOf(),
@JsonProperty("get_conversation_key")
val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@JsonProperty("decrypt") val decrypt: ArrayList<Decrypt> = arrayListOf(),
)
data class GetConversationKey(
val sec1: String? = null,
val pub2: String? = null,
val note: String? = null,
@JsonProperty("conversation_key") val conversationKey: String? = null,
)
data class GetMessageKeys(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val keys: ArrayList<Keys> = arrayListOf(),
)
data class Keys(
@JsonProperty("nonce") val nonce: String? = null,
@JsonProperty("chacha_key") val chachaKey: String? = null,
@JsonProperty("chacha_nonce") val chachaNonce: String? = null,
@JsonProperty("hmac_key") val hmacKey: String? = null,
)
data class EncryptDecrypt(
val sec1: String? = null,
val sec2: String? = null,
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
)
data class EncryptDecryptLongMsg(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val pattern: String? = null,
val repeat: Int? = null,
@JsonProperty("plaintext_sha256") val plaintextSha256: String? = null,
@JsonProperty("payload_sha256") val payloadSha256: String? = null,
)
data class Decrypt(
@JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
val note: String? = null,
)
@@ -119,6 +119,8 @@ class ContactListEvent(
const val KIND = 3
const val ALT = "Follow List"
fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:"
fun createFromScratch(
followUsers: List<Contact> = emptyList(),
followTags: List<String> = emptyList(),
@@ -107,6 +107,19 @@ open class Event(
}
}
override fun anyHashTag(onEach: (str: String) -> Boolean) = anyTagged("t", onEach)
private fun anyTagged(
tagName: String,
onEach: (str: String) -> Boolean,
) = tags.any {
if (it.size > 1 && it[0] == tagName) {
onEach(it[1])
} else {
false
}
}
override fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R) = mapTagged("e", map)
override fun <R> mapTaggedAddress(map: (address: String) -> R) = mapTagged("a", map)
@@ -121,6 +121,8 @@ interface EventInterface {
fun forEachHashTag(onEach: (eventId: HexKey) -> Unit)
fun anyHashTag(onEach: (str: String) -> Boolean): Boolean
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R>
fun <R> mapTaggedAddress(map: (address: String) -> R): List<R>
@@ -87,6 +87,11 @@ class TextNoteEvent(
tags.add(arrayOf("q", it))
}
}
addresses?.forEach {
if (it.toTag() in directMentions) {
tags.add(arrayOf("q", it.toTag()))
}
}
addresses
?.map { it.toTag() }
?.let {