diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt index 05c9e9f0c74c..8151cc0f4381 100644 --- a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt +++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChatSettingsScreens.kt @@ -22,3 +22,8 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter * Use this model to launch the DuckChat Settings screen */ object DuckChatSettingsNoParams : GlobalActivityStarter.ActivityParams + +/** + * Use this model to launch the DuckChat Settings screen and show native settings only + */ +data object DuckChatNativeSettingsNoParams : GlobalActivityStarter.ActivityParams diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsActivity.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsActivity.kt index 5aa3cc2b4ae8..27c8b52799e5 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsActivity.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/settings/DuckChatSettingsActivity.kt @@ -27,6 +27,8 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.ContributeToActivityStarter @@ -41,13 +43,16 @@ import com.duckduckgo.common.ui.store.AppTheme import com.duckduckgo.common.ui.view.addClickableSpan import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckchat.api.DuckChatNativeSettingsNoParams import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams import com.duckduckgo.duckchat.impl.R import com.duckduckgo.duckchat.impl.databinding.ActivityDuckChatSettingsBinding import com.duckduckgo.duckchat.impl.inputscreen.ui.metrics.discovery.InputScreenDiscoveryFunnel import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SETTINGS_DISPLAYED +import com.duckduckgo.duckchat.impl.ui.settings.DuckChatSettingsViewModel.DuckChatSettingsViewModelFactory import com.duckduckgo.duckchat.impl.ui.settings.DuckChatSettingsViewModel.ViewState import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.navigation.api.getActivityParams import com.duckduckgo.settings.api.SettingsPageFeature import com.duckduckgo.settings.api.SettingsWebViewScreenWithParams import kotlinx.coroutines.flow.launchIn @@ -57,10 +62,19 @@ import com.duckduckgo.mobile.android.R as CommonR @InjectWith(ActivityScope::class) @ContributeToActivityStarter(DuckChatSettingsNoParams::class, screenName = "duckai.settings") +@ContributeToActivityStarter(DuckChatNativeSettingsNoParams::class, screenName = "duckai.settings") class DuckChatSettingsActivity : DuckDuckGoActivity() { - private val viewModel: DuckChatSettingsViewModel by bindViewModel() + + @Inject + lateinit var duckChatSettingsViewModelFactory: DuckChatSettingsViewModelFactory + private val binding: ActivityDuckChatSettingsBinding by viewBinding() + private val viewModel by lazy { + val activityParams = intent.getActivityParams(GlobalActivityStarter.ActivityParams::class.java)!! + duckChatSettingsViewModel(activityParams) + } + private val userEnabledDuckChatToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> viewModel.onDuckChatUserEnabledToggled(isChecked) @@ -167,22 +181,7 @@ class DuckChatSettingsActivity : DuckDuckGoActivity() { viewModel.onDuckAiShortcutsClicked() } - binding.showDuckChatSearchSettingsLink.setOnClickListener { - viewModel.duckChatSearchAISettingsClicked() - } - - if (viewState.isHideGeneratedImagesOptionVisible) { - binding.searchSettingsSectionHeader.isVisible = true - binding.duckAiHideAiGeneratedImagesLink.apply { - isVisible = true - setOnClickListener { - viewModel.onDuckAiHideAiGeneratedImagesClicked() - } - } - } else { - binding.searchSettingsSectionHeader.isGone = true - binding.duckAiHideAiGeneratedImagesLink.isGone = true - } + renderSearchSettingsSection(viewState) binding.duckAiInputScreenWithoutAiContainer.setOnClickListener { viewModel.onDuckAiInputScreenWithoutAiSelected() @@ -192,6 +191,38 @@ class DuckChatSettingsActivity : DuckDuckGoActivity() { } } + private fun renderSearchSettingsSection(viewState: ViewState) { + with(binding) { + if (viewState.isSearchSectionVisible) { + with(showDuckChatSearchSettingsLink) { + isVisible = true + setOnClickListener { + viewModel.duckChatSearchAISettingsClicked() + } + } + + if (viewState.isHideGeneratedImagesOptionVisible) { + searchSettingsSectionHeader.isVisible = true + + with(duckAiHideAiGeneratedImagesLink) { + isVisible = true + setOnClickListener { + viewModel.onDuckAiHideAiGeneratedImagesClicked() + } + } + } else { + searchSettingsSectionHeader.isGone = true + duckAiHideAiGeneratedImagesLink.isGone = true + } + } else { + divider2.isGone = true + searchSettingsSectionHeader.isGone = true + showDuckChatSearchSettingsLink.isGone = true + duckAiHideAiGeneratedImagesLink.isGone = true + } + } + } + private fun processCommand(command: DuckChatSettingsViewModel.Command) { when (command) { is DuckChatSettingsViewModel.Command.OpenLink -> { @@ -283,4 +314,13 @@ class DuckChatSettingsActivity : DuckDuckGoActivity() { val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE) sendBroadcast(intent) } + + private fun duckChatSettingsViewModel(activityParams: GlobalActivityStarter.ActivityParams): DuckChatSettingsViewModel = ViewModelProvider.create( + store = viewModelStore, + factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class) = duckChatSettingsViewModelFactory.create(activityParams) as T + }, + extras = this.defaultViewModelCreationExtras, + )[DuckChatSettingsViewModel::class.java] } 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 8f4ec05db310..47adf86428c7 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 @@ -19,10 +19,10 @@ package com.duckduckgo.duckchat.impl.ui.settings import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckchat.api.DuckChatNativeSettingsNoParams +import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams import com.duckduckgo.duckchat.impl.DuckChatInternal import com.duckduckgo.duckchat.impl.R import com.duckduckgo.duckchat.impl.inputscreen.ui.metrics.discovery.InputScreenDiscoveryFunnel @@ -30,7 +30,11 @@ import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName import com.duckduckgo.duckchat.impl.ui.settings.DuckChatSettingsViewModel.Command.OpenLink import com.duckduckgo.duckchat.impl.ui.settings.DuckChatSettingsViewModel.Command.OpenLinkInNewTab import com.duckduckgo.duckchat.impl.ui.settings.DuckChatSettingsViewModel.Command.OpenShortcutSettings +import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.settings.api.SettingsPageFeature +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharingStarted @@ -40,10 +44,9 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@ContributesViewModel(ActivityScope::class) -class DuckChatSettingsViewModel @Inject constructor( +class DuckChatSettingsViewModel @AssistedInject constructor( + @Assisted duckChatActivityParams: GlobalActivityStarter.ActivityParams, private val duckChat: DuckChatInternal, private val pixel: Pixel, private val inputScreenDiscoveryFunnel: InputScreenDiscoveryFunnel, @@ -58,6 +61,7 @@ class DuckChatSettingsViewModel @Inject constructor( val isInputScreenEnabled: Boolean = false, val shouldShowShortcuts: Boolean = false, val shouldShowInputScreenToggle: Boolean = false, + val isSearchSectionVisible: Boolean = true, val isHideGeneratedImagesOptionVisible: Boolean = false, ) @@ -72,6 +76,7 @@ class DuckChatSettingsViewModel @Inject constructor( isInputScreenEnabled = isInputScreenEnabled, shouldShowShortcuts = isDuckChatUserEnabled, shouldShowInputScreenToggle = isDuckChatUserEnabled && duckChat.isInputScreenFeatureAvailable(), + isSearchSectionVisible = isSearchSectionVisible(duckChatActivityParams), isHideGeneratedImagesOptionVisible = isHideAiGeneratedImagesOptionVisible, ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ViewState()) @@ -190,6 +195,17 @@ class DuckChatSettingsViewModel @Inject constructor( } } + private fun isSearchSectionVisible(duckChatActivityParams: GlobalActivityStarter.ActivityParams): Boolean = when (duckChatActivityParams) { + is DuckChatSettingsNoParams -> true + is DuckChatNativeSettingsNoParams -> false + else -> throw IllegalArgumentException("Unknown params type: $duckChatActivityParams") + } + + @AssistedFactory + interface DuckChatSettingsViewModelFactory { + fun create(duckChatActivityParams: GlobalActivityStarter.ActivityParams): DuckChatSettingsViewModel + } + 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" 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 072ac69fc087..aab3b7610bbd 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 @@ -19,6 +19,8 @@ package com.duckduckgo.duckchat.impl.ui.settings import app.cash.turbine.test import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.duckchat.api.DuckChatNativeSettingsNoParams +import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams import com.duckduckgo.duckchat.impl.DuckChatInternal import com.duckduckgo.duckchat.impl.R import com.duckduckgo.duckchat.impl.inputscreen.ui.metrics.discovery.InputScreenDiscoveryFunnel @@ -64,6 +66,7 @@ class DuckChatSettingsViewModelTest { whenever(duckChat.observeShowInAddressBarUserSetting()).thenReturn(flowOf(false)) whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(false)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -147,6 +150,7 @@ class DuckChatSettingsViewModelTest { runTest { whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -164,6 +168,7 @@ class DuckChatSettingsViewModelTest { runTest { whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(false)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -182,6 +187,7 @@ class DuckChatSettingsViewModelTest { whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(true)) whenever(duckChat.isInputScreenFeatureAvailable()).thenReturn(true) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -201,6 +207,7 @@ class DuckChatSettingsViewModelTest { whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(true)) whenever(duckChat.isInputScreenFeatureAvailable()).thenReturn(false) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -220,6 +227,7 @@ class DuckChatSettingsViewModelTest { whenever(duckChat.observeEnableDuckChatUserSetting()).thenReturn(flowOf(false)) whenever(duckChat.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -381,6 +389,7 @@ class DuckChatSettingsViewModelTest { @Suppress("DenyListedApi") settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = true)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -400,6 +409,7 @@ class DuckChatSettingsViewModelTest { @Suppress("DenyListedApi") settingsPageFeature.hideAiGeneratedImagesOption().setRawStoredState(State(enable = false)) testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, duckChat = duckChat, pixel = mockPixel, inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, @@ -412,4 +422,40 @@ class DuckChatSettingsViewModelTest { assertFalse(state.isHideGeneratedImagesOptionVisible) } } + + @Test + fun `when DuckChatSettingsNoParams passed then viewState shows search section visible`() = + runTest { + testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatSettingsNoParams, + duckChat = duckChat, + pixel = mockPixel, + inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, + settingsPageFeature = settingsPageFeature, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + testee.viewState.test { + val state = awaitItem() + assertTrue(state.isSearchSectionVisible) + } + } + + @Test + fun `when DuckChatNativeSettingsNoParams passed then viewState shows search section hidden`() = + runTest { + testee = DuckChatSettingsViewModel( + duckChatActivityParams = DuckChatNativeSettingsNoParams, + duckChat = duckChat, + pixel = mockPixel, + inputScreenDiscoveryFunnel = mockInputScreenDiscoveryFunnel, + settingsPageFeature = settingsPageFeature, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + testee.viewState.test { + val state = awaitItem() + assertFalse(state.isSearchSectionVisible) + } + } } diff --git a/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/serpsettings/messaging/OpenNativeSettingsHandler.kt b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/serpsettings/messaging/OpenNativeSettingsHandler.kt index d846f49b310e..591bc5335546 100644 --- a/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/serpsettings/messaging/OpenNativeSettingsHandler.kt +++ b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/serpsettings/messaging/OpenNativeSettingsHandler.kt @@ -23,7 +23,7 @@ import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.contentscopescripts.api.ContentScopeJsMessageHandlersPlugin import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams +import com.duckduckgo.duckchat.api.DuckChatNativeSettingsNoParams import com.duckduckgo.js.messaging.api.JsMessage import com.duckduckgo.js.messaging.api.JsMessageCallback import com.duckduckgo.js.messaging.api.JsMessageHandler @@ -63,7 +63,7 @@ class OpenNativeSettingsHandler @Inject constructor( when (val screenParam = params.optString("screen", "")) { AI_FEATURES_SCREEN_NAME -> { - val intent = globalActivityStarter.startIntent(context, DuckChatSettingsNoParams) + val intent = globalActivityStarter.startIntent(context, DuckChatNativeSettingsNoParams) intent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) }