Skip to content

Commit f77064b

Browse files
committed
Split DuckAiChatStore.setPinned into pinChat / unpinChat
Intent-named methods read more naturally at call sites that already know which direction they're going, and dropping the Boolean return keeps the public contract free of JSON-validity concerns — not-found and malformed JSON are silent no-ops. The shared mutation logic stays as a private setPinned helper inside RealDuckAiChatStore. The repository keeps its boolean-toggle setPinned signature because its caller (ChatHistoryViewModel) computes the next state as a boolean; the impl dispatches to pinChat / unpinChat.
1 parent 2aea312 commit f77064b

3 files changed

Lines changed: 40 additions & 14 deletions

File tree

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/history/ChatHistoryRepository.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ class RealChatHistoryRepository @Inject constructor(
6565
withContext(dispatchers.io()) { chatStore.renameChat(chatId, newTitle) }
6666

6767
override suspend fun setPinned(chatId: String, pinned: Boolean) {
68-
withContext(dispatchers.io()) { chatStore.setPinned(chatId, pinned) }
68+
withContext(dispatchers.io()) {
69+
if (pinned) chatStore.pinChat(chatId) else chatStore.unpinChat(chatId)
70+
}
6971
}
7072

7173
private fun toChatHistoryItem(chat: DuckAiChat): ChatHistoryItem = ChatHistoryItem(

duckchat/duckchat-store/src/main/java/com/duckduckgo/duckchat/store/impl/RealDuckAiChatStore.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@ interface DuckAiChatStore {
6969
/** Renames [chatId] in the FE-owned JSON blob. Returns true if the chat existed and was updated, false otherwise. */
7070
suspend fun renameChat(chatId: String, newTitle: String): Boolean
7171

72-
/** Sets the pinned flag on [chatId]. Returns true if the chat existed and was updated, false otherwise. */
73-
suspend fun setPinned(chatId: String, pinned: Boolean): Boolean
72+
/** Pins [chatId]. Silent no-op if the chat is not found or the stored JSON is malformed. */
73+
suspend fun pinChat(chatId: String)
74+
75+
/** Unpins [chatId]. Silent no-op if the chat is not found or the stored JSON is malformed. */
76+
suspend fun unpinChat(chatId: String)
7477
}
7578

7679
@SingleInstanceIn(AppScope::class)
@@ -152,16 +155,20 @@ class RealDuckAiChatStore @Inject constructor(
152155
true
153156
}
154157

155-
override suspend fun setPinned(chatId: String, pinned: Boolean): Boolean =
158+
override suspend fun pinChat(chatId: String) = setPinned(chatId, pinned = true)
159+
160+
override suspend fun unpinChat(chatId: String) = setPinned(chatId, pinned = false)
161+
162+
private suspend fun setPinned(chatId: String, pinned: Boolean) {
156163
withContext(dispatchers.io()) {
157164
logcat { "DuckAI: RealDuckAiChatStore.setPinned($chatId, $pinned)" }
158-
val entity = chatsDao.getById(chatId) ?: return@withContext false
165+
val entity = chatsDao.getById(chatId) ?: return@withContext
159166
val updatedJson = runCatching {
160167
JSONObject(entity.data).put("pinned", pinned).toString()
161-
}.getOrNull() ?: return@withContext false
168+
}.getOrNull() ?: return@withContext
162169
chatsDao.upsert(entity.copy(data = updatedJson))
163-
true
164170
}
171+
}
165172

166173
private fun DuckAiBridgeChatEntity.toDuckAiChat(): DuckAiChat? = runCatching {
167174
val json = JSONObject(data)

duckchat/duckchat-store/src/test/kotlin/com/duckduckgo/duckchat/store/impl/RealDuckAiChatStoreTest.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,25 @@ class RealDuckAiChatStoreTest {
348348
verify(chatsDao).deleteAll()
349349
}
350350

351-
// --- setPinned ---
351+
// --- pinChat / unpinChat ---
352352

353353
@Test
354-
fun `setPinned returns false when chat not found`() = runTest {
354+
fun `pinChat is a no-op when chat not found`() = runTest {
355355
whenever(chatsDao.getById("missing")).thenReturn(null)
356356

357-
assertFalse(store.setPinned("missing", true))
357+
store.pinChat("missing")
358+
358359
verify(chatsDao, never()).upsert(any())
359360
}
360361

361362
@Test
362-
fun `setPinned updates only the pinned flag and preserves other JSON fields`() = runTest {
363+
fun `pinChat sets pinned to true and preserves other JSON fields`() = runTest {
363364
val originalJson = """
364365
{"chatId":"abc","title":"Old","model":"gpt-5-mini","lastEdit":"2026-04-01T21:31:54.260Z","pinned":false,"fileRefs":["uuid1"],"messages":[{"role":"user","text":"hi"}]}
365366
""".trimIndent()
366367
whenever(chatsDao.getById("abc")).thenReturn(DuckAiBridgeChatEntity("abc", originalJson))
367368

368-
assertTrue(store.setPinned("abc", true))
369+
store.pinChat("abc")
369370

370371
val entityCaptor = argumentCaptor<DuckAiBridgeChatEntity>()
371372
verify(chatsDao).upsert(entityCaptor.capture())
@@ -380,10 +381,26 @@ class RealDuckAiChatStoreTest {
380381
}
381382

382383
@Test
383-
fun `setPinned returns false when stored JSON is malformed`() = runTest {
384+
fun `unpinChat sets pinned to false`() = runTest {
385+
val originalJson = """
386+
{"chatId":"abc","title":"Old","model":"gpt-5-mini","lastEdit":"2026-04-01T21:31:54.260Z","pinned":true}
387+
""".trimIndent()
388+
whenever(chatsDao.getById("abc")).thenReturn(DuckAiBridgeChatEntity("abc", originalJson))
389+
390+
store.unpinChat("abc")
391+
392+
val entityCaptor = argumentCaptor<DuckAiBridgeChatEntity>()
393+
verify(chatsDao).upsert(entityCaptor.capture())
394+
val json = JSONObject(entityCaptor.firstValue.data)
395+
assertFalse(json.getBoolean("pinned"))
396+
}
397+
398+
@Test
399+
fun `pinChat is a no-op when stored JSON is malformed`() = runTest {
384400
whenever(chatsDao.getById("abc")).thenReturn(DuckAiBridgeChatEntity("abc", "not a json"))
385401

386-
assertFalse(store.setPinned("abc", true))
402+
store.pinChat("abc")
403+
387404
verify(chatsDao, never()).upsert(any())
388405
}
389406
}

0 commit comments

Comments
 (0)