- Unifies Location Flows and GeoHash Flows into one

- Make them react to changing location permissions
- Adds UI for when the location permission is rejected.
This commit is contained in:
Vitor Pamplona
2024-11-11 17:51:42 -05:00
parent 062e4af118
commit 37a92c25f0
10 changed files with 193 additions and 60 deletions

View File

@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import android.location.Location
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@@ -29,10 +28,10 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.tryAndWait
@@ -170,7 +169,7 @@ class Account(
val listName: String, val listName: String,
val peopleList: StateFlow<NoteState> = MutableStateFlow(NoteState(Note(" "))), val peopleList: StateFlow<NoteState> = MutableStateFlow(NoteState(Note(" "))),
val kind3: StateFlow<Account.LiveFollowList?> = MutableStateFlow(null), val kind3: StateFlow<Account.LiveFollowList?> = MutableStateFlow(null),
val location: StateFlow<Location?> = MutableStateFlow(null), val location: StateFlow<LocationState.LocationResult?> = MutableStateFlow(null),
) )
val connectToRelaysFlow = val connectToRelaysFlow =
@@ -541,7 +540,7 @@ class Account(
AROUND_ME -> AROUND_ME ->
FeedsBaseFlows( FeedsBaseFlows(
listName, listName,
location = Amethyst.instance.locationManager.locationStateFlow, location = Amethyst.instance.locationManager.geohashStateFlow,
) )
else -> { else -> {
val note = LocalCache.checkGetOrCreateAddressableNote(listName) val note = LocalCache.checkGetOrCreateAddressableNote(listName)
@@ -563,19 +562,19 @@ class Account(
listName: String, listName: String,
kind3: LiveFollowList?, kind3: LiveFollowList?,
noteState: NoteState, noteState: NoteState,
location: Location?, location: LocationState.LocationResult?,
): LiveFollowList? = ): LiveFollowList? =
if (listName == GLOBAL_FOLLOWS) { if (listName == GLOBAL_FOLLOWS) {
null null
} else if (listName == KIND3_FOLLOWS) { } else if (listName == KIND3_FOLLOWS) {
kind3 kind3
} else if (listName == AROUND_ME) { } else if (listName == AROUND_ME) {
val hash = location?.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits) val geohashResult = location ?: Amethyst.instance.locationManager.geohashStateFlow.value
if (hash != null) { if (geohashResult is LocationState.LocationResult.Success) {
// 2 neighbors deep = 25x25km // 2 neighbors deep = 25x25km
val hashes = val hashes =
listOf(hash.toString()) + listOf(geohashResult.geoHash.toString()) +
hash.adjacent geohashResult.geoHash.adjacent
.map { listOf(it.toString()) + it.adjacent.map { it.toString() } } .map { listOf(it.toString()) + it.adjacent.map { it.toString() } }
.flatten() .flatten()
.distinct() .distinct()

View File

@@ -26,21 +26,28 @@ import android.location.Geocoder
import android.location.Location import android.location.Location
import android.location.LocationListener import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import android.os.Build
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.fonfon.kgeohash.GeoHash
import com.fonfon.kgeohash.toGeoHash import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LocationFlow( class LocationFlow(
@@ -90,27 +97,48 @@ class LocationState(
const val MIN_DISTANCE: Float = 100.0f const val MIN_DISTANCE: Float = 100.0f
} }
private var latestLocation: Location = Location(LocationManager.NETWORK_PROVIDER) sealed class LocationResult {
data class Success(
val geoHash: GeoHash,
) : LocationResult()
val locationStateFlow = object LackPermission : LocationResult()
LocationFlow(context)
.get(MIN_TIME, MIN_DISTANCE) object Loading : LocationResult()
.onEach { }
latestLocation = it
private var hasLocationPermission = MutableStateFlow<Boolean>(false)
private var latestLocation: LocationResult = LocationResult.Loading
fun setLocationPermission(newValue: Boolean) {
if (newValue != hasLocationPermission.value) {
hasLocationPermission.tryEmit(newValue)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
val geohashStateFlow =
hasLocationPermission
.transformLatest {
emitAll(
LocationFlow
(context)
.get(MIN_TIME, MIN_DISTANCE)
.map {
LocationResult.Success(it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits)) as LocationResult
}.onEach {
latestLocation = it
}.catch { e ->
e.printStackTrace()
latestLocation = LocationResult.LackPermission
emit(LocationResult.LackPermission)
},
)
}.stateIn( }.stateIn(
scope, scope,
SharingStarted.WhileSubscribed(5000), SharingStarted.WhileSubscribed(5000),
latestLocation, latestLocation,
) )
val geohashStateFlow =
locationStateFlow
.map { it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits).toString() }
.stateIn(
scope,
SharingStarted.WhileSubscribed(5000),
"",
)
} }
object CachedGeoLocations { object CachedGeoLocations {
@@ -144,8 +172,11 @@ private class ReverseGeoLocationUtil {
): String? { ): String? {
return try { return try {
Geocoder(context) Geocoder(context)
.getFromLocation(location.latitude, location.longitude, 1) .getFromLocation(
?.firstOrNull() location.latitude,
location.longitude,
1,
)?.firstOrNull()
?.let { address -> ?.let { address ->
listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode) listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode)
.joinToString(", ") .joinToString(", ")
@@ -157,3 +188,52 @@ private class ReverseGeoLocationUtil {
} }
} }
} }
class ReverseGeoLocationFlow(
private val context: Context,
) {
@SuppressLint("MissingPermission")
fun get(location: Location): Flow<String?> =
callbackFlow {
val locationManager = Geocoder(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val locationCallback =
(
Geocoder.GeocodeListener { addresses ->
launch {
send(
addresses.firstOrNull()?.let {
listOfNotNull(it.locality ?: it.subAdminArea, it.countryCode).joinToString(", ")
},
)
}
}
)
Log.d("GeoLocation Service", "LocationState Start")
locationManager
.getFromLocation(
location.latitude,
location.longitude,
1,
locationCallback,
)
} else {
launch {
send(
Geocoder(context)
.getFromLocation(
location.latitude,
location.longitude,
1,
)?.firstOrNull()
?.let { address ->
listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode)
.joinToString(", ")
},
)
}
}
}
}

View File

@@ -34,7 +34,6 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
@@ -44,6 +43,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.Nip96Uploader import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.components.MediaCompressor
@@ -75,13 +75,8 @@ import com.vitorpamplona.quartz.events.ZapSplitSetup
import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.findURLs
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.UUID import java.util.UUID
@@ -166,7 +161,7 @@ open class NewPostViewModel : ViewModel() {
// GeoHash // GeoHash
var wantsToAddGeoHash by mutableStateOf(false) var wantsToAddGeoHash by mutableStateOf(false)
var location: StateFlow<String?>? = null var location: StateFlow<LocationState.LocationResult>? = null
// ZapRaiser // ZapRaiser
var canAddZapRaiser by mutableStateOf(false) var canAddZapRaiser by mutableStateOf(false)
@@ -535,7 +530,7 @@ open class NewPostViewModel : ViewModel() {
null null
} }
val geoHash = location?.value val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString()
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
nip95attachments.forEach { nip95attachments.forEach {
@@ -1259,13 +1254,9 @@ open class NewPostViewModel : ViewModel() {
contentToAddUrl = uri contentToAddUrl = uri
} }
@OptIn(ExperimentalCoroutinesApi::class) fun locationFlow(): StateFlow<LocationState.LocationResult> {
fun locationFlow(): Flow<String?> {
if (location == null) { if (location == null) {
location = location = Amethyst.instance.locationManager.geohashStateFlow
Amethyst.instance.locationManager.locationStateFlow
.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
} }
return location!! return location!!

View File

@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.AROUND_ME
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
@@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
class FilterByListParams( class FilterByListParams(
val isGlobal: Boolean, val isGlobal: Boolean,
val isHiddenList: Boolean, val isHiddenList: Boolean,
val isAroundMe: Boolean,
val followLists: Account.LiveFollowList?, val followLists: Account.LiveFollowList?,
val hiddenLists: Account.LiveHiddenUsers, val hiddenLists: Account.LiveHiddenUsers,
val now: Long = TimeUtils.oneMinuteFromNow(), val now: Long = TimeUtils.oneMinuteFromNow(),
@@ -43,6 +45,7 @@ class FilterByListParams(
fun isEventInList(noteEvent: Event): Boolean { fun isEventInList(noteEvent: Event): Boolean {
if (followLists == null) return false if (followLists == null) return false
if (isAroundMe && followLists.geotags.isEmpty() == true) return false
return if (noteEvent is LiveActivitiesEvent) { return if (noteEvent is LiveActivitiesEvent) {
noteEvent.participantsIntersect(followLists.authors) || noteEvent.participantsIntersect(followLists.authors) ||
@@ -95,6 +98,7 @@ class FilterByListParams(
FilterByListParams( FilterByListParams(
isGlobal = selectedListName == GLOBAL_FOLLOWS, isGlobal = selectedListName == GLOBAL_FOLLOWS,
isHiddenList = showHiddenKey(selectedListName, userHex), isHiddenList = showHiddenKey(selectedListName, userHex),
isAroundMe = selectedListName == AROUND_ME,
followLists = followLists, followLists = followLists,
hiddenLists = hiddenUsers, hiddenLists = hiddenUsers,
) )

View File

@@ -54,6 +54,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition
@@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
@@ -104,6 +104,12 @@ fun FeedFilterSpinner(
} }
} }
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION)
LaunchedEffect(locationPermissionState.status.isGranted) {
Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted)
}
Box( Box(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -115,27 +121,45 @@ fun FeedFilterSpinner(
Text(currentText) Text(currentText)
if (selected is AroundMeFeedDefinition) { if (selected is AroundMeFeedDefinition) {
val locationPermissionState =
rememberPermissionState(
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (!locationPermissionState.status.isGranted) { if (!locationPermissionState.status.isGranted) {
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() } LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
Text(
text = stringRes(R.string.lack_location_permissions),
fontSize = 12.sp,
lineHeight = 12.sp,
)
} else { } else {
val location by Amethyst.instance.locationManager.geohashStateFlow val location by Amethyst.instance.locationManager.geohashStateFlow
.collectAsStateWithLifecycle(null) .collectAsStateWithLifecycle()
location?.let { when (val myLocation = location) {
LoadCityName( is LocationState.LocationResult.Success -> {
geohashStr = it, LoadCityName(
onLoading = { geohashStr = myLocation.geoHash.toString(),
Spacer(modifier = StdHorzSpacer) onLoading = {
LoadingAnimation() Spacer(modifier = StdHorzSpacer)
}, LoadingAnimation()
) { cityName -> },
) { cityName ->
Text(
text = "($cityName)",
fontSize = 12.sp,
lineHeight = 12.sp,
)
}
}
LocationState.LocationResult.LackPermission -> {
Text( Text(
text = "($cityName)", text = stringRes(R.string.lack_location_permissions),
fontSize = 12.sp,
lineHeight = 12.sp,
)
}
LocationState.LocationResult.Loading -> {
Text(
text = stringRes(R.string.loading_location),
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 12.sp, lineHeight = 12.sp,
) )

View File

@@ -215,6 +215,7 @@ fun LoadCityName(
CachedGeoLocations CachedGeoLocations
.geoLocate(geohashStr, geoHash.toLocation(), context) .geoLocate(geohashStr, geoHash.toLocation(), context)
?.ifBlank { null } ?.ifBlank { null }
if (newCityName != null && newCityName != cityName) { if (newCityName != null && newCityName != cityName) {
cityName = newCityName cityName = newCityName
} }

View File

@@ -105,7 +105,7 @@ class FollowListState(
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)), unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
) )
val defaultLists = persistentListOf(kind3Follow, globalFollow, aroundMe, muteListFollow) val defaultLists = persistentListOf(kind3Follow, aroundMe, globalFollow, muteListFollow)
fun getPeopleLists(): List<FeedDefinition> = fun getPeopleLists(): List<FeedDefinition> =
account account

View File

@@ -128,10 +128,12 @@ import coil3.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
@@ -1285,6 +1287,10 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,
) )
LaunchedEffect(locationPermissionState.status.isGranted) {
Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted)
}
if (locationPermissionState.status.isGranted) { if (locationPermissionState.status.isGranted) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -1334,9 +1340,28 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
@Composable @Composable
fun DisplayLocationObserver(postViewModel: NewPostViewModel) { fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
val location by postViewModel.locationFlow().collectAsStateWithLifecycle(null) val location by postViewModel.locationFlow().collectAsStateWithLifecycle()
location?.let { DisplayLocationInTitle(geohash = it) } when (val myLocation = location) {
is LocationState.LocationResult.Success -> {
DisplayLocationInTitle(geohash = myLocation.geoHash.toString())
}
LocationState.LocationResult.LackPermission -> {
Text(
text = stringRes(R.string.lack_location_permissions),
fontSize = 12.sp,
lineHeight = 12.sp,
)
}
LocationState.LocationResult.Loading -> {
Text(
text = stringRes(R.string.loading_location),
fontSize = 12.sp,
lineHeight = 12.sp,
)
}
}
} }
@Composable @Composable

View File

@@ -645,6 +645,9 @@
<string name="geohash_title">Expose Location as </string> <string name="geohash_title">Expose Location as </string>
<string name="geohash_explainer">Adds a Geohash of your location to the post. The public will know you are within 5km (3mi) of the current location</string> <string name="geohash_explainer">Adds a Geohash of your location to the post. The public will know you are within 5km (3mi) of the current location</string>
<string name="loading_location">Loading location</string>
<string name="lack_location_permissions">No Location Permissions</string>
<string name="add_sensitive_content_explainer">Adds sensitive content warning before showing your content. This is ideal for any NSFW content or content some people may find offensive or disturbing</string> <string name="add_sensitive_content_explainer">Adds sensitive content warning before showing your content. This is ideal for any NSFW content or content some people may find offensive or disturbing</string>
<string name="new_feature_nip17_might_not_be_available_title">New Feature</string> <string name="new_feature_nip17_might_not_be_available_title">New Feature</string>

View File

@@ -280,7 +280,13 @@ open class Event(
return PoWRank.getCommited(id, commitedPoW) return PoWRank.getCommited(id, commitedPoW)
} }
override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null } override fun getGeoHash(): String? =
tags
.filter { it.size > 1 && it[0] == "g" }
.maxByOrNull {
it[1].length
}?.get(1)
?.ifBlank { null }
override fun getReward(): BigDecimal? = override fun getReward(): BigDecimal? =
try { try {