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.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.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -13,7 +9,6 @@ import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
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.launch
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -41,6 +36,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
private val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
|
||||
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)
|
||||
val messages: LiveData<List<BitchatMessage>> = state.messages
|
||||
val connectedPeers: LiveData<List<String>> = state.connectedPeers
|
||||
@@ -235,10 +241,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
privateChatManager.toggleFavorite(peerID)
|
||||
}
|
||||
|
||||
override fun isFavorite(peerID: String): Boolean {
|
||||
return privateChatManager.isFavorite(peerID)
|
||||
}
|
||||
|
||||
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
||||
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)
|
||||
|
||||
fun updateCommandSuggestions(input: String) {
|
||||
@@ -288,121 +269,47 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
return commandProcessor.selectCommandSuggestion(suggestion)
|
||||
}
|
||||
|
||||
// MARK: - BluetoothMeshDelegate Implementation
|
||||
// MARK: - BluetoothMeshDelegate Implementation (delegated)
|
||||
|
||||
override fun didReceiveMessage(message: BitchatMessage) {
|
||||
viewModelScope.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
|
||||
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()
|
||||
}
|
||||
}
|
||||
meshDelegateHandler.didReceiveMessage(message)
|
||||
}
|
||||
|
||||
override fun didConnectToPeer(peerID: String) {
|
||||
viewModelScope.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)
|
||||
}
|
||||
meshDelegateHandler.didConnectToPeer(peerID)
|
||||
}
|
||||
|
||||
override fun didDisconnectFromPeer(peerID: String) {
|
||||
viewModelScope.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)
|
||||
}
|
||||
meshDelegateHandler.didDisconnectFromPeer(peerID)
|
||||
}
|
||||
|
||||
override fun didUpdatePeerList(peers: List<String>) {
|
||||
viewModelScope.launch {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDelegateHandler.didUpdatePeerList(peers)
|
||||
}
|
||||
|
||||
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||
viewModelScope.launch {
|
||||
channelManager.removeChannelMember(channel, fromPeer)
|
||||
}
|
||||
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
||||
}
|
||||
|
||||
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
||||
viewModelScope.launch {
|
||||
messageManager.updateMessageDeliveryStatus(ack.originalMessageID, DeliveryStatus.Delivered(ack.recipientNickname, ack.timestamp))
|
||||
}
|
||||
meshDelegateHandler.didReceiveDeliveryAck(ack)
|
||||
}
|
||||
|
||||
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
||||
viewModelScope.launch {
|
||||
messageManager.updateMessageDeliveryStatus(receipt.originalMessageID, DeliveryStatus.Read(receipt.readerNickname, receipt.timestamp))
|
||||
}
|
||||
meshDelegateHandler.didReceiveReadReceipt(receipt)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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