From 5f02c242aa328c41d152e9e7e4b35b4f05f86fc2 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 19 Nov 2025 17:44:58 +0000 Subject: [PATCH 1/5] Refactor DuckChatSettingsViewModel to improve AI settings handling --- .../impl/ui/settings/DuckChatSettingsViewModel.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt index 86be6aaa43aa..76dcd76dbde4 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class DuckChatSettingsViewModel @AssistedInject constructor( @Assisted duckChatActivityParams: GlobalActivityStarter.ActivityParams, @@ -51,7 +52,7 @@ class DuckChatSettingsViewModel @AssistedInject constructor( private val pixel: Pixel, private val inputScreenDiscoveryFunnel: InputScreenDiscoveryFunnel, private val settingsPageFeature: SettingsPageFeature, - dispatcherProvider: DispatcherProvider, + private val dispatcherProvider: DispatcherProvider, ) : ViewModel() { private val commandChannel = Channel(capacity = 1, onBufferOverflow = DROP_OLDEST) val commands = commandChannel.receiveAsFlow() @@ -142,11 +143,15 @@ class DuckChatSettingsViewModel @AssistedInject constructor( fun duckChatSearchAISettingsClicked() { viewModelScope.launch { + val hideAiGeneratedImagesOptionEnabled = withContext(dispatcherProvider.io()) { + settingsPageFeature.hideAiGeneratedImagesOption().isEnabled() + } + if (settingsPageFeature.embeddedSettingsWebView().isEnabled()) { commandChannel.send( OpenLink( link = DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED, - titleRes = if (settingsPageFeature.hideAiGeneratedImagesOption().isEnabled()) { + titleRes = if (hideAiGeneratedImagesOptionEnabled) { R.string.duckAiSerpSettingsTitle } else { R.string.duck_chat_assist_settings_title From ec823061c23d10cf4d334be9fd0cf9736e2f8a6c Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 19 Nov 2025 17:46:54 +0000 Subject: [PATCH 2/5] Update embedded link for hiding AI generated images in DuckChatSettingsViewModel --- .../duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt index 76dcd76dbde4..fce08268e457 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt @@ -226,6 +226,6 @@ class DuckChatSettingsViewModel @AssistedInject constructor( const val DUCK_CHAT_LEARN_MORE_LINK = "https://duckduckgo.com/duckduckgo-help-pages/aichat/" const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK = "https://duckduckgo.com/settings?ko=-1#aifeatures" const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe#aifeatures" - const val DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbj#aifeatures" + const val DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbj&hideduckai=1#aifeatures" } } From cca8486ee6c2dd3801b0f81fc9e583f24d14d1ba Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 19 Nov 2025 17:54:08 +0000 Subject: [PATCH 3/5] Update AI settings link handling in DuckChatSettingsViewModel --- .../impl/ui/settings/DuckChatSettingsViewModel.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt index fce08268e457..7d75f2461606 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt @@ -150,7 +150,11 @@ class DuckChatSettingsViewModel @AssistedInject constructor( if (settingsPageFeature.embeddedSettingsWebView().isEnabled()) { commandChannel.send( OpenLink( - link = DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED, + link = if (hideAiGeneratedImagesOptionEnabled) { + DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED + } else { + LEGACY_DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED + }, titleRes = if (hideAiGeneratedImagesOptionEnabled) { R.string.duckAiSerpSettingsTitle } else { @@ -225,7 +229,8 @@ class DuckChatSettingsViewModel @AssistedInject constructor( companion object { const val DUCK_CHAT_LEARN_MORE_LINK = "https://duckduckgo.com/duckduckgo-help-pages/aichat/" const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK = "https://duckduckgo.com/settings?ko=-1#aifeatures" - const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe#aifeatures" + const val LEGACY_DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe#aifeatures" + const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe&hideduckai=1#aifeatures" const val DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbj&hideduckai=1#aifeatures" } } From 1baa5996e4a459af9a476f1f1c1eb204710cd48d Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 19 Nov 2025 21:47:52 +0000 Subject: [PATCH 4/5] Spotless --- .../duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt index 7d75f2461606..aadf338bfe67 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt @@ -230,7 +230,9 @@ class DuckChatSettingsViewModel @AssistedInject constructor( const val DUCK_CHAT_LEARN_MORE_LINK = "https://duckduckgo.com/duckduckgo-help-pages/aichat/" const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK = "https://duckduckgo.com/settings?ko=-1#aifeatures" const val LEGACY_DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe#aifeatures" - const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe&hideduckai=1#aifeatures" - const val DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED = "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbj&hideduckai=1#aifeatures" + const val DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED = + "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbe&hideduckai=1#aifeatures" + const val DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED = + "https://duckduckgo.com/settings?ko=-1&embedded=1&highlight=kbj&hideduckai=1#aifeatures" } } From 1800d4f72137d61da82826e9a781897908cd8d07 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 19 Nov 2025 22:41:22 +0000 Subject: [PATCH 5/5] Fix, add and update tests --- .../settings/DuckChatSettingsViewModelTest.kt | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModelTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModelTest.kt index 487f91f6b52e..8e3a44f447a7 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModelTest.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModelTest.kt @@ -285,10 +285,12 @@ class DuckChatSettingsViewModelTest { } @Test - fun whenDuckChatSearchAISettingsClickedAndSaveAndExitEnabledThenOpenSettingsLinkWithReturnParamEmitted() = + fun whenDuckChatSearchAISettingsClickedAndEmbeddedEnabledAndHideAiGeneratedImagesDisabledThenOpenSettingsLinkWithLegacyLink() = runTest { @Suppress("DenyListedApi") settingsPageFeature.embeddedSettingsWebView().setRawStoredState(State(enable = true)) + @Suppress("DenyListedApi") + settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = false)) testee.duckChatSearchAISettingsClicked() @@ -297,7 +299,7 @@ class DuckChatSettingsViewModelTest { assertTrue(command is OpenLink) command as OpenLink assertEquals( - DuckChatSettingsViewModel.DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED, + DuckChatSettingsViewModel.LEGACY_DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED, command.link, ) assertEquals(R.string.duck_chat_assist_settings_title, command.titleRes) @@ -305,6 +307,38 @@ class DuckChatSettingsViewModelTest { } } + @Test + fun whenDuckChatSearchAISettingsClickedAndEmbeddedEnabledAndHideAiGeneratedImagesEnabledThenOpenSettingsLinkWithNewLink() = + runTest { + @Suppress("DenyListedApi") + settingsPageFeature.embeddedSettingsWebView().setRawStoredState(State(enable = true)) + @Suppress("DenyListedApi") + settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = true)) + + testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, + duckChat = duckChat, + pixel = mockPixel, + inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, + settingsPageFeature = settingsPageFeature, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + testee.duckChatSearchAISettingsClicked() + + testee.commands.test { + val command = awaitItem() + assertTrue(command is OpenLink) + command as OpenLink + assertEquals( + DuckChatSettingsViewModel.DUCK_CHAT_SEARCH_AI_SETTINGS_LINK_EMBEDDED, + command.link, + ) + assertEquals(R.string.duckAiSerpSettingsTitle, command.titleRes) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `when onDuckChatUserEnabledToggled true then enabled pixel fired`() = runTest { @@ -557,4 +591,22 @@ class DuckChatSettingsViewModelTest { testee.onDuckAiHideAiGeneratedImagesClicked() verify(mockPixel).fire(DuckChatPixelName.SERP_SETTINGS_OPEN_HIDE_AI_GENERATED_IMAGES) } + + @Test + fun `when onDuckAiHideAiGeneratedImagesClicked then OpenLink command with correct link is emitted`() = + runTest { + testee.onDuckAiHideAiGeneratedImagesClicked() + + testee.commands.test { + val command = awaitItem() + assertTrue(command is OpenLink) + command as OpenLink + assertEquals( + DuckChatSettingsViewModel.DUCK_CHAT_HIDE_GENERATED_IMAGES_LINK_EMBEDDED, + command.link, + ) + assertEquals(R.string.duckAiSerpSettingsTitle, command.titleRes) + cancelAndIgnoreRemainingEvents() + } + } }