Skip to content

Commit

Permalink
WIP - DuckChat datastore
Browse files Browse the repository at this point in the history
  • Loading branch information
nshuba committed Dec 20, 2024
1 parent 4c83de8 commit 9ba9628
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.impl.SharedPreferencesDuckChatDataStore.Keys.DUCK_CHAT_SHOW_IN_MENU
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

interface DuckChatDataStore {
suspend fun setShowInBrowserMenu(showDuckChat: Boolean)
fun observeShowInBrowserMenu(): Flow<Boolean>
fun getShowInBrowserMenu(): Boolean
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class SharedPreferencesDuckChatDataStore @Inject constructor(
@DuckChat private val store: DataStore<Preferences>,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : DuckChatDataStore {

private object Keys {
val DUCK_CHAT_SHOW_IN_MENU = booleanPreferencesKey(name = "DUCK_CHAT_SHOW_IN_MENU")
}

private val duckChatShowInBrowserMenu: StateFlow<Boolean> = store.data
.map { prefs ->
prefs[DUCK_CHAT_SHOW_IN_MENU] ?: true
}
.distinctUntilChanged()
.stateIn(appCoroutineScope, SharingStarted.Eagerly, true)

override suspend fun setShowInBrowserMenu(showDuckChat: Boolean) {
store.edit { prefs -> prefs[DUCK_CHAT_SHOW_IN_MENU] = showDuckChat }
}

override fun observeShowInBrowserMenu(): Flow<Boolean> {
return duckChatShowInBrowserMenu
}

override fun getShowInBrowserMenu(): Boolean {
return duckChatShowInBrowserMenu.value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import javax.inject.Qualifier

@ContributesTo(AppScope::class)
@Module
object DuckChatDataStoreModule {

private val Context.duckChatDataStore: DataStore<Preferences> by preferencesDataStore(
name = "duck_chat",
)

@Provides
@DuckChat
fun provideDuckChatDataStore(context: Context): DataStore<Preferences> = context.duckChatDataStore
}

@Qualifier
internal annotation class DuckChat
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl

import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject

interface DuckChatFeatureRepository {
fun setShowInBrowserMenu(showDuckChat: Boolean)
fun observeShowInBrowserMenu(): Flow<Boolean>
fun shouldShowInBrowserMenu(): Boolean
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealDuckChatFeatureRepository @Inject constructor(
private val duckChatDataStore: DuckChatDataStore,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : DuckChatFeatureRepository {

override fun setShowInBrowserMenu(showDuckChat: Boolean) {
appCoroutineScope.launch {
duckChatDataStore.setShowInBrowserMenu(showDuckChat)
}
}

override fun observeShowInBrowserMenu(): Flow<Boolean> {
return duckChatDataStore.observeShowInBrowserMenu()
}

override fun shouldShowInBrowserMenu(): Boolean {
return duckChatDataStore.getShowInBrowserMenu()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,53 @@
package com.duckduckgo.duckchat.impl

import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams
import com.duckduckgo.duckchat.impl.databinding.ActivityDuckChatSettingsBinding
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(DuckChatSettingsNoParams::class)
class DuckChatSettingsActivity : DuckDuckGoActivity() {

private val viewModel: DuckChatSettingsViewModel by bindViewModel()
private val binding: ActivityDuckChatSettingsBinding by viewBinding()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(binding.root)

// TODO: learn more link

setupToolbar(binding.includeToolbar.toolbar)

configureUiEventHandlers()
observeViewModel()
}

private fun configureUiEventHandlers() {
binding.showDuckChatInMenuToggle.setOnCheckedChangeListener { _, isChecked ->
viewModel.onShowDuckChatInMenuToggled(isChecked)
}
}

private fun observeViewModel() {
viewModel.showInBrowserMenu
.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
.onEach { showInBrowserMenu -> renderViewState(showInBrowserMenu) }
.launchIn(lifecycleScope)
}

private fun renderViewState(showInBrowserMenu: Boolean) {
binding.showDuckChatInMenuToggle.setIsChecked(showInBrowserMenu)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.di.scopes.ActivityScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@ContributesViewModel(ActivityScope::class)
class DuckChatSettingsViewModel @Inject constructor(
private val duckChat: DuckChatInternal,
) : ViewModel() {

val showInBrowserMenu: StateFlow<Boolean> = duckChat.observeShowInBrowserMenu()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)

fun onShowDuckChatInMenuToggled(checked: Boolean) {
duckChat.setShowInBrowserMenu(checked)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl

import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.DuckChat
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

interface DuckChatInternal : DuckChat {
/**
* Stores setting to determine whether the DuckChat should be shown in browser menu
*/
fun setShowInBrowserMenu(showDuckChat: Boolean)

fun observeShowInBrowserMenu(): Flow<Boolean>
}

@ContributesBinding(AppScope::class, boundType = DuckChat::class)
@ContributesBinding(AppScope::class, boundType = DuckChatInternal::class)
class RealDuckChat @Inject constructor(
private val duckChatFeatureRepository: DuckChatFeatureRepository,
) : DuckChatInternal {
override fun setShowInBrowserMenu(showDuckChat: Boolean) {
duckChatFeatureRepository.setShowInBrowserMenu(showDuckChat)
}

override fun observeShowInBrowserMenu(): Flow<Boolean> {
return duckChatFeatureRepository.observeShowInBrowserMenu()
}

override fun showInBrowserMenu(): Boolean {
return duckChatFeatureRepository.shouldShowInBrowserMenu()
}
}

0 comments on commit 9ba9628

Please sign in to comment.