Merge pull request #5 from mattintech/feature/persistance

storing chats
This commit is contained in:
2025-07-03 21:45:14 -04:00
committed by GitHub
9 changed files with 169 additions and 9 deletions

View File

@@ -83,6 +83,12 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.50") implementation("com.google.dagger:hilt-android:2.50")
kapt("com.google.dagger:hilt-compiler:2.50") kapt("com.google.dagger:hilt-compiler:2.50")
// Room dependencies
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View File

@@ -0,0 +1,15 @@
package com.mattintech.lchat.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.mattintech.lchat.data.db.dao.MessageDao
import com.mattintech.lchat.data.db.entities.MessageEntity
@Database(
entities = [MessageEntity::class],
version = 1,
exportSchema = false
)
abstract class LChatDatabase : RoomDatabase() {
abstract fun messageDao(): MessageDao
}

View File

@@ -0,0 +1,29 @@
package com.mattintech.lchat.data.db.dao
import androidx.room.*
import com.mattintech.lchat.data.db.entities.MessageEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface MessageDao {
@Query("SELECT * FROM messages WHERE roomName = :roomName ORDER BY timestamp ASC")
fun getMessagesForRoom(roomName: String): Flow<List<MessageEntity>>
@Query("SELECT * FROM messages WHERE roomName = :roomName ORDER BY timestamp ASC")
suspend fun getMessagesForRoomOnce(roomName: String): List<MessageEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessage(message: MessageEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessages(messages: List<MessageEntity>)
@Query("DELETE FROM messages WHERE roomName = :roomName")
suspend fun deleteMessagesForRoom(roomName: String)
@Query("DELETE FROM messages")
suspend fun deleteAllMessages()
@Query("SELECT COUNT(*) FROM messages WHERE roomName = :roomName")
suspend fun getMessageCountForRoom(roomName: String): Int
}

View File

@@ -0,0 +1,16 @@
package com.mattintech.lchat.data.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "messages")
data class MessageEntity(
@PrimaryKey
val id: String,
val roomName: String,
val userId: String,
val userName: String,
val content: String,
val timestamp: Long,
val isOwnMessage: Boolean = false
)

View File

@@ -0,0 +1,27 @@
package com.mattintech.lchat.data.db.mappers
import com.mattintech.lchat.data.Message
import com.mattintech.lchat.data.db.entities.MessageEntity
fun MessageEntity.toMessage(): Message {
return Message(
id = id,
userId = userId,
userName = userName,
content = content,
timestamp = timestamp,
isOwnMessage = isOwnMessage
)
}
fun Message.toEntity(roomName: String): MessageEntity {
return MessageEntity(
id = id,
roomName = roomName,
userId = userId,
userName = userName,
content = content,
timestamp = timestamp,
isOwnMessage = isOwnMessage
)
}

View File

@@ -0,0 +1,14 @@
package com.mattintech.lchat.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
// Placeholder for future migrations
object Migrations {
// Example migration from version 1 to 2
// val MIGRATION_1_2 = object : Migration(1, 2) {
// override fun migrate(database: SupportSQLiteDatabase) {
// // Migration code here
// }
// }
}

View File

@@ -1,6 +1,9 @@
package com.mattintech.lchat.di package com.mattintech.lchat.di
import android.content.Context import android.content.Context
import androidx.room.Room
import com.mattintech.lchat.data.db.LChatDatabase
import com.mattintech.lchat.data.db.dao.MessageDao
import com.mattintech.lchat.network.WifiAwareManager import com.mattintech.lchat.network.WifiAwareManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@@ -20,4 +23,21 @@ object AppModule {
): WifiAwareManager { ): WifiAwareManager {
return WifiAwareManager(context) return WifiAwareManager(context)
} }
@Provides
@Singleton
fun provideLChatDatabase(
@ApplicationContext context: Context
): LChatDatabase {
return Room.databaseBuilder(
context,
LChatDatabase::class.java,
"lchat_database"
).build()
}
@Provides
fun provideMessageDao(database: LChatDatabase): MessageDao {
return database.messageDao()
}
} }

View File

@@ -2,26 +2,36 @@ package com.mattintech.lchat.repository
import android.content.Context import android.content.Context
import com.mattintech.lchat.data.Message import com.mattintech.lchat.data.Message
import com.mattintech.lchat.data.db.dao.MessageDao
import com.mattintech.lchat.data.db.mappers.toEntity
import com.mattintech.lchat.data.db.mappers.toMessage
import com.mattintech.lchat.network.WifiAwareManager import com.mattintech.lchat.network.WifiAwareManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class ChatRepository @Inject constructor( class ChatRepository @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context,
private val wifiAwareManager: WifiAwareManager,
private val messageDao: MessageDao
) { ) {
private val wifiAwareManager = WifiAwareManager(context) private var currentRoomName: String = ""
private val _messages = MutableStateFlow<List<Message>>(emptyList()) private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages.asStateFlow() val messages: StateFlow<List<Message>> = _messages.asStateFlow()
// Flow that combines in-memory and database messages
fun getMessagesFlow(roomName: String): Flow<List<Message>> {
return messageDao.getMessagesForRoom(roomName)
.map { entities -> entities.map { it.toMessage() } }
.onStart { loadMessagesFromDatabase(roomName) }
}
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected) private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow() val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
@@ -69,9 +79,19 @@ class ChatRepository @Inject constructor(
} }
fun startHostMode(roomName: String) { fun startHostMode(roomName: String) {
currentRoomName = roomName
wifiAwareManager.startHostMode(roomName) wifiAwareManager.startHostMode(roomName)
_connectionState.value = ConnectionState.Hosting(roomName) _connectionState.value = ConnectionState.Hosting(roomName)
startConnectionMonitoring() startConnectionMonitoring()
loadMessagesFromDatabase(roomName)
}
private fun loadMessagesFromDatabase(roomName: String) {
CoroutineScope(Dispatchers.IO).launch {
val storedMessages = messageDao.getMessagesForRoomOnce(roomName)
.map { it.toMessage() }
_messages.value = storedMessages
}
} }
fun startClientMode() { fun startClientMode() {
@@ -104,6 +124,11 @@ class ChatRepository @Inject constructor(
private fun addMessage(message: Message) { private fun addMessage(message: Message) {
_messages.value = _messages.value + message _messages.value = _messages.value + message
// Save to database
CoroutineScope(Dispatchers.IO).launch {
messageDao.insertMessage(message.toEntity(currentRoomName))
}
} }
fun clearMessages() { fun clearMessages() {
@@ -118,7 +143,9 @@ class ChatRepository @Inject constructor(
connectionCallback = callback connectionCallback = callback
wifiAwareManager.setConnectionCallback { roomName, isConnected -> wifiAwareManager.setConnectionCallback { roomName, isConnected ->
if (isConnected) { if (isConnected) {
currentRoomName = roomName
_connectionState.value = ConnectionState.Connected(roomName) _connectionState.value = ConnectionState.Connected(roomName)
loadMessagesFromDatabase(roomName)
} else { } else {
_connectionState.value = ConnectionState.Disconnected _connectionState.value = ConnectionState.Disconnected
} }

View File

@@ -7,9 +7,8 @@ import androidx.lifecycle.viewModelScope
import com.mattintech.lchat.data.Message import com.mattintech.lchat.data.Message
import com.mattintech.lchat.repository.ChatRepository import com.mattintech.lchat.repository.ChatRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@@ -20,6 +19,7 @@ sealed class ChatState {
data class Error(val message: String) : ChatState() data class Error(val message: String) : ChatState()
} }
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class ChatViewModel @Inject constructor( class ChatViewModel @Inject constructor(
private val chatRepository: ChatRepository private val chatRepository: ChatRepository
@@ -28,7 +28,10 @@ class ChatViewModel @Inject constructor(
private val _state = MutableLiveData<ChatState>(ChatState.Connected) private val _state = MutableLiveData<ChatState>(ChatState.Connected)
val state: LiveData<ChatState> = _state val state: LiveData<ChatState> = _state
val messages: StateFlow<List<Message>> = chatRepository.messages private val _messagesFlow = MutableStateFlow<Flow<List<Message>>>(flowOf(emptyList()))
val messages: StateFlow<List<Message>> = _messagesFlow
.flatMapLatest { it }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), started = SharingStarted.WhileSubscribed(5000),
@@ -60,6 +63,9 @@ class ChatViewModel @Inject constructor(
this.isHost = isHost this.isHost = isHost
this.currentUserId = UUID.randomUUID().toString() this.currentUserId = UUID.randomUUID().toString()
// Set up messages flow for this room
_messagesFlow.value = chatRepository.getMessagesFlow(roomName)
// Setup message callback if needed for additional processing // Setup message callback if needed for additional processing
chatRepository.setMessageCallback { userId, userName, content -> chatRepository.setMessageCallback { userId, userName, content ->
// Can add additional message processing here if needed // Can add additional message processing here if needed