Merge branch 'main' of https://github.com/vitorpamplona/amethyst into vitorpamplona-main
merging upstream changes.
This commit is contained in:
@@ -30,4 +30,6 @@ If applicable, add a video and/or screenshots to help explain your problem.
|
|||||||
- Amber Version (if using it to sign):
|
- Amber Version (if using it to sign):
|
||||||
|
|
||||||
**Bounty (in Bitcoin sats) offered for a solution**
|
**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.
|
||||||
|
|||||||
@@ -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**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Bounty (in Bitcoin sats) offered for the implementation**
|
**Bounty (in Bitcoin sats) offered for the implementation**
|
||||||
Incentivize developers to work on your feature. 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 feature will not be coded because it doesn't actually matter to you.
|
||||||
**Additional context**
|
We prioritize feature development by its bounty size compared to how much work is required to get it done.
|
||||||
Add any other context, video, or screenshots about the feature request here.
|
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.0.0" />
|
<option name="version" value="2.0.20" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -32,6 +32,7 @@ import androidx.security.crypto.EncryptedSharedPreferences
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import coil.memory.MemoryCache
|
import coil.memory.MemoryCache
|
||||||
|
import com.vitorpamplona.amethyst.service.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -50,6 +51,7 @@ class Amethyst : Application() {
|
|||||||
|
|
||||||
// Service Manager is only active when the activity is active.
|
// Service Manager is only active when the activity is active.
|
||||||
val serviceManager = ServiceManager(applicationIOScope)
|
val serviceManager = ServiceManager(applicationIOScope)
|
||||||
|
val locationManager = LocationState(this, applicationIOScope)
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -28,6 +29,8 @@ import androidx.lifecycle.asLiveData
|
|||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
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.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
@@ -120,10 +123,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.combineTransform
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -160,17 +161,19 @@ class Account(
|
|||||||
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class LiveFollowLists(
|
class LiveFollowList(
|
||||||
val users: Set<String> = emptySet(),
|
val authors: Set<String> = emptySet(),
|
||||||
val usersPlusMe: Set<String>,
|
val authorsPlusMe: Set<String>,
|
||||||
val hashtags: Set<String> = emptySet(),
|
val hashtags: Set<String> = emptySet(),
|
||||||
val geotags: 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 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 =
|
val connectToRelaysFlow =
|
||||||
@@ -218,7 +221,7 @@ class Account(
|
|||||||
localRelayList: Set<String>,
|
localRelayList: Set<String>,
|
||||||
): List<RelaySetupInfo> {
|
): List<RelaySetupInfo> {
|
||||||
val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
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 =
|
val nip65RelaySet =
|
||||||
nip65RelayEvent?.relays()?.map {
|
nip65RelayEvent?.relays()?.map {
|
||||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(
|
AdvertisedRelayListEvent.AdvertisedRelayInfo(
|
||||||
@@ -465,23 +468,26 @@ class Account(
|
|||||||
}.toTypedArray(),
|
}.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowLists {
|
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
|
||||||
// makes sure the output include only valid p tags
|
// makes sure the output include only valid p tags
|
||||||
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
||||||
|
|
||||||
return LiveFollowLists(
|
return LiveFollowList(
|
||||||
verifiedFollowingUsers,
|
authors = verifiedFollowingUsers,
|
||||||
verifiedFollowingUsers + signer.pubKey,
|
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
|
||||||
latestContactList
|
hashtags =
|
||||||
?.unverifiedFollowTagSet()
|
latestContactList
|
||||||
?.map { it.lowercase() }
|
?.unverifiedFollowTagSet()
|
||||||
?.toSet() ?: emptySet(),
|
?.map { it.lowercase() }
|
||||||
latestContactList
|
?.toSet() ?: emptySet(),
|
||||||
?.unverifiedFollowGeohashSet()
|
geotags =
|
||||||
?.toSet() ?: emptySet(),
|
latestContactList
|
||||||
latestContactList
|
?.unverifiedFollowGeohashSet()
|
||||||
?.verifiedFollowAddressSet()
|
?.toSet() ?: emptySet(),
|
||||||
?.toSet() ?: emptySet(),
|
addresses =
|
||||||
|
latestContactList
|
||||||
|
?.verifiedFollowAddressSet()
|
||||||
|
?.toSet() ?: emptySet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +522,7 @@ class Account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
|
val liveKind3FollowsFlow: Flow<LiveFollowList> =
|
||||||
userProfile().flow().follows.stateFlow.transformLatest {
|
userProfile().flow().follows.stateFlow.transformLatest {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(buildFollowLists(it.user.latestContactList))
|
emit(buildFollowLists(it.user.latestContactList))
|
||||||
@@ -531,80 +537,102 @@ class Account(
|
|||||||
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
|
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
fun loadFlowsFor(listName: String): FeedsBaseFlows =
|
||||||
private val liveHomeList: Flow<ListNameNotePair> =
|
when (listName) {
|
||||||
settings.defaultHomeFollowList.flatMapLatest { listName ->
|
GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
|
||||||
loadPeopleListFlowFromListName(listName)
|
KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
|
||||||
}
|
AROUND_ME ->
|
||||||
|
FeedsBaseFlows(
|
||||||
fun peopleListFromListNameStarter(listName: String): ListNameNotePair =
|
listName,
|
||||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
location = Amethyst.instance.locationManager.locationStateFlow,
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
)
|
||||||
val noteEvent = note?.event as? GeneralListEvent
|
else -> {
|
||||||
ListNameNotePair(listName, noteEvent)
|
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||||
} else {
|
if (note != null) {
|
||||||
ListNameNotePair(listName, null)
|
FeedsBaseFlows(
|
||||||
}
|
listName,
|
||||||
|
peopleList =
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
note
|
||||||
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
|
.flow()
|
||||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
.metadata.stateFlow,
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
)
|
||||||
note?.flow()?.metadata?.stateFlow?.mapLatest {
|
} else {
|
||||||
val noteEvent = it.note.event as? GeneralListEvent
|
FeedsBaseFlows(listName)
|
||||||
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) {
|
|
||||||
null
|
|
||||||
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
|
||||||
kind3Follows
|
|
||||||
} else if (peopleListFollows.event == null) {
|
|
||||||
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
|
|
||||||
} else {
|
|
||||||
val result = waitToDecrypt(peopleListFollows.event)
|
|
||||||
if (result == null) {
|
|
||||||
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun combinePeopleListFlows(
|
suspend fun mapIntoFollowLists(
|
||||||
kind3FollowsSource: Flow<LiveFollowLists>,
|
listName: String,
|
||||||
peopleListFollowsSource: Flow<ListNameNotePair>,
|
kind3: LiveFollowList?,
|
||||||
): Flow<LiveFollowLists?> =
|
noteState: NoteState,
|
||||||
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
location: Location?,
|
||||||
checkNotInMainThread()
|
): LiveFollowList? =
|
||||||
emit(combinePeopleList(kind3Follows, peopleListFollows))
|
if (listName == GLOBAL_FOLLOWS) {
|
||||||
|
null
|
||||||
|
} 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 {
|
||||||
|
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val peopleList = noteState.note.event as? GeneralListEvent
|
||||||
|
if (peopleList != null) {
|
||||||
|
waitToDecrypt(peopleList) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
} else {
|
||||||
|
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val liveHomeFollowListFlow: Flow<LiveFollowLists?> by lazy {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
combinePeopleListFlows(liveKind3Follows, liveHomeList)
|
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 liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
val liveHomeFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
liveHomeFollowListFlow
|
combinePeopleListFlows(settings.defaultHomeFollowList)
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultHomeFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(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
|
* filter onion and local host from write relays
|
||||||
* for each user pubkey, a list of valid relays.
|
* for each user pubkey, a list of valid relays.
|
||||||
@@ -701,7 +729,7 @@ class Account(
|
|||||||
liveHomeFollowLists
|
liveHomeFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -728,53 +756,33 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
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 },
|
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveNotificationFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultNotificationFollowList)
|
||||||
settings.defaultNotificationFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultNotificationFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultNotificationFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveStoriesFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultStoriesFollowList)
|
||||||
settings.defaultStoriesFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultStoriesFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultStoriesFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -784,7 +792,7 @@ class Account(
|
|||||||
liveStoriesFollowLists
|
liveStoriesFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -811,31 +819,21 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
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 },
|
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveDiscoveryFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultDiscoveryFollowList)
|
||||||
settings.defaultDiscoveryFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultDiscoveryFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultDiscoveryFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -845,7 +843,7 @@ class Account(
|
|||||||
liveDiscoveryFollowLists
|
liveDiscoveryFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -872,7 +870,7 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
authorsPerRelay(
|
||||||
liveDiscoveryFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
liveDiscoveryFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||||
connectToRelays.value.filter { it.read }.map { it.url },
|
connectToRelays.value.filter { it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
@@ -881,19 +879,17 @@ class Account(
|
|||||||
|
|
||||||
private fun decryptLiveFollows(
|
private fun decryptLiveFollows(
|
||||||
listEvent: GeneralListEvent,
|
listEvent: GeneralListEvent,
|
||||||
onReady: (LiveFollowLists) -> Unit,
|
onReady: (LiveFollowList) -> Unit,
|
||||||
) {
|
) {
|
||||||
listEvent.privateTags(signer) { privateTagList ->
|
listEvent.privateTags(signer) { privateTagList ->
|
||||||
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
||||||
onReady(
|
onReady(
|
||||||
LiveFollowLists(
|
LiveFollowList(
|
||||||
users = users,
|
authors = users,
|
||||||
usersPlusMe = users + userProfile().pubkeyHex,
|
authorsPlusMe = users + userProfile().pubkeyHex,
|
||||||
hashtags =
|
hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||||
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
||||||
geotags =
|
addresses =
|
||||||
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
|
||||||
communities =
|
|
||||||
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
||||||
.map { it.toTag() }
|
.map { it.toTag() }
|
||||||
.toSet(),
|
.toSet(),
|
||||||
@@ -902,7 +898,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
|
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
|
||||||
withTimeoutOrNull(1000) {
|
withTimeoutOrNull(1000) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
decryptLiveFollows(peopleListFollows) {
|
decryptLiveFollows(peopleListFollows) {
|
||||||
@@ -3210,7 +3206,7 @@ class Account(
|
|||||||
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
||||||
flowHiddenUsers.value.spammers.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 {
|
fun isAcceptable(user: User): Boolean {
|
||||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||||
@@ -3268,8 +3264,8 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
note.reportsBy(liveKind3Follows.value.usersPlusMe) +
|
note.reportsBy(liveKind3Follows.value.authorsPlusMe) +
|
||||||
(note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) +
|
(note.author?.reportsBy(liveKind3Follows.value.authorsPlusMe) ?: emptyList()) +
|
||||||
innerReports
|
innerReports
|
||||||
).toSet()
|
).toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ val DefaultNIP65List =
|
|||||||
|
|
||||||
val DefaultDMRelayList =
|
val DefaultDMRelayList =
|
||||||
listOf(
|
listOf(
|
||||||
RelayUrlFormatter.normalize("wss://auth.nostr1.com/"),
|
RelayUrlFormatter.normalize("wss://auth.nostr1.com"),
|
||||||
RelayUrlFormatter.normalize("wss://nostr.mom/"),
|
RelayUrlFormatter.normalize("wss://relay.0xchat.com"),
|
||||||
RelayUrlFormatter.normalize("wss://nos.lol/"),
|
RelayUrlFormatter.normalize("wss://nos.lol"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val DefaultSearchRelayList =
|
val DefaultSearchRelayList =
|
||||||
@@ -77,6 +77,7 @@ val DefaultSearchRelayList =
|
|||||||
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
|
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
|
||||||
RelayUrlFormatter.normalize("wss://nostr.wine"),
|
RelayUrlFormatter.normalize("wss://nostr.wine"),
|
||||||
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
|
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.
|
// 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.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val KIND3_FOLLOWS = " All Follows "
|
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
|
@Stable
|
||||||
class AccountSettings(
|
class AccountSettings(
|
||||||
val keyPair: KeyPair,
|
val keyPair: KeyPair,
|
||||||
@@ -357,7 +361,6 @@ class AccountSettings(
|
|||||||
|
|
||||||
// Events might be different objects, we have to compare their ids.
|
// Events might be different objects, we have to compare their ids.
|
||||||
if (backupAppSpecificData?.id != appSettings.id) {
|
if (backupAppSpecificData?.id != appSettings.id) {
|
||||||
println("AABBCC Update App Specific Data")
|
|
||||||
backupAppSpecificData = appSettings
|
backupAppSpecificData = appSettings
|
||||||
syncedSettings.updateFrom(newSyncedSettings)
|
syncedSettings.updateFrom(newSyncedSettings)
|
||||||
|
|
||||||
|
|||||||
@@ -2043,7 +2043,10 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findUsersStartingWith(username: String): List<User> {
|
fun findUsersStartingWith(
|
||||||
|
username: String,
|
||||||
|
forAccount: Account?,
|
||||||
|
): List<User> {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val key = decodePublicKeyAsHexOrNull(username)
|
val key = decodePublicKeyAsHexOrNull(username)
|
||||||
@@ -2056,26 +2059,19 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return users.filter { _, user: User ->
|
return users.filter { _, user: User ->
|
||||||
(user.anyNameStartsWith(username)) ||
|
(
|
||||||
user.pubkeyHex.startsWith(username, true) ||
|
(user.anyNameStartsWith(username)) ||
|
||||||
user.pubkeyNpub().startsWith(username, true)
|
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> {
|
fun findNotesStartingWith(
|
||||||
checkNotInMainThread()
|
text: String,
|
||||||
|
forAccount: Account,
|
||||||
return addressables
|
): List<Note> {
|
||||||
.filter { _, note ->
|
|
||||||
val listEvent = note.event
|
|
||||||
(
|
|
||||||
listEvent is PeopleListEvent &&
|
|
||||||
user.pubkeyHex == listEvent.pubKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findNotesStartingWith(text: String): List<Note> {
|
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val key = decodeEventIdAsHexOrNull(text)
|
val key = decodeEventIdAsHexOrNull(text)
|
||||||
@@ -2102,11 +2098,19 @@ object LocalCache {
|
|||||||
note.idHex.startsWith(text, true) ||
|
note.idHex.startsWith(text, true) ||
|
||||||
note.idNote().startsWith(text, true)
|
note.idNote().startsWith(text, true)
|
||||||
) {
|
) {
|
||||||
return@filter true
|
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
|
||||||
|
return@filter true
|
||||||
|
} else {
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.event?.isContentEncoded() == false) {
|
if (note.event?.isContentEncoded() == false) {
|
||||||
return@filter note.event?.content()?.contains(text, true) ?: false
|
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
|
||||||
|
return@filter note.event?.content()?.contains(text, true) ?: false
|
||||||
|
} else {
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@filter false
|
return@filter false
|
||||||
@@ -2125,11 +2129,19 @@ object LocalCache {
|
|||||||
if (addressable.event?.matchTag1With(text) == true ||
|
if (addressable.event?.matchTag1With(text) == true ||
|
||||||
addressable.idHex.startsWith(text, true)
|
addressable.idHex.startsWith(text, true)
|
||||||
) {
|
) {
|
||||||
return@filter true
|
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
|
||||||
|
return@filter true
|
||||||
|
} else {
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addressable.event?.isContentEncoded() == false) {
|
if (addressable.event?.isContentEncoded() == false) {
|
||||||
return@filter addressable.event?.content()?.contains(text, true) ?: false
|
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
|
||||||
|
return@filter addressable.event?.content()?.contains(text, true) ?: false
|
||||||
|
} else {
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@filter false
|
return@filter false
|
||||||
|
|||||||
@@ -771,29 +771,11 @@ open class Note(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author?.toBestDisplayName()?.containsAny(accountChoices.hiddenWordsCase) == true) {
|
if (thisEvent.anyHashTag { it.containsAny(accountChoices.hiddenWordsCase) }) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author?.profilePicture()?.containsAny(accountChoices.hiddenWordsCase) == true) {
|
if (author?.containsAny(accountChoices.hiddenWordsCase) == true) return 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
|||||||
import com.vitorpamplona.quartz.events.ReportEvent
|
import com.vitorpamplona.quartz.events.ReportEvent
|
||||||
import com.vitorpamplona.quartz.events.UserMetadata
|
import com.vitorpamplona.quartz.events.UserMetadata
|
||||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
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.collections.immutable.persistentSetOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -371,6 +373,36 @@ class User(
|
|||||||
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
||||||
} != null
|
} != 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
|
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
|
||||||
|
|
||||||
var liveSet: UserLiveSet? = null
|
var liveSet: UserLiveSet? = null
|
||||||
|
|||||||
+70
-52
@@ -26,75 +26,93 @@ import android.location.Geocoder
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationListener
|
import android.location.LocationListener
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.HandlerThread
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.MutableState
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.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(
|
class LocationFlow(
|
||||||
context: Context,
|
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")
|
@SuppressLint("MissingPermission")
|
||||||
fun start(
|
fun get(
|
||||||
minTimeMs: Long = MIN_TIME,
|
minTimeMs: Long = MIN_TIME,
|
||||||
minDistanceM: Float = MIN_DISTANCE,
|
minDistanceM: Float = MIN_DISTANCE,
|
||||||
) {
|
): Flow<Location> =
|
||||||
locationListener().let {
|
callbackFlow {
|
||||||
locationListener = it
|
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.requestLocationUpdates(
|
||||||
LocationManager.NETWORK_PROVIDER,
|
LocationManager.NETWORK_PROVIDER,
|
||||||
minTimeMs,
|
minTimeMs,
|
||||||
minDistanceM,
|
minDistanceM,
|
||||||
it,
|
locationCallback,
|
||||||
locHandlerThread.looper,
|
Looper.getMainLooper(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
|
||||||
isStart.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
awaitClose {
|
||||||
locationListener?.let { locationManager.removeUpdates(it) }
|
locationManager.removeUpdates(locationCallback)
|
||||||
isStart.value = false
|
Log.d("Location Service", "LocationState Stop")
|
||||||
}
|
|
||||||
|
|
||||||
private fun locationListener() =
|
|
||||||
object : LocationListener {
|
|
||||||
override fun onLocationChanged(location: Location) {
|
|
||||||
locationStateFlow.value = location
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProviderEnabled(provider: String) {
|
|
||||||
providerState.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProviderDisabled(provider: String) {
|
|
||||||
providerState.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LocationState(
|
||||||
|
context: Context,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val MIN_TIME: Long = 10000L
|
||||||
|
const val MIN_DISTANCE: Float = 100.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private var latestLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
|
||||||
|
|
||||||
|
val locationStateFlow =
|
||||||
|
LocationFlow(context)
|
||||||
|
.get(MIN_TIME, MIN_DISTANCE)
|
||||||
|
.onEach {
|
||||||
|
latestLocation = it
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(5000),
|
||||||
|
latestLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
object CachedGeoLocations {
|
||||||
val locationNames = LruCache<String, String>(20)
|
val locationNames = LruCache<String, String>(20)
|
||||||
|
|
||||||
+1
-1
@@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
|||||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||||
val follows =
|
val follows =
|
||||||
account.liveDiscoveryFollowLists.value
|
account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
?.toList()
|
?.toList()
|
||||||
?.ifEmpty { null }
|
?.ifEmpty { null }
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
mapOf(
|
mapOf(
|
||||||
"g" to
|
"g" to
|
||||||
hashToLoad
|
hashToLoad
|
||||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
.map { listOf(it.lowercase()) }
|
||||||
.flatten(),
|
.flatten(),
|
||||||
),
|
),
|
||||||
limit = 100,
|
limit = 100,
|
||||||
@@ -225,7 +225,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowCommunitiesFilter(): TypedFilter? {
|
fun createFollowCommunitiesFilter(): TypedFilter? {
|
||||||
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
|
val communitiesToLoad = account.liveHomeFollowLists.value?.addresses ?: return null
|
||||||
|
|
||||||
if (communitiesToLoad.isEmpty()) return null
|
if (communitiesToLoad.isEmpty()) return null
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
userSuggestions =
|
userSuggestions =
|
||||||
LocalCache
|
LocalCache
|
||||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
.findUsersStartingWith(lastWord.removePrefix("@"), account)
|
||||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
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.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.LocationUtil
|
|
||||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||||
@@ -78,7 +78,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -163,8 +166,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
// GeoHash
|
// GeoHash
|
||||||
var wantsToAddGeoHash by mutableStateOf(false)
|
var wantsToAddGeoHash by mutableStateOf(false)
|
||||||
var locUtil: LocationUtil? = null
|
var location: StateFlow<String?>? = null
|
||||||
var location: Flow<String>? = null
|
|
||||||
|
|
||||||
// ZapRaiser
|
// ZapRaiser
|
||||||
var canAddZapRaiser by mutableStateOf(false)
|
var canAddZapRaiser by mutableStateOf(false)
|
||||||
@@ -530,14 +532,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val geoLocation = locUtil?.locationStateFlow?.value
|
val geoHash = location?.value
|
||||||
val geoHash =
|
|
||||||
if (wantsToAddGeoHash && geoLocation != null) {
|
|
||||||
geoLocation.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
||||||
|
|
||||||
nip95attachments.forEach {
|
nip95attachments.forEach {
|
||||||
@@ -1002,7 +997,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
userSuggestions =
|
userSuggestions =
|
||||||
LocalCache
|
LocalCache
|
||||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
.findUsersStartingWith(lastWord.removePrefix("@"), account)
|
||||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
@@ -1031,7 +1026,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
userSuggestions =
|
userSuggestions =
|
||||||
LocalCache
|
LocalCache
|
||||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
.findUsersStartingWith(lastWord.removePrefix("@"), account)
|
||||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
@@ -1059,7 +1054,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
userSuggestions =
|
userSuggestions =
|
||||||
LocalCache
|
LocalCache
|
||||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
.findUsersStartingWith(lastWord.removePrefix("@"), account)
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ account?.isFollowing(it) },
|
{ account?.isFollowing(it) },
|
||||||
@@ -1262,28 +1257,20 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun startLocation(context: Context) {
|
fun locationFlow(): Flow<String?> {
|
||||||
locUtil = LocationUtil(context)
|
if (location == null) {
|
||||||
locUtil?.let {
|
|
||||||
location =
|
location =
|
||||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
Amethyst.instance.locationManager.locationStateFlow
|
||||||
saveDraft()
|
.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopLocation() {
|
return location!!
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
|
||||||
location = null
|
|
||||||
locUtil = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
|
||||||
location = null
|
|
||||||
locUtil = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleNIP04And24() {
|
fun toggleNIP04And24() {
|
||||||
@@ -1386,7 +1373,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
|
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) }
|
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
|
||||||
this.clear()
|
this.clear()
|
||||||
this.putAll(newEntries)
|
this.putAll(newEntries)
|
||||||
|
|||||||
+25
-3
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -42,6 +43,8 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
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.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import com.vitorpamplona.ammolite.relays.RelayStat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -66,6 +70,7 @@ fun AddDMRelayListDialog(
|
|||||||
onDismissRequest = onClose,
|
onDismissRequest = onClose,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
|
SetDialogToEdgeToEdge()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -114,7 +119,7 @@ fun AddDMRelayListDialog(
|
|||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.SpaceAround,
|
verticalArrangement = Arrangement.SpaceAround,
|
||||||
) {
|
) {
|
||||||
Explanation()
|
Explanation(postViewModel)
|
||||||
|
|
||||||
DMRelayList(postViewModel, accountViewModel, onClose, nav)
|
DMRelayList(postViewModel, accountViewModel, onClose, nav)
|
||||||
}
|
}
|
||||||
@@ -123,7 +128,7 @@ fun AddDMRelayListDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Explanation() {
|
private fun Explanation(postViewModel: DMRelayListViewModel) {
|
||||||
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
|
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -133,8 +138,25 @@ private fun Explanation() {
|
|||||||
Spacer(modifier = StdVertSpacer)
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
|
||||||
Text(
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+24
-2
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -42,6 +43,8 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
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.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import com.vitorpamplona.ammolite.relays.RelayStat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -66,6 +70,7 @@ fun AddSearchRelayListDialog(
|
|||||||
onDismissRequest = onClose,
|
onDismissRequest = onClose,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
|
SetDialogToEdgeToEdge()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -114,7 +119,7 @@ fun AddSearchRelayListDialog(
|
|||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.SpaceAround,
|
verticalArrangement = Arrangement.SpaceAround,
|
||||||
) {
|
) {
|
||||||
Explanation()
|
Explanation(postViewModel)
|
||||||
|
|
||||||
SearchRelayList(postViewModel, accountViewModel, onClose, nav)
|
SearchRelayList(postViewModel, accountViewModel, onClose, nav)
|
||||||
}
|
}
|
||||||
@@ -123,7 +128,7 @@ fun AddSearchRelayListDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Explanation() {
|
private fun Explanation(postViewModel: SearchRelayListViewModel) {
|
||||||
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
|
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -135,6 +140,23 @@ private fun Explanation() {
|
|||||||
Text(
|
Text(
|
||||||
text = stringRes(id = R.string.search_relays_not_found_examples),
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+94
-77
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -49,6 +50,8 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
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.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
|
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -171,91 +174,92 @@ fun MappedAllRelayListView(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { pad ->
|
) { pad ->
|
||||||
Column(
|
LazyColumn(
|
||||||
|
contentPadding = FeedPadding,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(
|
.padding(
|
||||||
16.dp,
|
start = 10.dp,
|
||||||
pad.calculateTopPadding(),
|
end = 10.dp,
|
||||||
16.dp,
|
top = pad.calculateTopPadding(),
|
||||||
pad.calculateBottomPadding(),
|
bottom = pad.calculateBottomPadding(),
|
||||||
),
|
).consumeWindowInsets(pad),
|
||||||
verticalArrangement = Arrangement.SpaceAround,
|
|
||||||
) {
|
) {
|
||||||
LazyColumn(contentPadding = FeedPadding) {
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
stringRes(R.string.public_home_section),
|
||||||
|
stringRes(R.string.public_home_section_explainer),
|
||||||
|
Modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
stringRes(R.string.public_notif_section),
|
||||||
|
stringRes(R.string.public_notif_section_explainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategoryWithButton(
|
||||||
|
stringRes(R.string.private_inbox_section),
|
||||||
|
stringRes(R.string.private_inbox_section_explainer),
|
||||||
|
action = {
|
||||||
|
ResetDMRelays(dmViewModel)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
stringRes(R.string.private_outbox_section),
|
||||||
|
stringRes(R.string.private_outbox_section_explainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategoryWithButton(
|
||||||
|
stringRes(R.string.search_section),
|
||||||
|
stringRes(R.string.search_section_explainer),
|
||||||
|
action = {
|
||||||
|
ResetSearchRelays(searchViewModel)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
stringRes(R.string.local_section),
|
||||||
|
stringRes(R.string.local_section_explainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategoryWithButton(
|
||||||
|
stringRes(R.string.kind_3_section),
|
||||||
|
stringRes(R.string.kind_3_section_description),
|
||||||
|
action = {
|
||||||
|
ResetKind3Relays(kind3ViewModel)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderKind3Items(kind3FeedState, kind3ViewModel, accountViewModel, newNav, relayToAdd)
|
||||||
|
|
||||||
|
if (kind3Proposals.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
stringRes(R.string.public_home_section),
|
stringRes(R.string.kind_3_recommended_section),
|
||||||
stringRes(R.string.public_home_section_explainer),
|
stringRes(R.string.kind_3_recommended_section_description),
|
||||||
Modifier.padding(bottom = 8.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, newNav)
|
renderKind3ProposalItems(kind3Proposals, kind3ViewModel, accountViewModel, newNav)
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategory(
|
|
||||||
stringRes(R.string.public_notif_section),
|
|
||||||
stringRes(R.string.public_notif_section_explainer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav)
|
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategory(
|
|
||||||
stringRes(R.string.private_inbox_section),
|
|
||||||
stringRes(R.string.private_inbox_section_explainer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav)
|
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategory(
|
|
||||||
stringRes(R.string.private_outbox_section),
|
|
||||||
stringRes(R.string.private_outbox_section_explainer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav)
|
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategoryWithButton(
|
|
||||||
stringRes(R.string.search_section),
|
|
||||||
stringRes(R.string.search_section_explainer),
|
|
||||||
action = {
|
|
||||||
ResetSearchRelays(searchViewModel)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, newNav)
|
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategory(
|
|
||||||
stringRes(R.string.local_section),
|
|
||||||
stringRes(R.string.local_section_explainer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav)
|
|
||||||
|
|
||||||
item {
|
|
||||||
SettingsCategoryWithButton(
|
|
||||||
stringRes(R.string.kind_3_section),
|
|
||||||
stringRes(R.string.kind_3_section_description),
|
|
||||||
action = {
|
|
||||||
ResetKind3Relays(kind3ViewModel)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderKind3Items(kind3FeedState, kind3ViewModel, accountViewModel, newNav, relayToAdd)
|
|
||||||
|
|
||||||
if (kind3Proposals.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
SettingsCategory(
|
|
||||||
stringRes(R.string.kind_3_recommended_section),
|
|
||||||
stringRes(R.string.kind_3_recommended_section_description),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderKind3ProposalItems(kind3Proposals, kind3ViewModel, accountViewModel, newNav)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +284,20 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
postViewModel.deleteAll()
|
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()
|
postViewModel.loadRelayDocuments()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
|||||||
val proposed =
|
val proposed =
|
||||||
RelayListRecommendationProcessor
|
RelayListRecommendationProcessor
|
||||||
.reliableRelaySetFor(
|
.reliableRelaySetFor(
|
||||||
account.liveKind3Follows.value.users.mapNotNull {
|
account.liveKind3Follows.value.authors.mapNotNull {
|
||||||
account.getNIP65RelayList(it)
|
account.getNIP65RelayList(it)
|
||||||
},
|
},
|
||||||
relayUrlsToIgnore =
|
relayUrlsToIgnore =
|
||||||
|
|||||||
+2
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
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.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
|
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
|
||||||
@@ -104,6 +105,7 @@ fun RelayInformationDialog(
|
|||||||
dismissOnClickOutside = false,
|
dismissOnClickOutside = false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
SetDialogToEdgeToEdge()
|
||||||
Surface {
|
Surface {
|
||||||
val color =
|
val color =
|
||||||
remember {
|
remember {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ open class DiscoverLiveFeedFilter(
|
|||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> {
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors
|
||||||
|
|
||||||
val counter = ParticipantListBuilder()
|
val counter = ParticipantListBuilder()
|
||||||
val participantCounts =
|
val participantCounts =
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
|||||||
class FilterByListParams(
|
class FilterByListParams(
|
||||||
val isGlobal: Boolean,
|
val isGlobal: Boolean,
|
||||||
val isHiddenList: Boolean,
|
val isHiddenList: Boolean,
|
||||||
val followLists: Account.LiveFollowLists?,
|
val followLists: Account.LiveFollowList?,
|
||||||
val hiddenLists: Account.LiveHiddenUsers,
|
val hiddenLists: Account.LiveHiddenUsers,
|
||||||
val now: Long = TimeUtils.oneMinuteFromNow(),
|
val now: Long = TimeUtils.oneMinuteFromNow(),
|
||||||
) {
|
) {
|
||||||
@@ -45,22 +45,22 @@ class FilterByListParams(
|
|||||||
if (followLists == null) return false
|
if (followLists == null) return false
|
||||||
|
|
||||||
return if (noteEvent is LiveActivitiesEvent) {
|
return if (noteEvent is LiveActivitiesEvent) {
|
||||||
noteEvent.participantsIntersect(followLists.users) ||
|
noteEvent.participantsIntersect(followLists.authors) ||
|
||||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||||
} else {
|
} else {
|
||||||
noteEvent.pubKey in followLists.users ||
|
noteEvent.pubKey in followLists.authors ||
|
||||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isATagInList(aTag: ATag): Boolean {
|
fun isATagInList(aTag: ATag): Boolean {
|
||||||
if (followLists == null) return false
|
if (followLists == null) return false
|
||||||
|
|
||||||
return aTag.pubKeyHex in followLists.users
|
return aTag.pubKeyHex in followLists.authors
|
||||||
}
|
}
|
||||||
|
|
||||||
fun match(
|
fun match(
|
||||||
@@ -89,7 +89,7 @@ class FilterByListParams(
|
|||||||
fun create(
|
fun create(
|
||||||
userHex: String,
|
userHex: String,
|
||||||
selectedListName: String,
|
selectedListName: String,
|
||||||
followLists: Account.LiveFollowLists?,
|
followLists: Account.LiveFollowList?,
|
||||||
hiddenUsers: Account.LiveHiddenUsers,
|
hiddenUsers: Account.LiveHiddenUsers,
|
||||||
): FilterByListParams =
|
): FilterByListParams =
|
||||||
FilterByListParams(
|
FilterByListParams(
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class GeoHashFeedFilter(
|
|||||||
it.event is AudioHeaderEvent
|
it.event is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
it.event?.isTaggedGeoHash(geoTag) == true &&
|
it.event?.isTaggedGeoHash(geoTag) == true &&
|
||||||
|
!it.isHiddenFor(account.flowHiddenUsers.value) &&
|
||||||
account.isAcceptable(it)
|
account.isAcceptable(it)
|
||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
|
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class HashtagFeedFilter(
|
|||||||
it.event is AudioHeaderEvent
|
it.event is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
it.event?.isTaggedHash(hashTag) == true &&
|
it.event?.isTaggedHash(hashTag) == true &&
|
||||||
|
!it.isHiddenFor(account.flowHiddenUsers.value) &&
|
||||||
account.isAcceptable(it)
|
account.isAcceptable(it)
|
||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
|
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class NotificationFeedFilter(
|
|||||||
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
||||||
it.event !is GiftWrapEvent &&
|
it.event !is GiftWrapEvent &&
|
||||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
(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 &&
|
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||||
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
||||||
tagsAnEventByUser(it, loggedInUserHex)
|
tagsAnEventByUser(it, loggedInUserHex)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ThreadFeedFilter(
|
|||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
||||||
val followingKeySet = account.liveKind3Follows.value.users
|
val followingKeySet = account.liveKind3Follows.value.authors
|
||||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
||||||
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
||||||
val now = TimeUtils.now()
|
val now = TimeUtils.now()
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
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.FollowListState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -112,7 +112,7 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
fun FollowListWithRoutes(
|
fun FollowListWithRoutes(
|
||||||
followListsModel: FollowListState,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (FeedDefinition) -> Unit,
|
||||||
) {
|
) {
|
||||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ fun FollowListWithRoutes(
|
|||||||
fun FollowListWithoutRoutes(
|
fun FollowListWithoutRoutes(
|
||||||
followListsModel: FollowListState,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (FeedDefinition) -> Unit,
|
||||||
) {
|
) {
|
||||||
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
followingCount.value.users.size
|
followingCount.value.authors.size
|
||||||
.toString(),
|
.toString(),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
|||||||
+62
-6
@@ -20,10 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -33,6 +35,8 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -41,12 +45,20 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.map
|
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.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
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.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.CommunityName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||||
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.Name
|
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.screen.loggedIn.SpinnerSelectionDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedFilterSpinner(
|
fun FeedFilterSpinner(
|
||||||
placeholderCode: String,
|
placeholderCode: String,
|
||||||
explainer: String,
|
explainer: String,
|
||||||
options: ImmutableList<CodeName>,
|
options: ImmutableList<FeedDefinition>,
|
||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -75,20 +90,61 @@ fun FeedFilterSpinner(
|
|||||||
id = R.string.select_an_option,
|
id = R.string.select_an_option,
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentText by
|
var selected by
|
||||||
remember(placeholderCode, options) {
|
remember(placeholderCode, options) {
|
||||||
mutableStateOf(
|
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(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(modifier = Size20Modifier)
|
Spacer(modifier = Size20Modifier)
|
||||||
Text(currentText)
|
|
||||||
|
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(
|
Icon(
|
||||||
imageVector = Icons.Default.ExpandMore,
|
imageVector = Icons.Default.ExpandMore,
|
||||||
contentDescription = explainer,
|
contentDescription = explainer,
|
||||||
@@ -115,7 +171,7 @@ fun FeedFilterSpinner(
|
|||||||
options = options,
|
options = options,
|
||||||
onDismiss = { optionsShowing = false },
|
onDismiss = { optionsShowing = false },
|
||||||
onSelect = {
|
onSelect = {
|
||||||
currentText = options[it].name.name(context)
|
selected = options[it]
|
||||||
optionsShowing = false
|
optionsShowing = false
|
||||||
onSelect(it)
|
onSelect(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -671,13 +671,13 @@ fun LoadModerators(
|
|||||||
|
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||||
|
|
||||||
@@ -724,7 +724,7 @@ private fun LoadParticipants(
|
|||||||
|
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
|
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
@@ -733,7 +733,7 @@ private fun LoadParticipants(
|
|||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
.followsThatParticipateOn(baseNote, allFollows)
|
.followsThatParticipateOn(baseNote, allFollows)
|
||||||
@@ -882,7 +882,7 @@ fun RenderChannelThumb(
|
|||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
.followsThatParticipateOn(baseNote, followingKeySet)
|
.followsThatParticipateOn(baseNote, followingKeySet)
|
||||||
@@ -890,7 +890,7 @@ fun RenderChannelThumb(
|
|||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
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.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -42,7 +44,6 @@ import androidx.compose.material3.Button
|
|||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -59,7 +60,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -375,16 +375,7 @@ private fun RenderOptionAfterVote(
|
|||||||
QuoteBorder,
|
QuoteBorder,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
LinearProgressIndicator(
|
DisplayProgress(poolOption, color, modifier = Modifier.matchParentSize())
|
||||||
modifier = Modifier.matchParentSize(),
|
|
||||||
color = color,
|
|
||||||
gapSize = 0.dp,
|
|
||||||
strokeCap = StrokeCap.Square,
|
|
||||||
drawStopIndicator = {},
|
|
||||||
progress = {
|
|
||||||
poolOption.tally.value.toFloat()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -398,10 +389,7 @@ private fun RenderOptionAfterVote(
|
|||||||
.width(45.dp)
|
.width(45.dp)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
TallyText(poolOption)
|
||||||
text = "${(poolOption.tally.value.toFloat() * 100).roundToInt()}%",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
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
|
@Composable
|
||||||
private fun RenderOptionBeforeVote(
|
private fun RenderOptionBeforeVote(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ data class PollOption(
|
|||||||
val option: Int,
|
val option: Int,
|
||||||
val descriptor: String,
|
val descriptor: String,
|
||||||
var zappedValue: MutableState<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
|
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 consensusThreadhold: MutableState<Boolean> = mutableStateOf(false),
|
||||||
var zappedByLoggedIn: MutableState<Boolean> = mutableStateOf(false),
|
var zappedByLoggedIn: MutableState<Boolean> = mutableStateOf(false),
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ class PollNoteViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it.zappedValue.value = zappedValue
|
it.zappedValue.value = zappedValue
|
||||||
it.tally.value = tallyValue
|
it.tally.value = tallyValue.toFloat()
|
||||||
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
|
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
|
||||||
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
|
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ fun UpdateZapAmountDialog(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -373,6 +373,6 @@ fun WatchUserFollows(
|
|||||||
} else {
|
} else {
|
||||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
onFollowChanges(state.users.contains(userHex))
|
onFollowChanges(state.authors.contains(userHex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-6
@@ -144,12 +144,6 @@ fun AddInboxRelayForDMCard(
|
|||||||
|
|
||||||
Spacer(modifier = StdVertSpacer)
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringRes(id = R.string.dm_relays_not_found_examples),
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = StdVertSpacer)
|
|
||||||
|
|
||||||
var wantsToEditRelays by remember { mutableStateOf(false) }
|
var wantsToEditRelays by remember { mutableStateOf(false) }
|
||||||
if (wantsToEditRelays) {
|
if (wantsToEditRelays) {
|
||||||
AddDMRelayListDialog({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
AddDMRelayListDialog({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
||||||
|
|||||||
@@ -355,5 +355,5 @@ fun WatchAddressableNoteFollows(
|
|||||||
) {
|
) {
|
||||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
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
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Stable
|
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.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -280,6 +284,8 @@ abstract class FeedViewModel(
|
|||||||
|
|
||||||
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
|
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
|
||||||
|
|
||||||
|
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
|
||||||
|
|
||||||
private var collectorJob: Job? = null
|
private var collectorJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
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.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
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.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
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.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -56,33 +71,56 @@ class FollowListState(
|
|||||||
val viewModelScope: CoroutineScope,
|
val viewModelScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
val kind3Follow =
|
val kind3Follow =
|
||||||
CodeName(
|
PeopleListOutBoxFeedDefinition(
|
||||||
KIND3_FOLLOWS,
|
code = KIND3_FOLLOWS,
|
||||||
ResourceName(R.string.follow_list_kind3follows),
|
name = ResourceName(R.string.follow_list_kind3follows),
|
||||||
CodeNameType.HARDCODED,
|
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
|
account
|
||||||
.getAllPeopleLists()
|
.getAllPeopleLists()
|
||||||
.map {
|
.map {
|
||||||
CodeName(
|
PeopleListOutBoxFeedDefinition(
|
||||||
it.idHex,
|
it.idHex,
|
||||||
PeopleListName(it),
|
PeopleListName(it),
|
||||||
CodeNameType.PEOPLE_LIST,
|
CodeNameType.PEOPLE_LIST,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
listOf(it.idHex),
|
||||||
)
|
)
|
||||||
}.sortedBy { it.name.name() }
|
}.sortedBy { it.name.name() }
|
||||||
|
|
||||||
val livePeopleListsFlow = MutableStateFlow(emptyList<CodeName>())
|
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
||||||
|
|
||||||
fun updateFeedWith(newNotes: Set<Note>) {
|
fun updateFeedWith(newNotes: Set<Note>) {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
@@ -118,29 +156,46 @@ class FollowListState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
val liveKind3FollowsFlow: Flow<List<FeedDefinition>> =
|
||||||
account.liveKind3Follows.transformLatest {
|
account.liveKind3Follows.transformLatest {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val communities =
|
val communities =
|
||||||
it.communities.mapNotNull {
|
it.addresses.mapNotNull {
|
||||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||||
CodeName(
|
TagFeedDefinition(
|
||||||
"Community/${communityNote.idHex}",
|
"Community/${communityNote.idHex}",
|
||||||
CommunityName(communityNote),
|
CommunityName(communityNote),
|
||||||
CodeNameType.ROUTE,
|
CodeNameType.ROUTE,
|
||||||
|
kinds = DEFAULT_COMMUNITY_FEEDS,
|
||||||
|
aTags = listOf(communityNote.idHex),
|
||||||
|
relays = account.activeGlobalRelays().toList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hashtags =
|
val hashtags =
|
||||||
it.hashtags.map {
|
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 =
|
val geotags =
|
||||||
it.geotags.map {
|
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(
|
emit(
|
||||||
@@ -156,7 +211,7 @@ class FollowListState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, globalFollow),
|
listOf(kind3Follow, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
myLiveKind3FollowsFlow,
|
myLiveKind3FollowsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
@@ -172,7 +227,7 @@ class FollowListState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, globalFollow),
|
listOf(kind3Follow, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
).flatten().toImmutableList(),
|
).flatten().toImmutableList(),
|
||||||
@@ -238,8 +293,78 @@ class CommunityName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CodeName(
|
abstract class FeedDefinition(
|
||||||
val code: String,
|
val code: String,
|
||||||
val name: Name,
|
val name: Name,
|
||||||
val type: CodeNameType,
|
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,
|
||||||
|
)
|
||||||
|
|||||||
+1
-2
@@ -120,7 +120,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -396,7 +395,7 @@ class AccountViewModel(
|
|||||||
note.flow().metadata.stateFlow,
|
note.flow().metadata.stateFlow,
|
||||||
note.flow().reports.stateFlow,
|
note.flow().reports.stateFlow,
|
||||||
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
||||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
|
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
|
|||||||
+2
-15
@@ -188,7 +188,6 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
@@ -1335,21 +1334,9 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
||||||
val context = LocalContext.current
|
val location by postViewModel.locationFlow().collectAsStateWithLifecycle(null)
|
||||||
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
|
|
||||||
|
|
||||||
DisposableEffect(key1 = context) {
|
location?.let { DisplayLocationInTitle(geohash = it) }
|
||||||
postViewModel.startLocation(context = context)
|
|
||||||
locationDescriptionFlow = postViewModel.location
|
|
||||||
|
|
||||||
onDispose { postViewModel.stopLocation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
locationDescriptionFlow?.let {
|
|
||||||
val location by it.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
location?.let { DisplayLocationInTitle(geohash = it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+2
-2
@@ -75,7 +75,7 @@ class SearchBarViewModel(
|
|||||||
_hashtagResults.emit(findHashtags(searchValue))
|
_hashtagResults.emit(findHashtags(searchValue))
|
||||||
_searchResultsUsers.emit(
|
_searchResultsUsers.emit(
|
||||||
LocalCache
|
LocalCache
|
||||||
.findUsersStartingWith(searchValue)
|
.findUsersStartingWith(searchValue, account)
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ it.toBestDisplayName().startsWith(searchValue, true) },
|
{ it.toBestDisplayName().startsWith(searchValue, true) },
|
||||||
@@ -86,7 +86,7 @@ class SearchBarViewModel(
|
|||||||
)
|
)
|
||||||
_searchResultsNotes.emit(
|
_searchResultsNotes.emit(
|
||||||
LocalCache
|
LocalCache
|
||||||
.findNotesStartingWith(searchValue)
|
.findNotesStartingWith(searchValue, account)
|
||||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||||
.reversed(),
|
.reversed(),
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-2
@@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -178,7 +177,7 @@ fun WatchAccountForSearchScreen(accountViewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
@OptIn(FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchBar(
|
private fun SearchBar(
|
||||||
searchBarViewModel: SearchBarViewModel,
|
searchBarViewModel: SearchBarViewModel,
|
||||||
|
|||||||
+9
-9
@@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
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.TorrentEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoEvent
|
import com.vitorpamplona.quartz.events.VideoEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -211,17 +209,15 @@ fun ThreadFeedView(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
RefresheableBox(viewModel) {
|
RefresheableBox(viewModel) {
|
||||||
RenderFeedState(
|
RenderFeedState(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
listState = listState,
|
listState = viewModel.llState,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
routeForLastRead = null,
|
routeForLastRead = null,
|
||||||
onLoaded = {
|
onLoaded = {
|
||||||
RenderThreadFeed(noteId, it, listState, accountViewModel, nav)
|
RenderThreadFeed(noteId, it, viewModel.llState, accountViewModel, nav)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -236,7 +232,6 @@ fun RenderThreadFeed(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||||
val firstTimeScrolled = remember { TimeUtils.now() }
|
|
||||||
|
|
||||||
LaunchedEffect(noteId, items.list) {
|
LaunchedEffect(noteId, items.list) {
|
||||||
// hack to allow multiple scrolls to Item while posts on the screen load.
|
// hack to allow multiple scrolls to Item while posts on the screen load.
|
||||||
@@ -252,10 +247,15 @@ fun RenderThreadFeed(
|
|||||||
// records before setting up the position on the feed.
|
// records before setting up the position on the feed.
|
||||||
//
|
//
|
||||||
// It jumps around, but it is the best we can do.
|
// 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 }
|
val position = items.list.indexOfFirst { it.idHex == noteId }
|
||||||
|
|
||||||
if (position >= 0) {
|
if (position >= 0) {
|
||||||
listState.scrollToItem(position, -200)
|
if (position > items.list.size - 3) {
|
||||||
|
listState.scrollToItem(position, 0)
|
||||||
|
} else {
|
||||||
|
listState.scrollToItem(position, -200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,23 @@
|
|||||||
<string name="point_to_the_qr_code">کد QR را نشانه بگیرید</string>
|
<string name="point_to_the_qr_code">کد QR را نشانه بگیرید</string>
|
||||||
<string name="show_qr">کد QR را نشان بده</string>
|
<string name="show_qr">کد QR را نشان بده</string>
|
||||||
<string name="profile_image">تصویر نمایه</string>
|
<string name="profile_image">تصویر نمایه</string>
|
||||||
|
<string name="your_profile_image">تصویر نمایه شما</string>
|
||||||
<string name="scan_qr">اسکن کد QR</string>
|
<string name="scan_qr">اسکن کد QR</string>
|
||||||
<string name="show_anyway">بهر حال نشان بده</string>
|
<string name="show_anyway">بهر حال نشان بده</string>
|
||||||
|
<string name="post_was_hidden">این پست پنهان شده چون به کلمات یا کاربران پنهان شده شما اشاره دارد</string>
|
||||||
<string name="post_was_flagged_as_inappropriate_by">این یادداشت توسط این افراد گزارش شده:</string>
|
<string name="post_was_flagged_as_inappropriate_by">این یادداشت توسط این افراد گزارش شده:</string>
|
||||||
<string name="post_not_found">این یادداشت یافت نشد</string>
|
<string name="post_not_found">این یادداشت یافت نشد</string>
|
||||||
|
<string name="post_not_found_short">👀</string>
|
||||||
<string name="channel_image">تصویر کانال</string>
|
<string name="channel_image">تصویر کانال</string>
|
||||||
<string name="referenced_event_not_found">رویداد مورد نظر یافت نشد</string>
|
<string name="referenced_event_not_found">رویداد مورد نظر یافت نشد</string>
|
||||||
<string name="could_not_decrypt_the_message">پیام رمزگشایی نشد</string>
|
<string name="could_not_decrypt_the_message">پیام رمزگشایی نشد</string>
|
||||||
<string name="group_picture">تصویر گروه</string>
|
<string name="group_picture">تصویر گروه</string>
|
||||||
<string name="explicit_content">محتوای نامناسب</string>
|
<string name="explicit_content">محتوای نامناسب</string>
|
||||||
<string name="spam">اسپم</string>
|
<string name="spam">اسپم</string>
|
||||||
|
<string name="spam_description">تعداد رویدادهای اسپم که از این رله می آید</string>
|
||||||
<string name="impersonation">جعل هویت</string>
|
<string name="impersonation">جعل هویت</string>
|
||||||
<string name="illegal_behavior">رفتار ناهنجار</string>
|
<string name="illegal_behavior">رفتار ناهنجار</string>
|
||||||
|
<string name="other">سایر</string>
|
||||||
<string name="unknown">ناشناس</string>
|
<string name="unknown">ناشناس</string>
|
||||||
<string name="relay_icon">آیکن رله</string>
|
<string name="relay_icon">آیکن رله</string>
|
||||||
<string name="unknown_author">نویسنده ناشناس</string>
|
<string name="unknown_author">نویسنده ناشناس</string>
|
||||||
@@ -22,6 +27,9 @@
|
|||||||
<string name="copy_user_pubkey">کپی شناسه نویسنده</string>
|
<string name="copy_user_pubkey">کپی شناسه نویسنده</string>
|
||||||
<string name="copy_note_id">کپی شناسه یادداشت</string>
|
<string name="copy_note_id">کپی شناسه یادداشت</string>
|
||||||
<string name="broadcast">انتشار</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></string>
|
<string name="request_deletion">درخواست حذف/string></string>
|
||||||
<string name="block_report">مسدود/گزارش</string>
|
<string name="block_report">مسدود/گزارش</string>
|
||||||
<string name="block_hide_user"><![CDATA[بلاک/پنهان کردن کاربر]]></string>
|
<string name="block_hide_user"><![CDATA[بلاک/پنهان کردن کاربر]]></string>
|
||||||
@@ -29,6 +37,10 @@
|
|||||||
<string name="report_impersonation">گزارش جعل هویت</string>
|
<string name="report_impersonation">گزارش جعل هویت</string>
|
||||||
<string name="report_explicit_content">گزارش محتوای نامناسب</string>
|
<string name="report_explicit_content">گزارش محتوای نامناسب</string>
|
||||||
<string name="report_illegal_behaviour">گزارش رفتار ناهنجار</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_reply">با کلید خصوصی وارد شوید تا بتوانید پاسخ دهید</string>
|
||||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را بازنشر کنید</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>
|
<string name="login_with_a_private_key_to_like_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را لایک کنید</string>
|
||||||
@@ -42,7 +54,12 @@
|
|||||||
<string name="view_count">دیدن تعداد</string>
|
<string name="view_count">دیدن تعداد</string>
|
||||||
<string name="boost">بازنشر</string>
|
<string name="boost">بازنشر</string>
|
||||||
<string name="boosted">بازنشر شده</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="quote">نقل قول</string>
|
||||||
|
<string name="fork">فورک</string>
|
||||||
|
<string name="propose_an_edit">پیشنهاد ویرایش</string>
|
||||||
<string name="new_amount_in_sats">مبلغ جدید به ساتوشی</string>
|
<string name="new_amount_in_sats">مبلغ جدید به ساتوشی</string>
|
||||||
<string name="add">افزودن</string>
|
<string name="add">افزودن</string>
|
||||||
<string name="replying_to">" پاسخ به"</string>
|
<string name="replying_to">" پاسخ به"</string>
|
||||||
@@ -82,6 +99,7 @@
|
|||||||
<string name="posts">یادداشت ها</string>
|
<string name="posts">یادداشت ها</string>
|
||||||
<string name="bytes">بایت</string>
|
<string name="bytes">بایت</string>
|
||||||
<string name="errors">خطاها</string>
|
<string name="errors">خطاها</string>
|
||||||
|
<string name="errors_description">تعداد خطاهای اتصال در این نوبت</string>
|
||||||
<string name="home_feed">خبرنامه اصلی</string>
|
<string name="home_feed">خبرنامه اصلی</string>
|
||||||
<string name="private_message_feed">پیام خصوصی</string>
|
<string name="private_message_feed">پیام خصوصی</string>
|
||||||
<string name="public_chat_feed">گفتگوی عمومی</string>
|
<string name="public_chat_feed">گفتگوی عمومی</string>
|
||||||
@@ -100,8 +118,11 @@
|
|||||||
<string name="website_url">آدری وبسایت</string>
|
<string name="website_url">آدری وبسایت</string>
|
||||||
<string name="ln_address">آدرس لایتنینگ</string>
|
<string name="ln_address">آدرس لایتنینگ</string>
|
||||||
<string name="ln_url_outdated">آدرس لایتنینگ(نقل شده)</string>
|
<string name="ln_url_outdated">آدرس لایتنینگ(نقل شده)</string>
|
||||||
|
<string name="save_to_gallery">در گالری ذخیره کن</string>
|
||||||
<string name="image_saved_to_the_gallery">تصویر در گالری ذخیره شد</string>
|
<string name="image_saved_to_the_gallery">تصویر در گالری ذخیره شد</string>
|
||||||
<string name="failed_to_save_the_image">تصویر ذخیره نشد</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="upload_image">بارگذاری تصویر</string>
|
||||||
<string name="uploading">...در حال بارگذاری</string>
|
<string name="uploading">...در حال بارگذاری</string>
|
||||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">این کاربر آدرس لایتنینگ برای دریافت ساتوشی تنظیم نکرده است</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="conversations">گفتگوها</string>
|
||||||
<string name="notes">یادداشت ها</string>
|
<string name="notes">یادداشت ها</string>
|
||||||
<string name="replies">پاسخ ها</string>
|
<string name="replies">پاسخ ها</string>
|
||||||
|
<string name="gallery">گالری</string>
|
||||||
<string name="follows">"دنبال شوندگان"</string>
|
<string name="follows">"دنبال شوندگان"</string>
|
||||||
<string name="reports">"گزارش ها"</string>
|
<string name="reports">"گزارش ها"</string>
|
||||||
<string name="more_options">انتخاب های بیشتر</string>
|
<string name="more_options">انتخاب های بیشتر</string>
|
||||||
@@ -138,12 +160,15 @@
|
|||||||
<string name="clear">پاک کردن</string>
|
<string name="clear">پاک کردن</string>
|
||||||
<string name="app_logo">لوگوی اپ</string>
|
<string name="app_logo">لوگوی اپ</string>
|
||||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string>
|
<string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string>
|
||||||
|
<string name="ncryptsec_password">پسورد باز کردن کلید</string>
|
||||||
<string name="show_password">نشان دادن گذرواژه</string>
|
<string name="show_password">نشان دادن گذرواژه</string>
|
||||||
<string name="hide_password">پنهان کردن گذرواژه</string>
|
<string name="hide_password">پنهان کردن گذرواژه</string>
|
||||||
<string name="invalid_key">کلید نامعتبر</string>
|
<string name="invalid_key">کلید نامعتبر</string>
|
||||||
|
<string name="invalid_key_with_message">کلید نامعتبر: %1$s</string>
|
||||||
<string name="i_accept_the">"می پذیرم "</string>
|
<string name="i_accept_the">"می پذیرم "</string>
|
||||||
<string name="terms_of_use">شرایط استفاده</string>
|
<string name="terms_of_use">شرایط استفاده</string>
|
||||||
<string name="acceptance_of_terms_is_required">پذیرش شرایط الازمیست</string>
|
<string name="acceptance_of_terms_is_required">پذیرش شرایط الازمیست</string>
|
||||||
|
<string name="password_is_required">رمز عبور لازم است</string>
|
||||||
<string name="key_is_required">کلید الزامیست</string>
|
<string name="key_is_required">کلید الزامیست</string>
|
||||||
<string name="name_is_required">نام الزامی است</string>
|
<string name="name_is_required">نام الزامی است</string>
|
||||||
<string name="login">ورود</string>
|
<string name="login">ورود</string>
|
||||||
@@ -194,8 +219,18 @@
|
|||||||
<string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string>
|
<string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string>
|
||||||
<string name="mark_all_as_read">همه را خوانده شده کن</string>
|
<string name="mark_all_as_read">همه را خوانده شده کن</string>
|
||||||
<string name="backup_keys">بازیابی کلیدها</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="secret_key_copied_to_clipboard">کلید خصوصی در کلیپبورد کپی شد</string>
|
||||||
<string name="copy_my_secret_key">کلید خصوصیم را کپی کن</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">احراز هویت انجام نشد</string>
|
||||||
<string name="biometric_authentication_failed_explainer">مشخصات بیومتریک نتوانست مالک این تلفن را احراز هویت کند</string>
|
<string name="biometric_authentication_failed_explainer">مشخصات بیومتریک نتوانست مالک این تلفن را احراز هویت کند</string>
|
||||||
<string name="biometric_authentication_failed_explainer_with_error">مشخصات بیومتریک نتوانست مالک این تلفن را احراز هویت کند. خطا: %1$s</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_delete">حذف</string>
|
||||||
<string name="quick_action_unfollow">دنبال نکردن</string>
|
<string name="quick_action_unfollow">دنبال نکردن</string>
|
||||||
<string name="quick_action_follow">دنبال کردن</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_title">درخواست حذف</string>
|
||||||
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
|
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
|
||||||
<string name="quick_action_block_dialog_btn">بلاک</string>
|
<string name="quick_action_block_dialog_btn">بلاک</string>
|
||||||
@@ -239,6 +276,7 @@
|
|||||||
<string name="report_dialog_impersonation">جعل هویت بدخواهانه</string>
|
<string name="report_dialog_impersonation">جعل هویت بدخواهانه</string>
|
||||||
<string name="report_dialog_nudity">محتوای برهنگی یا نمایان</string>
|
<string name="report_dialog_nudity">محتوای برهنگی یا نمایان</string>
|
||||||
<string name="report_dialog_illegal">رفتار ناهنجار</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_blocking_a_user">بلاک کردن کاربر محتوای ایشان را در اپ شما پنهان می کند. یادداشت های شما همچنان بطور عمومی دیده می شوند که شامل کاربران بلاک شده توسط شما نیز هست. کاربران بلاک شده در صفحه فیلترهای ایمنی فهرست می شوند.</string>
|
||||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[مسدود و پنهان کردن کاربر]]></string>
|
<string name="report_dialog_block_hide_user_btn"><![CDATA[مسدود و پنهان کردن کاربر]]></string>
|
||||||
<string name="report_dialog_report_btn">گزارش سواستفاده</string>
|
<string name="report_dialog_report_btn">گزارش سواستفاده</string>
|
||||||
@@ -251,6 +289,7 @@
|
|||||||
<string name="report_dialog_title">بلاک و گزارش</string>
|
<string name="report_dialog_title">بلاک و گزارش</string>
|
||||||
<string name="block_only">بلاک</string>
|
<string name="block_only">بلاک</string>
|
||||||
<string name="bookmarks">علامت گذاشته ها</string>
|
<string name="bookmarks">علامت گذاشته ها</string>
|
||||||
|
<string name="drafts">پیشنویس</string>
|
||||||
<string name="private_bookmarks">علامت گاشته های خصوصی</string>
|
<string name="private_bookmarks">علامت گاشته های خصوصی</string>
|
||||||
<string name="public_bookmarks">علامت گذاشته های عمومی</string>
|
<string name="public_bookmarks">علامت گذاشته های عمومی</string>
|
||||||
<string name="add_to_private_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_min">حداقل زپ</string>
|
||||||
<string name="poll_zap_value_max">حداکثر زپ</string>
|
<string name="poll_zap_value_max">حداکثر زپ</string>
|
||||||
<string name="poll_consensus_threshold">توافق نظر</string>
|
<string name="poll_consensus_threshold">توافق نظر</string>
|
||||||
|
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||||
<string name="poll_closing_time">بسته شدن پس از</string>
|
<string name="poll_closing_time">بسته شدن پس از</string>
|
||||||
<string name="poll_closing_time_days">روز</string>
|
<string name="poll_closing_time_days">روز</string>
|
||||||
<string name="poll_unable_to_vote">نمی توان رای داد</string>
|
<string name="poll_unable_to_vote">نمی توان رای داد</string>
|
||||||
@@ -309,18 +349,31 @@
|
|||||||
<string name="zap_type_nonzap_explainer">هیچ ردی در نوستر نمی ماند، فقط در شبکه لایتنینگ انجام می شود</string>
|
<string name="zap_type_nonzap_explainer">هیچ ردی در نوستر نمی ماند، فقط در شبکه لایتنینگ انجام می شود</string>
|
||||||
<string name="file_server">سرور فایل</string>
|
<string name="file_server">سرور فایل</string>
|
||||||
<string name="zap_forward_lnAddress">آدرس لایتنینگ یا @User</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">رله های شما (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">فایل ها در رله های شما میزبانی می شوند. NIPجدید: بررسی کنید آیا پشتیبانی می کنند یا خیر </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_short">Tor/Orbot تنظیمات</string>
|
||||||
<string name="connect_via_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_title">قطع اتصال از Orbot/Tor?</string>
|
||||||
<string name="do_you_really_want_to_disable_tor_text">داده های شما بلافاصله در شبکه معمولی منتقل می شوند</string>
|
<string name="do_you_really_want_to_disable_tor_text">داده های شما بلافاصله در شبکه معمولی منتقل می شوند</string>
|
||||||
<string name="yes">بله</string>
|
<string name="yes">بله</string>
|
||||||
<string name="no">خیر</string>
|
<string name="no">خیر</string>
|
||||||
<string name="follow_list_selection">لیست دنبال ها</string>
|
<string name="follow_list_selection">لیست دنبال ها</string>
|
||||||
<string name="follow_list_kind3follows">همه دنبال ها</string>
|
<string name="follow_list_kind3follows">همه دنبال ها</string>
|
||||||
|
<string name="follow_list_aroundme">اطراف من</string>
|
||||||
<string name="follow_list_global">همگانی</string>
|
<string name="follow_list_global">همگانی</string>
|
||||||
<string name="follow_list_mute_list">لیست خموش</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 متصل شوید
|
<string name="connect_through_your_orbot_setup_markdown"> ## از طریق Tor با Orbot متصل شوید
|
||||||
\n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
\n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||||
\n2. شروع Orbot
|
\n2. شروع Orbot
|
||||||
@@ -330,6 +383,45 @@
|
|||||||
\n6. دکمه Activate را بفشارید تا از Orbot به عنوان پراکسی استفده نمایید
|
\n6. دکمه Activate را بفشارید تا از Orbot به عنوان پراکسی استفده نمایید
|
||||||
</string>
|
</string>
|
||||||
<string name="orbot_socks_port">درگاه Orbot Socks</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="invalid_port_number">شماره پورت نامعتبر</string>
|
||||||
<string name="use_orbot">استفاده از Orbot</string>
|
<string name="use_orbot">استفاده از Orbot</string>
|
||||||
<string name="disconnect_from_your_orbot_setup">قطع اتصال Tor/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="sats_to_complete">زپ گیری تا %1$s. %2$s مانده تا هدف</string>
|
||||||
<string name="read_from_relay">خواندن از رله</string>
|
<string name="read_from_relay">خواندن از رله</string>
|
||||||
<string name="write_to_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="an_error_occurred_trying_to_get_relay_information">در تلاش برای گرفتن اطلاعات رله از %1$s خطایی رخ داد</string>
|
||||||
<string name="owner">مالک</string>
|
<string name="owner">مالک</string>
|
||||||
<string name="version">نگارش</string>
|
<string name="version">نگارش</string>
|
||||||
@@ -376,6 +470,7 @@
|
|||||||
<string name="languages">زبانها</string>
|
<string name="languages">زبانها</string>
|
||||||
<string name="tags">برچسبها</string>
|
<string name="tags">برچسبها</string>
|
||||||
<string name="posting_policy">سیاست ارسال محتوا</string>
|
<string name="posting_policy">سیاست ارسال محتوا</string>
|
||||||
|
<string name="relay_error_messages">خطاها و هشدارهای از این رله</string>
|
||||||
<string name="message_length">طول پیام</string>
|
<string name="message_length">طول پیام</string>
|
||||||
<string name="subscriptions">اشتراک</string>
|
<string name="subscriptions">اشتراک</string>
|
||||||
<string name="filters">فیلترها</string>
|
<string name="filters">فیلترها</string>
|
||||||
@@ -384,6 +479,7 @@
|
|||||||
<string name="maximum_event_tags">حداکثر تعداد برچسب رویداد</string>
|
<string name="maximum_event_tags">حداکثر تعداد برچسب رویداد</string>
|
||||||
<string name="content_length">طول محتوا</string>
|
<string name="content_length">طول محتوا</string>
|
||||||
<string name="minimum_pow">حداقل PoW</string>
|
<string name="minimum_pow">حداقل PoW</string>
|
||||||
|
<string name="auth">احراز هویت</string>
|
||||||
<string name="payment">پرداخت</string>
|
<string name="payment">پرداخت</string>
|
||||||
<string name="cashu">توکن Cashu</string>
|
<string name="cashu">توکن Cashu</string>
|
||||||
<string name="cashu_redeem">بازپرداخت</string>
|
<string name="cashu_redeem">بازپرداخت</string>
|
||||||
@@ -401,6 +497,7 @@
|
|||||||
<string name="are_you_sure_you_want_to_log_out">خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟</string>
|
<string name="are_you_sure_you_want_to_log_out">خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟</string>
|
||||||
<string name="followed_tags">برچسب های دنبال شده</string>
|
<string name="followed_tags">برچسب های دنبال شده</string>
|
||||||
<string name="relay_setup">رله ها</string>
|
<string name="relay_setup">رله ها</string>
|
||||||
|
<string name="discover_content">اکتشاف یادداشت</string>
|
||||||
<string name="discover_marketplace">بازار</string>
|
<string name="discover_marketplace">بازار</string>
|
||||||
<string name="discover_live">زنده</string>
|
<string name="discover_live">زنده</string>
|
||||||
<string name="discover_community">انجمن</string>
|
<string name="discover_community">انجمن</string>
|
||||||
@@ -410,14 +507,20 @@
|
|||||||
<string name="community_no_descriptor">این انجمن هیچ توضیح و قوانینی ندارد. با مالک گروه برای افزودن آن صحبت کنید.</string>
|
<string name="community_no_descriptor">این انجمن هیچ توضیح و قوانینی ندارد. با مالک گروه برای افزودن آن صحبت کنید.</string>
|
||||||
<string name="add_sensitive_content_label">محتوای حساس</string>
|
<string name="add_sensitive_content_label">محتوای حساس</string>
|
||||||
<string name="add_sensitive_content_description">هشدار محتوای حساس پیش از نمایش محتوا اضافه می کند</string>
|
<string name="add_sensitive_content_description">هشدار محتوای حساس پیش از نمایش محتوا اضافه می کند</string>
|
||||||
|
<string name="preferences">ترجیحات اپ</string>
|
||||||
<string name="settings">تنظیمات</string>
|
<string name="settings">تنظیمات</string>
|
||||||
<string name="connectivity_type_always">همیشه</string>
|
<string name="connectivity_type_always">همیشه</string>
|
||||||
<string name="connectivity_type_wifi_only">فقط Wifi</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="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="system">سیستم</string>
|
||||||
<string name="light">روشن</string>
|
<string name="light">روشن</string>
|
||||||
<string name="dark">تاریک</string>
|
<string name="dark">تاریک</string>
|
||||||
<string name="application_preferences">ترجیحات اپلیکیشن</string>
|
<string name="application_preferences">ترجیحات اپلیکیشن</string>
|
||||||
|
<string name="wallet_connect">اتصال کیف پول</string>
|
||||||
<string name="language">زبان</string>
|
<string name="language">زبان</string>
|
||||||
<string name="theme">پوسته</string>
|
<string name="theme">پوسته</string>
|
||||||
<string name="automatically_load_images_gifs">پیش نمایش تصویر</string>
|
<string name="automatically_load_images_gifs">پیش نمایش تصویر</string>
|
||||||
@@ -425,6 +528,8 @@
|
|||||||
<string name="automatically_show_url_preview">پیش نمایش URL</string>
|
<string name="automatically_show_url_preview">پیش نمایش URL</string>
|
||||||
<string name="automatically_hide_nav_bars">مرور ژرف</string>
|
<string name="automatically_hide_nav_bars">مرور ژرف</string>
|
||||||
<string name="automatically_hide_nav_bars_description">پنهان کردن نوار پیمایش هنگام مرور</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="load_image">بارگیری تصویر</string>
|
||||||
<string name="spamming_users">اسپمر</string>
|
<string name="spamming_users">اسپمر</string>
|
||||||
<string name="muted_button">بیصدا شده. برای لغو کلیک کنید</string>
|
<string name="muted_button">بیصدا شده. برای لغو کلیک کنید</string>
|
||||||
@@ -435,6 +540,7 @@
|
|||||||
<string name="nip05_checking">بررسی آدرس ناستر</string>
|
<string name="nip05_checking">بررسی آدرس ناستر</string>
|
||||||
<string name="select_deselect_all">انتخاب یا لغو انتخاب همه</string>
|
<string name="select_deselect_all">انتخاب یا لغو انتخاب همه</string>
|
||||||
<string name="default_relays">پیشفرض</string>
|
<string name="default_relays">پیشفرض</string>
|
||||||
|
<string name="default_relays_longer">بازگشت به حالت پیش فرض</string>
|
||||||
<string name="select_a_relay_to_continue">برای ادامه یک رله انتخاب کنید</string>
|
<string name="select_a_relay_to_continue">برای ادامه یک رله انتخاب کنید</string>
|
||||||
<string name="zap_forward_title">باز ارسال زپ ها به:</string>
|
<string name="zap_forward_title">باز ارسال زپ ها به:</string>
|
||||||
<string name="zap_forward_explainer">کلاینت هایی که این قابلیت را دارند زپ ها را به جای شما به LNAddress یا نمایه کاربر زیر می فرستند</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_might_not_be_available_description">برای فعال سازی این حالت لازم است اماتیست یک پیغام NIP-17 بفرستد (پیغام های GiftWrapped, Sealed Direct and Group). مطمئن شوید که گیرنده از کلاینتی سازگاز استفاده می کند.</string>
|
||||||
<string name="new_feature_nip17_activate">فعالسازی</string>
|
<string name="new_feature_nip17_activate">فعالسازی</string>
|
||||||
<string name="messages_create_public_chat">عمومی</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">خصوصی</string>
|
||||||
<string name="messages_new_message_to">به</string>
|
<string name="messages_new_message_to">به</string>
|
||||||
<string name="messages_new_message_subject">موضوع</string>
|
<string name="messages_new_message_subject">موضوع</string>
|
||||||
@@ -460,9 +567,13 @@
|
|||||||
<string name="automatically_play_videos_description">پخش خودکار ویدئوها و جیف ها</string>
|
<string name="automatically_play_videos_description">پخش خودکار ویدئوها و جیف ها</string>
|
||||||
<string name="automatically_show_url_preview_description">نشان دادن پیش نمایش URL</string>
|
<string name="automatically_show_url_preview_description">نشان دادن پیش نمایش URL</string>
|
||||||
<string name="load_image_description">هنگام بارگیری تصاویر</string>
|
<string name="load_image_description">هنگام بارگیری تصاویر</string>
|
||||||
|
<string name="copy_stack_to_clipboard">کپی استک</string>
|
||||||
<string name="copy_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_url_to_clipboard">کپی URL به کلیپبورد</string>
|
||||||
<string name="copy_the_note_id_to_the_clipboard">کپی شناسه یادداشت به کلیپبورد</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="created_at">ایجاد شده در</string>
|
||||||
<string name="rules">قوانین</string>
|
<string name="rules">قوانین</string>
|
||||||
<string name="login_with_external_signer">ورود با Amber</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_talk_to_user">پیام به کاربر</string>
|
||||||
<string name="error_dialog_button_ok">قبول</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_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_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_parse_result">نتایج %1$s تفسیر نشد: %2$s</string>
|
||||||
<string name="relay_information_document_error_http_status">%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_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="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">خطا در تفسیر 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">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">خطا در تفسیر 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="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">نمی توان صورتحساب لایتنینگ را پیش از زپ زدن ارسال کرد. کیف پول لایتنینگی گیرنده خطای روبرو را داد: %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">نمی توان صورتحساب لایتنینگ را پیش از زپ زدن ارسال کرد. المنت 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="read_only_user">کاربر با امکان خواندن فقط</string>
|
||||||
<string name="no_reactions_setup">هیچ واکنشی تنظیم نشده است</string>
|
<string name="no_reactions_setup">هیچ واکنشی تنظیم نشده است</string>
|
||||||
<string name="select_push_server">یک اپلیکیشن UnifiedPush انتخاب کنید</string>
|
<string name="select_push_server">یک اپلیکیشن UnifiedPush انتخاب کنید</string>
|
||||||
@@ -555,6 +675,7 @@
|
|||||||
<string name="classifieds_condition">شرایط</string>
|
<string name="classifieds_condition">شرایط</string>
|
||||||
<string name="classifieds_category">دسته بندی</string>
|
<string name="classifieds_category">دسته بندی</string>
|
||||||
<string name="classifieds_price">قیمت (به ساتوشی)</string>
|
<string name="classifieds_price">قیمت (به ساتوشی)</string>
|
||||||
|
<string name="classifieds_price_placeholder">1000</string>
|
||||||
<string name="classifieds_location">موقعیت مکانی</string>
|
<string name="classifieds_location">موقعیت مکانی</string>
|
||||||
<string name="classifieds_location_placeholder">شهر، استان، کشور</string>
|
<string name="classifieds_location_placeholder">شهر، استان، کشور</string>
|
||||||
<string name="classifieds_condition_new">جدید</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="server_did_not_provide_a_url_after_uploading">سرور پس از بارگذاری URL نداد</string>
|
||||||
<string name="could_not_download_from_the_server">فایل بارگذاری شده از سرور بارگیری نشد</string>
|
<string name="could_not_download_from_the_server">فایل بارگذاری شده از سرور بارگیری نشد</string>
|
||||||
<string name="could_not_prepare_local_file_to_upload">فایل محلی برای بارگذاری آماده نشد: %1$s</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>
|
</resources>
|
||||||
|
|||||||
@@ -540,6 +540,7 @@
|
|||||||
<string name="nip05_checking">Vérification de l\'adresse Nostr</string>
|
<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="select_deselect_all">Tout sélectionner/désélectionner</string>
|
||||||
<string name="default_relays">Défaut</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="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_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>
|
<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">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_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_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_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="dm_relays_not_found_create_now">Configurer maintenant</string>
|
||||||
<string name="search_relays_title">Relais de recherche</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="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="select_deselect_all">Mind kijelölése/kijelölés visszavonása</string>
|
||||||
<string name="default_relays">Alapértelmezett</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="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_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>
|
<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">Á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_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_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á 1–3 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_editing">Adj hozzá 1–3 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="dm_relays_not_found_create_now">Állítsd be most</string>
|
||||||
<string name="search_relays_title">Kereső Csomópontok</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_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="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="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="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="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/).
|
<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.
|
Po namestitvi izberite aplikacijo, ki jo želite uporabljati, v nastavitvah.
|
||||||
</string>
|
</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 \"request’s Range header\" polju. </string>
|
<string name="http_status_416">Razpon ni zadovoljiv – Strežnik ne more izpolniti vrednosti, navedene v \"request’s Range header\" polju. </string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -416,6 +416,7 @@
|
|||||||
|
|
||||||
<string name="follow_list_selection">Follow List</string>
|
<string name="follow_list_selection">Follow List</string>
|
||||||
<string name="follow_list_kind3follows">All Follows</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_global">Global</string>
|
||||||
<string name="follow_list_mute_list">Mute List</string>
|
<string name="follow_list_mute_list">Mute List</string>
|
||||||
|
|
||||||
@@ -635,6 +636,7 @@
|
|||||||
<string name="nip05_checking">Checking Nostr address</string>
|
<string name="nip05_checking">Checking Nostr address</string>
|
||||||
<string name="select_deselect_all">Select/Deselect all</string>
|
<string name="select_deselect_all">Select/Deselect all</string>
|
||||||
<string name="default_relays">Default</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="select_a_relay_to_continue">Select a relay to continue</string>
|
||||||
|
|
||||||
<string name="zap_forward_title">Forward Zaps to:</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">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_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_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 1–3 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_editing">Insert between 1–3 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>
|
<string name="dm_relays_not_found_create_now">Set up now</string>
|
||||||
|
|
||||||
|
|||||||
@@ -14,23 +14,23 @@ benchmarkJunit4 = "1.3.3"
|
|||||||
biometricKtx = "1.2.0-alpha05"
|
biometricKtx = "1.2.0-alpha05"
|
||||||
blurhash = "1.0.0"
|
blurhash = "1.0.0"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
composeBom = "2024.10.00"
|
composeBom = "2024.10.01"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.15.0"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
firebaseBom = "33.4.0"
|
firebaseBom = "33.5.1"
|
||||||
fragmentKtx = "1.8.4"
|
fragmentKtx = "1.8.5"
|
||||||
gms = "4.4.2"
|
gms = "4.4.2"
|
||||||
jacksonModuleKotlin = "2.17.2"
|
jacksonModuleKotlin = "2.17.2"
|
||||||
jna = "5.14.0"
|
jna = "5.14.0"
|
||||||
jtorctl = "0.4.5.7"
|
jtorctl = "0.4.5.7"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.20"
|
||||||
kotlinxCollectionsImmutable = "0.3.7"
|
kotlinxCollectionsImmutable = "0.3.7"
|
||||||
kotlinxSerialization = "1.7.2"
|
kotlinxSerialization = "1.7.2"
|
||||||
kotlinxSerializationPlugin = "2.0.0"
|
kotlinxSerializationPlugin = "2.0.0"
|
||||||
languageId = "17.0.6"
|
languageId = "17.0.6"
|
||||||
lazysodiumAndroid = "5.1.0"
|
lazysodiumAndroid = "5.1.0"
|
||||||
lifecycleRuntimeKtx = "2.8.6"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
lightcompressor = "1.3.2"
|
lightcompressor = "1.3.2"
|
||||||
markdown = "077a2cde64"
|
markdown = "077a2cde64"
|
||||||
media3 = "1.4.1"
|
media3 = "1.4.1"
|
||||||
@@ -53,7 +53,7 @@ zoomable = "1.6.2"
|
|||||||
zxing = "3.5.3"
|
zxing = "3.5.3"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
windowCoreAndroid = "1.3.0"
|
windowCoreAndroid = "1.3.0"
|
||||||
androidxCamera = "1.3.4"
|
androidxCamera = "1.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ dependencies {
|
|||||||
implementation "com.goterl:lazysodium-android:5.1.0@aar"
|
implementation "com.goterl:lazysodium-android:5.1.0@aar"
|
||||||
implementation 'net.java.dev.jna:jna:5.14.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
|
// Performant Parser of JSONs into Events
|
||||||
api libs.jackson.module.kotlin
|
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.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.crypto.nip01.Nip01
|
import com.vitorpamplona.quartz.crypto.nip01.Nip01
|
||||||
@@ -191,74 +190,3 @@ class NIP44v2Test {
|
|||||||
return MessageDigest.getInstance("SHA-256").digest(data).toHexKey()
|
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,
|
|
||||||
)
|
|
||||||
|
|||||||
+94
@@ -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 KIND = 3
|
||||||
const val ALT = "Follow List"
|
const val ALT = "Follow List"
|
||||||
|
|
||||||
|
fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:"
|
||||||
|
|
||||||
fun createFromScratch(
|
fun createFromScratch(
|
||||||
followUsers: List<Contact> = emptyList(),
|
followUsers: List<Contact> = emptyList(),
|
||||||
followTags: List<String> = 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> mapTaggedEvent(map: (eventId: HexKey) -> R) = mapTagged("e", map)
|
||||||
|
|
||||||
override fun <R> mapTaggedAddress(map: (address: String) -> R) = mapTagged("a", 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 forEachHashTag(onEach: (eventId: HexKey) -> Unit)
|
||||||
|
|
||||||
|
fun anyHashTag(onEach: (str: String) -> Boolean): Boolean
|
||||||
|
|
||||||
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R>
|
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R>
|
||||||
|
|
||||||
fun <R> mapTaggedAddress(map: (address: String) -> R): List<R>
|
fun <R> mapTaggedAddress(map: (address: String) -> R): List<R>
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ class TextNoteEvent(
|
|||||||
tags.add(arrayOf("q", it))
|
tags.add(arrayOf("q", it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
if (it.toTag() in directMentions) {
|
||||||
|
tags.add(arrayOf("q", it.toTag()))
|
||||||
|
}
|
||||||
|
}
|
||||||
addresses
|
addresses
|
||||||
?.map { it.toTag() }
|
?.map { it.toTag() }
|
||||||
?.let {
|
?.let {
|
||||||
|
|||||||
Reference in New Issue
Block a user