Skip to content

Commit

Permalink
Shared, iOS: Code blanks mechanics onboarding (#1183)
Browse files Browse the repository at this point in the history
^ALTAPPS-1357
ivan-magda authored Sep 18, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent c49c6f6 commit 1123c3a
Showing 23 changed files with 240 additions and 77 deletions.
2 changes: 0 additions & 2 deletions config/detekt/baseline.xml
Original file line number Diff line number Diff line change
@@ -25,9 +25,7 @@
<ID>CyclomaticComplexMethod:LeaderboardWidgetReducer.kt$LeaderboardWidgetReducer$override fun reduce(state: State, message: Message): LeaderboardWidgetReducerResult</ID>
<ID>CyclomaticComplexMethod:LoadingView.kt$LoadingView$override fun onDraw(canvas: Canvas)</ID>
<ID>CyclomaticComplexMethod:ProfileReducer.kt$ProfileReducer$override fun reduce(state: State, message: Message): ReducerResult</ID>
<ID>CyclomaticComplexMethod:StepQuizActionDispatcher.kt$StepQuizActionDispatcher$override suspend fun doSuspendableAction(action: Action)</ID>
<ID>CyclomaticComplexMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?</ID>
<ID>CyclomaticComplexMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSuggestionClicked( state: State, message: Message.SuggestionClicked ): StepQuizCodeBlanksReducerResult?</ID>
<ID>CyclomaticComplexMethod:StepQuizCodeBlanksViewStateMapper.kt$StepQuizCodeBlanksViewStateMapper$private fun mapContentState( state: StepQuizCodeBlanksFeature.State.Content ): StepQuizCodeBlanksViewState.Content</ID>
<ID>CyclomaticComplexMethod:StepQuizHintsReducer.kt$StepQuizHintsReducer$override fun reduce(state: State, message: Message): StepQuizHintsReducerResult</ID>
<ID>CyclomaticComplexMethod:StepQuizReducer.kt$StepQuizReducer$override fun reduce(state: State, message: Message): StepQuizReducerResult</ID>
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@ struct StepQuizCodeBlanksActionButton: View {

let imageSystemName: String

var isAnimationEffectActive = false

let action: () -> Void

@Environment(\.isEnabled) private var isEnabled
@@ -33,22 +35,35 @@ struct StepQuizCodeBlanksActionButton: View {
)
.foregroundColor(Color(ColorPalette.onPrimary))
.cornerRadius(appearance.cornerRadius)
.shineEffect(isActive: isEnabled && isAnimationEffectActive)
.pulseEffect(
shape: RoundedRectangle(cornerRadius: appearance.cornerRadius),
isActive: isEnabled && isAnimationEffectActive
)
}
)
.buttonStyle(BounceButtonStyle())
}
}

extension StepQuizCodeBlanksActionButton {
static func delete(action: @escaping () -> Void) -> StepQuizCodeBlanksActionButton {
StepQuizCodeBlanksActionButton(imageSystemName: "delete.left", action: action)
static func delete(
isAnimationEffectActive: Bool,
action: @escaping () -> Void
) -> StepQuizCodeBlanksActionButton {
StepQuizCodeBlanksActionButton(
imageSystemName: "delete.left",
isAnimationEffectActive: isAnimationEffectActive,
action: action
)
}

static func enter(action: @escaping () -> Void) -> StepQuizCodeBlanksActionButton {
StepQuizCodeBlanksActionButton(imageSystemName: "return", action: action)
}

static func space(
isAnimationEffectActive: Bool,
action: @escaping () -> Void
) -> StepQuizCodeBlanksActionButton {
StepQuizCodeBlanksActionButton(
@@ -59,6 +74,7 @@ extension StepQuizCodeBlanksActionButton {
)
),
imageSystemName: "space",
isAnimationEffectActive: isAnimationEffectActive,
action: action
)
}
@@ -81,16 +97,16 @@ extension StepQuizCodeBlanksActionButton {
#Preview {
VStack {
HStack {
StepQuizCodeBlanksActionButton.delete(action: {})
StepQuizCodeBlanksActionButton.delete(isAnimationEffectActive: false, action: {})
StepQuizCodeBlanksActionButton.enter(action: {})
StepQuizCodeBlanksActionButton.space(action: {})
StepQuizCodeBlanksActionButton.space(isAnimationEffectActive: false, action: {})
StepQuizCodeBlanksActionButton.decreaseIndentLevel(action: {})
}

HStack {
StepQuizCodeBlanksActionButton.delete(action: {})
StepQuizCodeBlanksActionButton.delete(isAnimationEffectActive: false, action: {})
StepQuizCodeBlanksActionButton.enter(action: {})
StepQuizCodeBlanksActionButton.space(action: {})
StepQuizCodeBlanksActionButton.space(isAnimationEffectActive: false, action: {})
StepQuizCodeBlanksActionButton.decreaseIndentLevel(action: {})
}
.disabled(true)
Original file line number Diff line number Diff line change
@@ -5,6 +5,9 @@ struct StepQuizCodeBlanksActionButtonsView: View {
let isSpaceButtonHidden: Bool
let isDecreaseIndentLevelButtonHidden: Bool

let isDeleteButtonHighlightEffectActive: Bool
let isSpaceButtonHighlightEffectActive: Bool

let onSpaceTap: () -> Void
let onDeleteTap: () -> Void
let onEnterTap: () -> Void
@@ -21,11 +24,17 @@ struct StepQuizCodeBlanksActionButtonsView: View {

if !isSpaceButtonHidden {
StepQuizCodeBlanksActionButton
.space(action: onSpaceTap)
.space(
isAnimationEffectActive: isSpaceButtonHighlightEffectActive,
action: onSpaceTap
)
}

StepQuizCodeBlanksActionButton
.delete(action: onDeleteTap)
.delete(
isAnimationEffectActive: isDeleteButtonHighlightEffectActive,
action: onDeleteTap
)
.disabled(!isDeleteButtonEnabled)

StepQuizCodeBlanksActionButton
@@ -42,6 +51,8 @@ struct StepQuizCodeBlanksActionButtonsView: View {
isDeleteButtonEnabled: false,
isSpaceButtonHidden: false,
isDecreaseIndentLevelButtonHidden: false,
isDeleteButtonHighlightEffectActive: false,
isSpaceButtonHighlightEffectActive: true,
onSpaceTap: {},
onDeleteTap: {},
onEnterTap: {},
@@ -52,6 +63,8 @@ struct StepQuizCodeBlanksActionButtonsView: View {
isDeleteButtonEnabled: true,
isSpaceButtonHidden: true,
isDecreaseIndentLevelButtonHidden: true,
isDeleteButtonHighlightEffectActive: true,
isSpaceButtonHighlightEffectActive: false,
onSpaceTap: {},
onDeleteTap: {},
onEnterTap: {},
Original file line number Diff line number Diff line change
@@ -39,6 +39,8 @@ struct StepQuizCodeBlanksCodeBlocksView: View {
isDeleteButtonEnabled: state.isDeleteButtonEnabled,
isSpaceButtonHidden: state.isSpaceButtonHidden,
isDecreaseIndentLevelButtonHidden: state.isDecreaseIndentLevelButtonHidden,
isDeleteButtonHighlightEffectActive: state.isDeleteButtonHighlightEffectActive,
isSpaceButtonHighlightEffectActive: state.isSpaceButtonHighlightEffectActive,
onSpaceTap: onSpaceTap,
onDeleteTap: onDeleteTap,
onEnterTap: onEnterTap,
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import org.hyperskill.app.subscriptions.domain.model.SubscriptionType
import org.hyperskill.onboarding.domain.model.stub
import org.hyperskill.step.domain.model.stub
import org.hyperskill.step_quiz.domain.model.stub
import org.hyperskill.step_quiz_code_blanks.presentation.stub
import org.hyperskill.subscriptions.stub
import org.junit.Test

@@ -78,8 +79,8 @@ class AndroidStepQuizTest {
stepQuizChildFeatureReducer = StepQuizChildFeatureReducer(
stepQuizHintsReducer = StepQuizHintsReducer(stepRoute),
stepQuizToolbarReducer = StepQuizToolbarReducer(stepRoute),
stepQuizCodeBlanksReducer = StepQuizCodeBlanksReducer(stepRoute)
),
stepQuizCodeBlanksReducer = StepQuizCodeBlanksReducer.stub(stepRoute)
)
)

val (state, _) = reducer.reduce(
Original file line number Diff line number Diff line change
@@ -4,14 +4,15 @@ import org.hyperskill.app.core.injection.AppGraph
import org.hyperskill.app.core.presentation.ActionDispatcherOptions
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksActionDispatcher
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksOnboardingReducer
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer

internal class StepQuizCodeBlanksComponentImpl(
private val appGraph: AppGraph,
private val stepRoute: StepRoute
) : StepQuizCodeBlanksComponent {
override val stepQuizCodeBlanksReducer: StepQuizCodeBlanksReducer
get() = StepQuizCodeBlanksReducer(stepRoute)
get() = StepQuizCodeBlanksReducer(stepRoute, StepQuizCodeBlanksOnboardingReducer())

override val stepQuizCodeBlanksActionDispatcher: StepQuizCodeBlanksActionDispatcher
get() = StepQuizCodeBlanksActionDispatcher(
Original file line number Diff line number Diff line change
@@ -39,8 +39,14 @@ object StepQuizCodeBlanksFeature {

sealed interface OnboardingState {
data object Unavailable : OnboardingState
data object HighlightSuggestions : OnboardingState
data object HighlightCallToActionButton : OnboardingState

data object HighlightDeleteButton : OnboardingState
data object HighlightSpaceButton : OnboardingState

sealed interface PrintSuggestionAndCallToAction : OnboardingState {
data object HighlightSuggestions : PrintSuggestionAndCallToAction
data object HighlightCallToActionButton : PrintSuggestionAndCallToAction
}
}

sealed interface Message {
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.hyperskill.app.step_quiz_code_blanks.presentation

import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.Action
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalAction
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalMessage
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.State

class StepQuizCodeBlanksOnboardingReducer {
companion object {
private const val DELETE_BUTTON_STEP_ID = 50969L
private const val SPACE_BUTTON_STEP_ID = 50970L
private val PRINT_SUGGESTION_AND_CALL_TO_ACTION_STEP_IDS = setOf(47329L, 50968L)
}

internal fun reduceInitializeMessage(
message: InternalMessage.Initialize
): OnboardingState =
when (message.step.id) {
in PRINT_SUGGESTION_AND_CALL_TO_ACTION_STEP_IDS ->
OnboardingState.PrintSuggestionAndCallToAction.HighlightSuggestions
DELETE_BUTTON_STEP_ID -> OnboardingState.HighlightDeleteButton
SPACE_BUTTON_STEP_ID -> OnboardingState.HighlightSpaceButton
else -> OnboardingState.Unavailable
}

internal fun reduceSuggestionClickedMessage(
state: State.Content,
activeCodeBlock: CodeBlock?,
newCodeBlock: CodeBlock
): Pair<OnboardingState, Set<Action>> {
val isFulfilledOnboardingPrintCodeBlock =
state.onboardingState is OnboardingState.PrintSuggestionAndCallToAction.HighlightSuggestions &&
activeCodeBlock is CodeBlock.Print && activeCodeBlock.hasAnyUnselectedChild() &&
newCodeBlock is CodeBlock.Print && newCodeBlock.areAllChildrenSelected()
return if (isFulfilledOnboardingPrintCodeBlock) {
OnboardingState.PrintSuggestionAndCallToAction.HighlightCallToActionButton to
setOf(
InternalAction.ParentFeatureActionRequested(
StepQuizCodeBlanksFeature.ParentFeatureAction.HighlightCallToActionButton
)
)
} else {
state.onboardingState to emptySet()
}
}
}
Original file line number Diff line number Diff line change
@@ -20,16 +20,18 @@ import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksF
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalAction
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalMessage
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.Message
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.State
import ru.nobird.app.core.model.mutate
import ru.nobird.app.presentation.redux.reducer.StateReducer

private typealias StepQuizCodeBlanksReducerResult = Pair<State, Set<Action>>

class StepQuizCodeBlanksReducer(
private val stepRoute: StepRoute
private val stepRoute: StepRoute,
private val stepQuizCodeBlanksOnboardingReducer: StepQuizCodeBlanksOnboardingReducer
) : StateReducer<State, Message, Action> {
companion object;

override fun reduce(state: State, message: Message): StepQuizCodeBlanksReducerResult =
when (message) {
is InternalMessage.Initialize -> initialize(message)
@@ -48,11 +50,7 @@ class StepQuizCodeBlanksReducer(
State.Content(
step = message.step,
codeBlocks = createInitialCodeBlocks(step = message.step),
onboardingState = if (StepQuizCodeBlanksResolver.isOnboardingAvailable(message.step)) {
OnboardingState.HighlightSuggestions
} else {
OnboardingState.Unavailable
}
onboardingState = stepQuizCodeBlanksOnboardingReducer.reduceInitializeMessage(message)
) to emptySet()

private fun handleSuggestionClicked(
@@ -209,21 +207,12 @@ class StepQuizCodeBlanksReducer(
}
}

val isFulfilledOnboardingPrintCodeBlock =
state.onboardingState is OnboardingState.HighlightSuggestions &&
activeCodeBlock is CodeBlock.Print && activeCodeBlock.hasAnyUnselectedChild() &&
newCodeBlock is CodeBlock.Print && newCodeBlock.areAllChildrenSelected()
val (onboardingState, onboardingActions) =
if (isFulfilledOnboardingPrintCodeBlock) {
OnboardingState.HighlightCallToActionButton to
setOf(
InternalAction.ParentFeatureActionRequested(
StepQuizCodeBlanksFeature.ParentFeatureAction.HighlightCallToActionButton
)
)
} else {
state.onboardingState to emptySet()
}
stepQuizCodeBlanksOnboardingReducer.reduceSuggestionClickedMessage(
state = state,
activeCodeBlock = activeCodeBlock,
newCodeBlock = newCodeBlock
)

return state.copy(
codeBlocks = newCodeBlocks,
Original file line number Diff line number Diff line change
@@ -6,13 +6,8 @@ import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
import ru.nobird.app.core.model.slice

internal object StepQuizCodeBlanksResolver {
private const val ONBOARDING_STEP_ID = 47329L

private const val MINIMUM_POSSIBLE_INDEX_FOR_ELIF_AND_ELSE_STATEMENTS = 2

fun isOnboardingAvailable(step: Step): Boolean =
step.id == ONBOARDING_STEP_ID

fun isVariableSuggestionsAvailable(step: Step): Boolean =
step.block.options.codeBlanksVariables?.isNotEmpty() == true

Original file line number Diff line number Diff line change
@@ -15,10 +15,21 @@ sealed interface StepQuizCodeBlanksViewState {
internal val onboardingState: OnboardingState = OnboardingState.Unavailable
) : StepQuizCodeBlanksViewState {
val isActionButtonsHidden: Boolean
get() = onboardingState != OnboardingState.Unavailable
get() = when (onboardingState) {
is OnboardingState.PrintSuggestionAndCallToAction -> true
OnboardingState.HighlightDeleteButton,
OnboardingState.HighlightSpaceButton,
OnboardingState.Unavailable -> false
}

val isDeleteButtonHighlightEffectActive: Boolean
get() = isDeleteButtonEnabled && onboardingState == OnboardingState.HighlightDeleteButton

val isSpaceButtonHighlightEffectActive: Boolean
get() = !isSpaceButtonHidden && onboardingState == OnboardingState.HighlightSpaceButton

val isSuggestionsHighlightEffectActive: Boolean
get() = onboardingState == OnboardingState.HighlightSuggestions
get() = onboardingState == OnboardingState.PrintSuggestionAndCallToAction.HighlightSuggestions
}

sealed interface CodeBlockItem {
Original file line number Diff line number Diff line change
@@ -5,10 +5,11 @@ import org.hyperskill.app.step_quiz.presentation.StepQuizChildFeatureReducer
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
import org.hyperskill.app.step_quiz_hints.presentation.StepQuizHintsReducer
import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarReducer
import org.hyperskill.step_quiz_code_blanks.presentation.stub

internal fun StepQuizChildFeatureReducer.Companion.stub(stepRoute: StepRoute) =
StepQuizChildFeatureReducer(
stepQuizHintsReducer = StepQuizHintsReducer(stepRoute),
stepQuizToolbarReducer = StepQuizToolbarReducer(stepRoute),
stepQuizCodeBlanksReducer = StepQuizCodeBlanksReducer(stepRoute)
stepQuizCodeBlanksReducer = StepQuizCodeBlanksReducer.stub(stepRoute)
)
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ package org.hyperskill.step_quiz_code_blanks.presentation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
@@ -12,7 +11,7 @@ import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksR
import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState

class StepQuizCodeBlanksReducerCodeBlockChildClickedTest {
private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
private val reducer = StepQuizCodeBlanksReducer.stub()

@Test
fun `CodeBlockChildClicked should not update state if state is not Content`() {
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ package org.hyperskill.step_quiz_code_blanks.presentation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
@@ -12,7 +11,7 @@ import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksR
import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState

class StepQuizCodeBlanksReducerCodeBlockClickedTest {
private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
private val reducer = StepQuizCodeBlanksReducer.stub()

@Test
fun `CodeBlockClicked should not update state if state is not Content`() {
Loading

0 comments on commit 1123c3a

Please sign in to comment.