Skip to content

Commit 1c6fbac

Browse files
committed
core: add support for groups chats into folders
- add a new Folders table to store folder metadata - add 'folderId' attribute to the Chat table - update chat list drawer to show folders - add dialog to create folders - add dialog to show folder options (rename folder, delete folder, delete folder with chats) - add ChangeFolderDialog to add/remove chat from folders
1 parent 0d04492 commit 1c6fbac

File tree

14 files changed

+731
-29
lines changed

14 files changed

+731
-29
lines changed

app/src/main/java/io/shubham0204/smollmandroid/data/AppDB.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import org.koin.core.annotation.Single
1212
import java.util.Date
1313

1414
@Database(
15-
entities = [Chat::class, ChatMessage::class, LLMModel::class, Task::class],
15+
entities = [Chat::class, ChatMessage::class, LLMModel::class, Task::class, Folder::class],
1616
version = 1,
1717
)
1818
@TypeConverters(Converters::class)
@@ -24,6 +24,8 @@ abstract class AppRoomDatabase : RoomDatabase() {
2424
abstract fun llmModelDao(): LLMModelDao
2525

2626
abstract fun taskDao(): TaskDao
27+
28+
abstract fun folderDao(): FolderDao
2729
}
2830

2931
@Single
@@ -106,6 +108,8 @@ class AppDB(
106108
db.chatsDao().getChatsCount()
107109
}
108110

111+
fun getChatsForFolder(folderId: Long): Flow<List<Chat>> = db.chatsDao().getChatsForFolder(folderId)
112+
109113
// Chat Messages
110114

111115
fun getMessages(chatId: Long): Flow<List<ChatMessage>> = db.chatMessagesDao().getMessages(chatId)
@@ -207,4 +211,37 @@ class AppDB(
207211
runBlocking(Dispatchers.IO) {
208212
db.taskDao().updateTask(task)
209213
}
214+
215+
// Folders
216+
217+
fun getFolders(): Flow<List<Folder>> = db.folderDao().getFolders()
218+
219+
fun addFolder(folderName: String) =
220+
runBlocking(Dispatchers.IO) {
221+
db.folderDao().insertFolder(Folder(name = folderName))
222+
}
223+
224+
fun updateFolder(folder: Folder) =
225+
runBlocking(Dispatchers.IO) {
226+
db.folderDao().updateFolder(folder)
227+
}
228+
229+
/**
230+
* Deletes the folder from the Folder table only
231+
*/
232+
fun deleteFolder(folderId: Long) =
233+
runBlocking(Dispatchers.IO) {
234+
db.folderDao().deleteFolder(folderId)
235+
db.chatsDao().updateFolderIds(folderId, -1L)
236+
}
237+
238+
/**
239+
* Deletes the folder from the Folder table
240+
* and corresponding chats from the Chat table
241+
*/
242+
fun deleteFolderWithChats(folderId: Long) =
243+
runBlocking(Dispatchers.IO) {
244+
db.folderDao().deleteFolder(folderId)
245+
db.chatsDao().deleteChatsInFolder(folderId)
246+
}
210247
}

app/src/main/java/io/shubham0204/smollmandroid/data/ChatsDB.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ data class Chat(
8585
* They do not store conversation messages thus being 'stateless' in nature.
8686
*/
8787
var isTask: Boolean = false,
88+
/**
89+
* The ID of the folder that this chat belongs to.
90+
* -1 indicates that the chat does not belong to any folder.
91+
*/
92+
var folderId: Long = -1L,
8893
)
8994

9095
@Dao
@@ -101,9 +106,21 @@ interface ChatsDao {
101106
@Query("DELETE FROM Chat WHERE id = :chatId")
102107
suspend fun deleteChat(chatId: Long)
103108

109+
@Query("DELETE FROM Chat WHERE folderId = :folderId")
110+
suspend fun deleteChatsInFolder(folderId: Long)
111+
104112
@Update
105113
suspend fun updateChat(chat: Chat)
106114

107115
@Query("SELECT COUNT(*) FROM Chat")
108116
suspend fun getChatsCount(): Long
117+
118+
@Query("SELECT * FROM Chat WHERE folderId = :folderId")
119+
fun getChatsForFolder(folderId: Long): Flow<List<Chat>>
120+
121+
@Query("UPDATE Chat SET folderId = :newFolderId WHERE folderId = :oldFolderId")
122+
fun updateFolderIds(
123+
oldFolderId: Long,
124+
newFolderId: Long,
125+
)
109126
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (C) 2024 Shubham Panchal
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.shubham0204.smollmandroid.data
18+
19+
import androidx.room.Dao
20+
import androidx.room.Entity
21+
import androidx.room.Insert
22+
import androidx.room.PrimaryKey
23+
import androidx.room.Query
24+
import androidx.room.Update
25+
import kotlinx.coroutines.flow.Flow
26+
27+
@Entity(tableName = "Folder")
28+
data class Folder(
29+
@PrimaryKey(autoGenerate = true) var id: Long = 0,
30+
var name: String = "",
31+
)
32+
33+
@Dao
34+
interface FolderDao {
35+
@Insert
36+
suspend fun insertFolder(folder: Folder)
37+
38+
@Update
39+
suspend fun updateFolder(folder: Folder)
40+
41+
@Query("SELECT * FROM Folder")
42+
fun getFolders(): Flow<List<Folder>>
43+
44+
@Query("DELETE FROM Folder WHERE id = :folderId")
45+
suspend fun deleteFolder(folderId: Long)
46+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.shubham0204.smollmandroid.ui.components
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.interaction.MutableInteractionSource
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.ui.Modifier
7+
import androidx.compose.ui.composed
8+
9+
fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier =
10+
composed {
11+
this.clickable(
12+
indication = null,
13+
interactionSource = remember { MutableInteractionSource() },
14+
) {
15+
onClick()
16+
}
17+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.shubham0204.smollmandroid.ui.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.shape.RoundedCornerShape
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.automirrored.filled.ArrowForward
14+
import androidx.compose.material.icons.filled.ArrowForward
15+
import androidx.compose.material.icons.filled.Close
16+
import androidx.compose.material3.Button
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Surface
20+
import androidx.compose.material3.Text
21+
import androidx.compose.material3.TextField
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.getValue
24+
import androidx.compose.runtime.mutableStateOf
25+
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.setValue
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.text.style.TextAlign
30+
import androidx.compose.ui.unit.dp
31+
import androidx.compose.ui.window.Dialog
32+
import io.shubham0204.smollmandroid.R
33+
34+
private var title = ""
35+
private var defaultText = ""
36+
private var placeholder = ""
37+
private var buttonText = ""
38+
private lateinit var buttonOnClick: ((String) -> Unit)
39+
private val textFieldDialogShowStatus = mutableStateOf(false)
40+
41+
@Composable
42+
fun TextFieldDialog() {
43+
var visible by remember { textFieldDialogShowStatus }
44+
if (visible) {
45+
Surface {
46+
Dialog(onDismissRequest = { visible = false }) {
47+
Column(
48+
modifier =
49+
Modifier
50+
.fillMaxWidth()
51+
.background(
52+
MaterialTheme.colorScheme.surfaceContainer,
53+
RoundedCornerShape(8.dp),
54+
).padding(16.dp),
55+
) {
56+
var text by remember { mutableStateOf(defaultText) }
57+
var label by remember { mutableStateOf("") }
58+
var isError by remember { mutableStateOf(false) }
59+
Text(
60+
text = title,
61+
style = MaterialTheme.typography.titleLarge,
62+
modifier = Modifier.fillMaxWidth(),
63+
textAlign = TextAlign.Center,
64+
)
65+
Spacer(modifier = Modifier.height(8.dp))
66+
TextField(
67+
value = text,
68+
onValueChange = { text = it },
69+
placeholder = { Text(placeholder) },
70+
isError = isError,
71+
label = { Text(label) },
72+
)
73+
Spacer(modifier = Modifier.height(8.dp))
74+
Row(
75+
modifier = Modifier.fillMaxWidth(),
76+
horizontalArrangement = Arrangement.SpaceAround,
77+
) {
78+
Button(onClick = {
79+
if (text.trim().isEmpty()) {
80+
label = "The field cannot be empty"
81+
isError = true
82+
} else {
83+
buttonOnClick(text)
84+
visible = false
85+
}
86+
}) {
87+
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
88+
Text(buttonText)
89+
}
90+
Button(onClick = { visible = false }) {
91+
Icon(Icons.Default.Close, contentDescription = null)
92+
Text(stringResource(R.string.dialog_err_close))
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
101+
fun createTextFieldDialog(
102+
dialogTitle: String,
103+
dialogDefaultText: String,
104+
dialogPlaceholder: String,
105+
dialogButtonText: String,
106+
onButtonClick: ((String) -> Unit),
107+
) {
108+
title = dialogTitle
109+
defaultText = dialogDefaultText
110+
placeholder = dialogPlaceholder
111+
buttonOnClick = onButtonClick
112+
buttonText = dialogButtonText
113+
textFieldDialogShowStatus.value = true
114+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.shubham0204.smollmandroid.ui.screens.chat
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.width
12+
import androidx.compose.foundation.lazy.LazyColumn
13+
import androidx.compose.foundation.lazy.items
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.material3.MaterialTheme
16+
import androidx.compose.material3.OutlinedButton
17+
import androidx.compose.material3.RadioButton
18+
import androidx.compose.material3.Surface
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableLongStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.res.stringResource
28+
import androidx.compose.ui.unit.dp
29+
import androidx.compose.ui.window.Dialog
30+
import io.shubham0204.smollmandroid.R
31+
import io.shubham0204.smollmandroid.data.Chat
32+
import io.shubham0204.smollmandroid.data.Folder
33+
34+
@Composable
35+
fun ChangeFolderDialogUI(
36+
onDismissRequest: () -> Unit,
37+
chat: Chat,
38+
folders: List<Folder>,
39+
onUpdateFolderId: (Long) -> Unit,
40+
) {
41+
val modifiedFolders = ArrayList(folders)
42+
modifiedFolders.add(0, Folder(id = -1L, name = "No Folder"))
43+
var selectedFolderId by remember { mutableLongStateOf(chat.folderId) }
44+
Surface {
45+
Dialog(onDismissRequest) {
46+
Column(
47+
modifier =
48+
Modifier
49+
.fillMaxWidth()
50+
.background(
51+
MaterialTheme.colorScheme.surfaceContainer,
52+
RoundedCornerShape(8.dp),
53+
).padding(16.dp),
54+
horizontalAlignment = Alignment.CenterHorizontally,
55+
) {
56+
Text(
57+
text = stringResource(R.string.dialog_select_folder_title),
58+
style = MaterialTheme.typography.titleMedium,
59+
)
60+
LazyColumn {
61+
items(modifiedFolders) { folder ->
62+
Row(
63+
modifier =
64+
Modifier
65+
.padding(8.dp)
66+
.fillMaxWidth()
67+
.clickable {
68+
selectedFolderId = folder.id
69+
onUpdateFolderId(folder.id)
70+
},
71+
verticalAlignment = Alignment.CenterVertically,
72+
) {
73+
RadioButton(selected = folder.id == selectedFolderId, onClick = null)
74+
Spacer(modifier = Modifier.width(4.dp))
75+
Text(folder.name)
76+
}
77+
}
78+
}
79+
Spacer(modifier = Modifier.height(4.dp))
80+
OutlinedButton(onClick = onDismissRequest) { Text(stringResource(R.string.dialog_err_close)) }
81+
}
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)