mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 14:45:22 +00:00
343 lines
13 KiB
Kotlin
343 lines
13 KiB
Kotlin
package com.bitchat.android.ui
|
|
|
|
import android.app.Application
|
|
import android.content.Context
|
|
import androidx.lifecycle.AndroidViewModel
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.viewModelScope
|
|
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.ReadReceipt
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.delay
|
|
import java.util.*
|
|
import kotlin.random.Random
|
|
|
|
/**
|
|
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
|
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
|
|
*/
|
|
class ChatViewModel(application: Application) : AndroidViewModel(application), BluetoothMeshDelegate {
|
|
|
|
private val context: Context = application.applicationContext
|
|
|
|
// Core services
|
|
val meshService = BluetoothMeshService(context)
|
|
|
|
// State management
|
|
private val state = ChatState()
|
|
|
|
// Specialized managers
|
|
private val dataManager = DataManager(context)
|
|
private val messageManager = MessageManager(state)
|
|
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
|
|
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
|
|
val nickname: LiveData<String> = state.nickname
|
|
val isConnected: LiveData<Boolean> = state.isConnected
|
|
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = state.privateChats
|
|
val selectedPrivateChatPeer: LiveData<String?> = state.selectedPrivateChatPeer
|
|
val unreadPrivateMessages: LiveData<Set<String>> = state.unreadPrivateMessages
|
|
val joinedChannels: LiveData<Set<String>> = state.joinedChannels
|
|
val currentChannel: LiveData<String?> = state.currentChannel
|
|
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = state.channelMessages
|
|
val unreadChannelMessages: LiveData<Map<String, Int>> = state.unreadChannelMessages
|
|
val passwordProtectedChannels: LiveData<Set<String>> = state.passwordProtectedChannels
|
|
val showPasswordPrompt: LiveData<Boolean> = state.showPasswordPrompt
|
|
val passwordPromptChannel: LiveData<String?> = state.passwordPromptChannel
|
|
val showSidebar: LiveData<Boolean> = state.showSidebar
|
|
val hasUnreadChannels = state.hasUnreadChannels
|
|
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
|
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
|
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
|
|
|
init {
|
|
meshService.delegate = this
|
|
loadAndInitialize()
|
|
}
|
|
|
|
private fun loadAndInitialize() {
|
|
// Load nickname
|
|
val nickname = dataManager.loadNickname()
|
|
state.setNickname(nickname)
|
|
|
|
// Load data
|
|
val (joinedChannels, protectedChannels) = channelManager.loadChannelData()
|
|
state.setJoinedChannels(joinedChannels)
|
|
state.setPasswordProtectedChannels(protectedChannels)
|
|
|
|
// Initialize channel messages
|
|
joinedChannels.forEach { channel ->
|
|
if (!state.getChannelMessagesValue().containsKey(channel)) {
|
|
val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap()
|
|
updatedChannelMessages[channel] = emptyList()
|
|
state.setChannelMessages(updatedChannelMessages)
|
|
}
|
|
}
|
|
|
|
// Load other data
|
|
dataManager.loadFavorites()
|
|
dataManager.loadBlockedUsers()
|
|
|
|
// Start mesh service
|
|
meshService.startServices()
|
|
|
|
// Show welcome message if no peers after delay
|
|
viewModelScope.launch {
|
|
delay(3000)
|
|
if (state.getConnectedPeersValue().isEmpty() && state.getMessagesValue().isEmpty()) {
|
|
val welcomeMessage = BitchatMessage(
|
|
sender = "system",
|
|
content = "get people around you to download bitchat…and chat with them here!",
|
|
timestamp = Date(),
|
|
isRelay = false
|
|
)
|
|
messageManager.addMessage(welcomeMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCleared() {
|
|
super.onCleared()
|
|
meshService.stopServices()
|
|
}
|
|
|
|
// MARK: - Nickname Management
|
|
|
|
fun setNickname(newNickname: String) {
|
|
state.setNickname(newNickname)
|
|
dataManager.saveNickname(newNickname)
|
|
meshService.sendBroadcastAnnounce()
|
|
}
|
|
|
|
// MARK: - Channel Management (delegated)
|
|
|
|
fun joinChannel(channel: String, password: String? = null): Boolean {
|
|
return channelManager.joinChannel(channel, password, meshService.myPeerID)
|
|
}
|
|
|
|
fun switchToChannel(channel: String?) {
|
|
channelManager.switchToChannel(channel)
|
|
}
|
|
|
|
fun leaveChannel(channel: String) {
|
|
channelManager.leaveChannel(channel)
|
|
meshService.sendMessage("left $channel")
|
|
}
|
|
|
|
// MARK: - Private Chat Management (delegated)
|
|
|
|
fun startPrivateChat(peerID: String) {
|
|
privateChatManager.startPrivateChat(peerID, meshService)
|
|
}
|
|
|
|
fun endPrivateChat() {
|
|
privateChatManager.endPrivateChat()
|
|
}
|
|
|
|
// MARK: - Message Sending
|
|
|
|
fun sendMessage(content: String) {
|
|
if (content.isEmpty()) return
|
|
|
|
// Check for commands
|
|
if (content.startsWith("/")) {
|
|
commandProcessor.processCommand(content, meshService, meshService.myPeerID) { messageContent, mentions, channel ->
|
|
meshService.sendMessage(messageContent, mentions, channel)
|
|
}
|
|
return
|
|
}
|
|
|
|
val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue())
|
|
val channels = messageManager.parseChannels(content)
|
|
|
|
// Auto-join mentioned channels
|
|
channels.forEach { channel ->
|
|
if (!state.getJoinedChannelsValue().contains(channel)) {
|
|
joinChannel(channel)
|
|
}
|
|
}
|
|
|
|
val selectedPeer = state.getSelectedPrivateChatPeerValue()
|
|
val currentChannelValue = state.getCurrentChannelValue()
|
|
|
|
if (selectedPeer != null) {
|
|
// Send private message
|
|
val recipientNickname = meshService.getPeerNicknames()[selectedPeer]
|
|
privateChatManager.sendPrivateMessage(
|
|
content,
|
|
selectedPeer,
|
|
recipientNickname,
|
|
state.getNicknameValue(),
|
|
meshService.myPeerID
|
|
) { messageContent, peerID, recipientNicknameParam, messageId ->
|
|
meshService.sendPrivateMessage(messageContent, peerID, recipientNicknameParam, messageId)
|
|
}
|
|
} else {
|
|
// Send public/channel message
|
|
val message = BitchatMessage(
|
|
sender = state.getNicknameValue() ?: meshService.myPeerID,
|
|
content = content,
|
|
timestamp = Date(),
|
|
isRelay = false,
|
|
senderPeerID = meshService.myPeerID,
|
|
mentions = if (mentions.isNotEmpty()) mentions else null,
|
|
channel = currentChannelValue
|
|
)
|
|
|
|
if (currentChannelValue != null) {
|
|
channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID)
|
|
|
|
// Check if encrypted channel
|
|
if (channelManager.hasChannelKey(currentChannelValue)) {
|
|
channelManager.sendEncryptedChannelMessage(
|
|
content,
|
|
mentions,
|
|
currentChannelValue,
|
|
state.getNicknameValue(),
|
|
meshService.myPeerID,
|
|
onEncryptedPayload = { encryptedData ->
|
|
// This would need proper mesh service integration
|
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
|
},
|
|
onFallback = {
|
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
|
}
|
|
)
|
|
} else {
|
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
|
}
|
|
} else {
|
|
messageManager.addMessage(message)
|
|
meshService.sendMessage(content, mentions, null)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Utility Functions
|
|
|
|
fun getPeerIDForNickname(nickname: String): String? {
|
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
|
}
|
|
|
|
fun toggleFavorite(peerID: String) {
|
|
privateChatManager.toggleFavorite(peerID)
|
|
}
|
|
|
|
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
|
privateChatManager.registerPeerPublicKey(peerID, publicKeyData)
|
|
}
|
|
|
|
// MARK: - Debug and Troubleshooting
|
|
|
|
fun getDebugStatus(): String {
|
|
return meshService.getDebugStatus()
|
|
}
|
|
|
|
fun restartMeshServices() {
|
|
viewModelScope.launch {
|
|
meshService.stopServices()
|
|
delay(1000)
|
|
meshService.startServices()
|
|
}
|
|
}
|
|
|
|
fun setAppBackgroundState(inBackground: Boolean) {
|
|
// Forward to connection manager for power optimization
|
|
meshService.connectionManager.setAppBackgroundState(inBackground)
|
|
}
|
|
|
|
// MARK: - Command Autocomplete (delegated)
|
|
|
|
fun updateCommandSuggestions(input: String) {
|
|
commandProcessor.updateCommandSuggestions(input)
|
|
}
|
|
|
|
fun selectCommandSuggestion(suggestion: CommandSuggestion): String {
|
|
return commandProcessor.selectCommandSuggestion(suggestion)
|
|
}
|
|
|
|
// MARK: - BluetoothMeshDelegate Implementation (delegated)
|
|
|
|
override fun didReceiveMessage(message: BitchatMessage) {
|
|
meshDelegateHandler.didReceiveMessage(message)
|
|
}
|
|
|
|
override fun didConnectToPeer(peerID: String) {
|
|
meshDelegateHandler.didConnectToPeer(peerID)
|
|
}
|
|
|
|
override fun didDisconnectFromPeer(peerID: String) {
|
|
meshDelegateHandler.didDisconnectFromPeer(peerID)
|
|
}
|
|
|
|
override fun didUpdatePeerList(peers: List<String>) {
|
|
meshDelegateHandler.didUpdatePeerList(peers)
|
|
}
|
|
|
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
|
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
|
}
|
|
|
|
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
|
meshDelegateHandler.didReceiveDeliveryAck(ack)
|
|
}
|
|
|
|
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
|
meshDelegateHandler.didReceiveReadReceipt(receipt)
|
|
}
|
|
|
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
|
return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel)
|
|
}
|
|
|
|
override fun getNickname(): String? {
|
|
return meshDelegateHandler.getNickname()
|
|
}
|
|
|
|
override fun isFavorite(peerID: String): Boolean {
|
|
return meshDelegateHandler.isFavorite(peerID)
|
|
}
|
|
|
|
// MARK: - Emergency Clear
|
|
|
|
fun panicClearAllData() {
|
|
// Clear all managers
|
|
messageManager.clearAllMessages()
|
|
channelManager.clearAllChannels()
|
|
privateChatManager.clearAllPrivateChats()
|
|
dataManager.clearAllData()
|
|
|
|
// Reset nickname
|
|
val newNickname = "anon${Random.nextInt(1000, 9999)}"
|
|
state.setNickname(newNickname)
|
|
dataManager.saveNickname(newNickname)
|
|
|
|
// Disconnect from mesh
|
|
meshService.stopServices()
|
|
|
|
// Restart services with new identity
|
|
viewModelScope.launch {
|
|
delay(500)
|
|
meshService.startServices()
|
|
}
|
|
}
|
|
}
|