mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:05:26 +00:00
ui cleanup
This commit is contained in:
@@ -51,31 +51,51 @@ object LocationNotesCounter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to count notes for a specific geohash
|
* Subscribe to count notes for a specific geohash
|
||||||
|
* iOS: Validates building-level precision (8 chars) and checks for relay availability
|
||||||
*/
|
*/
|
||||||
fun subscribe(geohash: String) {
|
fun subscribe(geohash: String) {
|
||||||
if (_geohash.value == geohash && subscriptionID != null) {
|
val normalized = geohash.lowercase()
|
||||||
Log.d(TAG, "Already subscribed to geohash: $geohash")
|
|
||||||
|
// Skip if already subscribed to this geohash
|
||||||
|
if (_geohash.value == normalized && subscriptionID != null) {
|
||||||
|
Log.d(TAG, "Already subscribed to geohash: $normalized")
|
||||||
return
|
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
|
Log.d(TAG, "Subscribing to count notes for geohash: $normalized")
|
||||||
cancel()
|
|
||||||
|
// Unsubscribe previous without clearing count to avoid flicker (iOS pattern)
|
||||||
|
subscriptionID?.let { subId ->
|
||||||
|
unsubscribeFunc?.invoke(subId)
|
||||||
|
}
|
||||||
|
subscriptionID = null
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
_geohash.value = geohash
|
_geohash.value = normalized
|
||||||
_count.value = 0
|
|
||||||
eventIDs.clear()
|
eventIDs.clear()
|
||||||
_initialLoadComplete.value = false
|
_initialLoadComplete.value = false
|
||||||
|
_relayAvailable.value = true
|
||||||
|
|
||||||
// Check relay availability
|
// Get geo-specific relays (iOS pattern: dependencies.relayLookup(norm, TransportConfig.nostrGeoRelayCount))
|
||||||
val relayManager = relayLookup?.invoke()
|
val relays = try {
|
||||||
val hasRelays = relayManager?.getRelayStatuses()?.any { it.isConnected } == true
|
com.bitchat.android.nostr.RelayDirectory.closestRelaysForGeohash(normalized, 5)
|
||||||
_relayAvailable.value = hasRelays
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to lookup relays for geohash $normalized: ${e.message}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasRelays) {
|
// Check if we have relays (iOS pattern: guard !relays.isEmpty())
|
||||||
Log.w(TAG, "No relays available for counter subscription")
|
if (relays.isEmpty()) {
|
||||||
|
Log.w(TAG, "LocationNotesCounter: no geo relays for geohash=$normalized")
|
||||||
|
_relayAvailable.value = false
|
||||||
|
_initialLoadComplete.value = true
|
||||||
|
_count.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +106,16 @@ object LocationNotesCounter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val filter = NostrFilter.geohashNotes(
|
val filter = NostrFilter.geohashNotes(
|
||||||
geohash = geohash,
|
geohash = normalized,
|
||||||
since = null,
|
since = null,
|
||||||
limit = 200 // Count up to 200 recent notes
|
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 ->
|
subscriptionID = subscribe(filter, subId) { event ->
|
||||||
handleEvent(event, geohash)
|
handleEvent(event, normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark initial load complete after brief delay
|
// 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
|
* Handle incoming event
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,10 +38,19 @@ class LocationNotesManager private constructor() {
|
|||||||
val nickname: String?
|
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
|
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
|
* Set geohash and start subscription
|
||||||
|
* iOS: Validates building-level precision (8 characters)
|
||||||
*/
|
*/
|
||||||
fun setGeohash(newGeohash: String) {
|
fun setGeohash(newGeohash: String) {
|
||||||
if (_geohash.value == newGeohash) {
|
val normalized = newGeohash.lowercase()
|
||||||
Log.d(TAG, "Geohash unchanged, skipping: $newGeohash")
|
|
||||||
|
if (_geohash.value == normalized) {
|
||||||
|
Log.d(TAG, "Geohash unchanged, skipping: $normalized")
|
||||||
return
|
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 existing subscription
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
// Clear state
|
// Set loading state before clearing to prevent empty state flicker (iOS pattern)
|
||||||
_notes.value = emptyList()
|
_state.value = State.LOADING
|
||||||
noteIDs.clear()
|
|
||||||
_initialLoadComplete.value = false
|
_initialLoadComplete.value = false
|
||||||
_errorMessage.value = null
|
_errorMessage.value = null
|
||||||
_geohash.value = newGeohash
|
|
||||||
|
// Clear notes
|
||||||
|
_notes.value = emptyList()
|
||||||
|
noteIDs.clear()
|
||||||
|
_geohash.value = normalized
|
||||||
|
|
||||||
// Start new subscription
|
// Start new subscription
|
||||||
subscribe()
|
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
|
* 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
|
@Composable
|
||||||
private fun LocationNotesInputSection(
|
private fun LocationNotesInputSection(
|
||||||
@@ -466,61 +466,82 @@ private fun LocationNotesInputSection(
|
|||||||
onSend: () -> Unit
|
onSend: () -> Unit
|
||||||
) {
|
) {
|
||||||
val isDark = isSystemInDarkTheme()
|
val isDark = isSystemInDarkTheme()
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp), // Match main chat padding
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
verticalAlignment = Alignment.Top
|
horizontalArrangement = Arrangement.spacedBy(8.dp) // Match main chat spacing
|
||||||
) {
|
) {
|
||||||
// Text field
|
// Text input with placeholder overlay (matches main chat exactly)
|
||||||
TextField(
|
Box(
|
||||||
value = draft,
|
modifier = Modifier.weight(1f)
|
||||||
onValueChange = onDraftChange,
|
) {
|
||||||
modifier = Modifier.weight(1f),
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
placeholder = {
|
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(
|
||||||
text = "add a note for this place",
|
text = "add a note for this place",
|
||||||
fontFamily = FontFamily.Monospace,
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
fontSize = 14.sp,
|
fontFamily = FontFamily.Monospace
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
|
),
|
||||||
|
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 - circular with icon (matches main chat exactly)
|
||||||
|
IconButton(
|
||||||
// Send button - iOS arrow.up.circle.fill icon
|
onClick = { if (sendButtonEnabled) onSend() },
|
||||||
Box(
|
enabled = sendButtonEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(32.dp)
|
||||||
.padding(top = 2.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
.clickable(enabled = sendButtonEnabled, onClick = onSend),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Box(
|
||||||
imageVector = Icons.Default.ArrowUpward,
|
modifier = Modifier
|
||||||
contentDescription = "Send",
|
.size(30.dp)
|
||||||
tint = if (sendButtonEnabled) accentGreen else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
.background(
|
||||||
modifier = Modifier.size(20.dp)
|
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