From a5c4a53afe56455f2885d2328be3d7313f6ba5af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Oct 2024 16:01:39 -0400 Subject: [PATCH] - 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 --- .../com/vitorpamplona/amethyst/Amethyst.kt | 2 + .../vitorpamplona/amethyst/model/Account.kt | 284 +++++++++--------- .../amethyst/model/AccountSettings.kt | 3 + .../{LocationUtil.kt => LocationState.kt} | 121 ++++---- .../service/NostrDiscoveryDataSource.kt | 2 +- .../amethyst/service/NostrHomeDataSource.kt | 4 +- .../amethyst/ui/actions/NewPostViewModel.kt | 39 +-- .../actions/relays/Kind3RelayListViewModel.kt | 2 +- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 2 +- .../amethyst/ui/dal/FilterByListParams.kt | 14 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../amethyst/ui/dal/ThreadFeedFilter.kt | 2 +- .../amethyst/ui/navigation/AppTopBar.kt | 6 +- .../amethyst/ui/navigation/DrawerContent.kt | 2 +- .../ui/navigation/FeedFilterSpinner.kt | 68 ++++- .../amethyst/ui/note/ChannelCardCompose.kt | 12 +- .../amethyst/ui/note/UserProfilePicture.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 173 +++++++++-- .../ui/screen/loggedIn/AccountViewModel.kt | 3 +- .../ui/screen/loggedIn/NewPostScreen.kt | 17 +- amethyst/src/main/res/values/strings.xml | 1 + .../quartz/events/ContactListEvent.kt | 2 + 23 files changed, 473 insertions(+), 292 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/{LocationUtil.kt => LocationState.kt} (57%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index fe2d755b6..41702f74c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a81e76089..7346043fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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> = MutableStateFlow(emptySet()) @Immutable - class LiveFollowLists( - val users: Set = emptySet(), - val usersPlusMe: Set, + class LiveFollowList( + val authors: Set = emptySet(), + val authorsPlusMe: Set, val hashtags: Set = emptySet(), val geotags: Set = emptySet(), - val communities: Set = emptySet(), + val addresses: Set = emptySet(), ) - class ListNameNotePair( + class FeedsBaseFlows( val listName: String, - val event: GeneralListEvent?, + val peopleList: StateFlow = MutableStateFlow(NoteState(Note(" "))), + val kind3: StateFlow = MutableStateFlow(null), + val location: StateFlow = MutableStateFlow(null), ) val connectToRelaysFlow = @@ -216,7 +219,7 @@ class Account( localRelayList: Set, ): List { 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 = + val liveKind3FollowsFlow: Flow = 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 = - 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 = - 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, - peopleListFollowsSource: Flow, - ): Flow = - 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 by lazy { - combinePeopleListFlows(liveKind3Follows, liveHomeList) - } + @OptIn(ExperimentalCoroutinesApi::class) + fun combinePeopleListFlows(peopleListFollowsSource: Flow): Flow = + 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 by lazy { - liveHomeFollowListFlow + val liveHomeFollowLists: StateFlow 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 by lazy { - settings.defaultNotificationFollowList.flatMapLatest { listName -> - loadPeopleListFlowFromListName(listName) - } - } - - val liveNotificationFollowLists: StateFlow by lazy { - combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList) + val liveNotificationFollowLists: StateFlow 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 by lazy { - settings.defaultStoriesFollowList.flatMapLatest { listName -> - loadPeopleListFlowFromListName(listName) - } - } - - val liveStoriesFollowLists: StateFlow by lazy { - combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList) + val liveStoriesFollowLists: StateFlow 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 by lazy { - settings.defaultDiscoveryFollowList.flatMapLatest { listName -> - loadPeopleListFlowFromListName(listName) - } - } - - val liveDiscoveryFollowLists: StateFlow by lazy { - combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList) + val liveDiscoveryFollowLists: StateFlow 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 = liveKind3Follows.value.users + fun followingKeySet(): Set = 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() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 17ef7e69d..b767838f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationState.kt similarity index 57% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationState.kt index 79ffb292c..7b2a41d81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/LocationState.kt @@ -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(LocationManager.NETWORK_PROVIDER)) - val providerState = mutableStateOf(false) - val isStart: MutableState = 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 = + 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(20) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 08729ce67..bcdcb0998 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") { fun createLiveStreamFilter(): List { val follows = account.liveDiscoveryFollowLists.value - ?.users + ?.authors ?.toList() ?.ifEmpty { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 7d0daa2c4..e3a9f050b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index d6a34b59e..1bd8df1da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -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? = null + var location: StateFlow? = 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 { + 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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt index b58ec3a14..8a0b6256e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt @@ -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 = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 67d38a0ae..544407858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -67,7 +67,7 @@ open class DiscoverLiveFeedFilter( override fun sort(collection: Set): List { val followingKeySet = - account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users + account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors val counter = ParticipantListBuilder() val participantCounts = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 80b5bd65b..be8680f15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index e9b6facaa..0351d9fb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt index a907fc9ae..2ee4d12b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt @@ -37,7 +37,7 @@ class ThreadFeedFilter( override fun feed(): List { val cachedSignatures: MutableMap = 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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 47dacdf75..6eafc0a02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 809cc2e3f..ef6327f85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts( ) { Text( text = - followingCount.value.users.size + followingCount.value.authors.size .toString(), fontWeight = FontWeight.Bold, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt index ee9cffda3..a1855640d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt @@ -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, + options: ImmutableList, 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) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 37c8209fe..b04a43f54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index c7a431a98..a1f447424 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -373,6 +373,6 @@ fun WatchUserFollows( } else { val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle() - onFollowChanges(state.users.contains(userHex)) + onFollowChanges(state.authors.contains(userHex)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index f4331cee1..2743b646b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -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)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index 4dae43fc6..11f61d91b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -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 = + 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 = 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()) + val livePeopleListsFlow = MutableStateFlow(emptyList()) fun updateFeedWith(newNotes: Set) { checkNotInMainThread() @@ -118,29 +156,46 @@ class FollowListState( } @OptIn(ExperimentalCoroutinesApi::class) - val liveKind3FollowsFlow: Flow> = + val liveKind3FollowsFlow: Flow> = 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, + val relays: List, +) : FeedDefinition(code, name, type) + +@Immutable +class TagFeedDefinition( + code: String, + name: Name, + type: CodeNameType, + val kinds: List, + val relays: List, + val pTags: List? = null, + val eTags: List? = null, + val aTags: List? = null, + val tTags: List? = null, + val gTags: List? = null, +) : FeedDefinition(code, name, type) + +@Immutable +class AroundMeFeedDefinition( + code: String, + name: Name, + type: CodeNameType, + val kinds: List, +) : FeedDefinition(code, name, type) + +@Immutable +class PeopleListOutBoxFeedDefinition( + code: String, + name: Name, + type: CodeNameType, + val kinds: List, + val unpackList: List, +) : 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, + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1cb4bf7ff..e61eca018 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt index 7dff3251e..a72ecbf96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt @@ -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?>(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 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9b8ad00c8..2a85da27a 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -416,6 +416,7 @@ Follow List All Follows + Around Me Global Mute List diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index 720f8c286..360c21d17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -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 = emptyList(), followTags: List = emptyList(),