mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 01:45:22 +00:00
ui cleanup
This commit is contained in:
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user