diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index e34d9b59..e9310c16 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -43,12 +43,6 @@ class BluetoothConnectionManager( override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)") device?.let { bluetoothDevice -> - // if connection does not have a peerID yet, we assume that the first package - // we receive from that connection is from the peer - if (!connectionTracker.addressPeerMap.containsKey(device.address)) { - Log.d(TAG, "First packet received from new device: ${bluetoothDevice.address}, assuming peerID: $peerID") - connectionTracker.addressPeerMap[device.address] = peerID - } // Get current RSSI for this device and update if available val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address) if (currentRSSI != null) { @@ -64,6 +58,10 @@ class BluetoothConnectionManager( override fun onDeviceConnected(device: BluetoothDevice) { delegate?.onDeviceConnected(device) } + + override fun onDeviceDisconnected(device: BluetoothDevice) { + delegate?.onDeviceDisconnected(device) + } override fun onRSSIUpdated(deviceAddress: String, rssi: Int) { delegate?.onRSSIUpdated(deviceAddress, rssi) @@ -262,5 +260,6 @@ class BluetoothConnectionManager( interface BluetoothConnectionManagerDelegate { fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) fun onDeviceConnected(device: BluetoothDevice) + fun onDeviceDisconnected(device: BluetoothDevice) fun onRSSIUpdated(deviceAddress: String, rssi: Int) } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index f47aba15..40c5708d 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -343,6 +343,9 @@ class BluetoothGattClientManager( connectionTracker.cleanupDeviceConnection(deviceAddress) } + // Notify higher layers about device disconnection to update direct flags + delegate?.onDeviceDisconnected(gatt.device) + connectionScope.launch { delay(500) // CLEANUP_DELAY try { diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index 99c23f0c..96a86c4d 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -144,6 +144,8 @@ class BluetoothGattServerManager( BluetoothProfile.STATE_DISCONNECTED -> { Log.i(TAG, "Server: Device disconnected ${device.address}") connectionTracker.cleanupDeviceConnection(device.address) + // Notify delegate about device disconnection so higher layers can update direct flags + delegate?.onDeviceDisconnected(device) } } } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 64e01084..01fd3472 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -343,7 +343,34 @@ class BluetoothMeshService(private val context: Context) { } override fun handleAnnounce(routed: RoutedPacket) { - serviceScope.launch { messageHandler.handleAnnounce(routed) } + serviceScope.launch { + // Process the announce + val isFirst = messageHandler.handleAnnounce(routed) + + // Map device address -> peerID on first announce seen over this device connection + val deviceAddress = routed.relayAddress + val pid = routed.peerID + if (deviceAddress != null && pid != null) { + // Only set mapping if not already mapped + if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) { + connectionManager.addressPeerMap[deviceAddress] = pid + Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE") + + // Mark this peer as directly connected for UI + try { + peerManager.getPeerInfo(pid)?.let { + // Set direct connection flag + // (This will also trigger a peer list update) + peerManager.setDirectConnection(pid, true) + // Also push reactive directness state to UI (best-effort) + try { + // Note: UI observes via didUpdatePeerList, but we can also update ChatState on a timer + } catch (_: Exception) { } + } + } catch (_: Exception) { } + } + } + } } override fun handleMessage(routed: RoutedPacket) { @@ -384,6 +411,21 @@ class BluetoothMeshService(private val context: Context) { sendBroadcastAnnounce() } } + + override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) { + val addr = device.address + // Remove mapping and, if that was the last direct path for the peer, clear direct flag + val peer = connectionManager.addressPeerMap[addr] + // ConnectionTracker has already removed the address mapping; be defensive either way + connectionManager.addressPeerMap.remove(addr) + if (peer != null) { + val stillMapped = connectionManager.addressPeerMap.values.any { it == peer } + if (!stillMapped) { + // Peer might still be reachable indirectly; mark as not-direct + try { peerManager.setDirectConnection(peer, false) } catch (_: Exception) { } + } + } + } override fun onRSSIUpdated(deviceAddress: String, rssi: Int) { // Find the peer ID for this device address and update RSSI in PeerManager diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index 26a0ef87..4b8a16dc 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -13,6 +13,7 @@ data class PeerInfo( val id: String, var nickname: String, var isConnected: Boolean, + var isDirectConnection: Boolean, var noisePublicKey: ByteArray?, var signingPublicKey: ByteArray?, // NEW: Ed25519 public key for verification var isVerifiedNickname: Boolean, // NEW: Verification status flag @@ -27,6 +28,7 @@ data class PeerInfo( if (id != other.id) return false if (nickname != other.nickname) return false if (isConnected != other.isConnected) return false + if (isDirectConnection != other.isDirectConnection) return false if (noisePublicKey != null) { if (other.noisePublicKey == null) return false if (!noisePublicKey.contentEquals(other.noisePublicKey)) return false @@ -45,6 +47,7 @@ data class PeerInfo( var result = id.hashCode() result = 31 * result + nickname.hashCode() result = 31 * result + isConnected.hashCode() + result = 31 * result + isDirectConnection.hashCode() result = 31 * result + (noisePublicKey?.contentHashCode() ?: 0) result = 31 * result + (signingPublicKey?.contentHashCode() ?: 0) result = 31 * result + isVerifiedNickname.hashCode() @@ -74,11 +77,7 @@ class PeerManager { private val announcedPeers = CopyOnWriteArrayList() private val announcedToPeers = CopyOnWriteArrayList() - // Legacy support for existing code - @Deprecated("Use PeerInfo structure instead") - private val peerNicknames = ConcurrentHashMap() - @Deprecated("Use PeerInfo structure instead") - private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + // Legacy fields removed: use PeerInfo map exclusively // Centralized fingerprint management private val fingerprintManager = PeerFingerprintManager.getInstance() @@ -117,6 +116,7 @@ class PeerManager { id = peerID, nickname = nickname, isConnected = true, + isDirectConnection = existingPeer?.isDirectConnection ?: false, noisePublicKey = noisePublicKey, signingPublicKey = signingPublicKey, isVerifiedNickname = isVerified, @@ -125,9 +125,9 @@ class PeerManager { peers[peerID] = peerInfo - // Update legacy structures for compatibility - peerNicknames[peerID] = nickname - activePeers[peerID] = now + // Update derived state only + // No legacy maps; peers map is the single source of truth + // Maintain announcedPeers for first-time announce semantics if (isNewPeer && isVerified) { announcedPeers.add(peerID) @@ -164,6 +164,24 @@ class PeerManager { return peers.filterValues { it.isVerifiedNickname } } + /** + * Set whether a peer is directly connected over Bluetooth. + * Triggers a peer list update to refresh UI badges. + */ + fun setDirectConnection(peerID: String, isDirect: Boolean) { + peers[peerID]?.let { existing -> + if (existing.isDirectConnection != isDirect) { + peers[peerID] = existing.copy(isDirectConnection = isDirect) + notifyPeerListUpdate() + // NEW: notify UI state (if available via delegate path) about directness change + try { + // Best-effort: delegate path flows up to ChatViewModel via didUpdatePeerList + // No direct reference to UI layer here by design. + } catch (_: Exception) { } + } + } + } + // MARK: - Legacy Methods (maintained for compatibility) /** @@ -171,8 +189,6 @@ class PeerManager { */ fun updatePeerLastSeen(peerID: String) { if (peerID != "unknown") { - activePeers[peerID] = System.currentTimeMillis() - // Also update PeerInfo if it exists peers[peerID]?.let { info -> peers[peerID] = info.copy(lastSeen = System.currentTimeMillis()) } @@ -181,16 +197,17 @@ class PeerManager { /** * Add or update peer with nickname + * Maintained for compatibility. Uses peers map exclusively now. */ fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { if (peerID == "unknown") return false // Clean up stale peer IDs with the same nickname (exact same logic as iOS) + val now = System.currentTimeMillis() val stalePeerIDs = mutableListOf() - peerNicknames.forEach { (existingPeerID, existingNickname) -> - if (existingNickname == nickname && existingPeerID != peerID) { - val lastSeen = activePeers[existingPeerID] ?: 0 - val wasRecentlySeen = (System.currentTimeMillis() - lastSeen) < 10000 + peers.forEach { (existingPeerID, info) -> + if (info.nickname == nickname && existingPeerID != peerID) { + val wasRecentlySeen = (now - info.lastSeen) < 10000 if (!wasRecentlySeen) { stalePeerIDs.add(existingPeerID) } @@ -206,8 +223,21 @@ class PeerManager { val isFirstAnnounce = !announcedPeers.contains(peerID) // Update peer data - peerNicknames[peerID] = nickname - activePeers[peerID] = System.currentTimeMillis() + val existing = peers[peerID] + if (existing != null) { + peers[peerID] = existing.copy(nickname = nickname, lastSeen = now, isConnected = true) + } else { + peers[peerID] = PeerInfo( + id = peerID, + nickname = nickname, + isConnected = true, + isDirectConnection = false, + noisePublicKey = null, + signingPublicKey = null, + isVerifiedNickname = false, + lastSeen = now + ) + } // Handle first announcement if (isFirstAnnounce) { @@ -223,8 +253,7 @@ class PeerManager { * Remove peer */ fun removePeer(peerID: String, notifyDelegate: Boolean = true) { - val nickname = peerNicknames.remove(peerID) - activePeers.remove(peerID) + val removed = peers.remove(peerID) peerRSSI.remove(peerID) announcedPeers.remove(peerID) announcedToPeers.remove(peerID) @@ -232,7 +261,7 @@ class PeerManager { // Also remove fingerprint mappings fingerprintManager.removePeer(peerID) - if (notifyDelegate && nickname != null) { + if (notifyDelegate && removed != null) { notifyPeerListUpdate() } } @@ -266,21 +295,23 @@ class PeerManager { * Check if peer is active */ fun isPeerActive(peerID: String): Boolean { - return activePeers.containsKey(peerID) + val info = peers[peerID] ?: return false + val now = System.currentTimeMillis() + return (now - info.lastSeen) <= STALE_PEER_TIMEOUT && info.isConnected } /** * Get peer nickname */ fun getPeerNickname(peerID: String): String? { - return peerNicknames[peerID] + return peers[peerID]?.nickname } /** * Get all peer nicknames */ fun getAllPeerNicknames(): Map { - return peerNicknames.toMap() + return peers.mapValues { it.value.nickname } } /** @@ -294,22 +325,25 @@ class PeerManager { * Get list of active peer IDs */ fun getActivePeerIDs(): List { - return activePeers.keys.toList().sorted() + val now = System.currentTimeMillis() + return peers.filterValues { (now - it.lastSeen) <= STALE_PEER_TIMEOUT && it.isConnected } + .keys + .toList() + .sorted() } /** * Get active peer count */ fun getActivePeerCount(): Int { - return activePeers.size + return getActivePeerIDs().size } /** * Clear all peer data */ fun clearAllPeers() { - peerNicknames.clear() - activePeers.clear() + peers.clear() peerRSSI.clear() announcedPeers.clear() announcedToPeers.clear() @@ -324,19 +358,19 @@ class PeerManager { * Get debug information */ fun getDebugInfo(addressPeerMap: Map? = null): String { + val now = System.currentTimeMillis() + val activeIds = getActivePeerIDs().toSet() return buildString { appendLine("=== Peer Manager Debug Info ===") - appendLine("Active Peers: ${activePeers.size}") - activePeers.forEach { (peerID, lastSeen) -> - val nickname = peerNicknames[peerID] ?: "Unknown" - val timeSince = (System.currentTimeMillis() - lastSeen) / 1000 + appendLine("Active Peers: ${activeIds.size}") + peers.forEach { (peerID, info) -> + val timeSince = (now - info.lastSeen) / 1000 val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI" - - // Find device address for this peer ID val deviceAddress = addressPeerMap?.entries?.find { it.value == peerID }?.key val addressInfo = deviceAddress?.let { " [Device: $it]" } ?: " [Device: Unknown]" - - appendLine(" - $peerID ($nickname)$addressInfo - last seen ${timeSince}s ago, RSSI: $rssi") + val status = if (activeIds.contains(peerID)) "ACTIVE" else "INACTIVE" + val direct = if (info.isDirectConnection) "DIRECT" else "ROUTED" + appendLine(" - $peerID (${info.nickname})$addressInfo - $status/$direct, last seen ${timeSince}s ago, RSSI: $rssi") } appendLine("Announced Peers: ${announcedPeers.size}") appendLine("Announced To Peers: ${announcedToPeers.size}") @@ -353,8 +387,8 @@ class PeerManager { appendLine("No device address mappings available") } else { addressPeerMap.forEach { (deviceAddress, peerID) -> - val nickname = peerNicknames[peerID] ?: "Unknown" - val isActive = activePeers.containsKey(peerID) + val nickname = peers[peerID]?.nickname ?: "Unknown" + val isActive = isPeerActive(peerID) val status = if (isActive) "ACTIVE" else "INACTIVE" appendLine(" Device: $deviceAddress -> Peer: $peerID ($nickname) [$status]") } @@ -390,9 +424,9 @@ class PeerManager { private fun cleanupStalePeers() { val now = System.currentTimeMillis() - val peersToRemove = activePeers.entries.filter { (_, lastSeen) -> - now - lastSeen > STALE_PEER_TIMEOUT - }.map { it.key } + val peersToRemove = peers.filterValues { (now - it.lastSeen) > STALE_PEER_TIMEOUT } + .keys + .toList() peersToRemove.forEach { peerID -> Log.d(TAG, "Removing stale peer: $peerID") diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt index 17462712..9b34f7fa 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt @@ -242,10 +242,9 @@ class NostrGeohashService( */ fun sendGeohashMessage(content: String, channel: com.bitchat.android.geohash.GeohashChannel, myPeerID: String, nickname: String?) { coroutineScope.launch { + // Generate a temporary message ID for tracking animation + val tempMessageId = "temp_${System.currentTimeMillis()}_${Random.nextInt(1000)}" try { - // Generate a temporary message ID for tracking animation - val tempMessageId = "temp_${System.currentTimeMillis()}_${Random.nextInt(1000)}" - // Add local echo message IMMEDIATELY (with temporary ID) val powSettingsLocal = PoWPreferenceManager.getCurrentSettings() val localMessage = BitchatMessage( @@ -289,12 +288,6 @@ class NostrGeohashService( teleported = teleported ) - // Stop animation when PoW completes - if (powSettings.enabled && powSettings.difficulty > 0) { - com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage(tempMessageId) - Log.d(TAG, "🎭 Stopped matrix animation for message: $tempMessageId") - } - val nostrRelayManager = NostrRelayManager.getInstance(application) nostrRelayManager.sendEventToGeohash( event = event, @@ -307,8 +300,8 @@ class NostrGeohashService( } catch (e: Exception) { Log.e(TAG, "Failed to send geohash message: ${e.message}") - // Make sure to stop animation even if there's an error - com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage("temp_${System.currentTimeMillis()}") + } finally { + com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage(tempMessageId) } } } @@ -1275,6 +1268,7 @@ class NostrGeohashService( // val mentions = messageManager.parseMentions(content, peerNicknames, nickname) // Calculate actual PoW difficulty from the finalized event ID so we can show it for incoming messages too + val eventHasNonseTag = event.tags.any { it.isNotEmpty() && it[0] == "nonce" } val actualPow = try { NostrProofOfWork.calculateDifficulty(event.id) } catch (e: Exception) { 0 } val message = BitchatMessage( @@ -1287,7 +1281,7 @@ class NostrGeohashService( senderPeerID = "nostr:${event.pubkey.take(8)}", mentions = null, // mentions need to be passed from outside channel = "#$geohash", - powDifficulty = actualPow.takeIf { it > 0 } + powDifficulty = actualPow.takeIf { it > 0 && eventHasNonseTag } ?: null ) // Store in geohash history for persistence across channel switches diff --git a/app/src/main/java/com/bitchat/android/nostr/PoWPreferenceManager.kt b/app/src/main/java/com/bitchat/android/nostr/PoWPreferenceManager.kt index a5300033..1bb3ff8a 100644 --- a/app/src/main/java/com/bitchat/android/nostr/PoWPreferenceManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/PoWPreferenceManager.kt @@ -16,8 +16,8 @@ object PoWPreferenceManager { private const val KEY_POW_DIFFICULTY = "pow_difficulty" // Default values - private const val DEFAULT_POW_ENABLED = true - private const val DEFAULT_POW_DIFFICULTY = 10 // Reasonable default for geohash spam prevention + private const val DEFAULT_POW_ENABLED = false + private const val DEFAULT_POW_DIFFICULTY = 16 // Reasonable default for geohash spam prevention // State flows for reactive UI private val _powEnabled = MutableStateFlow(DEFAULT_POW_ENABLED) diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 9e238d4c..50f9a5dc 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -248,8 +248,8 @@ fun AboutSheet( Slider( value = powDifficulty.toFloat(), onValueChange = { PoWPreferenceManager.setPowDifficulty(it.toInt()) }, - valueRange = 0f..20f, - steps = 21, // 20 discrete values (0-20) + valueRange = 0f..32f, + steps = 33, // 33 discrete values (0-32) colors = SliderDefaults.colors( thumbColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D), activeTrackColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index a0770218..352cd807 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -103,6 +103,10 @@ class ChatState { private val _peerRSSI = MutableLiveData>(emptyMap()) val peerRSSI: LiveData> = _peerRSSI + + // Direct connection status per peer (for live UI updates) + private val _peerDirect = MutableLiveData>(emptyMap()) + val peerDirect: LiveData> = _peerDirect // peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager @@ -276,6 +280,10 @@ class ChatState { fun setPeerRSSI(rssi: Map) { _peerRSSI.value = rssi } + + fun setPeerDirect(direct: Map) { + _peerDirect.value = direct + } fun setShowAppInfo(show: Boolean) { _showAppInfo.value = show diff --git a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt index 858f8962..bc0a14f4 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -7,6 +7,9 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Shield +import androidx.compose.ui.graphics.vector.ImageVector import com.bitchat.android.model.BitchatMessage import com.bitchat.android.mesh.BluetoothMeshService import androidx.compose.material3.ColorScheme @@ -124,11 +127,11 @@ fun formatMessageAsAnnotatedString( )) builder.append(" [${timeFormatter.format(message.timestamp)}]") // If message has valid PoW difficulty, append bits immediately after timestamp with minimal spacing - //message.powDifficulty?.let { bits -> - // if (bits > 0) { - // builder.append(" ${bits}b") - // } - //} + message.powDifficulty?.let { bits -> + if (bits > 0) { + builder.append(" ⛨${bits}b") + } + } builder.pop() } else { diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 0a0ea6a7..a9a75e44 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -106,6 +106,7 @@ class ChatViewModel( val peerFingerprints: LiveData> = state.peerFingerprints val peerNicknames: LiveData> = state.peerNicknames val peerRSSI: LiveData> = state.peerRSSI + val peerDirect: LiveData> = state.peerDirect val showAppInfo: LiveData = state.showAppInfo val selectedLocationChannel: LiveData = state.selectedLocationChannel val isTeleported: LiveData = state.isTeleported @@ -437,6 +438,14 @@ class ChatViewModel( val rssiValues = meshService.getPeerRSSI() state.setPeerRSSI(rssiValues) + + // Update directness per peer (driven by PeerManager state) + try { + val directMap = state.getConnectedPeersValue().associateWith { pid -> + meshService.getPeerInfo(pid)?.isDirectConnection == true + } + state.setPeerDirect(directMap) + } catch (_: Exception) { } } // MARK: - Debug and Troubleshooting diff --git a/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt b/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt index 59aaed06..55a3e694 100644 --- a/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt +++ b/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt @@ -128,67 +128,3 @@ enum class PoWIndicatorStyle { COMPACT, // Small icon + difficulty number DETAILED // Icon + status text + time estimate } - -/** - * Shows mining progress with animated indicator - */ -@Composable -fun PoWMiningIndicator( - modifier: Modifier = Modifier, - difficulty: Int, - iterations: Int? = null -) { - val colorScheme = MaterialTheme.colorScheme - - Surface( - modifier = modifier, - color = Color(0xFFFF9500).copy(alpha = 0.1f), - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // Animated security icon - val rotation by rememberInfiniteTransition(label = "mining-rotation").animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable( - animation = tween(1000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "mining-icon-rotation" - ) - - Icon( - imageVector = Icons.Filled.Security, - contentDescription = "Mining Proof of Work", - tint = Color(0xFFFF9500), - modifier = Modifier - .size(16.dp) - .graphicsLayer { rotationZ = rotation } - ) - - Column( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - text = "mining proof of work...", - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Medium, - color = Color(0xFFFF9500) - ) - - Text( - text = "difficulty: ${difficulty}bit (~${NostrProofOfWork.estimateMiningTime(difficulty)})" + - if (iterations != null) " • ${iterations} attempts" else "", - fontSize = 10.sp, - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.7f) - ) - } - } - } -} diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 2823896c..502e63cb 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -384,10 +384,12 @@ fun PeopleSection( val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName) val showHash = (baseNameCounts[bName] ?: 0) > 1 + val directMap by viewModel.peerDirect.observeAsState(emptyMap()) + val isDirectLive = directMap[peerID] ?: try { viewModel.meshService.getPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false } PeerItem( peerID = peerID, displayName = displayName, - signalStrength = convertRSSIToSignalStrength(peerRSSI[peerID]), + isDirect = isDirectLive, isSelected = peerID == selectedPrivatePeer, isFavorite = isFavorite, hasUnreadDM = combinedHasUnread, @@ -421,7 +423,7 @@ fun PeopleSection( PeerItem( peerID = favPeerID, displayName = dn, - signalStrength = 0, + isDirect = false, isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer, isFavorite = true, hasUnreadDM = hasUnreadPrivateMessages.contains(favPeerID), @@ -458,15 +460,15 @@ fun PeopleSection( val (bName, _) = com.bitchat.android.ui.splitSuffix(dn) val showHash = (baseNameCounts[bName] ?: 0) > 1 - PeerItem( - peerID = convKey, - displayName = dn, - signalStrength = 0, - isSelected = convKey == selectedPrivatePeer, - isFavorite = false, - hasUnreadDM = hasUnreadPrivateMessages.contains(convKey), - colorScheme = colorScheme, - viewModel = viewModel, + PeerItem( + peerID = convKey, + displayName = dn, + isDirect = false, + isSelected = convKey == selectedPrivatePeer, + isFavorite = false, + hasUnreadDM = hasUnreadPrivateMessages.contains(convKey), + colorScheme = colorScheme, + viewModel = viewModel, onItemClick = { onPrivateChatStart(convKey) }, onToggleFavorite = { viewModel.toggleFavorite(convKey) }, unreadCount = privateChats[convKey]?.count { msg -> @@ -483,7 +485,7 @@ fun PeopleSection( private fun PeerItem( peerID: String, displayName: String, - signalStrength: Int, + isDirect: Boolean, isSelected: Boolean, isFavorite: Boolean, hasUnreadDM: Boolean, @@ -527,7 +529,7 @@ private fun PeerItem( tint = Color(0xFFFF9500) // iOS orange ) } else { - // Signal strength indicators + // Connection indicator icons if (showNostrGlobe) { // Purple globe to indicate Nostr availability Icon( @@ -537,9 +539,11 @@ private fun PeerItem( tint = Color(0xFF9C27B0) // Purple ) } else { - SignalStrengthIndicator( - signalStrength = signalStrength, - colorScheme = colorScheme + Icon( + imageVector = if (isDirect) Icons.Outlined.SettingsInputAntenna else Icons.Filled.Route, + contentDescription = if (isDirect) "Direct Bluetooth" else "Routed", + modifier = Modifier.size(16.dp), + tint = colorScheme.onSurface.copy(alpha = 0.8f) ) } }