mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
- Adds an Around ME feed
- Refactors location to operate as a flow - Refactors FeedStructures to prepare for custom feeds - Moves Account to operate feeds with location
This commit is contained in:
parent
fb6137f99a
commit
a5c4a53afe
@ -32,6 +32,7 @@ import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.service.LocationState
|
||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -50,6 +51,7 @@ class Amethyst : Application() {
|
||||
|
||||
// Service Manager is only active when the activity is active.
|
||||
val serviceManager = ServiceManager(applicationIOScope)
|
||||
val locationManager = LocationState(this, applicationIOScope)
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
@ -28,6 +29,8 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.fonfon.kgeohash.toGeoHash
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
@ -119,10 +122,8 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
@ -158,17 +159,19 @@ class Account(
|
||||
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
||||
|
||||
@Immutable
|
||||
class LiveFollowLists(
|
||||
val users: Set<String> = emptySet(),
|
||||
val usersPlusMe: Set<String>,
|
||||
class LiveFollowList(
|
||||
val authors: Set<String> = emptySet(),
|
||||
val authorsPlusMe: Set<String>,
|
||||
val hashtags: Set<String> = emptySet(),
|
||||
val geotags: Set<String> = emptySet(),
|
||||
val communities: Set<String> = emptySet(),
|
||||
val addresses: Set<String> = emptySet(),
|
||||
)
|
||||
|
||||
class ListNameNotePair(
|
||||
class FeedsBaseFlows(
|
||||
val listName: String,
|
||||
val event: GeneralListEvent?,
|
||||
val peopleList: StateFlow<NoteState> = MutableStateFlow(NoteState(Note(" "))),
|
||||
val kind3: StateFlow<Account.LiveFollowList?> = MutableStateFlow(null),
|
||||
val location: StateFlow<Location?> = MutableStateFlow(null),
|
||||
)
|
||||
|
||||
val connectToRelaysFlow =
|
||||
@ -216,7 +219,7 @@ class Account(
|
||||
localRelayList: Set<String>,
|
||||
): List<RelaySetupInfo> {
|
||||
val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
||||
val searchRelaySet = (searchRelayEvent?.relays() ?: Constants.defaultSearchRelaySet).map { RelayUrlFormatter.normalize(it) }.toSet()
|
||||
val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet()
|
||||
val nip65RelaySet =
|
||||
nip65RelayEvent?.relays()?.map {
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(
|
||||
@ -463,23 +466,26 @@ class Account(
|
||||
}.toTypedArray(),
|
||||
)
|
||||
|
||||
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowLists {
|
||||
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
|
||||
// makes sure the output include only valid p tags
|
||||
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
||||
|
||||
return LiveFollowLists(
|
||||
verifiedFollowingUsers,
|
||||
verifiedFollowingUsers + signer.pubKey,
|
||||
latestContactList
|
||||
?.unverifiedFollowTagSet()
|
||||
?.map { it.lowercase() }
|
||||
?.toSet() ?: emptySet(),
|
||||
latestContactList
|
||||
?.unverifiedFollowGeohashSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
latestContactList
|
||||
?.verifiedFollowAddressSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
return LiveFollowList(
|
||||
authors = verifiedFollowingUsers,
|
||||
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
|
||||
hashtags =
|
||||
latestContactList
|
||||
?.unverifiedFollowTagSet()
|
||||
?.map { it.lowercase() }
|
||||
?.toSet() ?: emptySet(),
|
||||
geotags =
|
||||
latestContactList
|
||||
?.unverifiedFollowGeohashSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
addresses =
|
||||
latestContactList
|
||||
?.verifiedFollowAddressSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -514,7 +520,7 @@ class Account(
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
|
||||
val liveKind3FollowsFlow: Flow<LiveFollowList> =
|
||||
userProfile().flow().follows.stateFlow.transformLatest {
|
||||
checkNotInMainThread()
|
||||
emit(buildFollowLists(it.user.latestContactList))
|
||||
@ -529,80 +535,108 @@ class Account(
|
||||
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveHomeList: Flow<ListNameNotePair> =
|
||||
settings.defaultHomeFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
|
||||
fun peopleListFromListNameStarter(listName: String): ListNameNotePair =
|
||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
val noteEvent = note?.event as? GeneralListEvent
|
||||
ListNameNotePair(listName, noteEvent)
|
||||
} else {
|
||||
ListNameNotePair(listName, null)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
|
||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
note?.flow()?.metadata?.stateFlow?.mapLatest {
|
||||
val noteEvent = it.note.event as? GeneralListEvent
|
||||
ListNameNotePair(listName, noteEvent)
|
||||
} ?: MutableStateFlow(ListNameNotePair(listName, null))
|
||||
} else {
|
||||
MutableStateFlow(ListNameNotePair(listName, null))
|
||||
}
|
||||
|
||||
suspend fun combinePeopleList(
|
||||
kind3Follows: LiveFollowLists,
|
||||
peopleListFollows: ListNameNotePair,
|
||||
): LiveFollowLists? =
|
||||
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
|
||||
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 loadFlowsFor(listName: String): FeedsBaseFlows =
|
||||
when (listName) {
|
||||
GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
|
||||
KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
|
||||
AROUND_ME ->
|
||||
FeedsBaseFlows(
|
||||
listName,
|
||||
location = Amethyst.instance.locationManager.locationStateFlow,
|
||||
)
|
||||
else -> {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
if (note != null) {
|
||||
FeedsBaseFlows(
|
||||
listName,
|
||||
peopleList =
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow,
|
||||
)
|
||||
} else {
|
||||
FeedsBaseFlows(listName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun combinePeopleListFlows(
|
||||
kind3FollowsSource: Flow<LiveFollowLists>,
|
||||
peopleListFollowsSource: Flow<ListNameNotePair>,
|
||||
): Flow<LiveFollowLists?> =
|
||||
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
||||
checkNotInMainThread()
|
||||
emit(combinePeopleList(kind3Follows, peopleListFollows))
|
||||
suspend fun mapIntoFollowLists(
|
||||
listName: String,
|
||||
kind3: LiveFollowList?,
|
||||
noteState: NoteState,
|
||||
location: Location?,
|
||||
): LiveFollowList? =
|
||||
if (listName == GLOBAL_FOLLOWS) {
|
||||
println("AABBCC combinePeopleList $listName Global $listName")
|
||||
null
|
||||
} else if (listName == KIND3_FOLLOWS) {
|
||||
println("AABBCC combinePeopleList $listName Kind3 $kind3")
|
||||
kind3
|
||||
} else if (listName == AROUND_ME) {
|
||||
val hash = location?.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits)
|
||||
if (hash != null) {
|
||||
println("AABBCC combinePeopleList AROUND ME Started $listName Kind3 $kind3")
|
||||
// 2 neighbors deep = 25x25km
|
||||
val hashes =
|
||||
listOf(hash.toString()) +
|
||||
hash.adjacent
|
||||
.map { listOf(it.toString()) + it.adjacent.map { it.toString() } }
|
||||
.flatten()
|
||||
.distinct()
|
||||
|
||||
println("AABBCC combinePeopleList AROUND ME Finished $listName Kind3 $kind3")
|
||||
|
||||
LiveFollowList(
|
||||
authorsPlusMe = setOf(signer.pubKey),
|
||||
geotags = hashes.toSet(),
|
||||
)
|
||||
} else {
|
||||
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||
}
|
||||
} else {
|
||||
val peopleList = noteState.note.event as? GeneralListEvent
|
||||
println("AABBCC combinePeopleList $listName General List ${noteState.note.idHex}")
|
||||
if (peopleList != null) {
|
||||
waitToDecrypt(peopleList) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||
} else {
|
||||
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||
}
|
||||
}
|
||||
|
||||
val liveHomeFollowListFlow: Flow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3Follows, liveHomeList)
|
||||
}
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun combinePeopleListFlows(peopleListFollowsSource: Flow<String>): Flow<LiveFollowList?> =
|
||||
peopleListFollowsSource
|
||||
.transformLatest { listName ->
|
||||
val followList = loadFlowsFor(listName)
|
||||
emitAll(
|
||||
combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location ->
|
||||
mapIntoFollowLists(followList.listName, kind3, peopleList, location)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
liveHomeFollowListFlow
|
||||
val liveHomeFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||
combinePeopleListFlows(settings.defaultHomeFollowList)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
runBlocking {
|
||||
combinePeopleList(
|
||||
liveKind3Follows.value,
|
||||
peopleListFromListNameStarter(settings.defaultHomeFollowList.value),
|
||||
)
|
||||
loadAndCombineFlows(settings.defaultHomeFollowList.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
|
||||
val flows = loadFlowsFor(listName)
|
||||
return mapIntoFollowLists(
|
||||
flows.listName,
|
||||
flows.kind3.value,
|
||||
flows.peopleList.value,
|
||||
flows.location.value,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* filter onion and local host from write relays
|
||||
* for each user pubkey, a list of valid relays.
|
||||
@ -699,7 +733,7 @@ class Account(
|
||||
liveHomeFollowLists
|
||||
.transformLatest { followList ->
|
||||
if (followList != null) {
|
||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
@ -726,53 +760,33 @@ class Account(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
authorsPerRelay(
|
||||
liveHomeFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||
settings.torSettings.torType.value,
|
||||
).ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
|
||||
settings.defaultNotificationFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
|
||||
val liveNotificationFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||
combinePeopleListFlows(settings.defaultNotificationFollowList)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
runBlocking {
|
||||
combinePeopleList(
|
||||
liveKind3Follows.value,
|
||||
peopleListFromListNameStarter(settings.defaultNotificationFollowList.value),
|
||||
)
|
||||
loadAndCombineFlows(settings.defaultNotificationFollowList.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
|
||||
settings.defaultStoriesFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
|
||||
val liveStoriesFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||
combinePeopleListFlows(settings.defaultStoriesFollowList)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
runBlocking {
|
||||
combinePeopleList(
|
||||
liveKind3Follows.value,
|
||||
peopleListFromListNameStarter(settings.defaultStoriesFollowList.value),
|
||||
)
|
||||
loadAndCombineFlows(settings.defaultStoriesFollowList.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -782,7 +796,7 @@ class Account(
|
||||
liveStoriesFollowLists
|
||||
.transformLatest { followList ->
|
||||
if (followList != null) {
|
||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
@ -809,31 +823,21 @@ class Account(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
authorsPerRelay(
|
||||
liveStoriesFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
liveStoriesFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||
settings.torSettings.torType.value,
|
||||
).ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
|
||||
settings.defaultDiscoveryFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
|
||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||
combinePeopleListFlows(settings.defaultDiscoveryFollowList)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
runBlocking {
|
||||
combinePeopleList(
|
||||
liveKind3Follows.value,
|
||||
peopleListFromListNameStarter(settings.defaultDiscoveryFollowList.value),
|
||||
)
|
||||
loadAndCombineFlows(settings.defaultDiscoveryFollowList.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -843,7 +847,7 @@ class Account(
|
||||
liveDiscoveryFollowLists
|
||||
.transformLatest { followList ->
|
||||
if (followList != null) {
|
||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
@ -870,7 +874,7 @@ class Account(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
authorsPerRelay(
|
||||
liveDiscoveryFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
liveDiscoveryFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||
connectToRelays.value.filter { it.read }.map { it.url },
|
||||
settings.torSettings.torType.value,
|
||||
).ifEmpty { null },
|
||||
@ -879,19 +883,17 @@ class Account(
|
||||
|
||||
private fun decryptLiveFollows(
|
||||
listEvent: GeneralListEvent,
|
||||
onReady: (LiveFollowLists) -> Unit,
|
||||
onReady: (LiveFollowList) -> Unit,
|
||||
) {
|
||||
listEvent.privateTags(signer) { privateTagList ->
|
||||
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
||||
onReady(
|
||||
LiveFollowLists(
|
||||
users = users,
|
||||
usersPlusMe = users + userProfile().pubkeyHex,
|
||||
hashtags =
|
||||
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||
geotags =
|
||||
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
||||
communities =
|
||||
LiveFollowList(
|
||||
authors = users,
|
||||
authorsPlusMe = users + userProfile().pubkeyHex,
|
||||
hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||
geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
||||
addresses =
|
||||
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
||||
.map { it.toTag() }
|
||||
.toSet(),
|
||||
@ -900,7 +902,7 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
|
||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
|
||||
withTimeoutOrNull(1000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
decryptLiveFollows(peopleListFollows) {
|
||||
@ -3192,7 +3194,7 @@ class Account(
|
||||
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
||||
flowHiddenUsers.value.spammers.contains(userHex)
|
||||
|
||||
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.users
|
||||
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.authors
|
||||
|
||||
fun isAcceptable(user: User): Boolean {
|
||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||
@ -3250,8 +3252,8 @@ class Account(
|
||||
}
|
||||
|
||||
return (
|
||||
note.reportsBy(liveKind3Follows.value.usersPlusMe) +
|
||||
(note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) +
|
||||
note.reportsBy(liveKind3Follows.value.authorsPlusMe) +
|
||||
(note.author?.reportsBy(liveKind3Follows.value.authorsPlusMe) ?: emptyList()) +
|
||||
innerReports
|
||||
).toSet()
|
||||
}
|
||||
|
@ -86,6 +86,9 @@ val GLOBAL_FOLLOWS = " Global "
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val KIND3_FOLLOWS = " All Follows "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val AROUND_ME = " Around Me "
|
||||
|
||||
@Stable
|
||||
class AccountSettings(
|
||||
val keyPair: KeyPair,
|
||||
|
@ -26,75 +26,92 @@ import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.fonfon.kgeohash.toGeoHash
|
||||
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE
|
||||
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LocationUtil(
|
||||
context: Context,
|
||||
class LocationFlow(
|
||||
private val context: Context,
|
||||
) {
|
||||
companion object {
|
||||
const val MIN_TIME: Long = 1000L
|
||||
const val MIN_DISTANCE: Float = 0.0f
|
||||
}
|
||||
|
||||
private val locationManager =
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private var locationListener: LocationListener? = null
|
||||
|
||||
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
|
||||
val providerState = mutableStateOf(false)
|
||||
val isStart: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
private val locHandlerThread = HandlerThread("LocationUtil Thread")
|
||||
|
||||
init {
|
||||
locHandlerThread.start()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun start(
|
||||
fun get(
|
||||
minTimeMs: Long = MIN_TIME,
|
||||
minDistanceM: Float = MIN_DISTANCE,
|
||||
) {
|
||||
locationListener().let {
|
||||
locationListener = it
|
||||
): Flow<Location> =
|
||||
callbackFlow {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
val locationCallback =
|
||||
object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
launch { send(location) }
|
||||
}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
}
|
||||
|
||||
println("AABBCC LocationState Start")
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
minTimeMs,
|
||||
minDistanceM,
|
||||
it,
|
||||
locHandlerThread.looper,
|
||||
locationCallback,
|
||||
Looper.getMainLooper(),
|
||||
)
|
||||
}
|
||||
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
isStart.value = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
locationListener?.let { locationManager.removeUpdates(it) }
|
||||
isStart.value = false
|
||||
}
|
||||
|
||||
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
|
||||
awaitClose {
|
||||
locationManager.removeUpdates(locationCallback)
|
||||
println("AABBCC LocationState Stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
val locationNames = LruCache<String, String>(20)
|
||||
|
@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.authors
|
||||
?.toList()
|
||||
?.ifEmpty { null }
|
||||
|
||||
|
@ -211,7 +211,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
mapOf(
|
||||
"g" to
|
||||
hashToLoad
|
||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
||||
.map { listOf(it.lowercase()) }
|
||||
.flatten(),
|
||||
),
|
||||
limit = 100,
|
||||
@ -225,7 +225,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
}
|
||||
|
||||
fun createFollowCommunitiesFilter(): TypedFilter? {
|
||||
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
|
||||
val communitiesToLoad = account.liveHomeFollowLists.value?.addresses ?: return null
|
||||
|
||||
if (communitiesToLoad.isEmpty()) return null
|
||||
|
||||
|
@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fonfon.kgeohash.toGeoHash
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.LocationUtil
|
||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
@ -78,7 +78,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
@ -163,8 +166,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
|
||||
// GeoHash
|
||||
var wantsToAddGeoHash by mutableStateOf(false)
|
||||
var locUtil: LocationUtil? = null
|
||||
var location: Flow<String>? = null
|
||||
var location: StateFlow<String?>? = null
|
||||
|
||||
// ZapRaiser
|
||||
var canAddZapRaiser by mutableStateOf(false)
|
||||
@ -530,14 +532,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
null
|
||||
}
|
||||
|
||||
val geoLocation = locUtil?.locationStateFlow?.value
|
||||
val geoHash =
|
||||
if (wantsToAddGeoHash && geoLocation != null) {
|
||||
geoLocation.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val geoHash = location?.value
|
||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
||||
|
||||
nip95attachments.forEach {
|
||||
@ -1262,28 +1257,20 @@ open class NewPostViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun startLocation(context: Context) {
|
||||
locUtil = LocationUtil(context)
|
||||
locUtil?.let {
|
||||
fun locationFlow(): Flow<String?> {
|
||||
if (location == null) {
|
||||
location =
|
||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||
saveDraft()
|
||||
Amethyst.instance.locationManager.locationStateFlow
|
||||
.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
||||
}
|
||||
|
||||
fun stopLocation() {
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
||||
location = null
|
||||
locUtil = null
|
||||
return location!!
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
||||
location = null
|
||||
locUtil = null
|
||||
}
|
||||
|
||||
fun toggleNIP04And24() {
|
||||
@ -1386,7 +1373,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
|
||||
}
|
||||
}
|
||||
elementList.removeLast()
|
||||
elementList.removeAt(elementList.size - 1)
|
||||
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
|
||||
this.clear()
|
||||
this.putAll(newEntries)
|
||||
|
@ -143,7 +143,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
||||
val proposed =
|
||||
RelayListRecommendationProcessor
|
||||
.reliableRelaySetFor(
|
||||
account.liveKind3Follows.value.users.mapNotNull {
|
||||
account.liveKind3Follows.value.authors.mapNotNull {
|
||||
account.getNIP65RelayList(it)
|
||||
},
|
||||
relayUrlsToIgnore =
|
||||
|
@ -67,7 +67,7 @@ open class DiscoverLiveFeedFilter(
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val followingKeySet =
|
||||
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
||||
account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors
|
||||
|
||||
val counter = ParticipantListBuilder()
|
||||
val participantCounts =
|
||||
|
@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
class FilterByListParams(
|
||||
val isGlobal: Boolean,
|
||||
val isHiddenList: Boolean,
|
||||
val followLists: Account.LiveFollowLists?,
|
||||
val followLists: Account.LiveFollowList?,
|
||||
val hiddenLists: Account.LiveHiddenUsers,
|
||||
val now: Long = TimeUtils.oneMinuteFromNow(),
|
||||
) {
|
||||
@ -45,22 +45,22 @@ class FilterByListParams(
|
||||
if (followLists == null) return false
|
||||
|
||||
return if (noteEvent is LiveActivitiesEvent) {
|
||||
noteEvent.participantsIntersect(followLists.users) ||
|
||||
noteEvent.participantsIntersect(followLists.authors) ||
|
||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
||||
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||
} else {
|
||||
noteEvent.pubKey in followLists.users ||
|
||||
noteEvent.pubKey in followLists.authors ||
|
||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
||||
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||
}
|
||||
}
|
||||
|
||||
fun isATagInList(aTag: ATag): Boolean {
|
||||
if (followLists == null) return false
|
||||
|
||||
return aTag.pubKeyHex in followLists.users
|
||||
return aTag.pubKeyHex in followLists.authors
|
||||
}
|
||||
|
||||
fun match(
|
||||
@ -89,7 +89,7 @@ class FilterByListParams(
|
||||
fun create(
|
||||
userHex: String,
|
||||
selectedListName: String,
|
||||
followLists: Account.LiveFollowLists?,
|
||||
followLists: Account.LiveFollowList?,
|
||||
hiddenUsers: Account.LiveHiddenUsers,
|
||||
): FilterByListParams =
|
||||
FilterByListParams(
|
||||
|
@ -115,7 +115,7 @@ class NotificationFeedFilter(
|
||||
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
||||
it.event !is GiftWrapEvent &&
|
||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
|
||||
(filterParams.isGlobal || filterParams.followLists?.authors?.contains(notifAuthor) == true) &&
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
||||
tagsAnEventByUser(it, loggedInUserHex)
|
||||
|
@ -37,7 +37,7 @@ class ThreadFeedFilter(
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
||||
val followingKeySet = account.liveKind3Follows.value.users
|
||||
val followingKeySet = account.liveKind3Follows.value.authors
|
||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
||||
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
||||
val now = TimeUtils.now()
|
||||
|
@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.CodeName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.FollowListState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@ -112,7 +112,7 @@ private fun LoggedInUserPictureDrawer(
|
||||
fun FollowListWithRoutes(
|
||||
followListsModel: FollowListState,
|
||||
listName: String,
|
||||
onChange: (CodeName) -> Unit,
|
||||
onChange: (FeedDefinition) -> Unit,
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
||||
|
||||
@ -128,7 +128,7 @@ fun FollowListWithRoutes(
|
||||
fun FollowListWithoutRoutes(
|
||||
followListsModel: FollowListState,
|
||||
listName: String,
|
||||
onChange: (CodeName) -> Unit,
|
||||
onChange: (FeedDefinition) -> Unit,
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts(
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
followingCount.value.users.size
|
||||
followingCount.value.authors.size
|
||||
.toString(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
@ -20,10 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -33,6 +35,8 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -41,12 +45,20 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.map
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.screen.CodeName
|
||||
import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
||||
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
||||
import com.vitorpamplona.amethyst.ui.screen.Name
|
||||
@ -55,15 +67,18 @@ import com.vitorpamplona.amethyst.ui.screen.ResourceName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun FeedFilterSpinner(
|
||||
placeholderCode: String,
|
||||
explainer: String,
|
||||
options: ImmutableList<CodeName>,
|
||||
options: ImmutableList<FeedDefinition>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@ -75,20 +90,61 @@ fun FeedFilterSpinner(
|
||||
id = R.string.select_an_option,
|
||||
)
|
||||
|
||||
var currentText by
|
||||
var selected by
|
||||
remember(placeholderCode, options) {
|
||||
mutableStateOf(
|
||||
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption,
|
||||
options.firstOrNull { it.code == placeholderCode },
|
||||
)
|
||||
}
|
||||
|
||||
val currentText by
|
||||
remember(placeholderCode, options) {
|
||||
derivedStateOf {
|
||||
selected?.name?.name(context) ?: selectAnOption
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Size20Modifier)
|
||||
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(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
contentDescription = explainer,
|
||||
@ -115,7 +171,7 @@ fun FeedFilterSpinner(
|
||||
options = options,
|
||||
onDismiss = { optionsShowing = false },
|
||||
onSelect = {
|
||||
currentText = options[it].name.name(context)
|
||||
selected = options[it]
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
},
|
||||
|
@ -671,13 +671,13 @@ fun LoadModerators(
|
||||
|
||||
val followingKeySet =
|
||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.authors
|
||||
val allParticipants =
|
||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||
|
||||
@ -724,7 +724,7 @@ private fun LoadParticipants(
|
||||
|
||||
val followingKeySet =
|
||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.authors
|
||||
|
||||
val allParticipants =
|
||||
ParticipantListBuilder()
|
||||
@ -733,7 +733,7 @@ private fun LoadParticipants(
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder()
|
||||
.followsThatParticipateOn(baseNote, allFollows)
|
||||
@ -882,7 +882,7 @@ fun RenderChannelThumb(
|
||||
launch(Dispatchers.IO) {
|
||||
val followingKeySet =
|
||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.authors
|
||||
val allParticipants =
|
||||
ParticipantListBuilder()
|
||||
.followsThatParticipateOn(baseNote, followingKeySet)
|
||||
@ -890,7 +890,7 @@ fun RenderChannelThumb(
|
||||
|
||||
val newParticipantUsers =
|
||||
if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
||||
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||
val followingParticipants =
|
||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||
|
||||
|
@ -373,6 +373,6 @@ fun WatchUserFollows(
|
||||
} else {
|
||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||
|
||||
onFollowChanges(state.users.contains(userHex))
|
||||
onFollowChanges(state.authors.contains(userHex))
|
||||
}
|
||||
}
|
||||
|
@ -355,5 +355,5 @@ fun WatchAddressableNoteFollows(
|
||||
) {
|
||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||
|
||||
onFollowChanges(state.communities.contains(note.idHex))
|
||||
onFollowChanges(state.addresses.contains(note.idHex))
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
@ -33,10 +34,24 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -56,33 +71,56 @@ class FollowListState(
|
||||
val viewModelScope: CoroutineScope,
|
||||
) {
|
||||
val kind3Follow =
|
||||
CodeName(
|
||||
KIND3_FOLLOWS,
|
||||
ResourceName(R.string.follow_list_kind3follows),
|
||||
CodeNameType.HARDCODED,
|
||||
PeopleListOutBoxFeedDefinition(
|
||||
code = KIND3_FOLLOWS,
|
||||
name = ResourceName(R.string.follow_list_kind3follows),
|
||||
type = CodeNameType.HARDCODED,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
||||
)
|
||||
val globalFollow =
|
||||
CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
|
||||
val muteListFollow =
|
||||
CodeName(
|
||||
MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
|
||||
ResourceName(R.string.follow_list_mute_list),
|
||||
CodeNameType.HARDCODED,
|
||||
)
|
||||
val defaultLists = persistentListOf(kind3Follow, globalFollow, muteListFollow)
|
||||
|
||||
fun getPeopleLists(): List<CodeName> =
|
||||
val globalFollow =
|
||||
GlobalFeedDefinition(
|
||||
code = GLOBAL_FOLLOWS,
|
||||
name = ResourceName(R.string.follow_list_global),
|
||||
type = CodeNameType.HARDCODED,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
relays = account.activeGlobalRelays().toList(),
|
||||
)
|
||||
|
||||
val aroundMe =
|
||||
AroundMeFeedDefinition(
|
||||
code = AROUND_ME,
|
||||
name = ResourceName(R.string.follow_list_aroundme),
|
||||
type = CodeNameType.HARDCODED,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
)
|
||||
|
||||
val muteListFollow =
|
||||
PeopleListOutBoxFeedDefinition(
|
||||
code = MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
|
||||
name = ResourceName(R.string.follow_list_mute_list),
|
||||
type = CodeNameType.HARDCODED,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
|
||||
)
|
||||
|
||||
val defaultLists = persistentListOf(kind3Follow, globalFollow, aroundMe, muteListFollow)
|
||||
|
||||
fun getPeopleLists(): List<FeedDefinition> =
|
||||
account
|
||||
.getAllPeopleLists()
|
||||
.map {
|
||||
CodeName(
|
||||
PeopleListOutBoxFeedDefinition(
|
||||
it.idHex,
|
||||
PeopleListName(it),
|
||||
CodeNameType.PEOPLE_LIST,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
listOf(it.idHex),
|
||||
)
|
||||
}.sortedBy { it.name.name() }
|
||||
|
||||
val livePeopleListsFlow = MutableStateFlow(emptyList<CodeName>())
|
||||
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
||||
|
||||
fun updateFeedWith(newNotes: Set<Note>) {
|
||||
checkNotInMainThread()
|
||||
@ -118,29 +156,46 @@ class FollowListState(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
||||
val liveKind3FollowsFlow: Flow<List<FeedDefinition>> =
|
||||
account.liveKind3Follows.transformLatest {
|
||||
checkNotInMainThread()
|
||||
|
||||
val communities =
|
||||
it.communities.mapNotNull {
|
||||
it.addresses.mapNotNull {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||
CodeName(
|
||||
TagFeedDefinition(
|
||||
"Community/${communityNote.idHex}",
|
||||
CommunityName(communityNote),
|
||||
CodeNameType.ROUTE,
|
||||
kinds = DEFAULT_COMMUNITY_FEEDS,
|
||||
aTags = listOf(communityNote.idHex),
|
||||
relays = account.activeGlobalRelays().toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val hashtags =
|
||||
it.hashtags.map {
|
||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
||||
TagFeedDefinition(
|
||||
"Hashtag/$it",
|
||||
HashtagName(it),
|
||||
CodeNameType.ROUTE,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
tTags = listOf(it),
|
||||
relays = account.activeGlobalRelays().toList(),
|
||||
)
|
||||
}
|
||||
|
||||
val geotags =
|
||||
it.geotags.map {
|
||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||
TagFeedDefinition(
|
||||
"Geohash/$it",
|
||||
GeoHashName(it),
|
||||
CodeNameType.ROUTE,
|
||||
kinds = DEFAULT_FEED_KINDS,
|
||||
gTags = listOf(it),
|
||||
relays = account.activeGlobalRelays().toList(),
|
||||
)
|
||||
}
|
||||
|
||||
emit(
|
||||
@ -156,7 +211,7 @@ class FollowListState(
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
listOf(
|
||||
listOf(kind3Follow, globalFollow),
|
||||
listOf(kind3Follow, aroundMe, globalFollow),
|
||||
myLivePeopleListsFlow,
|
||||
myLiveKind3FollowsFlow,
|
||||
listOf(muteListFollow),
|
||||
@ -172,7 +227,7 @@ class FollowListState(
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
listOf(
|
||||
listOf(kind3Follow, globalFollow),
|
||||
listOf(kind3Follow, aroundMe, globalFollow),
|
||||
myLivePeopleListsFlow,
|
||||
listOf(muteListFollow),
|
||||
).flatten().toImmutableList(),
|
||||
@ -238,8 +293,78 @@ class CommunityName(
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CodeName(
|
||||
abstract class FeedDefinition(
|
||||
val code: String,
|
||||
val name: Name,
|
||||
val type: CodeNameType,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
class GlobalFeedDefinition(
|
||||
code: String,
|
||||
name: Name,
|
||||
type: CodeNameType,
|
||||
val kinds: List<Int>,
|
||||
val relays: List<String>,
|
||||
) : FeedDefinition(code, name, type)
|
||||
|
||||
@Immutable
|
||||
class TagFeedDefinition(
|
||||
code: String,
|
||||
name: Name,
|
||||
type: CodeNameType,
|
||||
val kinds: List<Int>,
|
||||
val relays: List<String>,
|
||||
val pTags: List<String>? = null,
|
||||
val eTags: List<String>? = null,
|
||||
val aTags: List<String>? = null,
|
||||
val tTags: List<String>? = null,
|
||||
val gTags: List<String>? = null,
|
||||
) : FeedDefinition(code, name, type)
|
||||
|
||||
@Immutable
|
||||
class AroundMeFeedDefinition(
|
||||
code: String,
|
||||
name: Name,
|
||||
type: CodeNameType,
|
||||
val kinds: List<Int>,
|
||||
) : FeedDefinition(code, name, type)
|
||||
|
||||
@Immutable
|
||||
class PeopleListOutBoxFeedDefinition(
|
||||
code: String,
|
||||
name: Name,
|
||||
type: CodeNameType,
|
||||
val kinds: List<Int>,
|
||||
val unpackList: List<String>,
|
||||
) : FeedDefinition(code, name, type)
|
||||
|
||||
val DEFAULT_FEED_KINDS =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
LiveActivitiesEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
)
|
||||
|
||||
val DEFAULT_COMMUNITY_FEEDS =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
)
|
||||
|
@ -120,7 +120,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
@ -396,7 +395,7 @@ class AccountViewModel(
|
||||
note.flow().metadata.stateFlow,
|
||||
note.flow().reports.stateFlow,
|
||||
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
|
||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
|
@ -188,7 +188,6 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
@ -1335,21 +1334,9 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
||||
|
||||
@Composable
|
||||
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
||||
val context = LocalContext.current
|
||||
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
|
||||
val location by postViewModel.locationFlow().collectAsStateWithLifecycle(null)
|
||||
|
||||
DisposableEffect(key1 = context) {
|
||||
postViewModel.startLocation(context = context)
|
||||
locationDescriptionFlow = postViewModel.location
|
||||
|
||||
onDispose { postViewModel.stopLocation() }
|
||||
}
|
||||
|
||||
locationDescriptionFlow?.let {
|
||||
val location by it.collectAsStateWithLifecycle(null)
|
||||
|
||||
location?.let { DisplayLocationInTitle(geohash = it) }
|
||||
}
|
||||
location?.let { DisplayLocationInTitle(geohash = it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -416,6 +416,7 @@
|
||||
|
||||
<string name="follow_list_selection">Follow List</string>
|
||||
<string name="follow_list_kind3follows">All Follows</string>
|
||||
<string name="follow_list_aroundme">Around Me</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Mute List</string>
|
||||
|
||||
|
@ -119,6 +119,8 @@ class ContactListEvent(
|
||||
const val KIND = 3
|
||||
const val ALT = "Follow List"
|
||||
|
||||
fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:"
|
||||
|
||||
fun createFromScratch(
|
||||
followUsers: List<Contact> = emptyList(),
|
||||
followTags: List<String> = emptyList(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user