Skip to content

Commit 06c472f

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 2df6f97 commit 06c472f

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
@@ -67,8 +67,11 @@ interface DuckAiChatStore {
6767
/** Renames [chatId] in the FE-owned JSON blob. Returns true if the chat existed and was updated, false otherwise. */
6868
suspend fun renameChat(chatId: String, newTitle: String): Boolean
6969

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

7477
@SingleInstanceIn(AppScope::class)
@@ -146,16 +149,20 @@ class RealDuckAiChatStore @Inject constructor(
146149
true
147150
}
148151

149-
override suspend fun setPinned(chatId: String, pinned: Boolean): Boolean =
152+
override suspend fun pinChat(chatId: String) = setPinned(chatId, pinned = true)
153+
154+
override suspend fun unpinChat(chatId: String) = setPinned(chatId, pinned = false)
155+
156+
private suspend fun setPinned(chatId: String, pinned: Boolean) {
150157
withContext(dispatchers.io()) {
151158
logcat { "DuckAI: RealDuckAiChatStore.setPinned($chatId, $pinned)" }
152-
val entity = chatsDao.getById(chatId) ?: return@withContext false
159+
val entity = chatsDao.getById(chatId) ?: return@withContext
153160
val updatedJson = runCatching {
154161
JSONObject(entity.data).put("pinned", pinned).toString()
155-
}.getOrNull() ?: return@withContext false
162+
}.getOrNull() ?: return@withContext
156163
chatsDao.upsert(entity.copy(data = updatedJson))
157-
true
158164
}
165+
}
159166

160167
private fun DuckAiBridgeChatEntity.toDuckAiChat(): DuckAiChat? = runCatching {
161168
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
@@ -337,24 +337,25 @@ class RealDuckAiChatStoreTest {
337337
verify(chatsDao).deleteAll()
338338
}
339339

340-
// --- setPinned ---
340+
// --- pinChat / unpinChat ---
341341

342342
@Test
343-
fun `setPinned returns false when chat not found`() = runTest {
343+
fun `pinChat is a no-op when chat not found`() = runTest {
344344
whenever(chatsDao.getById("missing")).thenReturn(null)
345345

346-
assertFalse(store.setPinned("missing", true))
346+
store.pinChat("missing")
347+
347348
verify(chatsDao, never()).upsert(any())
348349
}
349350

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

357-
assertTrue(store.setPinned("abc", true))
358+
store.pinChat("abc")
358359

359360
val entityCaptor = argumentCaptor<DuckAiBridgeChatEntity>()
360361
verify(chatsDao).upsert(entityCaptor.capture())
@@ -369,10 +370,26 @@ class RealDuckAiChatStoreTest {
369370
}
370371

371372
@Test
372-
fun `setPinned returns false when stored JSON is malformed`() = runTest {
373+
fun `unpinChat sets pinned to false`() = runTest {
374+
val originalJson = """
375+
{"chatId":"abc","title":"Old","model":"gpt-5-mini","lastEdit":"2026-04-01T21:31:54.260Z","pinned":true}
376+
""".trimIndent()
377+
whenever(chatsDao.getById("abc")).thenReturn(DuckAiBridgeChatEntity("abc", originalJson))
378+
379+
store.unpinChat("abc")
380+
381+
val entityCaptor = argumentCaptor<DuckAiBridgeChatEntity>()
382+
verify(chatsDao).upsert(entityCaptor.capture())
383+
val json = JSONObject(entityCaptor.firstValue.data)
384+
assertFalse(json.getBoolean("pinned"))
385+
}
386+
387+
@Test
388+
fun `pinChat is a no-op when stored JSON is malformed`() = runTest {
373389
whenever(chatsDao.getById("abc")).thenReturn(DuckAiBridgeChatEntity("abc", "not a json"))
374390

375-
assertFalse(store.setPinned("abc", true))
391+
store.pinChat("abc")
392+
376393
verify(chatsDao, never()).upsert(any())
377394
}
378395
}

0 commit comments

Comments
 (0)