- Adds support for GeoHash

- Refactors New Post Buttons to make them more similar to one another.
pull/531/head
Vitor Pamplona 2023-07-25 16:52:32 -04:00
rodzic c20277a754
commit 1098c31787
19 zmienionych plików z 746 dodań i 235 usunięć

Wyświetl plik

@ -191,6 +191,9 @@ dependencies {
// immutable collections to avoid recomposition
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// GeoHash
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
// Video compression lib
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
// Image compression lib

Wyświetl plik

@ -37,6 +37,9 @@
<!-- This notification permission is needed for some phones -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Adds Geohash to posts if active -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Old permission to access media -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"

Wyświetl plik

@ -671,7 +671,8 @@ class Account(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
relayList: List<Relay>? = null
relayList: List<Relay>? = null,
geohash: String? = null
) {
if (!isWriteable()) return
@ -691,6 +692,7 @@ class Account(
replyingTo = replyingTo,
root = root,
directMentions = directMentions,
geohash = geohash,
privateKey = keyPair.privKey!!
)
@ -710,7 +712,8 @@ class Account(
zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null,
relayList: List<Relay>? = null
relayList: List<Relay>? = null,
geohash: String? = null
) {
if (!isWriteable()) return
@ -731,14 +734,15 @@ class Account(
closedAt = closedAt,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount
zapRaiserAmount = zapRaiserAmount,
geohash = geohash
)
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent)
}
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@ -753,13 +757,14 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
privateKey = keyPair.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent, null)
}
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@ -774,13 +779,14 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
privateKey = keyPair.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent, null)
}
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@ -795,6 +801,7 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
privateKey = keyPair.privKey!!,
advertiseNip18 = false
)

Wyświetl plik

@ -0,0 +1,83 @@
package com.vitorpamplona.amethyst.service
import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.HandlerThread
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.MutableStateFlow
class LocationUtil(context: Context) {
companion object {
const val MIN_TIME: Long = 1000L
const val MIN_DISTANCE: Float = 0.0f
}
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var locationListener: LocationListener? = null
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
val providerState = mutableStateOf(false)
val isStart: MutableState<Boolean> = mutableStateOf(false)
private val locHandlerThread = HandlerThread("LocationUtil Thread")
init {
locHandlerThread.start()
}
@SuppressLint("MissingPermission")
fun start(minTimeMs: Long = MIN_TIME, minDistanceM: Float = MIN_DISTANCE) {
locationListener().let {
locationListener = it
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minTimeMs, minDistanceM, it, locHandlerThread.looper)
}
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 onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
}
override fun onProviderEnabled(provider: String) {
providerState.value = true
}
override fun onProviderDisabled(provider: String) {
providerState.value = false
}
}
}
class ReverseGeoLocationUtil {
fun execute(
location: Location,
context: Context
): String? {
return try {
Geocoder(context).getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull()?.let { address ->
listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode).joinToString(", ")
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}

Wyświetl plik

@ -36,7 +36,8 @@ class ChannelMessageEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): ChannelMessageEvent {
val content = message
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
@ -58,6 +59,9 @@ class ChannelMessageEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)

Wyświetl plik

@ -122,6 +122,10 @@ open class Event(
return rank
}
override fun getGeoHash(): String? {
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getReward(): BigDecimal? {
return try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }

Wyświetl plik

@ -45,6 +45,7 @@ interface EventInterface {
fun getReward(): BigDecimal?
fun getPoWRank(): Int
fun getGeoHash(): String?
fun zapAddress(): String?
fun isSensitive(): Boolean

Wyświetl plik

@ -51,7 +51,8 @@ class LiveActivitiesChatMessageEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): LiveActivitiesChatMessageEvent {
val content = message
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
@ -73,6 +74,9 @@ class LiveActivitiesChatMessageEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)

Wyświetl plik

@ -53,7 +53,8 @@ class PollNoteEvent(
closedAt: Int?,
zapReceiver: String?,
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): PollNoteEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
@ -83,6 +84,9 @@ class PollNoteEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)

Wyświetl plik

@ -86,7 +86,8 @@ class PrivateDmEvent(
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true,
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): PrivateDmEvent {
val content = CryptoUtils.encrypt(
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
@ -113,6 +114,10 @@ class PrivateDmEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())

Wyświetl plik

@ -38,6 +38,7 @@ class TextNoteEvent(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
geohash: String? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
@ -93,6 +94,9 @@ class TextNoteEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
import android.Manifest
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
@ -22,6 +23,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.LocationOff
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.ShowChart
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@ -32,6 +35,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -66,10 +70,15 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.fonfon.kgeohash.toGeoHash
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.CancelIcon
@ -82,8 +91,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier
@ -92,6 +102,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -278,28 +289,72 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
)
if (postViewModel.wantsPoll) {
postViewModel.pollOptions.values.forEachIndexed { index, _ ->
NewPollOption(postViewModel, index)
}
Button(
onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" },
border = BorderStroke(1.dp, MaterialTheme.colors.placeholderText),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.placeholderText
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
Column(
modifier = Modifier.fillMaxWidth()
) {
postViewModel.pollOptions.values.forEachIndexed { index, _ ->
NewPollOption(postViewModel, index)
}
Button(
onClick = {
postViewModel.pollOptions[postViewModel.pollOptions.size] =
""
},
border = BorderStroke(
1.dp,
MaterialTheme.colors.placeholderText
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.placeholderText
)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
}
}
}
}
if (postViewModel.wantsToMarkAsSensitive) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
ContentSensitivityExplainer(postViewModel)
}
}
if (postViewModel.wantsToAddGeoHash) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
LocationAsHash(postViewModel)
}
}
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
FowardZapTo(postViewModel)
}
}
val url = postViewModel.contentToAddUrl
if (url != null) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ImageVideoDescription(
url,
account.defaultFileServer,
@ -324,26 +379,28 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val lud16 = user?.info?.lnAddress()
if (lud16 != null && postViewModel.wantsInvoice) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
},
onClose = {
postViewModel.wantsInvoice = false
}
)
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
Column(Modifier.fillMaxWidth()) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
},
onClose = {
postViewModel.wantsInvoice = false
}
)
}
}
}
if (lud16 != null && postViewModel.wantsZapraiser) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ZapRaiserRequest(
stringResource(id = R.string.zapraiser),
postViewModel
@ -353,7 +410,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase()
@ -456,6 +513,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
}
AddGeoHash(postViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@ -466,6 +527,227 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
}
}
@Composable
fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Default.VisibilityOff,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(18.dp)
.align(Alignment.BottomStart),
tint = Color.Red
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = Color.Yellow
)
}
Text(
text = stringResource(R.string.add_sensitive_content_label),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.add_sensitive_content_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
@Composable
fun FowardZapTo(postViewModel: NewPostViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Text(
text = stringResource(R.string.zap_forward_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.zap_forward_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationAsHash(postViewModel: NewPostViewModel) {
val context = LocalContext.current
val locationPermissionState = rememberPermissionState(
Manifest.permission.ACCESS_COARSE_LOCATION
)
if (locationPermissionState.status.isGranted) {
var locationDescriptionFlow by remember(postViewModel) {
mutableStateOf<Flow<String>?>(null)
}
DisposableEffect(key1 = Unit) {
postViewModel.startLocation(context = context)
locationDescriptionFlow = postViewModel.location
onDispose {
postViewModel.stopLocation()
}
}
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(20.dp)
) {
Icon(
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
}
Text(
text = stringResource(R.string.geohash_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
locationDescriptionFlow?.let { geoLocation ->
DisplayLocationObserver(geoLocation)
}
}
Divider()
Text(
text = stringResource(R.string.geohash_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
} else {
LaunchedEffect(locationPermissionState) {
locationPermissionState.launchPermissionRequest()
}
}
}
@Composable
fun DisplayLocationObserver(geoLocation: Flow<String>) {
val location by geoLocation.collectAsState(initial = null)
location?.let {
DisplayLocationInTitle(geohash = it)
}
}
@Composable
fun DisplayLocationInTitle(geohash: String) {
val context = LocalContext.current
val cityName = remember(geohash) {
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
}
Text(
text = cityName ?: geohash,
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = Size5dp)
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
@ -582,6 +864,31 @@ private fun AddZapraiserButton(
}
}
@Composable
fun AddGeoHash(postViewModel: NewPostViewModel, onClick: () -> Unit) {
IconButton(
onClick = {
onClick()
}
) {
if (!postViewModel.wantsToAddGeoHash) {
Icon(
imageVector = Icons.Default.LocationOff,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
}
}
}
@Composable
private fun AddLnInvoiceButton(
isLnInvoiceActive: Boolean,
@ -662,33 +969,6 @@ private fun ForwardZapTo(
}
}
}
if (postViewModel.wantsForwardZapTo) {
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
.padding(0.dp),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.placeholderText,
fontSize = Font14SP
)
},
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
@Composable
@ -913,7 +1193,9 @@ fun ImageVideoDescription(
)
IconButton(
modifier = Modifier.size(30.dp).padding(end = 5.dp),
modifier = Modifier
.size(30.dp)
.padding(end = 5.dp),
onClick = onCancel
) {
CancelIcon()

Wyświetl plik

@ -13,8 +13,10 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationUtil
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.model.AddressableEvent
import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
@ -26,7 +28,9 @@ import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.isValidURL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
@Stable
@ -77,6 +81,11 @@ open class NewPostViewModel() : ViewModel() {
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
// GeoHash
var wantsToAddGeoHash by mutableStateOf(false)
var locUtil: LocationUtil? = null
var location: Flow<String>? = null
// ZapRaiser
var canAddZapRaiser by mutableStateOf(false)
var wantsZapraiser by mutableStateOf(false)
@ -121,6 +130,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = null
@ -143,6 +153,13 @@ 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 localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
if (wantsPoll) {
@ -158,16 +175,17 @@ open class NewPostViewModel() : ViewModel() {
zapReceiver,
wantsToMarkAsSensitive,
localZapRaiserAmount,
relayList
relayList,
geoHash
)
} else if (originalNote?.channelHex() != null) {
if (originalNote is AddressableEvent && originalNote?.address() != null) {
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
} else {
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
}
} else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
} else {
// adds markers
val rootId =
@ -187,7 +205,8 @@ open class NewPostViewModel() : ViewModel() {
replyingTo = replyId,
root = rootId,
directMentions = tagger.directMentions,
relayList = relayList
relayList = relayList,
geohash = geoHash
)
}
@ -267,6 +286,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
forwardZapTo = null
forwardZapToEditting = TextFieldValue("")
@ -449,4 +469,50 @@ open class NewPostViewModel() : ViewModel() {
fun selectImage(uri: Uri) {
contentToAddUrl = uri
}
fun startLocation(context: Context) {
locUtil = LocationUtil(context)
locUtil?.let {
location = it.locationStateFlow.mapLatest {
it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
}
}
viewModelScope.launch(Dispatchers.IO) {
locUtil?.start()
}
}
fun stopLocation() {
viewModelScope.launch(Dispatchers.IO) {
locUtil?.stop()
}
location = null
locUtil = null
}
override fun onCleared() {
super.onCleared()
viewModelScope.launch(Dispatchers.IO) {
locUtil?.stop()
}
location = null
locUtil = null
}
}
enum class GeohashPrecision(val digits: Int) {
KM_5000_X_5000(1), // 5,000km × 5,000km
KM_1250_X_625(2), // 1,250km × 625km
KM_156_X_156(3), // 156km × 156km
KM_39_X_19(4), // 39.1km × 19.5km
KM_5_X_5(5), // 4.89km × 4.89km
M_1000_X_600(6), // 1.22km × 0.61km
M_153_X_153(7), // 153m × 153m
M_38_X_19(8), // 38.2m × 19.1m
M_5_X_5(9), // 4.77m × 4.77m
MM_1000_X_1000(10), // 1.19m × 0.596m
MM_149_X_149(11), // 149mm × 149mm
MM_37_X_18(12) // 37.2mm × 18.6mm
}

Wyświetl plik

@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.launch
@Composable
fun InvoiceRequest(
fun InvoiceRequestCard(
lud16: String,
toUserPubKeyHex: String,
account: Account,
@ -51,9 +51,6 @@ fun InvoiceRequest(
onSuccess: (String) -> Unit,
onClose: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxWidth()
@ -66,102 +63,118 @@ fun InvoiceRequest(
.fillMaxWidth()
.padding(30.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.lightning_tips),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
var message by remember { mutableStateOf("") }
var amount by remember { mutableStateOf(1000L) }
OutlinedTextField(
label = { Text(text = stringResource(R.string.note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = message,
onValueChange = { message = it },
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = amount.toString(),
onValueChange = {
runCatching {
if (it.isEmpty()) {
amount = 0
} else {
amount = it.toLong()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount * 1000,
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onProgress = {
}
)
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
}
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose)
}
}
}
@Composable
fun InvoiceRequest(
lud16: String,
toUserPubKeyHex: String,
account: Account,
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.lightning_tips),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
var message by remember { mutableStateOf("") }
var amount by remember { mutableStateOf(1000L) }
OutlinedTextField(
label = { Text(text = stringResource(R.string.note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = message,
onValueChange = { message = it },
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = amount.toString(),
onValueChange = {
runCatching {
if (it.isEmpty()) {
amount = 0
} else {
amount = it.toLong()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount * 1000,
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onProgress = {
}
)
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
}
}

Wyświetl plik

@ -35,66 +35,62 @@ fun ZapRaiserRequest(
Column(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.fillMaxWidth()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.zapraiser),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Text(
text = stringResource(R.string.zapraiser_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.zapraiser_target_amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = if (newPostViewModel.zapRaiserAmount != null) {
newPostViewModel.zapRaiserAmount.toString()
} else {
""
},
onValueChange = {
runCatching {
if (it.isEmpty()) {
newPostViewModel.zapRaiserAmount = null
} else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
text = titleText ?: stringResource(R.string.zapraiser),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.zapraiser_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.zapraiser_target_amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = if (newPostViewModel.zapRaiserAmount != null) {
newPostViewModel.zapRaiserAmount.toString()
} else {
""
},
onValueChange = {
runCatching {
if (it.isEmpty()) {
newPostViewModel.zapRaiserAmount = null
} else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
}
}

Wyświetl plik

@ -74,6 +74,7 @@ import androidx.lifecycle.map
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.request.SuccessResult
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel
@ -83,6 +84,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
@ -2405,6 +2407,11 @@ fun SecondUserInfoRow(
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
val geo = remember { noteEvent.getGeoHash() }
if (geo != null) {
DisplayLocation(geo)
}
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
DisplayReward(baseReward, note, accountViewModel, nav)
@ -2417,6 +2424,22 @@ fun SecondUserInfoRow(
}
}
@Composable
fun DisplayLocation(geohash: String) {
val context = LocalContext.current
val cityName = remember(geohash) {
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
}
Text(
text = cityName ?: geohash,
color = MaterialTheme.colors.lessImportantLink,
fontSize = Font14SP,
fontWeight = FontWeight.Bold,
maxLines = 1
)
}
@Composable
fun FirstUserInfoRow(
baseNote: Note,

Wyświetl plik

@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@ -1002,7 +1002,7 @@ fun DisplayLNAddress(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp)
) {
InvoiceRequest(
InvoiceRequestCard(
lud16,
userHex,
account,

Wyświetl plik

@ -46,6 +46,7 @@ val DoubleVertSpacer = Modifier.height(10.dp)
val HalfDoubleVertSpacer = Modifier.height(7.dp)
val Size0dp = 0.dp
val Size5dp = 5.dp
val Size10dp = 10.dp
val Size13dp = 13.dp
val Size15dp = 15.dp

Wyświetl plik

@ -508,4 +508,12 @@
<string name="nip05_checking">Checking Nostr address</string>
<string name="select_deselect_all">Select/Deselect all</string>
<string name="default_relays">Default</string>
<string name="zap_forward_title">Forward Zaps to:</string>
<string name="zap_forward_explainer">Supporting clients will forward zaps to the LNAddress or User Profile below instead of yours</string>
<string name="geohash_title">Expose Location as </string>
<string name="geohash_explainer">Adds a Geohash of your location to the post. People will know you are within 5x5km (3x3mi) or this hash</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>
</resources>