- 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:
Vitor Pamplona 2024-10-30 16:01:39 -04:00
parent fb6137f99a
commit a5c4a53afe
23 changed files with 473 additions and 292 deletions

View File

@ -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()

View File

@ -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()
}

View File

@ -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,

View File

@ -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)

View File

@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
fun createLiveStreamFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.authors
?.toList()
?.ifEmpty { null }

View File

@ -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

View File

@ -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)

View File

@ -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 =

View File

@ -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 =

View File

@ -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(

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts(
) {
Text(
text =
followingCount.value.users.size
followingCount.value.authors.size
.toString(),
fontWeight = FontWeight.Bold,
)

View File

@ -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)
},

View File

@ -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()

View File

@ -373,6 +373,6 @@ fun WatchUserFollows(
} else {
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
onFollowChanges(state.users.contains(userHex))
onFollowChanges(state.authors.contains(userHex))
}
}

View File

@ -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))
}

View File

@ -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,
)

View File

@ -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,

View File

@ -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

View File

@ -416,6 +416,7 @@
<string name="follow_list_selection">Follow List</string>
<string name="follow_list_kind3follows">All Follows</string>
<string name="follow_list_aroundme">Around Me</string>
<string name="follow_list_global">Global</string>
<string name="follow_list_mute_list">Mute List</string>

View File

@ -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(),