Skip to content

Commit

Permalink
- Adds an Around ME feed
Browse files Browse the repository at this point in the history
- Refactors location to operate as a flow
- Refactors FeedStructures to prepare for custom feeds
- Moves Account to operate feeds with location
  • Loading branch information
vitorpamplona committed Oct 30, 2024
1 parent fb6137f commit a5c4a53
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 284 deletions.
2 changes: 2 additions & 0 deletions amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
278 changes: 140 additions & 138 deletions amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,73 +26,90 @@ 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

class LocationUtil(
context: Context,
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 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
@SuppressLint("MissingPermission")
fun get(
minTimeMs: Long = MIN_TIME,
minDistanceM: Float = MIN_DISTANCE,
): Flow<Location> =
callbackFlow {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager

val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
val providerState = mutableStateOf(false)
val isStart: MutableState<Boolean> = mutableStateOf(false)
val locationCallback =
object : LocationListener {
override fun onLocationChanged(location: Location) {
launch { send(location) }
}

private val locHandlerThread = HandlerThread("LocationUtil Thread")
override fun onProviderEnabled(provider: String) {}

init {
locHandlerThread.start()
}
override fun onProviderDisabled(provider: String) {}
}

@SuppressLint("MissingPermission")
fun start(
minTimeMs: Long = MIN_TIME,
minDistanceM: Float = MIN_DISTANCE,
) {
locationListener().let {
locationListener = it
println("AABBCC LocationState Start")
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
minTimeMs,
minDistanceM,
it,
locHandlerThread.looper,
locationCallback,
Looper.getMainLooper(),
)

awaitClose {
locationManager.removeUpdates(locationCallback)
println("AABBCC LocationState Stop")
}
}
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isStart.value = true
}
}

fun stop() {
locationListener?.let { locationManager.removeUpdates(it) }
isStart.value = false
class LocationState(
context: Context,
scope: CoroutineScope,
) {
companion object {
const val MIN_TIME: Long = 10000L
const val MIN_DISTANCE: Float = 100.0f
}

private fun locationListener() =
object : LocationListener {
override fun onLocationChanged(location: Location) {
locationStateFlow.value = location
}

override fun onProviderEnabled(provider: String) {
providerState.value = true
}
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,
)

override fun onProviderDisabled(provider: String) {
providerState.value = false
}
}
val geohashStateFlow =
locationStateFlow
.map { it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits).toString() }
.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
"",
)
}

object CachedGeoLocations {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
fun createLiveStreamFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.authors
?.toList()
?.ifEmpty { null }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
) {
Expand All @@ -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(
Expand Down Expand Up @@ -89,7 +89,7 @@ class FilterByListParams(
fun create(
userHex: String,
selectedListName: String,
followLists: Account.LiveFollowLists?,
followLists: Account.LiveFollowList?,
hiddenUsers: Account.LiveHiddenUsers,
): FilterByListParams =
FilterByListParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -128,7 +128,7 @@ fun FollowListWithRoutes(
fun FollowListWithoutRoutes(
followListsModel: FollowListState,
listName: String,
onChange: (CodeName) -> Unit,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()

Expand Down
Loading

0 comments on commit a5c4a53

Please sign in to comment.