Skip to content

Commit a8d8e67

Browse files
authored
SERP Settings Sync: Update hide ai generated icon (#7027)
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211738764657607?focus=true ### Description - Moved the "Hide AI Generated Images" option visibility logic from the Activity to the ViewModel - Updated the AI image icon to use the new `ic_image_ai_hide_24` icon ### Steps to test this PR [Designs](https://www.figma.com/design/Oj9gZAXk4jpzhfWlDmq8v9/AI---Search-Settings-into-Browser-Settings?node-id=5810-388854&t=1gInBCWBRKkYqHHb-4) _Hide AI-Generated Images Option_ - [x] Enable the feature toggle `hideAiGeneratedImagesOption` - [x] Open AI Features settings - [x] Verify the "Hide AI-Generated Images" option is visible with the **updated icon** - [x] Switch to dark theme and check it matches "Search Assist Settings" in terms of theming - [x] Tap on the option and verify it opens the correct embedded settings page with "Hide AI generated images" focused at the top of the screen _Hide AI-Generated Images In_ _Prod_ Prerequisite: You should delete `@Toggle.InternalAlwaysEnabled` from the `hideAiGeneratedImagesOption` feature toggle so you can toggle it in app - [x] Disable the feature toggle `hideAiGeneratedImagesOption` - [x] Open AI Features settings - [x] Verify the "Hide AI-Generated Images" option is not visible ### UI changes | Before | After | | --- | --- | | <img width="1080" height="2424" alt="Screenshot_20251029_173901" src="https://github.com/user-attachments/assets/efc2cc24-5eff-4e0e-916a-8734191f9bd4" /> | <img width="1080" height="2424" alt="Screenshot_20251029_172331" src="https://github.com/user-attachments/assets/0ac8a3b7-d9a8-4e98-8e89-6ff0af55e05d" /> | | <img width="1080" height="2424" alt="Screenshot_20251029_173909" src="https://github.com/user-attachments/assets/c2ed33cd-1835-43ed-988b-c681839354e1" /> | <img width="1080" height="2424" alt="Screenshot_20251029_172323" src="https://github.com/user-attachments/assets/cc1b9df4-537e-4892-a379-837a5e5d643c" /> |
1 parent f9de3fb commit a8d8e67

File tree

6 files changed

+117
-20
lines changed

6 files changed

+117
-20
lines changed

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class DuckChatSettingsActivity : DuckDuckGoActivity() {
171171
viewModel.duckChatSearchAISettingsClicked()
172172
}
173173

174-
if (settingsPageFeature.hideAiGeneratedImagesOption().isEnabled()) {
174+
if (viewState.isHideGeneratedImagesOptionVisible) {
175175
binding.searchSettingsSectionHeader.isVisible = true
176176
binding.duckAiHideAiGeneratedImagesLink.apply {
177177
isVisible = true

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
2121
import androidx.lifecycle.viewModelScope
2222
import com.duckduckgo.anvil.annotations.ContributesViewModel
2323
import com.duckduckgo.app.statistics.pixels.Pixel
24+
import com.duckduckgo.common.utils.DispatcherProvider
2425
import com.duckduckgo.di.scopes.ActivityScope
2526
import com.duckduckgo.duckchat.impl.DuckChatInternal
2627
import com.duckduckgo.duckchat.impl.R
@@ -34,6 +35,8 @@ import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
3435
import kotlinx.coroutines.channels.Channel
3536
import kotlinx.coroutines.flow.SharingStarted
3637
import kotlinx.coroutines.flow.combine
38+
import kotlinx.coroutines.flow.flowOf
39+
import kotlinx.coroutines.flow.flowOn
3740
import kotlinx.coroutines.flow.receiveAsFlow
3841
import kotlinx.coroutines.flow.stateIn
3942
import kotlinx.coroutines.launch
@@ -45,6 +48,7 @@ class DuckChatSettingsViewModel @Inject constructor(
4548
private val pixel: Pixel,
4649
private val inputScreenDiscoveryFunnel: InputScreenDiscoveryFunnel,
4750
private val settingsPageFeature: SettingsPageFeature,
51+
dispatcherProvider: DispatcherProvider,
4852
) : ViewModel() {
4953
private val commandChannel = Channel<Command>(capacity = 1, onBufferOverflow = DROP_OLDEST)
5054
val commands = commandChannel.receiveAsFlow()
@@ -54,18 +58,21 @@ class DuckChatSettingsViewModel @Inject constructor(
5458
val isInputScreenEnabled: Boolean = false,
5559
val shouldShowShortcuts: Boolean = false,
5660
val shouldShowInputScreenToggle: Boolean = false,
61+
val isHideGeneratedImagesOptionVisible: Boolean = false,
5762
)
5863

5964
val viewState =
6065
combine(
6166
duckChat.observeEnableDuckChatUserSetting(),
6267
duckChat.observeInputScreenUserSettingEnabled(),
63-
) { isDuckChatUserEnabled, isInputScreenEnabled ->
68+
flowOf(settingsPageFeature.hideAiGeneratedImagesOption().isEnabled()).flowOn(dispatcherProvider.io()),
69+
) { isDuckChatUserEnabled, isInputScreenEnabled, isHideAiGeneratedImagesOptionVisible ->
6470
ViewState(
6571
isDuckChatUserEnabled = isDuckChatUserEnabled,
6672
isInputScreenEnabled = isInputScreenEnabled,
6773
shouldShowShortcuts = isDuckChatUserEnabled,
6874
shouldShowInputScreenToggle = isDuckChatUserEnabled && duckChat.isInputScreenFeatureAvailable(),
75+
isHideGeneratedImagesOptionVisible = isHideAiGeneratedImagesOptionVisible,
6976
)
7077
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ViewState())
7178

duckchat/duckchat-impl/src/main/res/drawable/ic_image_ai.xml

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
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+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:tools="http://schemas.android.com/tools"
19+
android:width="24dp"
20+
android:height="24dp"
21+
android:viewportWidth="24"
22+
android:viewportHeight="24">
23+
<path
24+
android:fillColor="?attr/daxColorPrimaryIcon"
25+
android:pathData="M17.5,13C19.985,13 22,15.015 22,17.5C22,19.985 19.985,22 17.5,22C15.015,22 13,19.985 13,17.5C13,15.015 15.015,13 17.5,13ZM15.481,3C15.135,3.611 14.549,4.066 13.848,4.242L13.547,4.317C13.371,4.361 13.211,4.423 13.066,4.5H7C5.067,4.5 3.5,6.067 3.5,8V16C3.5,16.225 3.521,16.444 3.562,16.657L7.94,12.28C8.233,11.987 8.708,11.987 9,12.28L11.532,14.811L15.833,10.51C15.962,10.978 16.222,11.333 16.549,11.575C13.872,12.001 11.785,14.198 11.527,16.927L8.47,13.87L4.218,18.123C4.857,18.96 5.865,19.5 7,19.5H11.842C11.868,19.574 11.896,19.647 11.925,19.72C12.151,20.288 11.774,21 11.163,21H7C4.239,21 2,18.761 2,16V8C2,5.239 4.239,3 7,3H15.481ZM15.25,16.75C14.974,16.75 14.75,16.974 14.75,17.25V17.75C14.75,18.026 14.974,18.25 15.25,18.25H19.75C20.026,18.25 20.25,18.026 20.25,17.75V17.25C20.25,16.974 20.026,16.75 19.75,16.75H15.25ZM22,11.584C22,12.263 21.087,12.643 20.5,12.303V8.887C20.818,8.588 21.212,8.368 21.652,8.259L21.953,8.183C21.969,8.179 21.985,8.174 22,8.17V11.584ZM17.272,2.41C17.397,1.913 18.103,1.913 18.228,2.41L18.303,2.712C18.67,4.182 19.818,5.329 21.288,5.697L21.59,5.772C22.087,5.897 22.087,6.603 21.59,6.728L21.288,6.803C19.818,7.17 18.67,8.318 18.303,9.788L18.228,10.09C18.103,10.587 17.397,10.587 17.272,10.09L17.197,9.788C16.83,8.318 15.682,7.17 14.212,6.803L13.91,6.728C13.413,6.603 13.413,5.897 13.91,5.772L14.212,5.697C15.682,5.329 16.83,4.182 17.197,2.712L17.272,2.41ZM8.5,7C9.328,7 10,7.672 10,8.5C10,9.328 9.328,10 8.5,10C7.672,10 7,9.328 7,8.5C7,7.672 7.672,7 8.5,7Z"
26+
tools:fillColor="#000000" />
27+
</vector>

duckchat/duckchat-impl/src/main/res/layout/activity_duck_chat_settings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@
233233
android:layout_width="match_parent"
234234
android:layout_height="wrap_content"
235235
android:visibility="gone"
236-
app:leadingIcon="@drawable/ic_image_ai"
236+
app:leadingIcon="@drawable/ic_image_ai_hide_24"
237237
app:leadingIconBackground="circular"
238238
app:primaryText="@string/duckAiHideAiGeneratedImagesTitle"
239239
app:secondaryText="@string/duckAiHideAiGeneratedImagesDescription"

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsViewModelTest.kt

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ class DuckChatSettingsViewModelTest {
6363
whenever(duckChat.observeShowInBrowserMenuUserSetting()).thenReturn(flowOf(false))
6464
whenever(duckChat.observeShowInAddressBarUserSetting()).thenReturn(flowOf(false))
6565
whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(false))
66-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
66+
testee = DuckChatSettingsViewModel(
67+
duckChat = duckChat,
68+
pixel = mockPixel,
69+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
70+
settingsPageFeature = settingsPageFeature,
71+
dispatcherProvider = coroutineRule.testDispatcherProvider,
72+
)
6773
}
6874

6975
@Test
@@ -140,7 +146,13 @@ class DuckChatSettingsViewModelTest {
140146
fun `input screen - user preference enabled then set correct state`() =
141147
runTest {
142148
whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true))
143-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
149+
testee = DuckChatSettingsViewModel(
150+
duckChat = duckChat,
151+
pixel = mockPixel,
152+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
153+
settingsPageFeature = settingsPageFeature,
154+
dispatcherProvider = coroutineRule.testDispatcherProvider,
155+
)
144156

145157
testee.viewState.test {
146158
assertTrue(awaitItem().isInputScreenEnabled)
@@ -151,7 +163,13 @@ class DuckChatSettingsViewModelTest {
151163
fun `input screen - user preference disabled then set correct state`() =
152164
runTest {
153165
whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(false))
154-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
166+
testee = DuckChatSettingsViewModel(
167+
duckChat = duckChat,
168+
pixel = mockPixel,
169+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
170+
settingsPageFeature = settingsPageFeature,
171+
dispatcherProvider = coroutineRule.testDispatcherProvider,
172+
)
155173

156174
testee.viewState.test {
157175
assertFalse(awaitItem().isInputScreenEnabled)
@@ -163,7 +181,13 @@ class DuckChatSettingsViewModelTest {
163181
runTest {
164182
whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(true))
165183
whenever(duckChat.isInputScreenFeatureAvailable()).thenReturn(true)
166-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
184+
testee = DuckChatSettingsViewModel(
185+
duckChat = duckChat,
186+
pixel = mockPixel,
187+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
188+
settingsPageFeature = settingsPageFeature,
189+
dispatcherProvider = coroutineRule.testDispatcherProvider,
190+
)
167191

168192
testee.viewState.test {
169193
val state = awaitItem()
@@ -176,7 +200,13 @@ class DuckChatSettingsViewModelTest {
176200
runTest {
177201
whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(true))
178202
whenever(duckChat.isInputScreenFeatureAvailable()).thenReturn(false)
179-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
203+
testee = DuckChatSettingsViewModel(
204+
duckChat = duckChat,
205+
pixel = mockPixel,
206+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
207+
settingsPageFeature = settingsPageFeature,
208+
dispatcherProvider = coroutineRule.testDispatcherProvider,
209+
)
180210

181211
testee.viewState.test {
182212
val state = awaitItem()
@@ -189,7 +219,13 @@ class DuckChatSettingsViewModelTest {
189219
runTest {
190220
whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(false))
191221
whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true))
192-
testee = DuckChatSettingsViewModel(duckChat, mockPixel, mockInputScreenDiscoveryFunnel, settingsPageFeature)
222+
testee = DuckChatSettingsViewModel(
223+
duckChat = duckChat,
224+
pixel = mockPixel,
225+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
226+
settingsPageFeature = settingsPageFeature,
227+
dispatcherProvider = coroutineRule.testDispatcherProvider,
228+
)
193229

194230
testee.viewState.test {
195231
val state = awaitItem()
@@ -338,4 +374,42 @@ class DuckChatSettingsViewModelTest {
338374
cancelAndIgnoreRemainingEvents()
339375
}
340376
}
377+
378+
@Test
379+
fun `when hideAiGeneratedImagesOption is enabled then viewState shows option visible`() =
380+
runTest {
381+
@Suppress("DenyListedApi")
382+
settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = true))
383+
testee = DuckChatSettingsViewModel(
384+
duckChat = duckChat,
385+
pixel = mockPixel,
386+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
387+
settingsPageFeature = settingsPageFeature,
388+
dispatcherProvider = coroutineRule.testDispatcherProvider,
389+
)
390+
391+
testee.viewState.test {
392+
val state = awaitItem()
393+
assertTrue(state.isHideGeneratedImagesOptionVisible)
394+
}
395+
}
396+
397+
@Test
398+
fun `when hideAiGeneratedImagesOption is disabled then viewState shows option not visible`() =
399+
runTest {
400+
@Suppress("DenyListedApi")
401+
settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = false))
402+
testee = DuckChatSettingsViewModel(
403+
duckChat = duckChat,
404+
pixel = mockPixel,
405+
inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel,
406+
settingsPageFeature = settingsPageFeature,
407+
dispatcherProvider = coroutineRule.testDispatcherProvider,
408+
)
409+
410+
testee.viewState.test {
411+
val state = awaitItem()
412+
assertFalse(state.isHideGeneratedImagesOptionVisible)
413+
}
414+
}
341415
}

0 commit comments

Comments
 (0)