From 48fa2c3663996b36137a865ba3fa390bf77d33ff Mon Sep 17 00:00:00 2001 From: ccoppo Date: Sun, 2 Feb 2025 12:19:46 -0700 Subject: [PATCH 01/81] refactor: Reorganize messaging functionality into dedicated module - Created messaging package structure - Moved message-related code to dedicated files - Added proper documentation - Improved code organization and maintainability --- .../greybox/projectmesh/messaging/README.md | 34 ++++++++++ .../{db => messaging/data}/dao/MessageDao.kt | 4 +- .../data}/entities/Message.kt | 2 +- .../network/MessageNetworkHandler.kt | 68 +++++++++++++++++++ .../messaging/network/MessageService.kt | 28 ++++++++ .../messaging/repository/MessageRepository.kt | 34 ++++++++++ .../ui/models}/ChatScreenModel.kt | 4 +- .../ui/screens}/ChatScreen.kt | 23 ++++--- .../ui/viewmodels/ChatScreenViewModel.kt | 47 +++++++++++++ .../messaging/utils/MessageUtils.kt | 13 ++++ .../viewModel/ChatScreenViewModel.kt | 68 ------------------- 11 files changed, 241 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/README.md rename app/src/main/java/com/greybox/projectmesh/{db => messaging/data}/dao/MessageDao.kt (85%) rename app/src/main/java/com/greybox/projectmesh/{db => messaging/data}/entities/Message.kt (88%) create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt rename app/src/main/java/com/greybox/projectmesh/{model => messaging/ui/models}/ChatScreenModel.kt (64%) rename app/src/main/java/com/greybox/projectmesh/{views => messaging/ui/screens}/ChatScreen.kt (86%) create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt delete mode 100644 app/src/main/java/com/greybox/projectmesh/viewModel/ChatScreenViewModel.kt diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/README.md b/app/src/main/java/com/greybox/projectmesh/messaging/README.md new file mode 100644 index 00000000..bf1672ee --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/README.md @@ -0,0 +1,34 @@ +# Messaging Module Documentation + +## Overview +The messaging module handles all peer-to-peer messaging functionality in Project Mesh. It provides a structured way to send, receive, and store messages between devices on the local network. + +## Package Structure +messaging/ +├── data/ # Data layer (entities and DAOs) +├── network/ # Network communication +├── repository/ # Single source of truth for messages +└── ui/ # User interface components + +## Key Components +1. MessageNetworkHandler: Handles network communication for messages +2. MessageRepository: Central point for message operations +3. MessageService: Coordinates between network and data layers +4. ChatScreen: UI for messaging functionality + +## Usage Example +```kotlin +// Send a message +messageService.sendMessage(address, message) + +// Receive messages +viewModel.uiState.collect { state -> + // Handle messages in UI +} +``` +## Flow of Messages +1. User sends message via ChatScreen +2. ChatScreenViewModel processes message +3. MessageService coordinates sending +4. MessageNetworkHandler handles network communication +5. Message is stored in local database diff --git a/app/src/main/java/com/greybox/projectmesh/db/dao/MessageDao.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt similarity index 85% rename from app/src/main/java/com/greybox/projectmesh/db/dao/MessageDao.kt rename to app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt index 7da5bef5..e97f5a44 100644 --- a/app/src/main/java/com/greybox/projectmesh/db/dao/MessageDao.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt @@ -1,10 +1,10 @@ -package com.greybox.projectmesh.db.dao +package com.greybox.projectmesh.messaging.data.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query -import com.greybox.projectmesh.db.entities.Message +import com.greybox.projectmesh.messaging.data.entities.Message import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/java/com/greybox/projectmesh/db/entities/Message.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt similarity index 88% rename from app/src/main/java/com/greybox/projectmesh/db/entities/Message.kt rename to app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt index 00fc45d9..bf13b089 100644 --- a/app/src/main/java/com/greybox/projectmesh/db/entities/Message.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt @@ -1,4 +1,4 @@ -package com.greybox.projectmesh.db.entities +package com.greybox.projectmesh.messaging.data.entities import androidx.room.ColumnInfo import androidx.room.Entity diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt new file mode 100644 index 00000000..8fa8ec98 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt @@ -0,0 +1,68 @@ +// Path: app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt +package com.greybox.projectmesh.messaging.network + +import android.util.Log +import com.greybox.projectmesh.GlobalApp +import com.greybox.projectmesh.messaging.data.entities.Message +import com.greybox.projectmesh.server.AppServer +import java.net.InetAddress +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import org.kodein.di.DI +import org.kodein.di.DIAware + +class MessageNetworkHandler( + private val httpClient: OkHttpClient, + private val localVirtualAddr: InetAddress, + override val di: DI +) : DIAware { + private val scope = CoroutineScope(Dispatchers.IO) + + fun sendChatMessage(address: InetAddress, time: Long, message: String) { + scope.launch { + try { + val httpUrl = HttpUrl.Builder() + .scheme("http") + .host(address.hostAddress) + .port(AppServer.DEFAULT_PORT) + .addPathSegment("chat") + .addQueryParameter("chatMessage", message) + .addQueryParameter("time", time.toString()) + .addQueryParameter("senderIp", localVirtualAddr.hostAddress) + .build() + + val request = Request.Builder() + .url(httpUrl) + .build() + + httpClient.newCall(request).execute().use { /* response closes automatically */ } + Log.d("MessageNetworkHandler", "Message sent successfully") + } catch (e: Exception) { + Log.e("MessageNetworkHandler", "Failed to send message to ${address.hostAddress}", e) + } + } + } + + companion object { + fun handleIncomingMessage( + chatMessage: String?, + time: Long, + senderIp: InetAddress + ): Message { + val sender = GlobalApp.DeviceInfoManager.getDeviceName(senderIp) ?: "Unknown" + val chatName = GlobalApp.DeviceInfoManager.getChatName(senderIp) + + return Message( + id = 0, + dateReceived = time, + content = chatMessage ?: "Error! No message found.", + sender = sender, + chat = chatName + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt new file mode 100644 index 00000000..5090b8c7 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt @@ -0,0 +1,28 @@ +// Path: app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt +package com.greybox.projectmesh.messaging.network + +import com.greybox.projectmesh.messaging.repository.MessageRepository +import com.greybox.projectmesh.messaging.data.entities.Message +import java.net.InetAddress +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.instance + +class MessageService( + override val di: DI +) : DIAware { + private val messageNetworkHandler: MessageNetworkHandler by di.instance() + private val messageRepository: MessageRepository by di.instance() + + suspend fun sendMessage(address: InetAddress, message: Message) { + // First save locally + messageRepository.addMessage(message) + + // Then send over network + messageNetworkHandler.sendChatMessage( + address = address, + time = message.dateReceived, + message = message.content + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt b/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt new file mode 100644 index 00000000..adf8b069 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt @@ -0,0 +1,34 @@ +// Path: app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt +package com.greybox.projectmesh.messaging.repository + +import com.greybox.projectmesh.messaging.data.dao.MessageDao +import com.greybox.projectmesh.messaging.data.entities.Message +import kotlinx.coroutines.flow.Flow +import org.kodein.di.DI +import org.kodein.di.DIAware + +// Changed to use Kodein instead of javax.inject +class MessageRepository( + private val messageDao: MessageDao, + override val di: DI +) : DIAware { + // Get all messages for a chat + fun getChatMessages(chatId: String): Flow> { + return messageDao.getChatMessagesFlow(chatId) + } + + // Add a new message + suspend fun addMessage(message: Message) { + messageDao.addMessage(message) + } + + // Get all messages + fun getAllMessages(): Flow> { + return messageDao.getAllFlow() + } + + // Clear all messages + suspend fun clearMessages() { + messageDao.clearTable() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/model/ChatScreenModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt similarity index 64% rename from app/src/main/java/com/greybox/projectmesh/model/ChatScreenModel.kt rename to app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt index 1fa09e17..42daa5ef 100644 --- a/app/src/main/java/com/greybox/projectmesh/model/ChatScreenModel.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt @@ -1,6 +1,6 @@ -package com.greybox.projectmesh.model +package com.greybox.projectmesh.messaging.ui.models -import com.greybox.projectmesh.db.entities.Message +import com.greybox.projectmesh.messaging.data.entities.Message import java.net.InetAddress data class ChatScreenModel( diff --git a/app/src/main/java/com/greybox/projectmesh/views/ChatScreen.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt similarity index 86% rename from app/src/main/java/com/greybox/projectmesh/views/ChatScreen.kt rename to app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt index 350a968e..ef2df1df 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/ChatScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt @@ -1,4 +1,4 @@ -package com.greybox.projectmesh.views +package com.greybox.projectmesh.messaging.ui.screens import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,17 +27,18 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.greybox.projectmesh.GlobalApp import com.greybox.projectmesh.ViewModelFactory -import com.greybox.projectmesh.buttonStyle.WhiteButton -import com.greybox.projectmesh.model.ChatScreenModel -import com.greybox.projectmesh.model.PingScreenModel -import com.greybox.projectmesh.viewModel.ChatScreenViewModel -import com.greybox.projectmesh.viewModel.PingScreenViewModel -import com.ustadmobile.meshrabiya.ext.addressToDotNotation -import com.ustadmobile.meshrabiya.mmcp.MmcpOriginatorMessage +import com.greybox.projectmesh.messaging.ui.models.ChatScreenModel +import com.greybox.projectmesh.messaging.ui.viewmodels.ChatScreenViewModel +import com.greybox.projectmesh.views.LongPressCopyableText import org.kodein.di.compose.localDI import java.net.InetAddress import java.text.SimpleDateFormat import java.util.Date +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow @Composable fun ChatScreen( @@ -47,13 +48,13 @@ fun ChatScreen( factory = ViewModelFactory( di = localDI(), owner = LocalSavedStateRegistryOwner.current, - vmFactory = { ChatScreenViewModel(it, virtualAddress) }, + vmFactory = { di -> ChatScreenViewModel(virtualAddress, di) }, defaultArgs = null ) ) ) { // declare the UI state, we can use the uiState to access the current state of the viewModel - val uiState: ChatScreenModel by viewModel.uiState.collectAsState(initial = ChatScreenModel()) + val uiState by viewModel.uiState.collectAsState() var textMessage by rememberSaveable { mutableStateOf("") } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier @@ -76,7 +77,7 @@ fun ChatScreen( Button(modifier = Modifier.weight(1f), onClick = { val message = textMessage.trimEnd() if(message.isNotEmpty()) { - viewModel.sendChatMessage(virtualAddress, message) + viewModel.sendChatMessage(message) // resets the text field textMessage = "" } diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt new file mode 100644 index 00000000..5b371c66 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt @@ -0,0 +1,47 @@ +package com.greybox.projectmesh.messaging.ui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.greybox.projectmesh.messaging.network.MessageService +import com.greybox.projectmesh.messaging.data.entities.Message +import com.greybox.projectmesh.messaging.ui.models.ChatScreenModel +import kotlinx.coroutines.launch +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.instance +import java.net.InetAddress +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class ChatScreenViewModel( + private val virtualAddress: InetAddress, + override val di: DI +) : ViewModel(), DIAware { + private val messageService: MessageService by di.instance() + + private val _uiState = MutableStateFlow( + ChatScreenModel( + deviceName = "My Device", // Or get from preferences + virtualAddress = virtualAddress, + allChatMessages = emptyList() + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun sendChatMessage(message: String) { + viewModelScope.launch { + val newMessage = Message( + id = 0, + dateReceived = System.currentTimeMillis(), + content = message, + sender = "Me", + chat = virtualAddress.hostAddress + ) + + messageService.sendMessage(virtualAddress, newMessage) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt new file mode 100644 index 00000000..af2d2027 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt @@ -0,0 +1,13 @@ +package com.greybox.projectmesh.messaging.utils + +object MessageUtils { + fun formatTimestamp(timestamp: Long): String { + //Adding timestamp formatting logic + return java.text.SimpleDateFormat("HH:mm").format(timestamp) + } + + fun generateChatId(sender: String, receiver: String): String { + //Create a consistent chat ID for two users + return listOf(sender, receiver).sorted().joinToString("-") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/viewModel/ChatScreenViewModel.kt b/app/src/main/java/com/greybox/projectmesh/viewModel/ChatScreenViewModel.kt deleted file mode 100644 index 78fa1ef4..00000000 --- a/app/src/main/java/com/greybox/projectmesh/viewModel/ChatScreenViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.greybox.projectmesh.viewModel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.greybox.projectmesh.GlobalApp -import com.greybox.projectmesh.db.MeshDatabase -import com.greybox.projectmesh.db.entities.Message -import com.greybox.projectmesh.model.ChatScreenModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.kodein.di.DI -import com.greybox.projectmesh.server.AppServer -import com.ustadmobile.meshrabiya.ext.addressToDotNotation -import com.ustadmobile.meshrabiya.ext.requireAddressAsInt -import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.kodein.di.instance -import java.net.InetAddress - -class ChatScreenViewModel( - di: DI, - virtualAddress: InetAddress -): ViewModel() { - // _uiState will be updated whenever there is a change in the UI state - private val deviceName = GlobalApp.DeviceInfoManager.getDeviceName(virtualAddress) ?: "Unknown" - private val addressDotNotation = virtualAddress.requireAddressAsInt().addressToDotNotation() - private val chatName: String = GlobalApp.DeviceInfoManager.getChatName(virtualAddress) - private val _uiState = MutableStateFlow( - ChatScreenModel( - deviceName = deviceName, - virtualAddress = virtualAddress - ) - ) - // uiState is a read-only property that shows the current UI state - val uiState: Flow = _uiState.asStateFlow() - // di is used to get the AndroidVirtualNode instance - private val db: MeshDatabase by di.instance() - private val appServer: AppServer by di.instance() - - - // launch a coroutine - init { - viewModelScope.launch { - // collect the state flow of the AndroidVirtualNode - db.messageDao().getChatMessagesFlow(chatName).collect{ - newChatMessages: List -> - - // update the UI state with the new state - _uiState.update { prev -> - prev.copy( - allChatMessages = newChatMessages - ) - } - } - } - } - - fun sendChatMessage(virtualAddress: InetAddress, message: String) { - val sendTime: Long = System.currentTimeMillis() - viewModelScope.launch { - db.messageDao().addMessage(Message(0, sendTime, message, "Me", chatName)) - } - appServer.sendChatMessage(virtualAddress, sendTime, message) - } -} \ No newline at end of file From c339d9665f2bda64579a30f30287dbf76d6da320 Mon Sep 17 00:00:00 2001 From: ccoppo Date: Tue, 4 Feb 2025 17:11:39 -0700 Subject: [PATCH 02/81] refactor: fixing oops some files did not get committed/pushed in my first push --- .../java/com/greybox/projectmesh/GlobalApp.kt | 5 --- .../com/greybox/projectmesh/MainActivity.kt | 2 +- .../greybox/projectmesh/db/MeshDatabase.kt | 4 +-- .../greybox/projectmesh/server/AppServer.kt | 31 ++++++++++--------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt index 9970517c..a83c4534 100644 --- a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt +++ b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt @@ -3,22 +3,17 @@ package com.greybox.projectmesh import android.app.Application import android.content.Context import android.content.SharedPreferences -import android.os.Environment import android.util.Log import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.room.Room -import androidx.room.RoomDatabase import com.greybox.projectmesh.db.MeshDatabase -import com.greybox.projectmesh.db.entities.Message import com.greybox.projectmesh.server.AppServer import com.ustadmobile.meshrabiya.ext.addressToDotNotation import com.ustadmobile.meshrabiya.ext.asInetAddress -import com.ustadmobile.meshrabiya.ext.requireAddressAsInt import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode import com.ustadmobile.meshrabiya.vnet.randomApipaAddr import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt index b5132968..6df53bdd 100644 --- a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt +++ b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt @@ -40,7 +40,7 @@ import com.greybox.projectmesh.server.AppServer import com.greybox.projectmesh.ui.theme.AppTheme import com.greybox.projectmesh.ui.theme.ProjectMeshTheme import com.greybox.projectmesh.viewModel.SharedUriViewModel -import com.greybox.projectmesh.views.ChatScreen +import com.greybox.projectmesh.messaging.ui.screens.ChatScreen import com.greybox.projectmesh.views.HomeScreen import com.greybox.projectmesh.views.SettingsScreen import com.greybox.projectmesh.views.NetworkScreen diff --git a/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt b/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt index 02b31b70..e77169d3 100644 --- a/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt +++ b/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt @@ -2,8 +2,8 @@ package com.greybox.projectmesh.db import androidx.room.Database import androidx.room.RoomDatabase -import com.greybox.projectmesh.db.dao.MessageDao -import com.greybox.projectmesh.db.entities.Message +import com.greybox.projectmesh.messaging.data.dao.MessageDao +import com.greybox.projectmesh.messaging.data.entities.Message @Database(entities = [Message::class], version = 2) abstract class MeshDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/greybox/projectmesh/server/AppServer.kt b/app/src/main/java/com/greybox/projectmesh/server/AppServer.kt index e6c921a7..e6603683 100644 --- a/app/src/main/java/com/greybox/projectmesh/server/AppServer.kt +++ b/app/src/main/java/com/greybox/projectmesh/server/AppServer.kt @@ -7,8 +7,8 @@ import android.os.Build import android.util.Log import com.greybox.projectmesh.GlobalApp import com.greybox.projectmesh.db.MeshDatabase -import com.greybox.projectmesh.db.entities.Message -import com.greybox.projectmesh.GlobalApp.Companion.TAG_VIRTUAL_ADDRESS +import com.greybox.projectmesh.messaging.data.entities.Message +import com.greybox.projectmesh.messaging.network.MessageNetworkHandler import com.greybox.projectmesh.extension.updateItem import com.ustadmobile.meshrabiya.ext.copyToWithProgressCallback import com.ustadmobile.meshrabiya.util.FileSerializer @@ -41,10 +41,8 @@ import com.greybox.projectmesh.extension.getUriNameAndSize import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance -import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody.Companion.toRequestBody import java.net.URLDecoder /* @@ -153,7 +151,7 @@ class AppServer( init { scope.launch { // fetch all files in receiveDir directory that end with .rx.json extension - val incomingFiles = receiveDir.listFiles { file, fileName: String? -> + val incomingFiles = receiveDir.listFiles { _ , fileName: String? -> fileName?.endsWith(".rx.json") == true }?.map { // It deserializes the JSON file into an IncomingTransferInfo object @@ -329,14 +327,18 @@ class AppServer( return newFixedLengthResponse(settingPref.getString("device_name", Build.MODEL) ?: Build.MODEL) } else if(path.startsWith("/chat")) { - val chatMessage = session.parameters["chatMessage"]?.first() ?: "Error! No message found." + val chatMessage = session.parameters["chatMessage"]?.first() val time = session.parameters["time"]?.first()?.toLong() ?: 0 - val senderIp = InetAddress.getByName(session.parameters["senderIp"]?.first() ?: "0.0.0.0") - val sender: String = GlobalApp.DeviceInfoManager.getDeviceName(senderIp) ?: "Unknown" - val chatName: String = GlobalApp.DeviceInfoManager.getChatName(senderIp) - Log.d("Appserver", "chatMessage: $chatMessage, time: $time, sender: $sender, chatName: $chatName") + val senderIp = InetAddress.getByName(session.parameters["senderIp"]?.first()) + + val message = MessageNetworkHandler.handleIncomingMessage( + chatMessage, + time, + senderIp + ) + scope.launch { - db.messageDao().addMessage(Message(0, time, chatMessage, sender, chatName)) + db.messageDao().addMessage(message) } return newFixedLengthResponse("OK") } @@ -517,7 +519,7 @@ class AppServer( val jsonFile = File(receiveDir, "${incomingTransfer.name}.rx.json") jsonFile.writeText(json.encodeToString(IncomingTransferInfo.serializer(), incomingTransfer)) } - val speedKBS = transfer.size / transferDurationMs + //val speedKBS = transfer.size / transferDurationMs } catch(e: Exception) { _incomingTransfers.update { prev -> @@ -553,9 +555,10 @@ class AppServer( .url("http://${transfer.fromHost.hostAddress}:$fromPort/decline/${transfer.id}") .build() try { + httpClient.newCall(request).execute() // Send the request to the sender and get the response - val response = httpClient.newCall(request).execute() - val strResponse = response.body?.string() + //val response = httpClient.newCall(request).execute() + //val strResponse = response.body?.string() }catch(_: Exception) { } } // update the _incomingTransfers list, setting the status to DECLINED From 67a52e2091ffb17e23b0fec4b07f2c3055c738c1 Mon Sep 17 00:00:00 2001 From: Collin Barney <127068052+cybarney@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:40:00 -0700 Subject: [PATCH 03/81] Added Craig's chat screen Created button for chat screen as well as the chat screen itself. Changes to files: MainActivity.kt BottomNavHost.kt BottomNavItem.kt strings.xml(All versions) --- .../com/greybox/projectmesh/MainActivity.kt | 38 +++++++++++++++++++ .../projectmesh/navigation/BottomNavHost.kt | 3 +- .../projectmesh/navigation/BottomNavItem.kt | 2 + app/src/main/res/values-cn/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt index 6df53bdd..bdbe8304 100644 --- a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt +++ b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt @@ -15,8 +15,12 @@ import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -247,10 +251,44 @@ fun BottomNavApp(di: DI, onSaveToFolderChange = onSaveToFolderChange ) } + + //I'm guessing I can put my Chat button here? + composable(BottomNavItem.Chat.route){//Chat Screen button, this stuff was written by Craig + var thisip by remember{mutableStateOf("")} + var loctxt = LocalContext.current//allows for display of error messages + Column{ + TextField(//Get user to reenter IP address + value = thisip,//string for IP address + onValueChange = {thisip = it}, + label = { Text("Enter your IP Address") } + ) + Button( + onClick= { + if(isipvalid(thisip) == true){ + navController.navigate("chatScreen/$thisip")//Directs to the chat screen using current IP address + //This functionality was already incorporated into the existing code via a composable, there just wasn't a button for it. + } else { + Toast.makeText(loctxt, "Invalid IP Address", Toast.LENGTH_SHORT).show()//Error message if invalid IP address + } + } + + ){ + Text("Chat") + } + } + } } } } +fun isipvalid(theip:String): Boolean{//this is a function for checking if the IP address is valid, if this is redundant let me know and I'll make changes + try{ + InetAddress.getByName(theip) + return true + }catch(e: Exception) + { return false} +} + @SuppressLint("ServiceCast", "ObsoleteSdkInt") fun isBatteryOptimizationDisabled(context: Context): Boolean { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager diff --git a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt index ce21cc07..69c9ef1b 100644 --- a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt +++ b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt @@ -21,7 +21,8 @@ fun BottomNavigationBar(navController: NavHostController) { NavigationItem(BottomNavItem.Network.route, stringResource(id = R.string.network), BottomNavItem.Network.icon), NavigationItem(BottomNavItem.Send.route, stringResource(id = R.string.send), BottomNavItem.Send.icon), NavigationItem(BottomNavItem.Receive.route, stringResource(id = R.string.receive), BottomNavItem.Receive.icon), - NavigationItem(BottomNavItem.Settings.route, stringResource(id = R.string.settings), BottomNavItem.Settings.icon) + NavigationItem(BottomNavItem.Settings.route, stringResource(id = R.string.settings), BottomNavItem.Settings.icon), + NavigationItem(BottomNavItem.Chat.route, stringResource(id=R.string.chat), BottomNavItem.Chat.icon) ) NavigationBar { val currentRoute = navController.currentDestination?.route diff --git a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt index 1396be76..625e0e17 100644 --- a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt @@ -12,4 +12,6 @@ sealed class BottomNavItem(val route: String, val title: String, val icon: Image data object Send : BottomNavItem("send", "Send", Icons.AutoMirrored.Filled.Send) data object Receive : BottomNavItem("receive", "Receive", Icons.Default.Download) data object Settings : BottomNavItem("settings", "Settings", Icons.Default.Settings) + data object Chat : BottomNavItem("chat", "Chat", Icons.Default.ChatBubble) + } \ No newline at end of file diff --git a/app/src/main/res/values-cn/strings.xml b/app/src/main/res/values-cn/strings.xml index f53850ee..ac680220 100644 --- a/app/src/main/res/values-cn/strings.xml +++ b/app/src/main/res/values-cn/strings.xml @@ -33,6 +33,8 @@ 首页 发送 + 交谈 + 频段 热点类型 diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6d90bd26..a98013ac 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -33,6 +33,7 @@ Inicio Enviar + Charlar Banda Tipo de punto de acceso diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2eea3095..ab43e26e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,8 +42,10 @@ Receive Auto Finish Save to folder + Home Send + Chat Band Hotspot Type From ce1a23670a6718e69d4f1488364564bad8f34860 Mon Sep 17 00:00:00 2001 From: Collin Barney <127068052+cybarney@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:23:52 -0700 Subject: [PATCH 04/81] Fix for app crashing when IP field empty Fix for the app crashing when the IP field in chat is empty --- app/src/main/java/com/greybox/projectmesh/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt index bdbe8304..1c0ffe35 100644 --- a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt +++ b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt @@ -264,7 +264,7 @@ fun BottomNavApp(di: DI, ) Button( onClick= { - if(isipvalid(thisip) == true){ + if(!thisip.equals("") && isipvalid(thisip) == true){ navController.navigate("chatScreen/$thisip")//Directs to the chat screen using current IP address //This functionality was already incorporated into the existing code via a composable, there just wasn't a button for it. } else { From ad9943b1a88c38b0576d621ce5d72353e2c5501d Mon Sep 17 00:00:00 2001 From: aryan-r-0 Date: Wed, 5 Feb 2025 21:18:22 -0700 Subject: [PATCH 05/81] feat(chat): Replace manual IP entry with ChatNodeListScreen for node-based chat selection - Removed the old text field + button approach in the Chat route - Introduced ChatNodeListScreen to display nodes from NetworkScreenViewModel - Users now tap a node to navigate to the existing ChatScreen --- .../com/greybox/projectmesh/MainActivity.kt | 30 ++++-------- .../ui/screens/ChatNodeListScreen.kt | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt diff --git a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt index 1c0ffe35..456cfedc 100644 --- a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt +++ b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt @@ -60,6 +60,7 @@ import org.kodein.di.instance import java.io.File import java.util.Locale import java.net.InetAddress +import com.greybox.projectmesh.messaging.ui.screens.ChatNodeListScreen class MainActivity : ComponentActivity(), DIAware { override val di by closestDI() @@ -253,29 +254,14 @@ fun BottomNavApp(di: DI, } //I'm guessing I can put my Chat button here? - composable(BottomNavItem.Chat.route){//Chat Screen button, this stuff was written by Craig - var thisip by remember{mutableStateOf("")} - var loctxt = LocalContext.current//allows for display of error messages - Column{ - TextField(//Get user to reenter IP address - value = thisip,//string for IP address - onValueChange = {thisip = it}, - label = { Text("Enter your IP Address") } - ) - Button( - onClick= { - if(!thisip.equals("") && isipvalid(thisip) == true){ - navController.navigate("chatScreen/$thisip")//Directs to the chat screen using current IP address - //This functionality was already incorporated into the existing code via a composable, there just wasn't a button for it. - } else { - Toast.makeText(loctxt, "Invalid IP Address", Toast.LENGTH_SHORT).show()//Error message if invalid IP address - } - } - - ){ - Text("Chat") + composable(BottomNavItem.Chat.route) { + // Show a list of nodes, let the user pick one + ChatNodeListScreen( + onNodeSelected = { ip -> + // Once a node is tapped, navigate to the actual chat + navController.navigate("chatScreen/$ip") } - } + ) } } } diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt new file mode 100644 index 00000000..2f5027e1 --- /dev/null +++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt @@ -0,0 +1,47 @@ +package com.greybox.projectmesh.messaging.ui.screens + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.viewmodel.compose.viewModel +import com.greybox.projectmesh.ViewModelFactory +import com.greybox.projectmesh.model.NetworkScreenModel +import com.greybox.projectmesh.viewModel.NetworkScreenViewModel +import com.greybox.projectmesh.views.WifiListItem +import org.kodein.di.compose.localDI +import androidx.compose.ui.platform.LocalSavedStateRegistryOwner + +@Composable +fun ChatNodeListScreen( + onNodeSelected: (String) -> Unit, + // Reuse NetworkScreenViewModel in the same way as NetworkScreen + viewModel: NetworkScreenViewModel = viewModel( + factory = ViewModelFactory( + di = localDI(), + owner = LocalSavedStateRegistryOwner.current, + vmFactory = { NetworkScreenViewModel(it) }, + defaultArgs = null + ) + ) +) { + // Provide the same initial state you used in NetworkScreen + val uiState: NetworkScreenModel by viewModel.uiState.collectAsState(initial = NetworkScreenModel()) + + LazyColumn { + items( + items = uiState.allNodes.entries.toList(), + key = { it.key } + ) { nodeEntry -> + // Tapping a WifiListItem calls 'onNodeSelected' instead of onClickNetworkNode + WifiListItem( + wifiAddress = nodeEntry.key, + wifiEntry = nodeEntry.value, + onClick = { selectedIp -> + onNodeSelected(selectedIp) + } + ) + } + } +} \ No newline at end of file From 071c595abae78533cbc70e87f763bee8dc7b7c71 Mon Sep 17 00:00:00 2001 From: Collin Barney <127068052+cybarney@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:49:56 -0700 Subject: [PATCH 06/81] Added Messaging Bubbles Added message bubble feature to the chat screen. work done in ChatScreen.kt --- .idea/inspectionProfiles/Project_Default.xml | 4 + .../messaging/ui/screens/ChatScreen.kt | 179 ++++++++++++++++-- 2 files changed, 170 insertions(+), 13 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index b67486ec..fb93d254 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -3,15 +3,19 @@