diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c07f02..1c79552 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,11 @@ dependencies { implementation(libs.androidx.material3.window.size.class1) implementation(libs.okhttp.sse) implementation(libs.androidx.datastore) + implementation(libs.hilt.navigation.compose) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.compiler) + implementation(libs.androidx.room.paging) + implementation(libs.androidx.room.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/eu/m724/chatapp/ChatApplication.kt b/app/src/main/java/eu/m724/chatapp/ChatApplication.kt index f9dacda..5b76615 100644 --- a/app/src/main/java/eu/m724/chatapp/ChatApplication.kt +++ b/app/src/main/java/eu/m724/chatapp/ChatApplication.kt @@ -4,5 +4,4 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class ChatApplication : Application() { -} \ No newline at end of file +class ChatApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/activity/chat/quick_settings/ChatQuickSettings.kt b/app/src/main/java/eu/m724/chatapp/activity/chat/quick_settings/ChatQuickSettings.kt index fbc07cd..8651681 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/chat/quick_settings/ChatQuickSettings.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/chat/quick_settings/ChatQuickSettings.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import eu.m724.chatapp.R import eu.m724.chatapp.activity.chat.quick_settings.composable.ModelCard import eu.m724.chatapp.api.data.response.models.LanguageModel @@ -38,7 +38,7 @@ fun ChatQuickSettings( modifier: Modifier = Modifier, onModelSelected: (LanguageModel) -> Unit, onDismiss: () -> Unit, - viewModel: ChatQuickSettingsViewModel = viewModel(), + viewModel: ChatQuickSettingsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/app/src/main/java/eu/m724/chatapp/activity/main/MainActivity.kt b/app/src/main/java/eu/m724/chatapp/activity/main/MainActivity.kt index 184e60d..779bc39 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/main/MainActivity.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/main/MainActivity.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint import eu.m724.chatapp.R import eu.m724.chatapp.activity.chat.ChatActivity @@ -52,7 +52,7 @@ class MainActivity : ComponentActivity() { @Composable fun Content( modifier: Modifier = Modifier, - viewModel: MainActivityViewModel = viewModel() + viewModel: MainActivityViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() val context = LocalContext.current diff --git a/app/src/main/java/eu/m724/chatapp/activity/main/MainActivityViewModel.kt b/app/src/main/java/eu/m724/chatapp/activity/main/MainActivityViewModel.kt index cac5e80..f7e4128 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/main/MainActivityViewModel.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/main/MainActivityViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import eu.m724.chatapp.api.AiApiService import eu.m724.chatapp.store.data.Chat +import eu.m724.chatapp.store.room.ChatDao import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,7 +17,8 @@ import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( - val aiApiService: AiApiService + val aiApiService: AiApiService, + val chatDao: ChatDao ) : ViewModel() { private val _uiState = MutableStateFlow(MainActivityUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -43,6 +45,8 @@ class MainActivityViewModel @Inject constructor( messages = emptyList() ) + chatDao.insertChat() + _uiEvents.send(MainActivityUiEvent.StartChat(chat)) } } diff --git a/app/src/main/java/eu/m724/chatapp/store/data/Chat.kt b/app/src/main/java/eu/m724/chatapp/store/data/Chat.kt index 39cc773..300e697 100644 --- a/app/src/main/java/eu/m724/chatapp/store/data/Chat.kt +++ b/app/src/main/java/eu/m724/chatapp/store/data/Chat.kt @@ -7,6 +7,11 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Chat( + /** + * The unique identifier of this chat. + */ + val id: Int, + /** * The title of this chat. */ diff --git a/app/src/main/java/eu/m724/chatapp/store/proto/DataStoreModule.kt b/app/src/main/java/eu/m724/chatapp/store/proto/DataStoreModule.kt new file mode 100644 index 0000000..94d6eea --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/proto/DataStoreModule.kt @@ -0,0 +1,7 @@ +package eu.m724.chatapp.store.proto + +import eu.m724.chatapp.proto.Chat + +class DataStoreModule { + val a: Chat +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/proto/ProtoChatSerializer.kt b/app/src/main/java/eu/m724/chatapp/store/proto/ProtoChatSerializer.kt new file mode 100644 index 0000000..762d8f7 --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/proto/ProtoChatSerializer.kt @@ -0,0 +1,20 @@ +package eu.m724.chatapp.store.proto + +import androidx.datastore.core.Serializer +import eu.m724.chatapp.proto.ProtoChat + +object ProtoChatSerializer : Serializer { + override val defaultValue: ProtoChat = Settings.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): Settings { + try { + return Settings.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo( + t: Settings, + output: OutputStream) = t.writeTo(output) +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/ChatDao.kt b/app/src/main/java/eu/m724/chatapp/store/room/ChatDao.kt new file mode 100644 index 0000000..778c49e --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/ChatDao.kt @@ -0,0 +1,30 @@ +package eu.m724.chatapp.store.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import eu.m724.chatapp.store.room.entity.ChatEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatDao { + @Query("SELECT * FROM chats") + fun getAllChats(): List + + @Query("SELECT * FROM chats WHERE id = :id") + fun getChatById(id: Int): ChatEntity? + + @Query(""" + SELECT * FROM chats + JOIN chats_fts ON chats.id = chats_fts.rowid + WHERE chats_fts MATCH :query + """) + fun searchChats(query: String): Flow> + + @Insert + fun insertChat(chat: ChatEntity) + + @Update + fun updateChat(chat: ChatEntity) +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/MessageDao.kt b/app/src/main/java/eu/m724/chatapp/store/room/MessageDao.kt new file mode 100644 index 0000000..4317b03 --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/MessageDao.kt @@ -0,0 +1,21 @@ +package eu.m724.chatapp.store.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import eu.m724.chatapp.store.room.entity.MessageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface MessageDao { + @Insert + suspend fun insertMessage(message: MessageEntity) + + @Update + suspend fun updateMessage(message: MessageEntity) + + @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY index ASC") + fun getMessagesForChat(chatId: Int): Flow> + +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/database/AppDatabase.kt b/app/src/main/java/eu/m724/chatapp/store/room/database/AppDatabase.kt new file mode 100644 index 0000000..72b1887 --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/database/AppDatabase.kt @@ -0,0 +1,19 @@ +package eu.m724.chatapp.store.room.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import eu.m724.chatapp.store.room.ChatDao +import eu.m724.chatapp.store.room.MessageDao +import eu.m724.chatapp.store.room.entity.ChatEntity +import eu.m724.chatapp.store.room.entity.ChatEntityFts +import eu.m724.chatapp.store.room.entity.MessageEntity + +@Database(entities = [ + ChatEntity::class, + ChatEntityFts::class, + MessageEntity::class +], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun chatDao(): ChatDao + abstract fun messageDao(): MessageDao +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/database/DatabaseModule.kt b/app/src/main/java/eu/m724/chatapp/store/room/database/DatabaseModule.kt new file mode 100644 index 0000000..f3c022c --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/database/DatabaseModule.kt @@ -0,0 +1,36 @@ +package eu.m724.chatapp.store.room.database + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import eu.m724.chatapp.store.room.ChatDao +import eu.m724.chatapp.store.room.MessageDao +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + @Provides + @Singleton + fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + "chatapp-database" + ).build() + } + + @Provides + fun provideChatDao(appDatabase: AppDatabase): ChatDao { + return appDatabase.chatDao() + } + + @Provides + fun provideMessageDao(appDatabase: AppDatabase): MessageDao { + return appDatabase.messageDao() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntity.kt b/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntity.kt new file mode 100644 index 0000000..6f769ac --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntity.kt @@ -0,0 +1,23 @@ +package eu.m724.chatapp.store.room.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "chats") +data class ChatEntity( + /** + * The unique identifier of this chat. + */ + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + + /** + * The title of this chat, null if not set. + */ + val title: String?, + + /** + * The model ID used in this chat. + */ + val model: String, +) \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntityFts.kt b/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntityFts.kt new file mode 100644 index 0000000..b5b8129 --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/entity/ChatEntityFts.kt @@ -0,0 +1,13 @@ +package eu.m724.chatapp.store.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Fts4 + +@Entity(tableName = "chats_fts") +@Fts4 +data class ChatEntityFts( + @ColumnInfo(name = "title") + val title: String + +) \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/store/room/entity/MessageEntity.kt b/app/src/main/java/eu/m724/chatapp/store/room/entity/MessageEntity.kt new file mode 100644 index 0000000..a77a473 --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/store/room/entity/MessageEntity.kt @@ -0,0 +1,34 @@ +package eu.m724.chatapp.store.room.entity + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "messages", + indices = [ + Index(value = ["chatId"]) + ] +) +data class MessageEntity( + /** + * The unique identifier of this message. TODO make random perhaps + */ + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + + /** + * The index of this message in the chat. + */ + val index: Int, + + /** + * The ID of the chat this message belongs to. + */ + val chatId: Int, + + /** + * The content of this message. + */ + val content: String +) \ No newline at end of file diff --git a/app/src/main/proto/chat.proto b/app/src/main/proto/chat.proto new file mode 100644 index 0000000..e0d0648 --- /dev/null +++ b/app/src/main/proto/chat.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +option java_package = "eu.m724.chatapp.proto"; +option java_multiple_files = true; + +enum ProtoChatMessageRole { + User = 0; + Assistant = 1; +} + +message ProtoChatMessage { + int64 id = 1; + string content = 2; + ProtoChatMessageRole role = 3; +} + +message ProtoChat { + int64 id = 1; + string title = 2; + string model_id = 3; + repeated ProtoChatMessage messages = 4; +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac0a676..43d42ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,8 @@ material = "1.12.0" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" composeBom = "2025.06.01" -hilt = "2.56.2" +hiltAndroid = "2.56.2" +hiltCompiler = "2.56.2" ksp = "2.1.21-2.0.2" retrofit = "3.0.0" secrets = "2.0.1" @@ -19,6 +20,13 @@ material3WindowSizeClass = "1.3.2" okhttpSse = "4.12.0" parcelize = "2.1.21" datastore = "1.1.7" +hiltNavigationCompose = "1.2.0" +roomRuntime = "2.7.2" +roomCompiler = "2.7.2" +roomPaging = "2.7.2" +roomKtx = "2.7.2" +pagingRuntime = "3.3.6" +pagingCompose = "3.3.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -37,20 +45,27 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } -hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltAndroid" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltCompiler" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit"} logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "loggingInterceptor" } androidx-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" } okhttp-sse = { group = "com.squareup.okhttp3", name = "okhttp-sse", version.ref = "okhttpSse" } androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomRuntime" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomCompiler" } +androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "roomPaging" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } +androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingRuntime" } +androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } -parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "parcelize" } \ No newline at end of file +parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "parcelize" }