Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit 5f410f7

Browse files
AdamGrzybkowskihamorillo
authored andcommitted
Store user share preferences
1 parent 63b410f commit 5f410f7

File tree

8 files changed

+199
-7
lines changed

8 files changed

+199
-7
lines changed

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.gravatar.app.usercomponent.domain.repository.UserRepository
66
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
7+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences
8+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences
79
import kotlinx.coroutines.flow.MutableStateFlow
810
import kotlinx.coroutines.flow.StateFlow
911
import kotlinx.coroutines.flow.asStateFlow
1012
import kotlinx.coroutines.flow.launchIn
1113
import kotlinx.coroutines.flow.onEach
1214
import kotlinx.coroutines.flow.update
15+
import kotlinx.coroutines.launch
1316

1417
internal class ShareViewModel(
1518
private val userRepository: UserRepository,
1619
private val getAvatarUrl: GetAvatarUrl,
20+
private val getUserSharePreferences: GetUserSharePreferences,
21+
private val updateUserSharePreferences: UpdateUserSharePreferences,
1722
) : ViewModel() {
1823

1924
private val _uiState = MutableStateFlow(ShareUiState())
@@ -22,6 +27,7 @@ internal class ShareViewModel(
2227
init {
2328
collectProfile()
2429
collectAvatarUrl()
30+
collectUserSharePreferences()
2531
}
2632

2733
fun onEvent(shareEvent: ShareEvent) {
@@ -68,13 +74,18 @@ internal class ShareViewModel(
6874

6975
is ShareEvent.OnAboutAppClicked -> showAboutAppDialog()
7076
is ShareEvent.OnDismissAboutAppDialog -> hideAboutAppDialog()
71-
is ShareEvent.OnUserSharePreferencesChanged -> updateUserSharePreferences(shareEvent.shareFieldType)
77+
is ShareEvent.OnUserSharePreferencesChanged -> handleUserSharePreferencesChange(shareEvent.shareFieldType)
7278
}
7379
}
7480

75-
private fun updateUserSharePreferences(shareFieldType: ShareFieldType) {
76-
_uiState.update { currentState ->
77-
currentState.copyWithUserSharePreferences(shareFieldType)
81+
private fun handleUserSharePreferencesChange(shareFieldType: ShareFieldType) {
82+
with(_uiState.value.copyWithUserSharePreferences(shareFieldType)) {
83+
// update the UI state with the new preferences
84+
_uiState.value = this
85+
// Save the updated preferences
86+
viewModelScope.launch {
87+
updateUserSharePreferences(this@with.userSharePreferences)
88+
}
7889
}
7990
}
8091

@@ -113,4 +124,16 @@ internal class ShareViewModel(
113124
}
114125
.launchIn(viewModelScope)
115126
}
127+
128+
private fun collectUserSharePreferences() {
129+
getUserSharePreferences()
130+
.onEach { preferences ->
131+
_uiState.update { currentState ->
132+
currentState.copy(
133+
userSharePreferences = preferences
134+
)
135+
}
136+
}
137+
.launchIn(viewModelScope)
138+
}
116139
}

homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModelTest.kt

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package com.gravatar.app.homeUi.presentation.home.share
22

33
import app.cash.turbine.test
44
import com.gravatar.app.testUtils.CoroutineTestRule
5+
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
56
import com.gravatar.app.usercomponent.domain.repository.UserRepository
67
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
8+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences
9+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences
710
import com.gravatar.restapi.models.Profile
811
import com.gravatar.restapi.models.ProfileContactInfo
912
import io.mockk.every
@@ -32,17 +35,26 @@ class ShareViewModelTest {
3235
private val getAvatarUrl: GetAvatarUrl = object : GetAvatarUrl {
3336
override fun invoke() = avatarUrlFlow
3437
}
38+
private val getUserSharePreferences: GetUserSharePreferences = object : GetUserSharePreferences {
39+
override fun invoke() = userSharePreferencesFlow
40+
}
41+
private val updateUserSharePreferences = object : UpdateUserSharePreferences {
42+
override suspend fun invoke(userSharePreferences: UserSharePreferences) {
43+
userSharePreferencesFlow.emit(userSharePreferences)
44+
}
45+
}
3546
private val userRepository = mockk<UserRepository>()
3647

3748
private lateinit var viewModel: ShareViewModel
3849

3950
private val avatarUrlFlow: MutableSharedFlow<URL?> = MutableSharedFlow()
4051
private val profileFlow: MutableSharedFlow<Profile?> = MutableSharedFlow()
52+
private val userSharePreferencesFlow: MutableSharedFlow<UserSharePreferences> = MutableSharedFlow()
4153

4254
@Before
4355
fun setup() {
4456
every { userRepository.getProfile() } returns profileFlow
45-
viewModel = ShareViewModel(userRepository, getAvatarUrl)
57+
viewModel = ShareViewModel(userRepository, getAvatarUrl, getUserSharePreferences, updateUserSharePreferences)
4658
}
4759

4860
@Test
@@ -298,6 +310,53 @@ class ShareViewModelTest {
298310
}
299311
}
300312

313+
@Test
314+
fun `when user share preferences are emitted then uiState is updated with preferences`() = runTest {
315+
// Given
316+
val testPreferences = UserSharePreferences(
317+
name = false,
318+
location = true,
319+
title = false,
320+
organization = true,
321+
description = false,
322+
profileUrl = true
323+
)
324+
325+
// When
326+
userSharePreferencesFlow.emit(testPreferences)
327+
advanceUntilIdle()
328+
329+
// Then
330+
viewModel.uiState.test {
331+
val state = awaitItem()
332+
assertEquals(testPreferences, state.userSharePreferences)
333+
}
334+
}
335+
336+
@Test
337+
fun `when user share preferences are changed then updateUserSharePreferences is called`() = runTest {
338+
// Given
339+
val initialState = viewModel.uiState.value
340+
val newNamePreference = !initialState.userSharePreferences.name
341+
val shareFieldType = ShareFieldType.Name(checked = newNamePreference)
342+
343+
userSharePreferencesFlow.test {
344+
// When
345+
viewModel.onEvent(ShareEvent.OnUserSharePreferencesChanged(shareFieldType))
346+
347+
// Then
348+
val expectedSharePreferences = UserSharePreferences(
349+
name = newNamePreference,
350+
location = initialState.userSharePreferences.location,
351+
title = initialState.userSharePreferences.title,
352+
organization = initialState.userSharePreferences.organization,
353+
description = initialState.userSharePreferences.description,
354+
profileUrl = initialState.userSharePreferences.profileUrl
355+
)
356+
assertEquals(expectedSharePreferences, awaitItem())
357+
}
358+
}
359+
301360
private fun createTestProfile() = Profile {
302361
hash = "test-hash"
303362
displayName = "Test User"

userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/UserPrefsStorage.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package com.gravatar.app.usercomponent.data
33
import androidx.datastore.core.DataStore
44
import androidx.datastore.core.IOException
55
import androidx.datastore.preferences.core.Preferences
6+
import androidx.datastore.preferences.core.booleanPreferencesKey
67
import androidx.datastore.preferences.core.edit
78
import androidx.datastore.preferences.core.stringPreferencesKey
89
import com.gravatar.app.foundations.DispatcherProvider
910
import com.gravatar.app.usercomponent.di.UserPrefs
11+
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
1012
import kotlinx.coroutines.flow.Flow
1113
import kotlinx.coroutines.flow.catch
1214
import kotlinx.coroutines.flow.firstOrNull
@@ -29,10 +31,16 @@ internal interface AvatarCacheBusterStorage {
2931
suspend fun saveAvatarCacheBuster(value: String)
3032
}
3133

34+
internal interface UserSharePreferencesStorage {
35+
fun getUserSharePreferences(): Flow<UserSharePreferences>
36+
37+
suspend fun saveUserSharePreferences(userSharePreferences: UserSharePreferences)
38+
}
39+
3240
/**
3341
* Convenient interface to clear all user related data in one call.
3442
*/
35-
internal interface UserStorage : AuthTokenStorage, AvatarCacheBusterStorage {
43+
internal interface UserStorage : AuthTokenStorage, AvatarCacheBusterStorage, UserSharePreferencesStorage {
3644
suspend fun clear()
3745
}
3846

@@ -44,10 +52,22 @@ internal class DatastoreUserPrefsStorage(
4452
companion object {
4553
private const val AUTH_TOKEN_KEY = "auth_token"
4654
private const val AVATAR_CACHE_BUSTER_KEY = "avatar_cache_buster"
55+
private const val USER_SHARE_NAME_KEY = "share_name"
56+
private const val USER_SHARE_LOCATION_KEY = "share_location"
57+
private const val USER_SHARE_TITLE_KEY = "share_title"
58+
private const val USER_SHARE_ORGANIZATION_KEY = "share_organization"
59+
private const val USER_SHARE_DESCRIPTION_KEY = "share_description"
60+
private const val USER_SHARE_PROFILE_URL_KEY = "share_profile_url"
4761
}
4862

4963
private val tokenKey = stringPreferencesKey(AUTH_TOKEN_KEY)
5064
private val avatarCacheBusterKey = stringPreferencesKey(AVATAR_CACHE_BUSTER_KEY)
65+
private val userShareNameKey = booleanPreferencesKey(USER_SHARE_NAME_KEY)
66+
private val userShareLocationKey = booleanPreferencesKey(USER_SHARE_LOCATION_KEY)
67+
private val userShareTitleKey = booleanPreferencesKey(USER_SHARE_TITLE_KEY)
68+
private val userShareOrganizationKey = booleanPreferencesKey(USER_SHARE_ORGANIZATION_KEY)
69+
private val userShareDescriptionKey = booleanPreferencesKey(USER_SHARE_DESCRIPTION_KEY)
70+
private val userShareProfileUrlKey = booleanPreferencesKey(USER_SHARE_PROFILE_URL_KEY)
5171

5272
override suspend fun getToken(): String? {
5373
return try {
@@ -94,4 +114,33 @@ internal class DatastoreUserPrefsStorage(
94114
preferences.clear()
95115
}
96116
}
117+
118+
override fun getUserSharePreferences(): Flow<UserSharePreferences> {
119+
return dataStore.data
120+
.map { preferences ->
121+
UserSharePreferences(
122+
name = preferences[userShareNameKey] ?: true,
123+
location = preferences[userShareLocationKey] ?: true,
124+
title = preferences[userShareTitleKey] ?: true,
125+
organization = preferences[userShareOrganizationKey] ?: true,
126+
description = preferences[userShareDescriptionKey] ?: true,
127+
profileUrl = preferences[userShareProfileUrlKey] ?: true
128+
)
129+
}
130+
.catch { emit(UserSharePreferences.Default) }
131+
.flowOn(dispatcherProvider.io)
132+
}
133+
134+
override suspend fun saveUserSharePreferences(
135+
userSharePreferences: UserSharePreferences
136+
): Unit = withContext(dispatcherProvider.io) {
137+
dataStore.edit { preferences ->
138+
preferences[userShareNameKey] = userSharePreferences.name
139+
preferences[userShareLocationKey] = userSharePreferences.location
140+
preferences[userShareTitleKey] = userSharePreferences.title
141+
preferences[userShareOrganizationKey] = userSharePreferences.organization
142+
preferences[userShareDescriptionKey] = userSharePreferences.description
143+
preferences[userShareProfileUrlKey] = userSharePreferences.profileUrl
144+
}
145+
}
97146
}

userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/DatastoreModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.datastore.preferences.preferencesDataStore
77
import com.gravatar.app.usercomponent.data.AuthTokenStorage
88
import com.gravatar.app.usercomponent.data.AvatarCacheBusterStorage
99
import com.gravatar.app.usercomponent.data.DatastoreUserPrefsStorage
10+
import com.gravatar.app.usercomponent.data.UserSharePreferencesStorage
1011
import com.gravatar.app.usercomponent.data.UserStorage
1112
import org.koin.android.ext.koin.androidContext
1213
import org.koin.core.annotation.Named
@@ -35,6 +36,12 @@ internal val datastoreModule = module {
3536
dispatcherProvider = get()
3637
)
3738
}
39+
factory<UserSharePreferencesStorage> {
40+
DatastoreUserPrefsStorage(
41+
dataStore = get(qualifier = named<UserPrefs>()),
42+
dispatcherProvider = get()
43+
)
44+
}
3845
}
3946

4047
private val Context.userPreferencesDataStore by preferencesDataStore(name = "user-preferences")

userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/UserComponentModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import com.gravatar.app.usercomponent.domain.usecase.DeleteUserAvatar
1313
import com.gravatar.app.usercomponent.domain.usecase.DeleteUserAvatarUseCase
1414
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
1515
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrlUseCase
16+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences
17+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferencesUseCase
1618
import com.gravatar.app.usercomponent.domain.usecase.IsUserLoggedIn
1719
import com.gravatar.app.usercomponent.domain.usecase.IsUserLoggedInUseCase
1820
import com.gravatar.app.usercomponent.domain.usecase.Login
@@ -21,6 +23,8 @@ import com.gravatar.app.usercomponent.domain.usecase.Logout
2123
import com.gravatar.app.usercomponent.domain.usecase.LogoutUseCase
2224
import com.gravatar.app.usercomponent.domain.usecase.SelectAvatarUseCase
2325
import com.gravatar.app.usercomponent.domain.usecase.SelectUserAvatar
26+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences
27+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferencesUseCase
2428
import com.gravatar.app.usercomponent.domain.usecase.UploadAvatarUseCase
2529
import com.gravatar.app.usercomponent.domain.usecase.UploadUserAvatar
2630
import org.koin.core.module.dsl.bind
@@ -39,6 +43,8 @@ val userComponentModule = module {
3943
factoryOf(::SelectAvatarUseCase) { bind<SelectUserAvatar>() }
4044
factoryOf(::DeleteUserAvatarUseCase) { bind<DeleteUserAvatar>() }
4145
factoryOf(::UploadAvatarUseCase) { bind<UploadUserAvatar>() }
46+
factoryOf(::GetUserSharePreferencesUseCase) { bind<GetUserSharePreferences>() }
47+
factoryOf(::UpdateUserSharePreferencesUseCase) { bind<UpdateUserSharePreferences>() }
4248
factoryOf(::WordPressClient)
4349
singleOf(::InMemoryUserSessionPersistence) { bind<UserSessionPersistence>() }
4450
includes(httpClientModule)

userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/model/UserSharePreferences.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,15 @@ data class UserSharePreferences(
77
val organization: Boolean,
88
val description: Boolean,
99
val profileUrl: Boolean,
10-
)
10+
) {
11+
companion object {
12+
val Default = UserSharePreferences(
13+
name = true,
14+
location = true,
15+
title = true,
16+
organization = true,
17+
description = true,
18+
profileUrl = true
19+
)
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.gravatar.app.usercomponent.domain.usecase
2+
3+
import com.gravatar.app.usercomponent.data.UserSharePreferencesStorage
4+
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.distinctUntilChanged
7+
8+
internal class GetUserSharePreferencesUseCase(
9+
private val userSharePreferencesStorage: UserSharePreferencesStorage
10+
) : GetUserSharePreferences {
11+
12+
override fun invoke(): Flow<UserSharePreferences> {
13+
return userSharePreferencesStorage.getUserSharePreferences()
14+
.distinctUntilChanged()
15+
}
16+
}
17+
18+
interface GetUserSharePreferences {
19+
operator fun invoke(): Flow<UserSharePreferences>
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.gravatar.app.usercomponent.domain.usecase
2+
3+
import com.gravatar.app.usercomponent.data.UserSharePreferencesStorage
4+
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
5+
6+
internal class UpdateUserSharePreferencesUseCase(
7+
private val userSharePreferencesStorage: UserSharePreferencesStorage
8+
) : UpdateUserSharePreferences {
9+
10+
override suspend fun invoke(userSharePreferences: UserSharePreferences) {
11+
userSharePreferencesStorage.saveUserSharePreferences(userSharePreferences)
12+
}
13+
}
14+
15+
interface UpdateUserSharePreferences {
16+
suspend operator fun invoke(userSharePreferences: UserSharePreferences)
17+
}

0 commit comments

Comments
 (0)