mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
Refactors cleanup (#372)
* cleanup peermanager * cleanup geohash code * direct connections fix * pow display * track disconnects too * display pow only if enabled * display pow only if enabled * direct connection tracking
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>()
|
||||
private val announcedToPeers = CopyOnWriteArrayList<String>()
|
||||
|
||||
// Legacy support for existing code
|
||||
@Deprecated("Use PeerInfo structure instead")
|
||||
private val peerNicknames = ConcurrentHashMap<String, String>()
|
||||
@Deprecated("Use PeerInfo structure instead")
|
||||
private val activePeers = ConcurrentHashMap<String, Long>() // 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<String>()
|
||||
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<String, String> {
|
||||
return peerNicknames.toMap()
|
||||
return peers.mapValues { it.value.nickname }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,22 +325,25 @@ class PeerManager {
|
||||
* Get list of active peer IDs
|
||||
*/
|
||||
fun getActivePeerIDs(): List<String> {
|
||||
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<String, String>? = 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -103,6 +103,10 @@ class ChatState {
|
||||
|
||||
private val _peerRSSI = MutableLiveData<Map<String, Int>>(emptyMap())
|
||||
val peerRSSI: LiveData<Map<String, Int>> = _peerRSSI
|
||||
|
||||
// Direct connection status per peer (for live UI updates)
|
||||
private val _peerDirect = MutableLiveData<Map<String, Boolean>>(emptyMap())
|
||||
val peerDirect: LiveData<Map<String, Boolean>> = _peerDirect
|
||||
|
||||
// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager
|
||||
|
||||
@@ -276,6 +280,10 @@ class ChatState {
|
||||
fun setPeerRSSI(rssi: Map<String, Int>) {
|
||||
_peerRSSI.value = rssi
|
||||
}
|
||||
|
||||
fun setPeerDirect(direct: Map<String, Boolean>) {
|
||||
_peerDirect.value = direct
|
||||
}
|
||||
|
||||
fun setShowAppInfo(show: Boolean) {
|
||||
_showAppInfo.value = show
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -106,6 +106,7 @@ class ChatViewModel(
|
||||
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
|
||||
val peerNicknames: LiveData<Map<String, String>> = state.peerNicknames
|
||||
val peerRSSI: LiveData<Map<String, Int>> = state.peerRSSI
|
||||
val peerDirect: LiveData<Map<String, Boolean>> = state.peerDirect
|
||||
val showAppInfo: LiveData<Boolean> = state.showAppInfo
|
||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
val isTeleported: LiveData<Boolean> = 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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user