ui cleanup

This commit is contained in:
callebtc
2025-10-17 23:03:15 +02:00
parent 7e9de140d4
commit fa1308f12b
3 changed files with 151 additions and 70 deletions
@@ -51,31 +51,51 @@ object LocationNotesCounter {
/**
* Subscribe to count notes for a specific geohash
* iOS: Validates building-level precision (8 chars) and checks for relay availability
*/
fun subscribe(geohash: String) {
if (_geohash.value == geohash && subscriptionID != null) {
Log.d(TAG, "Already subscribed to geohash: $geohash")
val normalized = geohash.lowercase()
// Skip if already subscribed to this geohash
if (_geohash.value == normalized && subscriptionID != null) {
Log.d(TAG, "Already subscribed to geohash: $normalized")
return
}
Log.d(TAG, "Subscribing to count notes for geohash: $geohash")
// Validate geohash (building-level precision: 8 chars) - matches iOS
if (!isValidBuildingGeohash(normalized)) {
Log.w(TAG, "LocationNotesCounter: rejecting invalid geohash '$normalized' (expected 8 valid base32 chars)")
return
}
// Cancel existing subscription
cancel()
Log.d(TAG, "Subscribing to count notes for geohash: $normalized")
// Unsubscribe previous without clearing count to avoid flicker (iOS pattern)
subscriptionID?.let { subId ->
unsubscribeFunc?.invoke(subId)
}
subscriptionID = null
// Reset state
_geohash.value = geohash
_count.value = 0
_geohash.value = normalized
eventIDs.clear()
_initialLoadComplete.value = false
_relayAvailable.value = true
// Check relay availability
val relayManager = relayLookup?.invoke()
val hasRelays = relayManager?.getRelayStatuses()?.any { it.isConnected } == true
_relayAvailable.value = hasRelays
// Get geo-specific relays (iOS pattern: dependencies.relayLookup(norm, TransportConfig.nostrGeoRelayCount))
val relays = try {
com.bitchat.android.nostr.RelayDirectory.closestRelaysForGeohash(normalized, 5)
} catch (e: Exception) {
Log.e(TAG, "Failed to lookup relays for geohash $normalized: ${e.message}")
emptyList()
}
if (!hasRelays) {
Log.w(TAG, "No relays available for counter subscription")
// Check if we have relays (iOS pattern: guard !relays.isEmpty())
if (relays.isEmpty()) {
Log.w(TAG, "LocationNotesCounter: no geo relays for geohash=$normalized")
_relayAvailable.value = false
_initialLoadComplete.value = true
_count.value = 0
return
}
@@ -86,15 +106,16 @@ object LocationNotesCounter {
}
val filter = NostrFilter.geohashNotes(
geohash = geohash,
geohash = normalized,
since = null,
limit = 200 // Count up to 200 recent notes
)
val subId = "location-notes-count-$geohash"
// iOS format: "locnotes-count-\(norm)-\(UUID().uuidString.prefix(6))"
val subId = "locnotes-count-$normalized-${java.util.UUID.randomUUID().toString().take(6)}"
subscriptionID = subscribe(filter, subId) { event ->
handleEvent(event, geohash)
handleEvent(event, normalized)
}
// Mark initial load complete after brief delay
@@ -107,6 +128,15 @@ object LocationNotesCounter {
}
}
/**
* Validate building-level geohash (precision 8) - matches iOS Geohash.isValidBuildingGeohash
*/
private fun isValidBuildingGeohash(geohash: String): Boolean {
if (geohash.length != 8) return false
val base32Chars = "0123456789bcdefghjkmnpqrstuvwxyz"
return geohash.all { it in base32Chars }
}
/**
* Handle incoming event
*/
@@ -38,10 +38,19 @@ class LocationNotesManager private constructor() {
val nickname: String?
) {
/**
* Display name for the note (nickname or truncated pubkey)
* Display name for the note - matches iOS exactly
* Format: "nickname#abcd" or "anon#abcd" where abcd is last 4 chars of pubkey
*/
val displayName: String
get() = nickname ?: "@${pubkey.take(8)}"
get() {
val suffix = pubkey.takeLast(4)
val nick = nickname?.trim()
return if (!nick.isNullOrEmpty()) {
"$nick#$suffix"
} else {
"anon#$suffix"
}
}
}
/**
@@ -103,29 +112,50 @@ class LocationNotesManager private constructor() {
/**
* Set geohash and start subscription
* iOS: Validates building-level precision (8 characters)
*/
fun setGeohash(newGeohash: String) {
if (_geohash.value == newGeohash) {
Log.d(TAG, "Geohash unchanged, skipping: $newGeohash")
val normalized = newGeohash.lowercase()
if (_geohash.value == normalized) {
Log.d(TAG, "Geohash unchanged, skipping: $normalized")
return
}
Log.d(TAG, "Setting geohash: $newGeohash")
// Validate geohash (building-level precision: 8 chars) - matches iOS
if (!isValidBuildingGeohash(normalized)) {
Log.w(TAG, "LocationNotesManager: rejecting invalid geohash '$normalized' (expected 8 valid base32 chars)")
return
}
Log.d(TAG, "Setting geohash: $normalized")
// Cancel existing subscription
cancel()
// Clear state
_notes.value = emptyList()
noteIDs.clear()
// Set loading state before clearing to prevent empty state flicker (iOS pattern)
_state.value = State.LOADING
_initialLoadComplete.value = false
_errorMessage.value = null
_geohash.value = newGeohash
// Clear notes
_notes.value = emptyList()
noteIDs.clear()
_geohash.value = normalized
// Start new subscription
subscribe()
}
/**
* Validate building-level geohash (precision 8) - matches iOS Geohash.isValidBuildingGeohash
*/
private fun isValidBuildingGeohash(geohash: String): Boolean {
if (geohash.length != 8) return false
val base32Chars = "0123456789bcdefghjkmnpqrstuvwxyz"
return geohash.all { it in base32Chars }
}
/**
* Refresh notes for current geohash
*/
@@ -454,7 +454,7 @@ private fun ErrorRow(message: String, onDismiss: () -> Unit) {
}
/**
* Input section - matches iOS inputSection exactly
* Input section - matches main chat input exactly
*/
@Composable
private fun LocationNotesInputSection(
@@ -466,61 +466,82 @@ private fun LocationNotesInputSection(
onSend: () -> Unit
) {
val isDark = isSystemInDarkTheme()
val colorScheme = MaterialTheme.colorScheme
Row(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
.padding(horizontal = 12.dp, vertical = 8.dp), // Match main chat padding
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) // Match main chat spacing
) {
// Text field
TextField(
value = draft,
onValueChange = onDraftChange,
modifier = Modifier.weight(1f),
placeholder = {
// Text input with placeholder overlay (matches main chat exactly)
Box(
modifier = Modifier.weight(1f)
) {
androidx.compose.foundation.text.BasicTextField(
value = draft,
onValueChange = onDraftChange,
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = colorScheme.primary,
fontFamily = FontFamily.Monospace
),
cursorBrush = androidx.compose.ui.graphics.SolidColor(colorScheme.primary),
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
imeAction = androidx.compose.ui.text.input.ImeAction.Send
),
keyboardActions = androidx.compose.foundation.text.KeyboardActions(
onSend = { if (sendButtonEnabled) onSend() }
),
modifier = Modifier.fillMaxWidth()
)
// Placeholder when empty (matches main chat)
if (draft.isEmpty()) {
Text(
text = "add a note for this place",
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
color = colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.fillMaxWidth()
)
},
textStyle = androidx.compose.ui.text.TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.primary
),
maxLines = 3
)
}
}
Spacer(modifier = Modifier.width(10.dp))
// Send button - iOS arrow.up.circle.fill icon
Box(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
.clickable(enabled = sendButtonEnabled, onClick = onSend),
contentAlignment = Alignment.Center
// Send button - circular with icon (matches main chat exactly)
IconButton(
onClick = { if (sendButtonEnabled) onSend() },
enabled = sendButtonEnabled,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.ArrowUpward,
contentDescription = "Send",
tint = if (sendButtonEnabled) accentGreen else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
modifier = Modifier.size(20.dp)
)
Box(
modifier = Modifier
.size(30.dp)
.background(
color = if (!sendButtonEnabled) {
colorScheme.onSurface.copy(alpha = 0.3f)
} else {
accentGreen.copy(alpha = 0.75f)
},
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.ArrowUpward,
contentDescription = "Send",
modifier = Modifier.size(20.dp),
tint = if (!sendButtonEnabled) {
colorScheme.onSurface.copy(alpha = 0.5f)
} else if (isDark) {
Color.Black // Black arrow on green in dark theme
} else {
Color.White // White arrow on green in light theme
}
)
}
}
}
}