This commit is contained in:
CC
2026-06-09 14:25:23 +02:00
parent b22629faa4
commit ff740229dc
31 changed files with 837 additions and 394 deletions
@@ -444,7 +444,7 @@ private fun MainHeader(
)
Spacer(modifier = Modifier.width(2.dp))
PeerCounter(
connectedPeers = connectedPeers.filter { it != viewModel.meshService.myPeerID },
connectedPeers = connectedPeers.filter { it != viewModel.myPeerID },
joinedChannels = joinedChannels,
hasUnreadChannels = hasUnreadChannels,
isConnected = isConnected,
@@ -133,7 +133,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
MessagesList(
messages = displayMessages,
currentUserNickname = nickname,
meshService = viewModel.meshService,
meshService = viewModel.meshServiceFacade,
modifier = Modifier.weight(1f),
forceScrollToBottom = forceScrollToBottom,
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
@@ -11,7 +11,7 @@ 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 com.bitchat.android.mesh.MeshService
import androidx.compose.material3.ColorScheme
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import java.text.SimpleDateFormat
@@ -42,7 +42,7 @@ fun getRSSIColor(rssi: Int): Color {
fun formatMessageAsAnnotatedString(
message: BitchatMessage,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
): AnnotatedString {
@@ -162,7 +162,7 @@ fun formatMessageAsAnnotatedString(
fun formatMessageHeaderAnnotatedString(
message: BitchatMessage,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
): AnnotatedString {
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.service.MeshServiceHolder
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
@@ -38,12 +39,16 @@ import java.security.MessageDigest
*/
class ChatViewModel(
application: Application,
initialMeshService: BluetoothMeshService
initialMeshService: BluetoothMeshService,
initialUnifiedMeshService: MeshService
) : AndroidViewModel(application), BluetoothMeshDelegate {
// Made var to support mesh service replacement after panic clear
var meshService: BluetoothMeshService = initialMeshService
private set
private var unifiedMeshService: MeshService = initialUnifiedMeshService
private val mesh: MeshService
get() = unifiedMeshService
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
companion object {
@@ -95,7 +100,7 @@ class ChatViewModel(
private val noiseSessionDelegate = object : NoiseSessionDelegate {
override fun hasEstablishedSession(peerID: String): Boolean = hasEstablishedSessionOnAnyLocalTransport(peerID)
override fun initiateHandshake(peerID: String) = initiateNoiseHandshakeOnBestLocalTransport(peerID)
override fun getMyPeerID(): String = meshService.myPeerID
override fun getMyPeerID(): String = mesh.myPeerID
}
val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
@@ -109,7 +114,7 @@ class ChatViewModel(
private val verificationHandler = VerificationHandler(
context = application.applicationContext,
scope = viewModelScope,
getMeshService = { meshService },
getMeshService = { mesh },
identityManager = identityManager,
state = state,
notificationManager = notificationManager,
@@ -118,7 +123,7 @@ class ChatViewModel(
val verifiedFingerprints = verificationHandler.verifiedFingerprints
// Media file sending manager
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager) { meshService }
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager) { mesh }
// Delegate handler for mesh callbacks
private val meshDelegateHandler = MeshDelegateHandler(
@@ -129,8 +134,8 @@ class ChatViewModel(
notificationManager = notificationManager,
coroutineScope = viewModelScope,
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) },
getMyPeerID = { meshService.myPeerID },
getMeshService = { meshService }
getMyPeerID = { mesh.myPeerID },
getMeshService = { mesh }
)
// New Geohash architecture ViewModel (replaces God object service usage in UI path)
@@ -184,6 +189,18 @@ class ChatViewModel(
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
val teleportedGeo: StateFlow<Set<String>> = state.teleportedGeo
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
val meshServiceFacade: MeshService
get() = mesh
val myPeerID: String
get() = mesh.myPeerID
fun getMeshPeerFingerprint(peerID: String): String? = mesh.getPeerFingerprint(peerID)
fun getMeshPeerInfo(peerID: String): com.bitchat.android.mesh.PeerInfo? = mesh.getPeerInfo(peerID)
fun initiateMeshHandshake(peerID: String) {
mesh.initiateNoiseHandshake(peerID)
}
init {
// Note: Mesh service delegate is now set by MainActivity
@@ -208,7 +225,7 @@ class ChatViewModel(
// Recompute unread set using SeenMessageStore for robustness across Activity recreation
try {
val seen = com.bitchat.android.services.SeenMessageStore.getInstance(getApplication())
val myNick = state.getNicknameValue() ?: meshService.myPeerID
val myNick = state.getNicknameValue() ?: mesh.myPeerID
val unread = mutableSetOf<String>()
byPeer.forEach { (peer, list) ->
if (list.any { msg -> msg.sender != myNick && !seen.hasRead(msg.id) }) unread.add(peer)
@@ -299,7 +316,7 @@ class ChatViewModel(
// Ensure NostrTransport knows our mesh peer ID for embedded packets
try {
val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication())
nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.senderPeerID = mesh.myPeerID
} catch (_: Exception) { }
// Note: Mesh service is now started by MainActivity
@@ -317,7 +334,7 @@ class ChatViewModel(
fun setNickname(newNickname: String) {
state.setNickname(newNickname)
dataManager.saveNickname(newNickname)
meshService.sendBroadcastAnnounce()
mesh.sendBroadcastAnnounce()
}
/**
@@ -363,7 +380,7 @@ class ChatViewModel(
// MARK: - Channel Management (delegated)
fun joinChannel(channel: String, password: String? = null): Boolean {
return channelManager.joinChannel(channel, password, meshService.myPeerID)
return channelManager.joinChannel(channel, password, mesh.myPeerID)
}
fun switchToChannel(channel: String?) {
@@ -372,7 +389,7 @@ class ChatViewModel(
fun leaveChannel(channel: String) {
channelManager.leaveChannel(channel)
meshService.sendMessage("left $channel", emptyList(), null)
mesh.sendMessage("left $channel", emptyList(), null)
}
// MARK: - Private Chat Management (delegated)
@@ -383,7 +400,7 @@ class ChatViewModel(
ensureGeohashDMSubscriptionIfNeeded(peerID)
}
val success = privateChatManager.startPrivateChat(peerID, meshService)
val success = privateChatManager.startPrivateChat(peerID, mesh)
if (success) {
// Notify notification manager about current private chat
setCurrentPrivateChatPeer(peerID)
@@ -420,7 +437,7 @@ class ChatViewModel(
val unreadKeys = state.getUnreadPrivateMessagesValue()
if (unreadKeys.isEmpty()) return
val me = state.getNicknameValue() ?: meshService.myPeerID
val me = state.getNicknameValue() ?: mesh.myPeerID
val chats = state.getPrivateChatsValue()
// Pick the latest incoming message among unread conversations
@@ -451,8 +468,8 @@ class ChatViewModel(
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
selectedPeerID = targetKey,
connectedPeers = state.getConnectedPeersValue(),
meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
meshNoiseKeyForPeer = { pid -> mesh.getPeerInfo(pid)?.noisePublicKey },
meshHasPeer = { pid -> mesh.getPeerInfo(pid)?.isConnected == true },
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
)
@@ -476,25 +493,23 @@ class ChatViewModel(
// Check for commands
if (content.startsWith("/")) {
val selectedLocationForCommand = state.selectedLocationChannel.value
commandProcessor.processCommand(content, meshService, meshService.myPeerID, { messageContent, mentions, channel ->
commandProcessor.processCommand(content, mesh, mesh.myPeerID, { messageContent, mentions, channel ->
if (selectedLocationForCommand is com.bitchat.android.geohash.ChannelID.Location) {
// Route command-generated public messages via Nostr in geohash channels
geohashViewModel.sendGeohashMessage(
messageContent,
selectedLocationForCommand.channel,
meshService.myPeerID,
mesh.myPeerID,
state.getNicknameValue()
)
} else {
// Default: route via mesh + WiFi Aware
meshService.sendMessage(messageContent, mentions, channel)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
mesh.sendMessage(messageContent, mentions, channel)
}
}, this)
return
}
val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue())
val mentions = messageManager.parseMentions(content, mesh.getPeerNicknames().values.toSet(), state.getNicknameValue())
// REMOVED: Auto-join mentioned channels feature that was incorrectly parsing hashtags from @mentions
// This was causing messages like "test @jack#1234 test" to auto-join channel "#1234"
@@ -506,13 +521,13 @@ class ChatViewModel(
selectedPeer = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
selectedPeerID = selectedPeer,
connectedPeers = state.getConnectedPeersValue(),
meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
meshNoiseKeyForPeer = { pid -> mesh.getPeerInfo(pid)?.noisePublicKey },
meshHasPeer = { pid -> mesh.getPeerInfo(pid)?.isConnected == true },
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
).also { canonical ->
if (canonical != state.getSelectedPrivateChatPeerValue()) {
privateChatManager.startPrivateChat(canonical, meshService)
privateChatManager.startPrivateChat(canonical, mesh)
// If we're in the private chat sheet, update its active peer too
if (state.getPrivateChatSheetPeerValue() != null) {
showPrivateChatSheet(canonical)
@@ -526,10 +541,10 @@ class ChatViewModel(
selectedPeer,
recipientNickname,
state.getNicknameValue(),
meshService.myPeerID
mesh.myPeerID
) { messageContent, peerID, recipientNicknameParam, messageId ->
// Route via MessageRouter (mesh when connected+established, else Nostr)
val router = com.bitchat.android.services.MessageRouter.getInstance(getApplication(), meshService)
val router = com.bitchat.android.services.MessageRouter.getInstance(getApplication(), mesh)
router.sendPrivate(messageContent, peerID, recipientNicknameParam, messageId)
}
} else {
@@ -537,21 +552,21 @@ class ChatViewModel(
val selectedLocationChannel = state.selectedLocationChannel.value
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
// Send to geohash channel via Nostr ephemeral event
geohashViewModel.sendGeohashMessage(content, selectedLocationChannel.channel, meshService.myPeerID, state.getNicknameValue())
geohashViewModel.sendGeohashMessage(content, selectedLocationChannel.channel, mesh.myPeerID, state.getNicknameValue())
} else {
// Send public/channel message via mesh
val message = BitchatMessage(
sender = state.getNicknameValue() ?: meshService.myPeerID,
sender = state.getNicknameValue() ?: mesh.myPeerID,
content = content,
timestamp = Date(),
isRelay = false,
senderPeerID = meshService.myPeerID,
senderPeerID = mesh.myPeerID,
mentions = if (mentions.isNotEmpty()) mentions else null,
channel = currentChannelValue
)
if (currentChannelValue != null) {
channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID)
channelManager.addChannelMessage(currentChannelValue, message, mesh.myPeerID)
// Check if encrypted channel
if (channelManager.hasChannelKey(currentChannelValue)) {
@@ -560,25 +575,20 @@ class ChatViewModel(
mentions,
currentChannelValue,
state.getNicknameValue(),
meshService.myPeerID,
mesh.myPeerID,
onEncryptedPayload = { encryptedData ->
// Send encrypted payload announcement over both transports for reachability
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
mesh.sendMessage(content, mentions, currentChannelValue)
},
onFallback = {
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
mesh.sendMessage(content, mentions, currentChannelValue)
}
)
} else {
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
mesh.sendMessage(content, mentions, currentChannelValue)
}
} else {
messageManager.addMessage(message)
meshService.sendMessage(content, mentions, null)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, null) } catch (_: Exception) {}
mesh.sendMessage(content, mentions, null)
}
}
}
@@ -587,7 +597,7 @@ class ChatViewModel(
// MARK: - Utility Functions
fun getPeerIDForNickname(nickname: String): String? {
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
return mesh.getPeerNicknames().entries.find { it.value == nickname }?.key
}
fun toggleFavorite(peerID: String) {
@@ -597,10 +607,10 @@ class ChatViewModel(
// Persist relationship in FavoritesPersistenceService
try {
var noiseKey: ByteArray? = null
var nickname: String = meshService.getPeerNicknames()[peerID] ?: peerID
var nickname: String = mesh.getPeerNicknames()[peerID] ?: peerID
// Case 1: Live mesh peer with known info
val peerInfo = meshService.getPeerInfo(peerID)
val peerInfo = mesh.getPeerInfo(peerID)
if (peerInfo?.noisePublicKey != null) {
noiseKey = peerInfo.noisePublicKey
nickname = peerInfo.nickname
@@ -630,22 +640,9 @@ class ChatViewModel(
// Send favorite notification via mesh or Nostr with our npub if available
try {
val myNostr = com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(getApplication())
val announcementContent = if (isNowFavorite) "[FAVORITED]:${myNostr?.npub ?: ""}" else "[UNFAVORITED]:${myNostr?.npub ?: ""}"
// Prefer mesh if session established, else try Nostr
if (meshService.hasEstablishedSession(peerID)) {
// Reuse existing private message path for notifications
meshService.sendPrivateMessage(
announcementContent,
peerID,
nickname,
java.util.UUID.randomUUID().toString()
)
} else {
val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication())
nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendFavoriteNotification(peerID, isNowFavorite)
}
com.bitchat.android.services.MessageRouter
.getInstance(getApplication(), mesh)
.sendFavoriteNotification(peerID, isNowFavorite)
} catch (_: Exception) { }
}
} catch (_: Exception) { }
@@ -662,21 +659,9 @@ class ChatViewModel(
Log.i("ChatViewModel", "==============================")
}
private fun getWifiAwareService(): com.bitchat.android.mesh.MeshService? {
return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
}
private fun isConnectedOnMesh(peerID: String): Boolean {
return try {
meshService.getPeerInfo(peerID)?.isConnected == true
} catch (_: Exception) {
false
}
}
private fun isConnectedOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
return try {
service != null && service.getPeerInfo(peerID)?.isConnected == true
mesh.getPeerInfo(peerID)?.isConnected == true
} catch (_: Exception) {
false
}
@@ -684,55 +669,28 @@ class ChatViewModel(
private fun hasEstablishedSessionOnMesh(peerID: String): Boolean {
return try {
meshService.getPeerInfo(peerID)?.isConnected == true &&
meshService.hasEstablishedSession(peerID)
} catch (_: Exception) {
false
}
}
private fun hasEstablishedSessionOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
return try {
service != null &&
service.getPeerInfo(peerID)?.isConnected == true &&
service.hasEstablishedSession(peerID)
mesh.getPeerInfo(peerID)?.isConnected == true &&
mesh.hasEstablishedSession(peerID)
} catch (_: Exception) {
false
}
}
private fun hasEstablishedSessionOnAnyLocalTransport(peerID: String): Boolean {
return hasEstablishedSessionOnMesh(peerID) ||
hasEstablishedSessionOnService(getWifiAwareService(), peerID)
return hasEstablishedSessionOnMesh(peerID)
}
private fun initiateNoiseHandshakeOnBestLocalTransport(peerID: String) {
val aware = getWifiAwareService()
when {
isConnectedOnMesh(peerID) -> meshService.initiateNoiseHandshake(peerID)
isConnectedOnService(aware, peerID) -> aware?.initiateNoiseHandshake(peerID)
else -> meshService.initiateNoiseHandshake(peerID)
}
mesh.initiateNoiseHandshake(peerID)
}
private fun nicknameForPeer(peerID: String): String? {
return state.peerNicknames.value[peerID]
?: try { meshService.getPeerNicknames()[peerID] } catch (_: Exception) { null }
?: try { getWifiAwareService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
?: try { mesh.getPeerNicknames()[peerID] } catch (_: Exception) { null }
}
private fun sessionStateForPeer(peerID: String): NoiseSession.NoiseSessionState {
val meshState = try { meshService.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
val awareState = try { getWifiAwareService()?.getSessionState(peerID) } catch (_: Exception) { null }
return when {
meshState is NoiseSession.NoiseSessionState.Established -> meshState
awareState is NoiseSession.NoiseSessionState.Established -> awareState
meshState is NoiseSession.NoiseSessionState.Handshaking -> meshState
awareState is NoiseSession.NoiseSessionState.Handshaking -> awareState
meshState !is NoiseSession.NoiseSessionState.Uninitialized -> meshState
awareState != null -> awareState
else -> meshState
}
return try { mesh.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
}
/**
@@ -766,7 +724,7 @@ class ChatViewModel(
val old = prevStates[peerID]
if (old != "established" && newState == "established") {
com.bitchat.android.services.MessageRouter
.getInstance(getApplication(), meshService)
.getInstance(getApplication(), mesh)
.onSessionEstablished(peerID)
}
}
@@ -775,7 +733,7 @@ class ChatViewModel(
state.setPeerFingerprints(fingerprints)
fingerprints.forEach { (peerID, fingerprint) ->
identityManager.cachePeerFingerprint(peerID, fingerprint)
val info = try { meshService.getPeerInfo(peerID) } catch (_: Exception) { null }
val info = try { mesh.getPeerInfo(peerID) } catch (_: Exception) { null }
val noiseKeyHex = info?.noisePublicKey?.hexEncodedString()
if (noiseKeyHex != null) {
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
@@ -786,23 +744,14 @@ class ChatViewModel(
}
}
// Merge nicknames from BLE and WiFi Aware to display names for all peers
val bleNick = meshService.getPeerNicknames()
val awareNickRaw = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknamesMap() } catch (_: Exception) { null }
val mergedNick = if (awareNickRaw != null) bleNick + awareNickRaw.filter { it.value != null }.mapValues { it.value!! }.filterKeys { it !in bleNick || bleNick[it].isNullOrBlank() } else bleNick
state.setPeerNicknames(mergedNick)
state.setPeerNicknames(mesh.getPeerNicknames())
val rssiValues = meshService.getPeerRSSI()
val awareRssi = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerRSSI() } catch (_: Exception) { null }
val mergedRssi = if (awareRssi != null) rssiValues + awareRssi.filterKeys { it !in rssiValues } else rssiValues
state.setPeerRSSI(mergedRssi)
state.setPeerRSSI(mesh.getPeerRSSI())
// Update directness per peer (driven by PeerManager state)
try {
val directMap = state.getConnectedPeersValue().associateWith { pid ->
val ble = meshService.getPeerInfo(pid)?.isDirectConnection == true
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerInfo(pid)?.isDirectConnection == true } catch (_: Exception) { false }
ble || aware
mesh.getPeerInfo(pid)?.isDirectConnection == true
}
state.setPeerDirect(directMap)
} catch (_: Exception) { }
@@ -839,7 +788,7 @@ class ChatViewModel(
// MARK: - Debug and Troubleshooting
fun getDebugStatus(): String {
return meshService.getDebugStatus()
return mesh.getDebugStatus()
}
fun setCurrentPrivateChatPeer(peerID: String?) {
@@ -936,7 +885,7 @@ class ChatViewModel(
// MARK: - Mention Autocomplete
fun updateMentionSuggestions(input: String) {
commandProcessor.updateMentionSuggestions(input, meshService, this)
commandProcessor.updateMentionSuggestions(input, mesh, this)
}
fun selectMentionSuggestion(nickname: String, currentText: String): String {
@@ -953,10 +902,6 @@ class ChatViewModel(
meshDelegateHandler.didUpdatePeerList(peers)
}
fun onWifiPeersUpdated(peers: List<String>) {
meshDelegateHandler.onWifiPeersUpdated(peers)
}
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
}
@@ -1040,7 +985,7 @@ class ChatViewModel(
// Recreate mesh service with fresh identity
recreateMeshServiceAfterPanic()
Log.w(TAG, "🚨 PANIC MODE COMPLETED - New identity: ${meshService.myPeerID}")
Log.w(TAG, "🚨 PANIC MODE COMPLETED - New identity: ${mesh.myPeerID}")
}
/**
@@ -1048,25 +993,27 @@ class ChatViewModel(
* This ensures the new cryptographic keys are used for a new peer ID.
*/
private fun recreateMeshServiceAfterPanic() {
val oldPeerID = meshService.myPeerID
val oldPeerID = mesh.myPeerID
// Clear the holder so getOrCreate() returns a fresh instance
MeshServiceHolder.clear()
// Create fresh mesh service with new identity (keys were regenerated in clearAllCryptographicData)
val freshMeshService = MeshServiceHolder.getOrCreate(getApplication())
val freshUnifiedMeshService = MeshServiceHolder.getUnifiedOrCreate(getApplication())
// Replace our reference and set up the new service
meshService = freshMeshService
meshService.delegate = this
unifiedMeshService = freshUnifiedMeshService
mesh.delegate = this
// Restart mesh operations with new identity
meshService.startServices()
meshService.sendBroadcastAnnounce()
mesh.startServices()
mesh.sendBroadcastAnnounce()
Log.d(
TAG,
"✅ Mesh service recreated. Old peerID: $oldPeerID, New peerID: ${meshService.myPeerID}"
"✅ Mesh service recreated. Old peerID: $oldPeerID, New peerID: ${mesh.myPeerID}"
)
}
@@ -1076,7 +1023,7 @@ class ChatViewModel(
private fun clearAllMeshServiceData() {
try {
// Request mesh service to clear all its internal data
meshService.clearAllInternalData()
mesh.clearAllInternalData()
Log.d(TAG, "✅ Cleared all mesh service data")
} catch (e: Exception) {
@@ -1090,7 +1037,7 @@ class ChatViewModel(
private fun clearAllCryptographicData() {
try {
// Clear encryption service persistent identity (Ed25519 signing keys)
meshService.clearAllEncryptionData()
mesh.clearAllEncryptionData()
// Clear secure identity state (if used)
try {
@@ -1,6 +1,6 @@
package com.bitchat.android.ui
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import java.util.Date
@@ -29,7 +29,7 @@ class CommandProcessor(
// MARK: - Command Processing
fun processCommand(command: String, meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit, viewModel: ChatViewModel? = null): Boolean {
fun processCommand(command: String, meshService: MeshService, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit, viewModel: ChatViewModel? = null): Boolean {
if (!command.startsWith("/")) return false
val parts = command.split(" ")
@@ -77,7 +77,7 @@ class CommandProcessor(
}
}
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService, viewModel: ChatViewModel?) {
private fun handleMessageCommand(parts: List<String>, meshService: MeshService, viewModel: ChatViewModel?) {
if (parts.size > 1) {
val targetName = parts[1].removePrefix("@")
val peerID = getPeerIDForNickname(targetName, meshService)
@@ -128,7 +128,7 @@ class CommandProcessor(
}
}
private fun handleWhoCommand(meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) {
private fun handleWhoCommand(meshService: MeshService, viewModel: ChatViewModel? = null) {
// Channel-aware who command (matches iOS behavior)
val (peerList, contextDescription) = if (viewModel != null) {
when (val selectedChannel = viewModel.selectedLocationChannel.value) {
@@ -247,7 +247,7 @@ class CommandProcessor(
}
}
private fun handleBlockCommand(parts: List<String>, meshService: BluetoothMeshService) {
private fun handleBlockCommand(parts: List<String>, meshService: MeshService) {
if (parts.size > 1) {
val targetName = parts[1].removePrefix("@")
privateChatManager.blockPeerByNickname(targetName, meshService)
@@ -264,7 +264,7 @@ class CommandProcessor(
}
}
private fun handleUnblockCommand(parts: List<String>, meshService: BluetoothMeshService) {
private fun handleUnblockCommand(parts: List<String>, meshService: MeshService) {
if (parts.size > 1) {
val targetName = parts[1].removePrefix("@")
privateChatManager.unblockPeerByNickname(targetName, meshService)
@@ -283,7 +283,7 @@ class CommandProcessor(
parts: List<String>,
verb: String,
object_: String,
meshService: BluetoothMeshService,
meshService: MeshService,
myPeerID: String,
onSendMessage: (String, List<String>, String?) -> Unit,
viewModel: ChatViewModel?
@@ -423,7 +423,7 @@ class CommandProcessor(
// MARK: - Mention Autocomplete
fun updateMentionSuggestions(input: String, meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) {
fun updateMentionSuggestions(input: String, meshService: MeshService, viewModel: ChatViewModel? = null) {
// Check if input contains @ and we're at the end of a word or at the end of input
val atIndex = input.lastIndexOf('@')
if (atIndex == -1) {
@@ -503,31 +503,21 @@ class CommandProcessor(
// MARK: - Utility Functions
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
private fun getPeerIDForNickname(nickname: String, meshService: MeshService): String? {
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
?: try {
com.bitchat.android.wifiaware.WifiAwareController.getService()
?.getPeerNicknames()
?.entries
?.find { it.value == nickname }
?.key
} catch (_: Exception) {
null
}
}
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
private fun getPeerNickname(peerID: String, meshService: MeshService): String {
return meshService.getPeerNicknames()[peerID]
?: try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
?: peerID
}
private fun getMyPeerID(meshService: BluetoothMeshService): String {
private fun getMyPeerID(meshService: MeshService): String {
return meshService.myPeerID
}
private fun sendPrivateMessageVia(
meshService: BluetoothMeshService,
meshService: MeshService,
content: String,
peerID: String,
recipientNickname: String,
@@ -656,7 +656,7 @@ private fun meshTitleWithCount(viewModel: ChatViewModel): String {
}
private fun meshCount(viewModel: ChatViewModel): Int {
val myID = viewModel.meshService.myPeerID
val myID = viewModel.myPeerID
return viewModel.connectedPeers.value?.count { peerID ->
peerID != myID
} ?: 0
@@ -77,7 +77,7 @@ fun MessageWithMatrixAnimation(
message: com.bitchat.android.model.BitchatMessage,
messages: List<com.bitchat.android.model.BitchatMessage> = emptyList(),
currentUserNickname: String,
meshService: com.bitchat.android.mesh.BluetoothMeshService,
meshService: com.bitchat.android.mesh.MeshService,
colorScheme: androidx.compose.material3.ColorScheme,
timeFormatter: java.text.SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
@@ -124,7 +124,7 @@ fun MessageWithMatrixAnimation(
private fun AnimatedMessageDisplay(
message: com.bitchat.android.model.BitchatMessage,
currentUserNickname: String,
meshService: com.bitchat.android.mesh.BluetoothMeshService,
meshService: com.bitchat.android.mesh.MeshService,
colorScheme: androidx.compose.material3.ColorScheme,
timeFormatter: java.text.SimpleDateFormat,
modifier: Modifier = Modifier
@@ -241,7 +241,7 @@ private fun AnimatedMessageDisplay(
private fun formatMessageAsAnnotatedStringWithoutTimestamp(
message: com.bitchat.android.model.BitchatMessage,
currentUserNickname: String,
meshService: com.bitchat.android.mesh.BluetoothMeshService,
meshService: com.bitchat.android.mesh.MeshService,
colorScheme: androidx.compose.material3.ColorScheme
): AnnotatedString {
// Get the full formatted text first
@@ -1,7 +1,7 @@
package com.bitchat.android.ui
import android.util.Log
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatFilePacket
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
@@ -16,10 +16,10 @@ class MediaSendingManager(
private val state: ChatState,
private val messageManager: MessageManager,
private val channelManager: ChannelManager,
private val getMeshService: () -> BluetoothMeshService
private val getMeshService: () -> MeshService
) {
// Helper to get current mesh service (may change after panic clear)
private val meshService: BluetoothMeshService
private val meshService: MeshService
get() = getMeshService()
companion object {
private const val TAG = "MediaSendingManager"
@@ -213,7 +213,6 @@ class MediaSendingManager(
Log.d(TAG, "📤 Calling meshService.sendFilePrivate to $toPeerID")
meshService.sendFilePrivate(toPeerID, filePacket)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFilePrivate(toPeerID, filePacket) } catch (_: Exception) {}
Log.d(TAG, "✅ File send completed successfully")
}
@@ -268,7 +267,6 @@ class MediaSendingManager(
Log.d(TAG, "📤 Calling meshService.sendFileBroadcast")
meshService.sendFileBroadcast(filePacket)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFileBroadcast(filePacket) } catch (_: Exception) {}
Log.d(TAG, "✅ File broadcast completed successfully")
}
@@ -2,7 +2,7 @@ package com.bitchat.android.ui
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.ui.NotificationTextUtils
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import kotlinx.coroutines.CoroutineScope
@@ -21,7 +21,7 @@ class MeshDelegateHandler(
private val coroutineScope: CoroutineScope,
private val onHapticFeedback: () -> Unit,
private val getMyPeerID: () -> String,
private val getMeshService: () -> BluetoothMeshService
private val getMeshService: () -> MeshService
) : BluetoothMeshDelegate {
override fun didReceiveMessage(message: BitchatMessage) {
@@ -94,30 +94,13 @@ class MeshDelegateHandler(
}
}
private var blePeers: Set<String> = emptySet()
private var wifiPeers: Set<String> = emptySet()
override fun didUpdatePeerList(peers: List<String>) {
coroutineScope.launch {
blePeers = peers.toSet()
try { com.bitchat.android.services.AppStateStore.setTransportPeers("BLE", peers) } catch (_: Exception) { }
processPeerUpdate()
processPeerUpdate(peers.distinct())
}
}
fun onWifiPeersUpdated(peers: List<String>) {
coroutineScope.launch {
wifiPeers = peers.toSet()
try { com.bitchat.android.services.AppStateStore.setTransportPeers("WIFI", peers) } catch (_: Exception) { }
processPeerUpdate()
}
}
private suspend fun processPeerUpdate() {
// Merge peers from multiple transports
val mergedPeers = com.bitchat.android.services.AppStateStore.peers.value
.ifEmpty { (blePeers + wifiPeers).toList() }
private suspend fun processPeerUpdate(mergedPeers: List<String>) {
state.setConnectedPeers(mergedPeers)
state.setIsConnected(mergedPeers.isNotEmpty())
notificationManager.showActiveUserNotification(mergedPeers)
@@ -323,14 +306,7 @@ class MeshDelegateHandler(
mesh.sendReadReceipt(message.id, senderPeerID, nickname)
true
} else {
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
val hasAware = try { aware?.getPeerInfo(senderPeerID)?.isConnected == true && aware.hasEstablishedSession(senderPeerID) } catch (_: Exception) { false }
if (hasAware) {
aware?.sendReadReceipt(message.id, senderPeerID, nickname)
true
} else {
false
}
false
}
} catch (_: Exception) {
false
@@ -337,7 +337,7 @@ fun PeopleSection(
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
try {
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
viewModel.getMeshPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
} catch (_: Exception) { null }
}.filterValues { it != null }.mapValues { it.value!! }
@@ -415,7 +415,7 @@ fun PeopleSection(
val showHash = (baseNameCounts[bName] ?: 0) > 1
val directMap by viewModel.peerDirect.collectAsStateWithLifecycle()
val isDirectLive = directMap[peerID] ?: try { viewModel.meshService.getPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
val isDirectLive = directMap[peerID] ?: try { viewModel.getMeshPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
PeerItem(
peerID = peerID,
displayName = displayName,
@@ -787,6 +787,8 @@ fun PrivateChatSheet(
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
val wifiAwareConnected by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsStateWithLifecycle()
val isWifiAware = peerID in wifiAwareConnected.keys
// Start private chat when screen opens
LaunchedEffect(peerID) {
@@ -855,7 +857,7 @@ fun PrivateChatSheet(
MessagesList(
messages = messages,
currentUserNickname = nickname,
meshService = viewModel.meshService,
meshService = viewModel.meshServiceFacade,
modifier = Modifier.weight(1f),
forceScrollToBottom = forceScrollToBottom,
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
@@ -939,10 +941,26 @@ fun PrivateChatSheet(
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
when {
isNostrPeer -> {
Icon(
imageVector = Icons.Filled.Public,
contentDescription = stringResource(R.string.cd_nostr_reachable),
modifier = Modifier.size(14.dp),
tint = Color(0xFF9C27B0)
)
}
isWifiAware -> {
Icon(
imageVector = Icons.Filled.Wifi,
contentDescription = "Direct Wi-Fi Aware",
modifier = Modifier.size(14.dp),
tint = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
isDirect -> {
Icon(
imageVector = Icons.Outlined.SettingsInputAntenna,
contentDescription = stringResource(R.string.cd_connected_peers),
imageVector = Icons.Outlined.Bluetooth,
contentDescription = "Direct Bluetooth",
modifier = Modifier.size(14.dp),
tint = colorScheme.onSurface.copy(alpha = 0.6f)
)
@@ -950,19 +968,11 @@ fun PrivateChatSheet(
isConnected -> {
Icon(
imageVector = Icons.Filled.Route,
contentDescription = stringResource(R.string.cd_ready_for_handshake),
contentDescription = "Routed",
modifier = Modifier.size(14.dp),
tint = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
isNostrPeer -> {
Icon(
imageVector = Icons.Filled.Public,
contentDescription = stringResource(R.string.cd_nostr_reachable),
modifier = Modifier.size(14.dp),
tint = Color(0xFF9C27B0)
)
}
}
Text(
@@ -31,7 +31,7 @@ import android.content.Intent
import android.net.Uri
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import java.text.SimpleDateFormat
import java.util.*
import com.bitchat.android.ui.media.VoiceNotePlayer
@@ -58,7 +58,7 @@ import androidx.compose.ui.res.stringResource
fun MessagesList(
messages: List<BitchatMessage>,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
modifier: Modifier = Modifier,
forceScrollToBottom: Boolean = false,
onScrolledUpChanged: ((Boolean) -> Unit)? = null,
@@ -137,7 +137,7 @@ fun MessagesList(
fun MessageItem(
message: BitchatMessage,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
messages: List<BitchatMessage> = emptyList(),
onNicknameClick: ((String) -> Unit)? = null,
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
@@ -201,7 +201,7 @@ fun MessageItem(
message: BitchatMessage,
messages: List<BitchatMessage>,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
@@ -3,9 +3,9 @@ package com.bitchat.android.ui
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.mesh.PeerFingerprintManager
import com.bitchat.android.mesh.MeshService
import java.security.MessageDigest
import com.bitchat.android.mesh.BluetoothMeshService
import java.util.*
import android.util.Log
@@ -42,7 +42,7 @@ class PrivateChatManager(
// MARK: - Private Chat Lifecycle
fun startPrivateChat(peerID: String, meshService: BluetoothMeshService): Boolean {
fun startPrivateChat(peerID: String, meshService: MeshService): Boolean {
if (isPeerBlocked(peerID)) {
val peerNickname = getPeerNickname(peerID, meshService)
val systemMessage = BitchatMessage(
@@ -188,7 +188,7 @@ class PrivateChatManager(
// MARK: - Block/Unblock Operations
fun blockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
fun blockPeer(peerID: String, meshService: MeshService): Boolean {
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
if (fingerprint != null) {
dataManager.addBlockedUser(fingerprint)
@@ -212,7 +212,7 @@ class PrivateChatManager(
return false
}
fun unblockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
fun unblockPeer(peerID: String, meshService: MeshService): Boolean {
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
dataManager.removeBlockedUser(fingerprint)
@@ -230,7 +230,7 @@ class PrivateChatManager(
return false
}
fun blockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
fun blockPeerByNickname(targetName: String, meshService: MeshService): Boolean {
val peerID = getPeerIDForNickname(targetName, meshService)
if (peerID != null) {
@@ -247,7 +247,7 @@ class PrivateChatManager(
}
}
fun unblockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
fun unblockPeerByNickname(targetName: String, meshService: MeshService): Boolean {
val peerID = getPeerIDForNickname(targetName, meshService)
if (peerID != null) {
@@ -333,7 +333,7 @@ class PrivateChatManager(
* Send read receipts for all unread messages from a specific peer
* Called when the user focuses on a private chat
*/
fun sendReadReceiptsForPeer(peerID: String, meshService: BluetoothMeshService) {
fun sendReadReceiptsForPeer(peerID: String, meshService: MeshService) {
// Collect candidate messages: all incoming messages from this peer in the conversation
val chats = try { state.getPrivateChatsValue() } catch (_: Exception) { emptyMap<String, List<BitchatMessage>>() }
val messages = chats[peerID].orEmpty()
@@ -343,9 +343,7 @@ class PrivateChatManager(
}
val myNickname = state.getNicknameValue() ?: "unknown"
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
val hasMesh = try { meshService.getPeerInfo(peerID)?.isConnected == true && meshService.hasEstablishedSession(peerID) } catch (_: Exception) { false }
val hasAware = try { aware?.getPeerInfo(peerID)?.isConnected == true && aware.hasEstablishedSession(peerID) } catch (_: Exception) { false }
var sentCount = 0
messages.forEach { msg ->
// Only for incoming messages from this peer
@@ -354,9 +352,6 @@ class PrivateChatManager(
if (hasMesh) {
meshService.sendReadReceipt(msg.id, peerID, myNickname)
sentCount += 1
} else if (hasAware) {
aware?.sendReadReceipt(msg.id, peerID, myNickname)
sentCount += 1
}
} catch (e: Exception) {
Log.w(TAG, "Failed to send read receipt for message ${msg.id}: ${e.message}")
@@ -388,7 +383,7 @@ class PrivateChatManager(
* Establish Noise session if needed before starting private chat
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
*/
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: BluetoothMeshService) {
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: MeshService) {
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
Log.d(TAG, "Noise session already established with $peerID")
return
@@ -421,11 +416,11 @@ class PrivateChatManager(
// MARK: - Utility Functions
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
private fun getPeerIDForNickname(nickname: String, meshService: MeshService): String? {
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
}
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
private fun getPeerNickname(peerID: String, meshService: MeshService): String {
return meshService.getPeerNicknames()[peerID] ?: peerID
}
@@ -134,7 +134,7 @@ fun SecurityVerificationSheet(
displayName = displayName,
accent = accent,
canStartHandshake = fingerprint == null && selectedPeerID.matches(peerHexRegex),
onStartHandshake = { viewModel.meshService.initiateNoiseHandshake(selectedPeerID) },
onStartHandshake = { viewModel.initiateMeshHandshake(selectedPeerID) },
onVerify = { fp -> viewModel.verifyFingerprintValue(fp) },
onUnverify = { fp -> viewModel.unverifyFingerprintValue(fp) }
)
@@ -4,7 +4,7 @@ import android.content.Context
import com.bitchat.android.R
import com.bitchat.android.favorites.FavoritesPersistenceService
import com.bitchat.android.identity.SecureIdentityStateManager
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.noise.NoiseSession
import com.bitchat.android.nostr.GeohashAliasRegistry
@@ -26,14 +26,14 @@ import java.util.concurrent.ConcurrentHashMap
class VerificationHandler(
private val context: Context,
private val scope: CoroutineScope,
private val getMeshService: () -> BluetoothMeshService,
private val getMeshService: () -> MeshService,
private val identityManager: SecureIdentityStateManager,
private val state: ChatState,
private val notificationManager: NotificationManager,
private val messageManager: MessageManager
) {
// Helper to get current mesh service (may change after panic clear)
private val meshService: BluetoothMeshService
private val meshService: MeshService
get() = getMeshService()
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
@@ -193,7 +193,7 @@ fun VerificationSheet(
val fingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
if (peerID != null) {
val fingerprint = viewModel.meshService.getPeerFingerprint(peerID!!)
val fingerprint = viewModel.getMeshPeerFingerprint(peerID!!)
if (fingerprint != null && fingerprints.contains(fingerprint)) {
Spacer(modifier = Modifier.height(16.dp))
Button(
@@ -21,7 +21,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import androidx.compose.material3.ColorScheme
import java.text.SimpleDateFormat
@@ -30,7 +30,7 @@ import java.text.SimpleDateFormat
fun AudioMessageItem(
message: BitchatMessage,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
@@ -27,7 +27,7 @@ import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.text.font.FontFamily
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
import androidx.compose.material3.ColorScheme
@@ -39,7 +39,7 @@ fun ImageMessageItem(
message: BitchatMessage,
messages: List<BitchatMessage>,
currentUserNickname: String,
meshService: BluetoothMeshService,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,