Skip to content

Commit ea1ac31

Browse files
authored
Merge pull request #4416 from CruGlobal/dashboardPersonalizationLessons
Add Lessons dashboard personalization
2 parents bb822f8 + b431c27 commit ea1ac31

67 files changed

Lines changed: 624 additions & 118 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.cru.godtools.ui.dashboard.lessons
2+
3+
import java.util.Locale
4+
import javax.inject.Inject
5+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.combine
8+
import kotlinx.coroutines.flow.distinctUntilChanged
9+
import kotlinx.coroutines.flow.flatMapLatest
10+
import kotlinx.coroutines.flow.map
11+
import org.cru.godtools.base.Settings
12+
import org.cru.godtools.db.repository.ToolsRepository
13+
import org.cru.godtools.model.Tool
14+
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState.Mode
15+
16+
internal class LessonsFlowProducer @Inject constructor(
17+
private val settings: Settings,
18+
private val toolsRepository: ToolsRepository,
19+
) {
20+
@OptIn(ExperimentalCoroutinesApi::class)
21+
fun getFlow(mode: Mode, locale: Locale): Flow<List<Tool>> {
22+
val baseFlow = when (mode) {
23+
Mode.PERSONALIZATION -> settings.getPersonalizationCountryFlow()
24+
.flatMapLatest { toolsRepository.getPersonalizedLessonsFlow(locale, it) }
25+
.combine(toolsRepository.getPersonalizedLessonsFlow(locale, null)) { lessons, fallback ->
26+
lessons.ifEmpty { fallback }
27+
}
28+
.distinctUntilChanged()
29+
30+
Mode.ALL_LESSONS -> toolsRepository.getLessonsFlowByLanguage(locale).map { it.sortedBy { it.defaultOrder } }
31+
}
32+
33+
return baseFlow.map { it.filterNot { it.isHidden } }
34+
}
35+
}

app/src/main/kotlin/org/cru/godtools/ui/dashboard/lessons/LessonsLayout.kt

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,47 @@ package org.cru.godtools.ui.dashboard.lessons
22

33
import androidx.compose.foundation.layout.Column
44
import androidx.compose.foundation.layout.PaddingValues
5+
import androidx.compose.foundation.layout.fillMaxWidth
56
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.layout.wrapContentWidth
68
import androidx.compose.foundation.lazy.LazyColumn
79
import androidx.compose.foundation.lazy.items
810
import androidx.compose.material3.HorizontalDivider
911
import androidx.compose.material3.MaterialTheme
12+
import androidx.compose.material3.SegmentedButton
13+
import androidx.compose.material3.SegmentedButtonDefaults
14+
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
1015
import androidx.compose.material3.Text
1116
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.Alignment
1218
import androidx.compose.ui.Modifier
1319
import androidx.compose.ui.res.stringResource
14-
import androidx.compose.ui.tooling.preview.Preview
1520
import androidx.compose.ui.unit.dp
1621
import com.slack.circuit.codegen.annotations.CircuitInject
1722
import dagger.hilt.components.SingletonComponent
1823
import org.cru.godtools.R
1924
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
25+
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiEvent
2026
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
2127
import org.cru.godtools.ui.tools.LessonToolCard
2228

2329
@Composable
2430
@CircuitInject(LessonsScreen::class, SingletonComponent::class)
2531
internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
26-
LazyColumn(contentPadding = PaddingValues(16.dp), modifier = modifier) {
32+
LazyColumn(contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp), modifier = modifier) {
33+
if (state.isPersonalizationEnabled) {
34+
item("mode-toggle", "mode-toggle") {
35+
PersonalizationToggle(
36+
state,
37+
modifier = Modifier
38+
.fillMaxWidth()
39+
.wrapContentWidth(Alignment.CenterHorizontally)
40+
)
41+
}
42+
}
43+
2744
item("header", "header") {
28-
LessonsHeader()
45+
LessonsHeader(state.mode, modifier = Modifier.padding(top = 16.dp))
2946
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
3047
LessonFilters(state)
3148
}
@@ -44,10 +61,35 @@ internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
4461
}
4562

4663
@Composable
47-
@Preview(showBackground = true)
48-
private fun LessonsHeader() = Column {
64+
private fun PersonalizationToggle(state: UiState, modifier: Modifier = Modifier) {
65+
SingleChoiceSegmentedButtonRow(modifier = modifier) {
66+
SegmentedButton(
67+
selected = state.mode == UiState.Mode.PERSONALIZATION,
68+
onClick = { state.eventSink(UiEvent.ChangeMode(UiState.Mode.PERSONALIZATION)) },
69+
shape = SegmentedButtonDefaults.itemShape(0, 2),
70+
) {
71+
Text(stringResource(R.string.dashboard_lessons_toggle_personalized))
72+
}
73+
74+
SegmentedButton(
75+
selected = state.mode == UiState.Mode.ALL_LESSONS,
76+
onClick = { state.eventSink(UiEvent.ChangeMode(UiState.Mode.ALL_LESSONS)) },
77+
shape = SegmentedButtonDefaults.itemShape(1, 2),
78+
) {
79+
Text(stringResource(R.string.dashboard_lessons_toggle_all))
80+
}
81+
}
82+
}
83+
84+
@Composable
85+
private fun LessonsHeader(mode: UiState.Mode, modifier: Modifier = Modifier) = Column(modifier = modifier) {
4986
Text(
50-
stringResource(R.string.dashboard_lessons_header_title),
87+
stringResource(
88+
when (mode) {
89+
UiState.Mode.PERSONALIZATION -> R.string.dashboard_lessons_header_title_personalized
90+
UiState.Mode.ALL_LESSONS -> R.string.dashboard_lessons_header_title_all
91+
}
92+
),
5193
style = MaterialTheme.typography.titleLarge
5294
)
5395
Text(

app/src/main/kotlin/org/cru/godtools/ui/dashboard/lessons/LessonsPresenter.kt

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.cru.godtools.ui.dashboard.lessons
22

33
import android.content.Context
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
56
import androidx.compose.runtime.LaunchedEffect
67
import androidx.compose.runtime.collectAsState
78
import androidx.compose.runtime.getValue
@@ -11,7 +12,10 @@ import androidx.compose.runtime.remember
1112
import androidx.compose.runtime.saveable.rememberSaveable
1213
import androidx.compose.runtime.setValue
1314
import androidx.compose.runtime.snapshotFlow
15+
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
1416
import com.slack.circuit.codegen.annotations.CircuitInject
17+
import com.slack.circuit.runtime.CircuitContext
18+
import com.slack.circuit.runtime.CircuitUiEvent
1519
import com.slack.circuit.runtime.CircuitUiState
1620
import com.slack.circuit.runtime.Navigator
1721
import com.slack.circuit.runtime.presenter.Presenter
@@ -28,56 +32,90 @@ import kotlinx.coroutines.CoroutineDispatcher
2832
import kotlinx.coroutines.ExperimentalCoroutinesApi
2933
import kotlinx.coroutines.flow.combine
3034
import kotlinx.coroutines.flow.distinctUntilChanged
35+
import kotlinx.coroutines.flow.first
3136
import kotlinx.coroutines.flow.flatMapLatest
3237
import kotlinx.coroutines.flow.flowOn
3338
import kotlinx.coroutines.flow.map
3439
import org.ccci.gto.android.common.dagger.coroutines.DispatcherType
3540
import org.ccci.gto.android.common.dagger.coroutines.DispatcherType.Type.IO
41+
import org.ccci.gto.android.common.sync.SyncTracker
3642
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent
3743
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_LESSON
3844
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_LESSONS
45+
import org.cru.godtools.base.CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED
3946
import org.cru.godtools.base.Settings
4047
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
4148
import org.cru.godtools.db.repository.LanguagesRepository
4249
import org.cru.godtools.db.repository.ToolsRepository
4350
import org.cru.godtools.db.repository.TranslationsRepository
4451
import org.cru.godtools.model.Language
4552
import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName
53+
import org.cru.godtools.sync.GodToolsSyncService
54+
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
4655
import org.cru.godtools.ui.dashboard.filters.FilterMenu
4756
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
4857
import org.cru.godtools.ui.tools.ToolCardPresenter
4958
import org.cru.godtools.ui.tools.ToolCardPresenter.ToolCardEvent
5059
import org.cru.godtools.util.createToolIntent
5160
import org.greenrobot.eventbus.EventBus
5261

53-
class LessonsPresenter @AssistedInject constructor(
62+
class LessonsPresenter @AssistedInject internal constructor(
5463
@param:ApplicationContext
5564
private val context: Context,
5665
private val eventBus: EventBus,
5766
private val languagesRepository: LanguagesRepository,
67+
private val lessonsFlowProducer: LessonsFlowProducer,
68+
private val remoteConfig: FirebaseRemoteConfig,
5869
private val settings: Settings,
70+
private val syncService: GodToolsSyncService,
5971
private val toolCardPresenter: ToolCardPresenter,
6072
private val toolsRepository: ToolsRepository,
6173
private val translationsRepository: TranslationsRepository,
6274
@param:DispatcherType(IO) private val ioDispatcher: CoroutineDispatcher,
75+
@Assisted private val circuitContext: CircuitContext,
6376
@Assisted private val navigator: Navigator,
6477
) : Presenter<UiState> {
65-
// region UiState
66-
data class UiState(
78+
// region UiState / UiEvent
79+
@ConsistentCopyVisibility
80+
data class UiState internal constructor(
81+
val mode: Mode = Mode.ALL_LESSONS,
82+
val isPersonalizationEnabled: Boolean = false,
6783
val languageFilter: FilterMenu.UiState<Language> = FilterMenu.UiState(),
6884
val lessons: List<ToolCardPresenter.UiState> = emptyList(),
69-
) : CircuitUiState
70-
// endregion UiState
85+
internal val eventSink: (UiEvent) -> Unit = {},
86+
) : CircuitUiState {
87+
enum class Mode { PERSONALIZATION, ALL_LESSONS }
88+
}
89+
90+
internal sealed interface UiEvent : CircuitUiEvent {
91+
data class ChangeMode(val mode: UiState.Mode) : UiEvent
92+
}
93+
// endregion UiState / UiEvent
7194

7295
@Composable
7396
override fun present(): UiState {
97+
val isPersonalizationEnabled = rememberSaveable {
98+
remoteConfig.getBoolean(CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED)
99+
}
100+
var mode by rememberSaveable {
101+
mutableStateOf(if (isPersonalizationEnabled) UiState.Mode.PERSONALIZATION else UiState.Mode.ALL_LESSONS)
102+
}
103+
74104
val appLanguage by settings.appLanguageFlow.collectAsState()
75105
val languageFilter = rememberLanguagesFilter()
76106

107+
RegisterSyncTask(languageFilter.selectedItem?.code ?: appLanguage)
108+
77109
return UiState(
110+
mode = mode,
111+
isPersonalizationEnabled = isPersonalizationEnabled,
78112
languageFilter = languageFilter,
79-
lessons = rememberLessons(languageFilter.selectedItem?.code ?: appLanguage),
80-
)
113+
lessons = rememberLessons(mode, languageFilter.selectedItem?.code ?: appLanguage),
114+
) {
115+
when (it) {
116+
is UiEvent.ChangeMode -> mode = it.mode
117+
}
118+
}
81119
}
82120

83121
@Composable
@@ -140,12 +178,8 @@ class LessonsPresenter @AssistedInject constructor(
140178
}
141179

142180
@Composable
143-
private fun rememberLessons(locale: Locale): List<ToolCardPresenter.UiState> {
144-
val lessons by remember(locale) {
145-
toolsRepository.getLessonsFlowByLanguage(locale)
146-
.map { it.filterNot { it.isHidden }.sortedBy { it.defaultOrder } }
147-
}.collectAsState(emptyList())
148-
181+
private fun rememberLessons(mode: UiState.Mode, locale: Locale): List<ToolCardPresenter.UiState> {
182+
val lessons by remember(mode, locale) { lessonsFlowProducer.getFlow(mode, locale) }.collectAsState(emptyList())
149183
return lessons.map { tool ->
150184
key(tool.code) {
151185
lateinit var toolState: ToolCardPresenter.UiState
@@ -176,9 +210,24 @@ class LessonsPresenter @AssistedInject constructor(
176210
}
177211
}
178212

213+
@Composable
214+
private fun RegisterSyncTask(locale: Locale) {
215+
val syncRegistry = circuitContext.syncTaskRegistry
216+
DisposableEffect(syncRegistry, locale) {
217+
if (syncRegistry == null) return@DisposableEffect onDispose { }
218+
val id = syncRegistry.registerSyncTask { force -> syncData(locale, force) }
219+
onDispose { syncRegistry.unregisterSyncTask(id) }
220+
}
221+
}
222+
223+
private fun SyncTracker.syncData(locale: Locale, force: Boolean = false) = launchSync {
224+
val country = settings.getCountrySettingFlow().first()
225+
syncService.syncToolOrder(locale, country, force)
226+
}
227+
179228
@AssistedFactory
180229
@CircuitInject(LessonsScreen::class, SingletonComponent::class)
181230
interface Factory {
182-
fun create(navigator: Navigator): LessonsPresenter
231+
fun create(circuitContext: CircuitContext, navigator: Navigator): LessonsPresenter
183232
}
184233
}

app/src/main/res/values-af/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<string name="menu_version">Weergawe: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">Lesse</string>
35-
<string name="dashboard_lessons_header_title">Lesse vir jou gemaak</string>
35+
<string name="dashboard_lessons_header_title_personalized">Lesse vir jou gemaak</string>
3636
<string name="dashboard_lessons_header_description">Praktiese idees om verhoudings te bou en betekenisvolle gesprekke te voer.</string>
3737
<string name="dashboard_lessons_section_filter_label">Lesse in:</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-am/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<string name="menu_version">ሥሪት: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">ትምህርቶች</string>
35-
<string name="dashboard_lessons_header_title">ለእርስዎ የተዘጋጁ ትምህርቶች</string>
35+
<string name="dashboard_lessons_header_title_personalized">ለእርስዎ የተዘጋጁ ትምህርቶች</string>
3636
<string name="dashboard_lessons_header_description">ግንኙነቶችን ለመፍጠር እና ትርጉም ያለው ውይይት ለመጀመር ተግባራዊ ሀሳቦች።</string>
3737
<string name="dashboard_lessons_section_filter_label">ትምህርቶች በ</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-ar/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ https://godtoolsapp.com \n\n
3333
<string name="menu_version">الإصدار: %1$s(%2$d)</string>
3434
<!-- Lessons -->
3535
<string name="nav_lessons">الدروس</string>
36-
<string name="dashboard_lessons_header_title">الدروس المُعدة لأجلك</string>
36+
<string name="dashboard_lessons_header_title_personalized">الدروس المُعدة لأجلك</string>
3737
<string name="dashboard_lessons_header_description">أفكار عملية لبناء علاقات وبدء محادثات هادفة.</string>
3838
<string name="dashboard_lessons_section_filter_label">دروس في:</string>
3939
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-bn/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ https://knowgod.com/-তে একটি অনলাইন সংস্কর
3232
<string name="menu_version">সংস্করণ: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">পাঠগুলি</string>
35-
<string name="dashboard_lessons_header_title">আপনার জন্য তৈরি পাঠগুলি</string>
35+
<string name="dashboard_lessons_header_title_personalized">আপনার জন্য তৈরি পাঠগুলি</string>
3636
<string name="dashboard_lessons_header_description">সম্পর্ক তৈরি করার এবং অর্থপূর্ণ কথাবার্তা শুরু করার ব্যবহারিক ধারণা</string>
3737
<string name="dashboard_lessons_section_filter_label">এগুলিতে পাঠসমূহ:</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-de/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Eine Online-Version findest du unter https://knowgod.com/</string>
3232
<string name="menu_version">Version: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">Lektionen</string>
35-
<string name="dashboard_lessons_header_title">Für dich gemachte Lektionen</string>
35+
<string name="dashboard_lessons_header_title_personalized">Für dich gemachte Lektionen</string>
3636
<string name="dashboard_lessons_header_description">Praktische Ideen zum Aufbau von Beziehungen und zum Beginn bedeutungsvoller Unterhaltungen.</string>
3737
<string name="dashboard_lessons_section_filter_label">Lektionen in:</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-es/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Puedes acceer a una versión online en https://knowgod.com/</string>
3232
<string name="menu_version">Versión: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">Lecciones</string>
35-
<string name="dashboard_lessons_header_title">Lecciones hechas para ti</string>
35+
<string name="dashboard_lessons_header_title_personalized">Lecciones hechas para ti</string>
3636
<string name="dashboard_lessons_header_description">Ideas prácticas para establecer relaciones e iniciar conversaciones significativas.</string>
3737
<string name="dashboard_lessons_section_filter_label">Lecciones en:</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

app/src/main/res/values-fr/strings_dashboard.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Une version en ligne est disponible sur https://knowgod.com</string>
3232
<string name="menu_version">Version: %1$s (%2$d)</string>
3333
<!-- Lessons -->
3434
<string name="nav_lessons">Leçons</string>
35-
<string name="dashboard_lessons_header_title">Des leçons faites pour vous</string>
35+
<string name="dashboard_lessons_header_title_personalized">Des leçons faites pour vous</string>
3636
<string name="dashboard_lessons_header_description">Des idées pratiques pour nouer des relations et entamer des conversations significatives.</string>
3737
<string name="dashboard_lessons_section_filter_label">Leçons en :</string>
3838
<plurals name="dashboard_lessons_section_filter_available_lessons">

0 commit comments

Comments
 (0)