Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import org.cru.godtools.base.ui.circuit.screen.dashboard.page.HomeScreen
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen
import org.cru.godtools.base.ui.compose.LocalEventBus
import org.cru.godtools.base.ui.theme.GodToolsTheme

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was to remove the blue bar at the top of the screen right? If not, then I'm confused with what's happening here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we kept the dashboard app bar, but removed the blue theme from it

import org.cru.godtools.shared.analytics.AnalyticsScreenNames
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiEvent
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState
Expand Down Expand Up @@ -91,7 +90,6 @@ internal fun DashboardLayout(state: UiState, modifier: Modifier = Modifier) {
}
}
},
colors = GodToolsTheme.topAppBarColors,
)
},
bottomBar = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package org.cru.godtools.ui.dashboard

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.foundation.NavEvent
import com.slack.circuit.foundation.onNavEvent
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.CircuitUiEvent
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.Navigator
Expand All @@ -18,18 +20,20 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.ccci.gto.android.common.sync.SyncTracker
import org.ccci.gto.android.common.sync.rememberSyncTracker
import org.cru.godtools.base.ui.circuit.screen.dashboard.DashboardScreen
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.DashboardPage
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.HomeScreen
import org.cru.godtools.sync.GodToolsSyncService
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.rememberSyncRegistry
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
import org.cru.godtools.ui.drawer.DrawerMenuPresenter
import org.cru.godtools.ui.drawer.DrawerMenuScreen

class DashboardPresenter @AssistedInject internal constructor(
private val drawerMenuPresenter: DrawerMenuPresenter,
private val syncService: GodToolsSyncService,
@Assisted private val circuitContext: CircuitContext,
@Assisted private val navigator: Navigator,
@Assisted private val screen: DashboardScreen,
) : Presenter<UiState> {
Expand All @@ -49,16 +53,25 @@ class DashboardPresenter @AssistedInject internal constructor(

@Composable
override fun present(): UiState {
val syncTracker = rememberSyncTracker { it.syncData() }
val syncRegistry = rememberSyncRegistry()
DisposableEffect(syncRegistry) {
circuitContext.syncTaskRegistry = syncRegistry
val id = syncRegistry.registerSyncTask { syncData(it) }

onDispose {
circuitContext.syncTaskRegistry = null
syncRegistry.unregisterSyncTask(id)
}
}

return UiState(
drawerState = drawerMenuPresenter.present(),
isSyncing = syncTracker.isSyncing.collectAsState().value,
isSyncing = syncRegistry.syncTracker.isSyncing.collectAsState().value,
initialPage = screen.initialPage,
snackbarState = remember { SnackbarHostState() },
) {
when (it) {
UiEvent.TriggerSync -> syncTracker.syncData(force = true)
UiEvent.TriggerSync -> syncRegistry.triggerSyncTasks(force = true)
is UiEvent.NestedNavEvent -> navigator.onNavEvent(it.event)
}
}
Expand All @@ -81,6 +94,6 @@ class DashboardPresenter @AssistedInject internal constructor(
@AssistedFactory
@CircuitInject(DashboardScreen::class, SingletonComponent::class)
interface Factory {
fun create(navigator: Navigator, screen: DashboardScreen): DashboardPresenter
fun create(context: CircuitContext, navigator: Navigator, screen: DashboardScreen): DashboardPresenter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.cru.godtools.ui.dashboard

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.slack.circuit.runtime.CircuitContext
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import org.ccci.gto.android.common.sync.SyncTracker
import org.ccci.gto.android.common.sync.rememberSyncTracker

internal class SyncTaskRegistry(val syncTracker: SyncTracker) {
private val tasks = mutableMapOf<String, SyncTracker.(force: Boolean) -> Unit>()

@OptIn(ExperimentalUuidApi::class)
fun registerSyncTask(task: SyncTracker.(force: Boolean) -> Unit): String {
val id = Uuid.generateV7().toString()
synchronized(tasks) { tasks[id] = task }
syncTracker.task(false)
return id
}

fun unregisterSyncTask(id: String) {
synchronized(tasks) { tasks.remove(id) }
}

fun triggerSyncTasks(force: Boolean = false) {
synchronized(tasks) { tasks.values.toList() }.forEach { syncTracker.it(force) }
}

companion object {
internal var CircuitContext.syncTaskRegistry: SyncTaskRegistry?
get() = tag() ?: parent?.syncTaskRegistry
set(value) = putTag(value)

@Composable
internal fun rememberSyncRegistry(): SyncTaskRegistry {
val syncTracker = rememberSyncTracker()
return remember(syncTracker) { SyncTaskRegistry(syncTracker) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,49 @@ package org.cru.godtools.ui.dashboard.tools

import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.ccci.gto.android.common.kotlin.coroutines.flow.combineTransformLatest
import org.cru.godtools.base.Settings
import org.cru.godtools.db.repository.ToolsRepository
import org.cru.godtools.model.Tool
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode

internal class FilteredToolsFlowProducer @Inject constructor(private val toolsRepository: ToolsRepository) {
fun getFlow(category: String? = null, language: Locale? = null): Flow<List<Tool>> {
val baseFlow = when (language) {
null -> toolsRepository.getNormalToolsFlow()
else -> toolsRepository.getNormalToolsFlowByLanguage(language)
}

val defaultVariantsFlow = toolsRepository.getMetaToolsFlow()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what a metatool is, and I think that might be hindering me from getting whats going on here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait... maybe I do know what that is. Is this how the 4 Spiritual Laws booklet relates to the KGP or Would You Like To Know God Personally?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metatools are a way of grouping tools together that are the same content just presented towards different audiences. You can see them if you open tool details of 4 laws or kgp, it's on the "Versions" tab.

We previously had been hiding those other versions from the all tools list, but part of the requested changes was to make them available on the all tools UI

.map { it.associateBy({ it.code }, { it.defaultVariantCode }) }
internal class FilteredToolsFlowProducer @Inject constructor(
private val settings: Settings,
private val toolsRepository: ToolsRepository
) {
@OptIn(ExperimentalCoroutinesApi::class)
fun getFlow(mode: Mode, category: String? = null, language: Locale? = null): Flow<List<Tool>> {
val baseFlow = when {
mode == Mode.PERSONALIZATION -> {
val languageFlow = if (language != null) flowOf(language) else settings.appLanguageFlow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to take into account if someone sets a personalization country and the language is different then their app language and we need to pull the tools for that specific language? Want to make sure I understand what is happening here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on the Dashboard they want the list of tools/lessons to take the language filter into account and default to the appLanguage

val fallbackFlow = languageFlow.flatMapLatest { toolsRepository.getPersonalizedToolsFlow(it, null) }

return baseFlow
.map { it.filterNot { it.isHidden }.sortedBy { it.defaultOrder } }
.combine(defaultVariantsFlow) { tools, defaultVariants ->
tools.filter { it.metatoolCode == null || it.code == defaultVariants[it.metatoolCode] }
languageFlow
.combineTransformLatest(settings.getPersonalizationCountryFlow()) { language, country ->
emitAll(toolsRepository.getPersonalizedToolsFlow(language, country))
}
.combine(fallbackFlow) { personalized, fallback -> personalized.ifEmpty { fallback } }
.distinctUntilChanged()
}
.map { tools -> if (category == null) tools else tools.filter { it.category == category } }

language != null -> toolsRepository.getNormalToolsFlowByLanguage(language)
.map { it.sortedBy { it.defaultOrder } }

else -> toolsRepository.getNormalToolsFlow().map { it.sortedBy { it.defaultOrder } }
}

return baseFlow.map {
it
.filterNot { it.isHidden }
.filter { category == null || it.category == category }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.cru.godtools.model.Language
import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName
import org.cru.godtools.ui.dashboard.filters.FilterMenu
import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode

interface ToolFiltersStateProducer {
data class Filters(
Expand All @@ -42,7 +43,7 @@ interface ToolFiltersStateProducer {
) : CircuitUiState

@Composable
fun produce(): Filters
fun produce(mode: Mode): Filters
}

internal class DefaultToolFiltersStateProducer @Inject constructor(
Expand All @@ -54,7 +55,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor(
@param:DispatcherType(IO) private val ioDispatcher: CoroutineDispatcher,
) : ToolFiltersStateProducer {
@Composable
override fun produce(): Filters {
override fun produce(mode: Mode): Filters {
val scope = rememberCoroutineScope()

val selectedCategory by remember { settings.getDashboardFilterCategoryFlow() }.collectAsState(null)
Expand All @@ -69,7 +70,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor(
return Filters(
categoryFilter = FilterMenu.UiState(
menuExpanded = rememberSaveable { mutableStateOf(false) },
items = rememberFilterCategories(selectedLocale),
items = rememberFilterCategories(mode, selectedLocale),
query = remember { mutableStateOf("") },
selectedItem = selectedCategory,
eventSink = {
Expand All @@ -82,7 +83,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor(
),
languageFilter = FilterMenu.UiState(
menuExpanded = languageMenuExpanded,
items = rememberFilterLanguages(selectedCategory, languageQuery.value),
items = rememberFilterLanguages(mode, selectedCategory, languageQuery.value),
selectedItem = languagesRepository.rememberLanguage(selectedLocale),
query = languageQuery,
eventSink = {
Expand All @@ -97,16 +98,20 @@ internal class DefaultToolFiltersStateProducer @Inject constructor(
}

@Composable
private fun rememberFilterCategories(selectedLanguage: Locale?) = remember(selectedLanguage) {
filteredToolsFlowProducer.getFlow(language = selectedLanguage).map {
private fun rememberFilterCategories(mode: Mode, selectedLanguage: Locale?) = remember(mode, selectedLanguage) {
filteredToolsFlowProducer.getFlow(mode, language = selectedLanguage).map {
it.groupBy { it.category }
.map { (category, tools) -> FilterMenu.UiState.Item(category, tools.size) }
}
}.collectAsState(emptyList()).value

@Composable
@OptIn(ExperimentalCoroutinesApi::class)
private fun rememberFilterLanguages(category: String?, query: String): List<FilterMenu.UiState.Item<Language?>> {
private fun rememberFilterLanguages(
mode: Mode,
category: String?,
query: String,
): List<FilterMenu.UiState.Item<Language?>> {
val scope = rememberCoroutineScope()

val categoryFlow = rememberStateFlow(category)
Expand All @@ -127,8 +132,8 @@ internal class DefaultToolFiltersStateProducer @Inject constructor(
.shareIn(scope, started = SharingStarted.WhileSubscribed(5_000), replay = 1)
}

return remember(category) {
val toolCountsFlow = filteredToolsFlowProducer.getFlow(category = category)
return remember(mode, category) {
val toolCountsFlow = filteredToolsFlowProducer.getFlow(mode, category = category)
.map { it.mapNotNullTo(mutableSetOf()) { it.code } }
.distinctUntilChanged()
.flatMapLatest { translationsRepository.getTranslationsFlowForTools(it) }
Expand Down
Loading