feat(composer): add "pick on map" location to the short-note composer

The composer could only attach the current GPS location (fixed ~5km), which
fails when GPS is denied/unavailable and can't tag a post with a different
place. Add a "pick a place on the map" action that opens the shared
GeohashLocationPickerDialog and stores the result in
ShortNotePostViewModel.pickedGeoHash, which overrides the GPS fix at build time
and round-trips through drafts. Picking a place also skips the GPS permission
prompt. The existing "use my location" GPS flow is unchanged; this is additive.

Scoped to the short-note composer for now; the other ILocationGrabber composers
(long-form, classifieds, DMs, comments, channel/nest messages) can adopt the
same pickedGeoHash override + section in a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
This commit is contained in:
Claude
2026-07-18 22:57:55 +00:00
parent 8658dcd512
commit 7b32ca1da3
3 changed files with 102 additions and 14 deletions
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.foundation.verticalScroll
@@ -106,6 +107,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePick
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
import com.vitorpamplona.amethyst.ui.note.creators.location.DisplayLocationInTitle
import com.vitorpamplona.amethyst.ui.note.creators.location.GeohashLocationPickerDialog
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
import com.vitorpamplona.amethyst.ui.note.creators.notify.Notifying
@@ -527,19 +530,7 @@ private fun NewPostScreenBody(
}
if (postViewModel.wantsToAddGeoHash) {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp),
) {
LocationAsHash(postViewModel) {
SettingsRow(
R.string.geohash_exclusive,
R.string.geohash_exclusive_explainer,
) {
Switch(postViewModel.wantsExclusiveGeoPost, onCheckedChange = { postViewModel.wantsExclusiveGeoPost = it })
}
}
}
GeoHashPostSection(postViewModel)
}
if (postViewModel.wantsForwardZapTo) {
@@ -881,6 +872,85 @@ private fun BottomRowActions(
}
}
/**
* The composer's location section. Defaults to the device GPS flow ([LocationAsHash]),
* but a "pick on map" action opens the shared [GeohashLocationPickerDialog] and stores
* the chosen geohash in [ShortNotePostViewModel.pickedGeoHash], which then overrides GPS
* at build time. Picking a place also skips the GPS permission prompt.
*/
@Composable
private fun GeoHashPostSection(postViewModel: ShortNotePostViewModel) {
var showPicker by remember { mutableStateOf(false) }
val picked = postViewModel.pickedGeoHash
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = Size10dp, horizontal = Size10dp),
) {
if (picked != null) {
// A map-picked place: show it, and let the user clear back to GPS.
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth()) {
Icon(
symbol = MaterialSymbols.LocationOn,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringRes(R.string.geohash_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
)
DisplayLocationInTitle(geohash = picked)
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { postViewModel.pickedGeoHash = null }) {
Icon(
symbol = MaterialSymbols.Close,
contentDescription = stringRes(R.string.remove_location),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
HorizontalDivider()
SettingsRow(R.string.geohash_exclusive, R.string.geohash_exclusive_explainer) {
Switch(postViewModel.wantsExclusiveGeoPost, onCheckedChange = { postViewModel.wantsExclusiveGeoPost = it })
}
} else {
// GPS mode (unchanged): current device location + the exclusive-post switch.
LocationAsHash(postViewModel) {
SettingsRow(R.string.geohash_exclusive, R.string.geohash_exclusive_explainer) {
Switch(postViewModel.wantsExclusiveGeoPost, onCheckedChange = { postViewModel.wantsExclusiveGeoPost = it })
}
}
}
TextButton(onClick = { showPicker = true }, modifier = Modifier.padding(top = 4.dp)) {
Icon(
symbol = MaterialSymbols.LocationOn,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringRes(if (picked != null) R.string.location_change_place else R.string.location_pick_on_map),
modifier = Modifier.padding(start = 6.dp),
)
}
}
if (showPicker) {
GeohashLocationPickerDialog(
initialGeohash = picked,
onDismiss = { showPicker = false },
onConfirm = { cell ->
postViewModel.pickedGeoHash = cell
showPicker = false
},
)
}
}
@SuppressLint("ViewModelConstructorInComposable")
@Preview
@Composable
@@ -329,6 +329,12 @@ open class ShortNotePostViewModel :
// GeoHash
var wantsToAddGeoHash by mutableStateOf(false)
var location: StateFlow<LocationState.LocationResult>? = null
/**
* A geohash the user picked on the map, which overrides the live GPS location at
* build time. Null means "use my current GPS location" (the default behavior).
*/
var pickedGeoHash by mutableStateOf<String?>(null)
var wantsExclusiveGeoPost by mutableStateOf(false)
// ZapRaiser
@@ -753,6 +759,7 @@ open class ShortNotePostViewModel :
val geohash = draftEvent.getGeoHash()
wantsToAddGeoHash = geohash != null
pickedGeoHash = geohash
if (geohash != null) {
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
}
@@ -858,6 +865,7 @@ open class ShortNotePostViewModel :
val geohash = draftEvent.getGeoHash()
wantsToAddGeoHash = geohash != null
pickedGeoHash = geohash
if (geohash != null) {
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
}
@@ -931,6 +939,7 @@ open class ShortNotePostViewModel :
val geohash = draftEvent.getGeoHash()
wantsToAddGeoHash = geohash != null
pickedGeoHash = geohash
if (geohash != null) {
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
}
@@ -1236,7 +1245,13 @@ open class ShortNotePostViewModel :
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
val geoHash =
if (wantsToAddGeoHash) {
// A map-picked geohash wins over the live GPS fix.
pickedGeoHash ?: (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString()
} else {
null
}
val localZapRaiserAmount = if (wantsZapRaiser) zapRaiserAmount.value else null
val emojis = account.emoji.findEmojiTags(tagger.message)
@@ -1563,6 +1578,7 @@ open class ShortNotePostViewModel :
wantsToMarkAsSensitive = false
contentWarningDescription = ""
wantsToAddGeoHash = false
pickedGeoHash = null
wantsExclusiveGeoPost = false
wantsSecretEmoji = false
wantsAnonymousPost = false
+2
View File
@@ -2149,6 +2149,8 @@
<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_teleport_title">Teleport</string>
<string name="geohash_teleport_action">✈ Teleport here</string>
<string name="location_pick_on_map">Pick a place on the map</string>
<string name="location_change_place">Change place on the map</string>
<string name="geohash_exclusive">Location-exclusive Post</string>
<string name="geohash_exclusive_explainer">Only followers of the location will see it. Your general followers won\'t see it.</string>