Skip to content

Commit

Permalink
generate the spotlight tools in the ToolsPresenter
Browse files Browse the repository at this point in the history
  • Loading branch information
frett committed Jan 2, 2024
1 parent 5343ede commit 55f9115
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import dagger.hilt.components.SingletonComponent
import org.ccci.gto.android.common.androidx.compose.foundation.layout.padding
import org.cru.godtools.R
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_ALL_TOOLS
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_SPOTLIGHT
import org.cru.godtools.ui.banner.Banners
import org.cru.godtools.ui.tools.SquareToolCard
import org.cru.godtools.ui.tools.ToolCard
Expand Down Expand Up @@ -63,8 +62,7 @@ internal fun ToolsLayout(state: ToolsScreen.State, modifier: Modifier = Modifier
if (spotlightTools.isNotEmpty()) {
item("tool-spotlight", "tool-spotlight") {
ToolSpotlight(
state,
toolViewModels,
spotlightTools,
modifier = Modifier
.animateItemPlacement()
.padding(top = 16.dp)
Expand Down Expand Up @@ -111,11 +109,7 @@ internal fun ToolsLayout(state: ToolsScreen.State, modifier: Modifier = Modifier
}

@Composable
private fun ToolSpotlight(state: ToolsScreen.State, toolViewModels: ToolViewModels, modifier: Modifier = Modifier) {
val spotlightTools by rememberUpdatedState(state.spotlightTools)
val selectedLanguage by rememberUpdatedState(state.filters.selectedLanguage)
val eventSink by rememberUpdatedState(state.eventSink)

private fun ToolSpotlight(tools: List<ToolCard.State>, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
stringResource(R.string.dashboard_tools_section_spotlight_label),
Expand All @@ -136,20 +130,11 @@ private fun ToolSpotlight(state: ToolsScreen.State, toolViewModels: ToolViewMode
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(vertical = 8.dp)
) {
items(spotlightTools, key = { it.code.orEmpty() }) { tool ->
val toolViewModel = toolViewModels[tool.code.orEmpty()]
val toolState = toolViewModel.toState(secondLanguage = selectedLanguage) {
when (it) {
ToolCard.Event.Click, ToolCard.Event.OpenTool, ToolCard.Event.OpenToolDetails ->
tool.code?.let { eventSink(ToolsScreen.Event.OpenToolDetails(it, SOURCE_SPOTLIGHT)) }
ToolCard.Event.PinTool -> toolViewModel.pinTool()
ToolCard.Event.UnpinTool -> toolViewModel.unpinTool()
}
}

items(tools, key = { it.tool?.code.orEmpty() }) { tool ->
SquareToolCard(
state = toolState,
state = tool,
showCategory = false,
showSecondLanguage = true,
showActions = false,
floatParallelLanguageUp = false,
confirmRemovalFromFavorites = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.runtime.Navigator
Expand All @@ -18,22 +19,31 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_SPOTLIGHT
import org.cru.godtools.base.Settings
import org.cru.godtools.db.repository.LanguagesRepository
import org.cru.godtools.db.repository.ToolsRepository
import org.cru.godtools.model.Language
import org.cru.godtools.model.Tool
import org.cru.godtools.ui.banner.BannerType
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen
import org.cru.godtools.ui.tools.ToolCard
import org.cru.godtools.ui.tools.ToolCardPresenter
import org.greenrobot.eventbus.EventBus

class ToolsPresenter @AssistedInject constructor(
private val eventBus: EventBus,
private val settings: Settings,
private val toolCardPresenter: ToolCardPresenter,
private val languagesRepository: LanguagesRepository,
private val toolsRepository: ToolsRepository,
@Assisted private val navigator: Navigator,
) : Presenter<ToolsScreen.State> {
@Composable
override fun present(): ToolsScreen.State {
val viewModel: ToolsViewModel = viewModel()

// selected language
val selectedLocale by viewModel.selectedLocale.collectAsState()
val selectedLanguage = rememberLanguage(selectedLocale)

Expand All @@ -55,7 +65,7 @@ class ToolsPresenter @AssistedInject constructor(

return ToolsScreen.State(
banner = rememberBanner(),
spotlightTools = viewModel.spotlightTools.collectAsState().value,
spotlightTools = rememberSpotlightTools(secondLanguage = selectedLanguage, eventSink = eventSink),
filters = ToolsScreen.State.Filters(
categories = viewModel.categories.collectAsState().value,
selectedCategory = viewModel.selectedCategory.collectAsState().value,
Expand All @@ -81,6 +91,41 @@ class ToolsPresenter @AssistedInject constructor(
locale?.let { languagesRepository.findLanguageFlow(it) } ?: flowOf(null)
}.collectAsState(null).value

@Composable
@VisibleForTesting
internal fun rememberSpotlightTools(
secondLanguage: Language?,
eventSink: (ToolsScreen.Event) -> Unit,
): List<ToolCard.State> {
val tools by remember {
toolsRepository.getNormalToolsFlow()
.map { it.filter { !it.isHidden && it.isSpotlight }.sortedWith(Tool.COMPARATOR_DEFAULT_ORDER) }
}.collectAsState(emptyList())
val eventSink by rememberUpdatedState(eventSink)

return tools.map { tool ->
val toolCode by rememberUpdatedState(tool.code)
val toolEventSink: (ToolCard.Event) -> Unit = remember {
{
when (it) {
ToolCard.Event.Click,
ToolCard.Event.OpenTool,
ToolCard.Event.OpenToolDetails ->
toolCode?.let { eventSink(ToolsScreen.Event.OpenToolDetails(it, SOURCE_SPOTLIGHT)) }
ToolCard.Event.PinTool,
ToolCard.Event.UnpinTool -> error("$it should be handled by the ToolCardPresenter")
}
}
}

toolCardPresenter.present(
tool = tool,
secondLanguage = secondLanguage,
eventSink = toolEventSink,
)
}
}

@AssistedFactory
@CircuitInject(ToolsScreen::class, SingletonComponent::class)
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import kotlinx.parcelize.Parcelize
import org.cru.godtools.model.Language
import org.cru.godtools.model.Tool
import org.cru.godtools.ui.banner.BannerType
import org.cru.godtools.ui.tools.ToolCard

@Parcelize
data object ToolsScreen : Screen {
data class State(
val banner: BannerType? = null,
val spotlightTools: List<Tool> = emptyList(),
val spotlightTools: List<ToolCard.State> = emptyList(),
val filters: Filters = Filters(),
val tools: List<Tool> = emptyList(),
val eventSink: (Event) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.cru.godtools.db.repository.LanguagesRepository
import org.cru.godtools.db.repository.ToolsRepository
import org.cru.godtools.model.Language
import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName
import org.cru.godtools.model.Tool

private const val KEY_SELECTED_CATEGORY = "selectedCategory"
private const val KEY_SELECTED_LANGUAGE = "selectedLanguage"
Expand All @@ -37,10 +36,6 @@ class ToolsViewModel @Inject constructor(
languagesRepository: LanguagesRepository,
private val savedState: SavedStateHandle,
) : ViewModel() {
val spotlightTools = toolsRepository.getNormalToolsFlow()
.map { it.filter { !it.isHidden && it.isSpotlight }.sortedWith(Tool.COMPARATOR_DEFAULT_ORDER) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

// region Tools
val selectedCategory = savedState.getStateFlow<String?>(KEY_SELECTED_CATEGORY, null)
fun setSelectedCategory(category: String?) = savedState.set(KEY_SELECTED_CATEGORY, category)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,43 +59,6 @@ class ToolsViewModelTest {
Dispatchers.resetMain()
}

// region Property spotlightTools
@Test
fun `Property spotlightTools`() = testScope.runTest {
viewModel.spotlightTools.test {
assertThat(awaitItem(), empty())

val normal = randomTool("normal", isHidden = false, isSpotlight = false)
val spotlight = randomTool("spotlight", isHidden = false, isSpotlight = true)
toolsFlow.value = listOf(normal, spotlight)
assertEquals(listOf(spotlight), awaitItem())
}
}

@Test
fun `Property spotlightTools - Don't show hidden tools`() = testScope.runTest {
viewModel.spotlightTools.test {
assertThat(awaitItem(), empty())

val hidden = randomTool("normal", isHidden = true, isSpotlight = true)
val spotlight = randomTool("spotlight", isHidden = false, isSpotlight = true)
toolsFlow.value = listOf(hidden, spotlight)
assertEquals(listOf(spotlight), awaitItem())
}
}

@Test
fun `Property spotlightTools - Sorted by default order`() = testScope.runTest {
viewModel.spotlightTools.test {
assertThat(awaitItem(), empty())

val tools = List(10) { randomTool("tool$it", Tool.Type.TRACT, isHidden = false, isSpotlight = true) }
toolsFlow.value = tools
assertEquals(tools.sortedWith(Tool.COMPARATOR_DEFAULT_ORDER), awaitItem())
}
}
// endregion Property spotlightTools

// region Property filteredTools
@Test
fun `Property filteredTools - return only default variants`() = testScope.runTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,27 @@ import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.cru.godtools.TestUtils.clearAndroidUiDispatcher
import org.cru.godtools.base.Settings
import org.cru.godtools.db.repository.LanguagesRepository
import org.cru.godtools.db.repository.ToolsRepository
import org.cru.godtools.model.Language
import org.cru.godtools.model.Tool
import org.cru.godtools.model.randomTool
import org.cru.godtools.ui.banner.BannerType
import org.cru.godtools.ui.tools.ToolCardPresenter
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
@Config(application = Application::class)
class ToolsPresenterTest {
private val isFavoritesFeatureDiscovered = MutableStateFlow(true)
private val toolsFlow = MutableSharedFlow<List<Tool>>(extraBufferCapacity = 1)

private val languagesRepository: LanguagesRepository = mockk {
every { findLanguageFlow(any()) } returns flowOf(null)
Expand All @@ -36,11 +42,25 @@ class ToolsPresenterTest {
private val settings: Settings = mockk {
every { isFeatureDiscoveredFlow(Settings.FEATURE_TOOL_FAVORITE) } returns isFavoritesFeatureDiscovered
}
private val toolsRepository: ToolsRepository = mockk {
every { getNormalToolsFlow() } returns toolsFlow
}

// TODO: figure out how to mock ToolCardPresenter
private val toolCardPresenter = ToolCardPresenter(
fileSystem = mockk(),
settings = mockk(relaxed = true),
attachmentsRepository = mockk(relaxed = true),
toolsRepository = mockk(),
translationsRepository = mockk(relaxed = true),
)

private val presenter = ToolsPresenter(
eventBus = mockk(),
settings = settings,
toolCardPresenter = toolCardPresenter,
languagesRepository = languagesRepository,
toolsRepository = toolsRepository,
navigator = navigator,
)

Expand Down Expand Up @@ -73,6 +93,57 @@ class ToolsPresenterTest {
}
// endregion State.banner

// region State.spotlightTools
@Test
fun `Property spotlightTools`() = runTest {
val language = Language(Locale.ENGLISH)
val normalTool = randomTool("normal", isHidden = false, isSpotlight = false)
val spotlightTool = randomTool("spotlight", isHidden = false, isSpotlight = true)

presenterTestOf(
presentFunction = {
ToolsScreen.State(spotlightTools = presenter.rememberSpotlightTools(language, {}), eventSink = {})
}
) {
toolsFlow.emit(listOf(normalTool, spotlightTool))
assertEquals(listOf(spotlightTool), expectMostRecentItem().spotlightTools.map { it.tool })
}
}

@Test
fun `Property spotlightTools - Don't show hidden tools`() = runTest {
val language = Language(Locale.ENGLISH)
val hiddenTool = randomTool("normal", isHidden = true, isSpotlight = true)
val spotlightTool = randomTool("spotlight", isHidden = false, isSpotlight = true)

presenterTestOf(
presentFunction = {
ToolsScreen.State(spotlightTools = presenter.rememberSpotlightTools(language, {}), eventSink = {})
}
) {
toolsFlow.emit(listOf(hiddenTool, spotlightTool))
assertEquals(listOf(spotlightTool), expectMostRecentItem().spotlightTools.map { it.tool })
}
}

@Test
fun `Property spotlightTools - Sorted by default order`() = runTest {
val language = Language(Locale.ENGLISH)
val tools = List(10) {
randomTool("tool$it", Tool.Type.TRACT, defaultOrder = it, isHidden = false, isSpotlight = true)
}

presenterTestOf(
presentFunction = {
ToolsScreen.State(spotlightTools = presenter.rememberSpotlightTools(language, {}), eventSink = {})
}
) {
toolsFlow.emit(tools.shuffled())
assertEquals(tools, expectMostRecentItem().spotlightTools.map { it.tool })
}
}
// endregion State.spotlightTools

// region State.filters.selectedLanguage
@Test
fun `State - filters - selectedLanguage - no language selected`() = runTest {
Expand Down
3 changes: 2 additions & 1 deletion library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ fun randomTool(
name: String? = UUID.randomUUID().toString().takeIf { Random.nextBoolean() },
description: String? = UUID.randomUUID().toString().takeIf { Random.nextBoolean() },
bannerId: Long? = Random.nextLong().takeIf { Random.nextBoolean() },
defaultOrder: Int = Random.nextInt(),
isFavorite: Boolean = Random.nextBoolean(),
isHidden: Boolean = Random.nextBoolean(),
isSpotlight: Boolean = Random.nextBoolean(),
Expand All @@ -236,7 +237,7 @@ fun randomTool(
detailsBannerId = Random.nextLong().takeIf { Random.nextBoolean() },
detailsBannerAnimationId = Random.nextLong().takeIf { Random.nextBoolean() },
detailsBannerYoutubeVideoId = UUID.randomUUID().toString().takeIf { Random.nextBoolean() },
defaultOrder = Random.nextInt(),
defaultOrder = defaultOrder,
order = Random.nextInt(),
isFavorite = isFavorite,
isHidden = isHidden,
Expand Down

0 comments on commit 55f9115

Please sign in to comment.