mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
REFACTOR STEP 2: Extract mesh delegate handling to separate class
- Created MeshDelegateHandler.kt to handle all BluetoothMeshDelegate callbacks - Created ChatViewModelUtils.kt for utility functions like haptic feedback - ChatViewModel now delegates all mesh events to MeshDelegateHandler - Final ChatViewModel size: 337 lines (down from 1000+ original) This completes the refactoring while maintaining 100% functionality.
This commit is contained in:
@@ -2,10 +2,6 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.VibrationEffect
|
|
||||||
import android.os.Vibrator
|
|
||||||
import android.os.VibratorManager
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -13,7 +9,6 @@ import com.bitchat.android.mesh.BluetoothMeshDelegate
|
|||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryAck
|
import com.bitchat.android.model.DeliveryAck
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
|
||||||
import com.bitchat.android.model.ReadReceipt
|
import com.bitchat.android.model.ReadReceipt
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -41,6 +36,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
private val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
|
private val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
|
||||||
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
|
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
|
||||||
|
|
||||||
|
// Delegate handler for mesh callbacks
|
||||||
|
private val meshDelegateHandler = MeshDelegateHandler(
|
||||||
|
state = state,
|
||||||
|
messageManager = messageManager,
|
||||||
|
channelManager = channelManager,
|
||||||
|
privateChatManager = privateChatManager,
|
||||||
|
coroutineScope = viewModelScope,
|
||||||
|
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) },
|
||||||
|
getMyPeerID = { meshService.myPeerID }
|
||||||
|
)
|
||||||
|
|
||||||
// Expose state through LiveData (maintaining the same interface)
|
// Expose state through LiveData (maintaining the same interface)
|
||||||
val messages: LiveData<List<BitchatMessage>> = state.messages
|
val messages: LiveData<List<BitchatMessage>> = state.messages
|
||||||
val connectedPeers: LiveData<List<String>> = state.connectedPeers
|
val connectedPeers: LiveData<List<String>> = state.connectedPeers
|
||||||
@@ -235,10 +241,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
privateChatManager.toggleFavorite(peerID)
|
privateChatManager.toggleFavorite(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isFavorite(peerID: String): Boolean {
|
|
||||||
return privateChatManager.isFavorite(peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
||||||
privateChatManager.registerPeerPublicKey(peerID, publicKeyData)
|
privateChatManager.registerPeerPublicKey(peerID, publicKeyData)
|
||||||
}
|
}
|
||||||
@@ -257,27 +259,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun triggerHapticFeedback() {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
||||||
val vibrator = vibratorManager.defaultVibrator
|
|
||||||
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
vibrator.vibrate(50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Silently ignore vibration errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Command Autocomplete (delegated)
|
// MARK: - Command Autocomplete (delegated)
|
||||||
|
|
||||||
fun updateCommandSuggestions(input: String) {
|
fun updateCommandSuggestions(input: String) {
|
||||||
@@ -288,121 +269,47 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
return commandProcessor.selectCommandSuggestion(suggestion)
|
return commandProcessor.selectCommandSuggestion(suggestion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - BluetoothMeshDelegate Implementation
|
// MARK: - BluetoothMeshDelegate Implementation (delegated)
|
||||||
|
|
||||||
override fun didReceiveMessage(message: BitchatMessage) {
|
override fun didReceiveMessage(message: BitchatMessage) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didReceiveMessage(message)
|
||||||
// FIXED: Deduplicate messages from dual connection paths
|
|
||||||
val messageKey = messageManager.generateMessageKey(message)
|
|
||||||
if (messageManager.isMessageProcessed(messageKey)) {
|
|
||||||
return@launch // Duplicate message, ignore
|
|
||||||
}
|
|
||||||
messageManager.markMessageProcessed(messageKey)
|
|
||||||
|
|
||||||
// Check if sender is blocked
|
|
||||||
message.senderPeerID?.let { senderPeerID ->
|
|
||||||
if (privateChatManager.isPeerBlocked(senderPeerID)) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger haptic feedback
|
|
||||||
triggerHapticFeedback()
|
|
||||||
|
|
||||||
if (message.isPrivate) {
|
|
||||||
// Private message
|
|
||||||
privateChatManager.handleIncomingPrivateMessage(message)
|
|
||||||
} else if (message.channel != null) {
|
|
||||||
// Channel message
|
|
||||||
if (state.getJoinedChannelsValue().contains(message.channel)) {
|
|
||||||
channelManager.addChannelMessage(message.channel, message, message.senderPeerID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Public message
|
|
||||||
messageManager.addMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Periodic cleanup
|
|
||||||
if (messageManager.isMessageProcessed("cleanup_check_${System.currentTimeMillis()/30000}")) {
|
|
||||||
messageManager.cleanupDeduplicationCaches()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didConnectToPeer(peerID: String) {
|
override fun didConnectToPeer(peerID: String) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didConnectToPeer(peerID)
|
||||||
// FIXED: Deduplicate connection events from dual connection paths
|
|
||||||
if (messageManager.isDuplicateSystemEvent("connect", peerID)) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val systemMessage = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = "$peerID connected",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false
|
|
||||||
)
|
|
||||||
messageManager.addMessage(systemMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didDisconnectFromPeer(peerID: String) {
|
override fun didDisconnectFromPeer(peerID: String) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didDisconnectFromPeer(peerID)
|
||||||
// FIXED: Deduplicate disconnection events from dual connection paths
|
|
||||||
if (messageManager.isDuplicateSystemEvent("disconnect", peerID)) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val systemMessage = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = "$peerID disconnected",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false
|
|
||||||
)
|
|
||||||
messageManager.addMessage(systemMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didUpdatePeerList(peers)
|
||||||
state.setConnectedPeers(peers)
|
|
||||||
state.setIsConnected(peers.isNotEmpty())
|
|
||||||
|
|
||||||
// Clean up channel members who disconnected
|
|
||||||
channelManager.cleanupDisconnectedMembers(peers, meshService.myPeerID)
|
|
||||||
|
|
||||||
// Exit private chat if peer disconnected
|
|
||||||
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
|
|
||||||
if (!peers.contains(currentPeer)) {
|
|
||||||
privateChatManager.cleanupDisconnectedPeer(currentPeer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
||||||
channelManager.removeChannelMember(channel, fromPeer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didReceiveDeliveryAck(ack)
|
||||||
messageManager.updateMessageDeliveryStatus(ack.originalMessageID, DeliveryStatus.Delivered(ack.recipientNickname, ack.timestamp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
||||||
viewModelScope.launch {
|
meshDelegateHandler.didReceiveReadReceipt(receipt)
|
||||||
messageManager.updateMessageDeliveryStatus(receipt.originalMessageID, DeliveryStatus.Read(receipt.readerNickname, receipt.timestamp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||||
return channelManager.decryptChannelMessage(encryptedContent, channel)
|
return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNickname(): String? = state.getNicknameValue()
|
override fun getNickname(): String? {
|
||||||
|
return meshDelegateHandler.getNickname()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isFavorite(peerID: String): Boolean {
|
||||||
|
return meshDelegateHandler.isFavorite(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Emergency Clear
|
// MARK: - Emergency Clear
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for the ChatViewModel
|
||||||
|
*/
|
||||||
|
object ChatViewModelUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger haptic feedback
|
||||||
|
*/
|
||||||
|
fun triggerHapticFeedback(context: Context) {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
val vibrator = vibratorManager.defaultVibrator
|
||||||
|
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
vibrator.vibrate(50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Silently ignore vibration errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
|
import com.bitchat.android.model.BitchatMessage
|
||||||
|
import com.bitchat.android.model.DeliveryAck
|
||||||
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
|
import com.bitchat.android.model.ReadReceipt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all BluetoothMeshDelegate callbacks and routes them to appropriate managers
|
||||||
|
*/
|
||||||
|
class MeshDelegateHandler(
|
||||||
|
private val state: ChatState,
|
||||||
|
private val messageManager: MessageManager,
|
||||||
|
private val channelManager: ChannelManager,
|
||||||
|
private val privateChatManager: PrivateChatManager,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val onHapticFeedback: () -> Unit,
|
||||||
|
private val getMyPeerID: () -> String
|
||||||
|
) : BluetoothMeshDelegate {
|
||||||
|
|
||||||
|
override fun didReceiveMessage(message: BitchatMessage) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
// FIXED: Deduplicate messages from dual connection paths
|
||||||
|
val messageKey = messageManager.generateMessageKey(message)
|
||||||
|
if (messageManager.isMessageProcessed(messageKey)) {
|
||||||
|
return@launch // Duplicate message, ignore
|
||||||
|
}
|
||||||
|
messageManager.markMessageProcessed(messageKey)
|
||||||
|
|
||||||
|
// Check if sender is blocked
|
||||||
|
message.senderPeerID?.let { senderPeerID ->
|
||||||
|
if (privateChatManager.isPeerBlocked(senderPeerID)) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger haptic feedback
|
||||||
|
onHapticFeedback()
|
||||||
|
|
||||||
|
if (message.isPrivate) {
|
||||||
|
// Private message
|
||||||
|
privateChatManager.handleIncomingPrivateMessage(message)
|
||||||
|
} else if (message.channel != null) {
|
||||||
|
// Channel message
|
||||||
|
if (state.getJoinedChannelsValue().contains(message.channel)) {
|
||||||
|
channelManager.addChannelMessage(message.channel, message, message.senderPeerID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Public message
|
||||||
|
messageManager.addMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic cleanup
|
||||||
|
if (messageManager.isMessageProcessed("cleanup_check_${System.currentTimeMillis()/30000}")) {
|
||||||
|
messageManager.cleanupDeduplicationCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didConnectToPeer(peerID: String) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
// FIXED: Deduplicate connection events from dual connection paths
|
||||||
|
if (messageManager.isDuplicateSystemEvent("connect", peerID)) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "$peerID connected",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didDisconnectFromPeer(peerID: String) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
// FIXED: Deduplicate disconnection events from dual connection paths
|
||||||
|
if (messageManager.isDuplicateSystemEvent("disconnect", peerID)) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "$peerID disconnected",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
state.setConnectedPeers(peers)
|
||||||
|
state.setIsConnected(peers.isNotEmpty())
|
||||||
|
|
||||||
|
// Clean up channel members who disconnected
|
||||||
|
channelManager.cleanupDisconnectedMembers(peers, getMyPeerID())
|
||||||
|
|
||||||
|
// Exit private chat if peer disconnected
|
||||||
|
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
|
||||||
|
if (!peers.contains(currentPeer)) {
|
||||||
|
privateChatManager.cleanupDisconnectedPeer(currentPeer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
channelManager.removeChannelMember(channel, fromPeer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
messageManager.updateMessageDeliveryStatus(ack.originalMessageID, DeliveryStatus.Delivered(ack.recipientNickname, ack.timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
messageManager.updateMessageDeliveryStatus(receipt.originalMessageID, DeliveryStatus.Read(receipt.readerNickname, receipt.timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||||
|
return channelManager.decryptChannelMessage(encryptedContent, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNickname(): String? = state.getNicknameValue()
|
||||||
|
|
||||||
|
override fun isFavorite(peerID: String): Boolean {
|
||||||
|
return privateChatManager.isFavorite(peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user