Skip to content

Commit 8ec98a6

Browse files
joshliebedaxmobile
andauthored
Add Duck.ai main toggle (#6002)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1210145045643977?focus=true ### Description - Adds a setting to turn off all Duck.ai browser integrations. - Enables the `DuckChatSettingsActivity` to be opened via RMF. - Shows the Duck.ai icon on the “Website view” for the visual design updates. ### Steps to test this PR _Change the remote config endpoint to the one linked in the task_ - [ ] Install from this branch - [ ] Verify that the RMF shown in the task is visible - [ ] Tap “View Duck.ai settings" - [ ] Disable Duck.ai - [ ] Verify that Duck.ai is disabled - [ ] Enable Duck.ai and turn on visual updates - [ ] Verify that Duck.ai is shown on the “website screen" ### UI changes ![combined](https://github.com/user-attachments/assets/ccb2d7a8-502e-41ab-a79c-35fecb470af0) ![combined1](https://github.com/user-attachments/assets/8aecea18-441d-471f-8250-183b4a2800d5) --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210158757002115 --------- Co-authored-by: Dax The Translator <[email protected]>
1 parent 765cc75 commit 8ec98a6

File tree

42 files changed

+675
-140
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+675
-140
lines changed

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
11191119
binding.bottomMockupToolbar.appBarLayoutMockup.gone()
11201120
binding.topMockupToolbar.appBarLayoutMockup.gone()
11211121

1122-
if (!duckChat.showInAddressBar()) {
1122+
if (!duckChat.showInAddressBar.value) {
11231123
experimentalToolbarMockupBinding.aiChatIconMockup.isVisible = false
11241124
}
11251125
} else {
@@ -1128,7 +1128,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
11281128
binding.topMockupToolbar.appBarLayoutMockup.gone()
11291129
binding.bottomMockupToolbar.appBarLayoutMockup.gone()
11301130

1131-
if (!duckChat.showInAddressBar()) {
1131+
if (!duckChat.showInAddressBar.value) {
11321132
experimentalToolbarMockupBottomBinding.aiChatIconMockup.isVisible = false
11331133
}
11341134
}
@@ -1149,7 +1149,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
11491149
}
11501150
}
11511151

1152-
if (!duckChat.showInAddressBar()) {
1152+
if (!duckChat.showInAddressBar.value) {
11531153
toolbarMockupBinding.aiChatIconMenuMockup.isVisible = false
11541154
}
11551155
}

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ open class OmnibarLayout @JvmOverloads constructor(
620620
showFireIcon = viewState.showFireIcon,
621621
showBrowserMenu = viewState.showBrowserMenu,
622622
showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
623-
showChatMenu = duckChat.showInAddressBar() && (viewState.showChatMenu || viewState.viewMode is NewTab),
623+
showChatMenu = viewState.showChatMenu,
624624
showSpacer = viewState.showClearButton || viewState.showVoiceSearch,
625625
)
626626

@@ -689,7 +689,7 @@ open class OmnibarLayout @JvmOverloads constructor(
689689
}
690690

691691
private fun renderHint(viewState: ViewState) {
692-
if (viewState.viewMode is NewTab && duckChat.showInAddressBar()) {
692+
if (viewState.viewMode is NewTab && duckChat.showInAddressBar.value) {
693693
omnibarTextInput.hint = context.getString(R.string.search)
694694
} else {
695695
omnibarTextInput.hint = context.getString(R.string.omnibarInputHint)

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import com.duckduckgo.browser.api.UserBrowserProperties
5656
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
5757
import com.duckduckgo.common.utils.DispatcherProvider
5858
import com.duckduckgo.di.scopes.FragmentScope
59+
import com.duckduckgo.duckchat.api.DuckChat
5960
import com.duckduckgo.duckplayer.api.DuckPlayer
6061
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
6162
import com.duckduckgo.voice.api.VoiceSearchAvailability
@@ -87,23 +88,31 @@ class OmnibarLayoutViewModel @Inject constructor(
8788
private val defaultBrowserPromptsExperiment: DefaultBrowserPromptsExperiment,
8889
visualDesignExperimentDataStore: VisualDesignExperimentDataStore,
8990
private val senseOfProtectionExperiment: SenseOfProtectionExperiment,
91+
private val duckChat: DuckChat,
9092
) : ViewModel() {
9193

92-
private val _viewState = MutableStateFlow(ViewState())
94+
private val _viewState = MutableStateFlow(
95+
ViewState(
96+
showChatMenu = duckChat.showInAddressBar.value,
97+
),
98+
)
9399
val viewState = combine(
94100
_viewState,
95101
tabRepository.flowTabs,
96102
defaultBrowserPromptsExperiment.highlightPopupMenu,
97103
visualDesignExperimentDataStore.experimentState,
98-
) { state, tabs, highlightOverflowMenu, visualDesignExperiment ->
104+
duckChat.showInAddressBar,
105+
) { state, tabs, highlightOverflowMenu, visualDesignExperiment, showInAddressBar ->
99106
state.copy(
100107
shouldUpdateTabsCount = tabs.size != state.tabCount && tabs.isNotEmpty(),
101108
tabCount = tabs.size,
102109
hasUnreadTabs = tabs.firstOrNull { !it.viewed } != null,
103110
showBrowserMenuHighlight = highlightOverflowMenu,
104111
isVisualDesignExperimentEnabled = visualDesignExperiment.isEnabled,
112+
showChatMenu = showInAddressBar && state.viewMode !is CustomTab &&
113+
(state.viewMode is NewTab || state.hasFocus && state.omnibarText.isNotBlank() || visualDesignExperiment.isEnabled),
105114
)
106-
}.flowOn(dispatcherProvider.io()).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), ViewState())
115+
}.flowOn(dispatcherProvider.io()).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), _viewState.value)
107116

108117
private val command = Channel<Command>(1, DROP_OLDEST)
109118
fun commands(): Flow<Command> = command.receiveAsFlow()
@@ -188,7 +197,6 @@ class OmnibarLayoutViewModel @Inject constructor(
188197
showTabsMenu = showControls,
189198
showFireIcon = showControls,
190199
showBrowserMenu = showControls,
191-
showChatMenu = !showControls,
192200
showVoiceSearch = shouldShowVoiceSearch(
193201
hasFocus = true,
194202
query = _viewState.value.omnibarText,
@@ -222,7 +230,6 @@ class OmnibarLayoutViewModel @Inject constructor(
222230
showTabsMenu = true,
223231
showFireIcon = true,
224232
showBrowserMenu = true,
225-
showChatMenu = false,
226233
showVoiceSearch = shouldShowVoiceSearch(
227234
hasFocus = false,
228235
query = _viewState.value.omnibarText,
@@ -319,7 +326,6 @@ class OmnibarLayoutViewModel @Inject constructor(
319326
showBrowserMenu = true,
320327
showTabsMenu = false,
321328
showFireIcon = false,
322-
showChatMenu = false,
323329
)
324330
}
325331
}
@@ -382,7 +388,6 @@ class OmnibarLayoutViewModel @Inject constructor(
382388
showBrowserMenu = showControls,
383389
showTabsMenu = showControls,
384390
showFireIcon = showControls,
385-
showChatMenu = false,
386391
)
387392
}
388393
}
@@ -462,7 +467,6 @@ class OmnibarLayoutViewModel @Inject constructor(
462467
showBrowserMenu = showControls,
463468
showTabsMenu = showControls,
464469
showFireIcon = showControls,
465-
showChatMenu = !showControls,
466470
showClearButton = showClearButton,
467471
showVoiceSearch = shouldShowVoiceSearch(
468472
hasFocus = hasFocus,

app/src/main/java/com/duckduckgo/app/browser/omnibar/experiments/FadeOmnibarLayout.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,8 @@ class FadeOmnibarLayout @JvmOverloads constructor(
187187
voiceSearchButton.isVisible = viewState.showVoiceSearch
188188
spacer.isVisible = false
189189

190-
val showAiChat = shouldShowExperimentalAIChatButton(viewState)
191-
aiChatMenu?.isVisible = showAiChat
192-
aiChatDivider.isVisible = (viewState.showVoiceSearch || viewState.showClearButton) && showAiChat
190+
aiChatMenu?.isVisible = viewState.showChatMenu
191+
aiChatDivider.isVisible = (viewState.showVoiceSearch || viewState.showClearButton) && viewState.showChatMenu
193192

194193
val showBackArrow = viewState.hasFocus
195194
if (showBackArrow) {
@@ -204,10 +203,6 @@ class FadeOmnibarLayout @JvmOverloads constructor(
204203
}
205204
}
206205

207-
private fun shouldShowExperimentalAIChatButton(viewState: ViewState): Boolean {
208-
return duckChat.showInAddressBar() && (viewState.hasFocus || viewState.viewMode is ViewMode.NewTab)
209-
}
210-
211206
/**
212207
* In focused state the Omnibar card will grow 4dp in each direction, where 2dp of that will be taken by the card's outline.
213208
* The growth is achieved by decreasing the card's margins.

app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.duckduckgo.browser.api.UserBrowserProperties
3131
import com.duckduckgo.common.test.CoroutineTestRule
3232
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
3333
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore.FeatureState
34+
import com.duckduckgo.duckchat.api.DuckChat
3435
import com.duckduckgo.duckplayer.api.DuckPlayer
3536
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
3637
import com.duckduckgo.voice.api.VoiceSearchAvailability
@@ -71,6 +72,8 @@ class OmnibarLayoutViewModelTest {
7172
private val defaultBrowserPromptsExperiment: DefaultBrowserPromptsExperiment = mock()
7273

7374
private val mockSenseOfProtectionExperiment: SenseOfProtectionExperiment = mock()
75+
private val duckChat: DuckChat = mock()
76+
private val duckChatShowInAddressBarFlow = MutableStateFlow(true)
7477

7578
private lateinit var testee: OmnibarLayoutViewModel
7679

@@ -87,6 +90,7 @@ class OmnibarLayoutViewModelTest {
8790
whenever(voiceSearchAvailability.shouldShowVoiceSearch(any(), any(), any(), any())).thenReturn(true)
8891
whenever(duckPlayer.isDuckPlayerUri(DUCK_PLAYER_URL)).thenReturn(true)
8992
whenever(mockVisualDesignExperimentDataStore.experimentState).thenReturn(defaultVisualExperimentNavBarStateFlow)
93+
whenever(duckChat.showInAddressBar).thenReturn(duckChatShowInAddressBarFlow)
9094

9195
initializeViewModel()
9296
}
@@ -125,6 +129,7 @@ class OmnibarLayoutViewModelTest {
125129
defaultBrowserPromptsExperiment = defaultBrowserPromptsExperiment,
126130
visualDesignExperimentDataStore = mockVisualDesignExperimentDataStore,
127131
senseOfProtectionExperiment = mockSenseOfProtectionExperiment,
132+
duckChat = duckChat,
128133
)
129134
}
130135

@@ -1018,25 +1023,49 @@ class OmnibarLayoutViewModelTest {
10181023
}
10191024

10201025
@Test
1021-
fun whenViewModelAttachedThenShowChatMenuTrue() = runTest {
1026+
fun whenViewModelAttachedThenShowChatMenuFalse() = runTest {
1027+
testee.viewState.test {
1028+
val viewState = awaitItem()
1029+
assertFalse(viewState.showChatMenu)
1030+
}
1031+
}
1032+
1033+
@Test
1034+
fun whenViewModeIsNewTabThenShowChatMenuTrue() = runTest {
1035+
testee.onViewModeChanged(ViewMode.NewTab)
1036+
10221037
testee.viewState.test {
10231038
val viewState = awaitItem()
10241039
assertTrue(viewState.showChatMenu)
10251040
}
10261041
}
10271042

10281043
@Test
1029-
fun whenOmnibarFocusedAndQueryNotBlankThenShowChatMenuTrue() = runTest {
1030-
testee.onOmnibarFocusChanged(true, QUERY)
1044+
fun whenViewModeIsNewTabAndChatEntryPointDisabledThenShowChatMenuFalse() = runTest {
1045+
duckChatShowInAddressBarFlow.value = false
1046+
testee.onViewModeChanged(ViewMode.NewTab)
1047+
1048+
testee.viewState.test {
1049+
val viewState = awaitItem()
1050+
assertFalse(viewState.showChatMenu)
1051+
}
1052+
}
1053+
1054+
@Test
1055+
fun whenNavigationBarExperimentEnabledThenShowChatMenuTrue() = runTest {
1056+
defaultVisualExperimentNavBarStateFlow.value = FeatureState(isAvailable = true, isEnabled = true)
1057+
10311058
testee.viewState.test {
10321059
val viewState = awaitItem()
10331060
assertTrue(viewState.showChatMenu)
10341061
}
10351062
}
10361063

10371064
@Test
1038-
fun whenOmnibarFocusedAndQueryBlankThenShowChatMenuFalse() = runTest {
1039-
testee.onOmnibarFocusChanged(true, "")
1065+
fun whenNavigationBarExperimentEnabledAndChatEntryPointDisabledThenShowChatMenuFalse() = runTest {
1066+
duckChatShowInAddressBarFlow.value = false
1067+
defaultVisualExperimentNavBarStateFlow.value = FeatureState(isAvailable = true, isEnabled = true)
1068+
10401069
testee.viewState.test {
10411070
val viewState = awaitItem()
10421071
assertFalse(viewState.showChatMenu)

duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@ interface DuckChat {
4040

4141
/**
4242
* Checks whether DuckChat should be shown in address bar based on user settings.
43-
* Uses cached values - does not perform disk I/O.
4443
*
4544
* @return true if DuckChat should be shown, false otherwise.
4645
*/
47-
fun showInAddressBar(): Boolean
46+
val showInAddressBar: StateFlow<Boolean>
4847

4948
/**
5049
* Opens the DuckChat WebView with optional pre-filled [String] query.

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ import kotlinx.coroutines.launch
5454
import kotlinx.coroutines.withContext
5555

5656
interface DuckChatInternal : DuckChat {
57+
/**
58+
* Set user setting to determine whether DuckChat should be enabled or disabled.
59+
*/
60+
suspend fun setEnableDuckChatUserSetting(enabled: Boolean)
61+
5762
/**
5863
* Set user setting to determine whether DuckChat should be shown in browser menu.
5964
*/
@@ -64,6 +69,11 @@ interface DuckChatInternal : DuckChat {
6469
*/
6570
suspend fun setShowInAddressBarUserSetting(showDuckChat: Boolean)
6671

72+
/**
73+
* Observes whether DuckChat is user enabled or disabled.
74+
*/
75+
fun observeEnableDuckChatUserSetting(): Flow<Boolean>
76+
6777
/**
6878
* Observes whether DuckChat should be shown in browser menu based on user settings only.
6979
*/
@@ -93,6 +103,11 @@ interface DuckChatInternal : DuckChat {
93103
* Returns whether address bar entry point is enabled or not.
94104
*/
95105
fun isAddressBarEntryPointEnabled(): Boolean
106+
107+
/**
108+
* Returns whether DuckChat is user enabled or not.
109+
*/
110+
fun isDuckChatUserEnabled(): Boolean
96111
}
97112

98113
data class DuckChatSettingJson(
@@ -121,13 +136,14 @@ class RealDuckChat @Inject constructor(
121136

122137
private val closeChatFlow = MutableSharedFlow<Unit>(replay = 0)
123138
private val _showInBrowserMenu = MutableStateFlow(false)
139+
private val _showInAddressBar = MutableStateFlow(false)
124140

125141
private val jsonAdapter: JsonAdapter<DuckChatSettingJson> by lazy {
126142
moshi.adapter(DuckChatSettingJson::class.java)
127143
}
128144

129145
private var isDuckChatEnabled = false
130-
private var showInAddressBar = false
146+
private var isDuckChatUserEnabled = false
131147
private var duckChatLink = DUCK_CHAT_WEB_LINK
132148
private var bangRegex: Regex? = null
133149
private var isAddressBarEntryPointEnabled: Boolean = false
@@ -142,6 +158,16 @@ class RealDuckChat @Inject constructor(
142158
cacheConfig()
143159
}
144160

161+
override suspend fun setEnableDuckChatUserSetting(enabled: Boolean) {
162+
if (enabled) {
163+
pixel.fire(DuckChatPixelName.DUCK_CHAT_USER_ENABLED)
164+
} else {
165+
pixel.fire(DuckChatPixelName.DUCK_CHAT_USER_DISABLED)
166+
}
167+
duckChatFeatureRepository.setDuckChatUserEnabled(enabled)
168+
cacheUserSettings()
169+
}
170+
145171
override suspend fun setShowInBrowserMenuUserSetting(showDuckChat: Boolean) = withContext(dispatchers.io()) {
146172
if (showDuckChat) {
147173
pixel.fire(DuckChatPixelName.DUCK_CHAT_MENU_SETTING_ON)
@@ -166,6 +192,10 @@ class RealDuckChat @Inject constructor(
166192
return isDuckChatEnabled
167193
}
168194

195+
override fun observeEnableDuckChatUserSetting(): Flow<Boolean> {
196+
return duckChatFeatureRepository.observeDuckChatUserEnabled()
197+
}
198+
169199
override fun observeShowInBrowserMenuUserSetting(): Flow<Boolean> {
170200
return duckChatFeatureRepository.observeShowInBrowserMenu()
171201
}
@@ -201,11 +231,13 @@ class RealDuckChat @Inject constructor(
201231
return isAddressBarEntryPointEnabled
202232
}
203233

234+
override fun isDuckChatUserEnabled(): Boolean {
235+
return isDuckChatUserEnabled
236+
}
237+
204238
override val showInBrowserMenu: StateFlow<Boolean> get() = _showInBrowserMenu.asStateFlow()
205239

206-
override fun showInAddressBar(): Boolean {
207-
return showInAddressBar && isAddressBarEntryPointEnabled
208-
}
240+
override val showInAddressBar: StateFlow<Boolean> get() = _showInAddressBar.asStateFlow()
209241

210242
override fun openDuckChat(query: String?) {
211243
val parameters = query?.let { originalQuery ->
@@ -320,10 +352,15 @@ class RealDuckChat @Inject constructor(
320352
}
321353

322354
private suspend fun cacheUserSettings() = withContext(dispatchers.io()) {
323-
val showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() && isDuckChatEnabled
355+
isDuckChatUserEnabled = duckChatFeatureRepository.isDuckChatUserEnabled()
356+
357+
val showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() &&
358+
isDuckChatEnabled && isDuckChatUserEnabled
324359
_showInBrowserMenu.emit(showInBrowserMenu)
325360

326-
showInAddressBar = duckChatFeatureRepository.shouldShowInAddressBar() && isDuckChatEnabled
361+
val showInAddressBar = duckChatFeatureRepository.shouldShowInAddressBar() &&
362+
isDuckChatEnabled && isDuckChatUserEnabled && isAddressBarEntryPointEnabled
363+
_showInAddressBar.emit(showInAddressBar)
327364
}
328365

329366
companion object {

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/pixel/DuckChatPixels.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_
3030
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_SETTING_ON
3131
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SETTINGS_DISPLAYED
3232
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SETTINGS_PRESSED
33+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_USER_DISABLED
34+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_USER_ENABLED
3335
import com.squareup.anvil.annotations.ContributesMultibinding
3436
import javax.inject.Inject
3537

3638
enum class DuckChatPixelName(override val pixelName: String) : Pixel.PixelName {
3739
DUCK_CHAT_OPEN("aichat_open"),
3840
DUCK_CHAT_OPEN_BROWSER_MENU("aichat_open_browser_menu"),
3941
DUCK_CHAT_OPEN_NEW_TAB_MENU("aichat_open_new_tab_menu"),
42+
DUCK_CHAT_USER_ENABLED("aichat_enabled"),
43+
DUCK_CHAT_USER_DISABLED("aichat_disabled"),
4044
DUCK_CHAT_MENU_SETTING_OFF("aichat_menu_setting_off"),
4145
DUCK_CHAT_MENU_SETTING_ON("aichat_menu_setting_on"),
4246
DUCK_CHAT_SEARCHBAR_SETTING_OFF("aichat_searchbar_setting_off"),
@@ -53,6 +57,8 @@ class DuckChatParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin
5357
DUCK_CHAT_OPEN.pixelName to PixelParameter.removeAtb(),
5458
DUCK_CHAT_OPEN_BROWSER_MENU.pixelName to PixelParameter.removeAtb(),
5559
DUCK_CHAT_OPEN_NEW_TAB_MENU.pixelName to PixelParameter.removeAtb(),
60+
DUCK_CHAT_USER_ENABLED.pixelName to PixelParameter.removeAtb(),
61+
DUCK_CHAT_USER_DISABLED.pixelName to PixelParameter.removeAtb(),
5662
DUCK_CHAT_MENU_SETTING_OFF.pixelName to PixelParameter.removeAtb(),
5763
DUCK_CHAT_MENU_SETTING_ON.pixelName to PixelParameter.removeAtb(),
5864
DUCK_CHAT_SEARCHBAR_SETTING_OFF.pixelName to PixelParameter.removeAtb(),

0 commit comments

Comments
 (0)