diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt index d4fc5328e2..84d2bf1af4 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt @@ -81,6 +81,9 @@ class StepDelegate( .newInstance(earnedGemsText = stepCompletionAction.earnedGemsText) .showIfNotExists(fragment.childFragmentManager, CompletedStepOfTheDayDialogFragment.TAG) } + is StepCompletionFeature.Action.ViewAction.ShowShareStreakModal -> { + // TODO: ALTAPPS-1028 Show share streak modal + } } } } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift index 2783833d63..4c17dd84a1 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift @@ -116,6 +116,8 @@ struct StepView: View { presentSendDailyStudyRemindersPermissionAlert() case .showProblemOfDaySolvedModal(let showProblemOfDaySolvedModalViewAction): presentDailyStepCompletedModal(earnedGemsText: showProblemOfDaySolvedModalViewAction.earnedGemsText) + case .showShareStreakModal: + #warning("TODO: ALTAPPS-1027 Show share streak modal") } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt index 7f98e48db5..9e4c707a0f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt @@ -34,5 +34,6 @@ enum class HyperskillAnalyticPart(val partName: String) { PROJECT_COMPLETED_MODAL("project_completed_modal"), NEXT_LEARNING_ACTIVITY_WIDGET("next_learning_activity_widget"), FULL_SCREEN_CODE_EDITOR("full_screen_code_editor"), - CODE_EDITOR("code_editor") + CODE_EDITOR("code_editor"), + SHARE_STREAK_MODAL("share_streak_modal") } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt index 1065fbe537..ec159f4da7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt @@ -96,5 +96,8 @@ enum class HyperskillAnalyticTarget(val targetName: String) { START_LEARNING("start_learning"), REMIND_ME_LATER("remind_me_later"), FULL_SCREEN_CODE_EDITOR("full_screen_code_editor"), - CODE_INPUT_ACCESSORY_BUTTON("code_input_accessory_button") + CODE_INPUT_ACCESSORY_BUTTON("code_input_accessory_button"), + SHARE_YOUR_STREAK("share_your_streak"), + SHARE_STREAK_MODAL("share_streak_modal"), + SHARE("share") } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt index 7ec0fe79e9..950261f7c1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt @@ -45,6 +45,7 @@ import org.hyperskill.app.projects.injection.ProjectsDataComponent import org.hyperskill.app.providers.injection.ProvidersDataComponent import org.hyperskill.app.reactions.injection.ReactionsDataComponent import org.hyperskill.app.sentry.injection.SentryComponent +import org.hyperskill.app.share_streak.injection.ShareStreakDataComponent import org.hyperskill.app.stage_implement.injection.StageImplementComponent import org.hyperskill.app.stages.injection.StagesDataComponent import org.hyperskill.app.step.domain.model.StepRoute @@ -151,4 +152,5 @@ interface AppGraph { fun buildBadgesDataComponent(): BadgesDataComponent fun buildNotificationsOnboardingComponent(): NotificationsOnboardingComponent fun buildFirstProblemOnboardingComponent(): FirstProblemOnboardingComponent + fun buildShareStreakDataComponent(): ShareStreakDataComponent } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt index 779e3e7673..b904435b4d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt @@ -82,6 +82,8 @@ import org.hyperskill.app.providers.injection.ProvidersDataComponent import org.hyperskill.app.providers.injection.ProvidersDataComponentImpl import org.hyperskill.app.reactions.injection.ReactionsDataComponent import org.hyperskill.app.reactions.injection.ReactionsDataComponentImpl +import org.hyperskill.app.share_streak.injection.ShareStreakDataComponent +import org.hyperskill.app.share_streak.injection.ShareStreakDataComponentImpl import org.hyperskill.app.stage_implement.injection.StageImplementComponent import org.hyperskill.app.stage_implement.injection.StageImplementComponentImpl import org.hyperskill.app.stages.injection.StagesDataComponent @@ -411,4 +413,7 @@ abstract class BaseAppGraph : AppGraph { override fun buildFirstProblemOnboardingComponent(): FirstProblemOnboardingComponent = FirstProblemOnboardingComponentImpl(this) + + override fun buildShareStreakDataComponent(): ShareStreakDataComponent = + ShareStreakDataComponentImpl(this) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt index c3fcd6e838..8adf98b71e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt @@ -1,5 +1,6 @@ package org.hyperskill.app.core.injection +import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository import org.hyperskill.app.study_plan.domain.repository.CurrentStudyPlanStateRepository import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository @@ -12,9 +13,12 @@ interface StateRepositoriesComponent { val nextLearningActivityStateRepository: NextLearningActivityStateRepository + val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository + suspend fun resetRepositories() { currentSubscriptionStateRepository.resetState() currentStudyPlanStateRepository.resetState() nextLearningActivityStateRepository.resetState() + currentGamificationToolbarDataStateRepository.resetState() } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt index 10f3edc444..53001af758 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt @@ -1,5 +1,8 @@ package org.hyperskill.app.core.injection +import org.hyperskill.app.gamification_toolbar.data.repository.CurrentGamificationToolbarDataStateRepositoryImpl +import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository +import org.hyperskill.app.gamification_toolbar.remote.GamificationToolbarRemoteDataSourceImpl import org.hyperskill.app.learning_activities.data.repository.NextLearningActivityStateRepositoryImpl import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository import org.hyperskill.app.learning_activities.remote.LearningActivitiesRemoteDataSourceImpl @@ -37,4 +40,13 @@ class StateRepositoriesComponentImpl(appGraph: AppGraph) : StateRepositoriesComp override val nextLearningActivityStateRepository: NextLearningActivityStateRepository by lazy { NextLearningActivityStateRepositoryImpl(LearningActivitiesRemoteDataSourceImpl(authorizedHttpClient)) } + + /** + * Gamification toolbar + */ + override val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository by lazy { + CurrentGamificationToolbarDataStateRepositoryImpl( + GamificationToolbarRemoteDataSourceImpl(authorizedHttpClient) + ) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/GamificationToolbarRepositoryImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/CurrentGamificationToolbarDataStateRepositoryImpl.kt similarity index 57% rename from shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/GamificationToolbarRepositoryImpl.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/CurrentGamificationToolbarDataStateRepositoryImpl.kt index 1691374a07..90d1037940 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/GamificationToolbarRepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/data/repository/CurrentGamificationToolbarDataStateRepositoryImpl.kt @@ -1,12 +1,13 @@ package org.hyperskill.app.gamification_toolbar.data.repository +import org.hyperskill.app.core.data.repository.BaseStateRepository import org.hyperskill.app.gamification_toolbar.data.source.GamificationToolbarRemoteDataSource import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarData -import org.hyperskill.app.gamification_toolbar.domain.repository.GamificationToolbarRepository +import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository -class GamificationToolbarRepositoryImpl( +class CurrentGamificationToolbarDataStateRepositoryImpl( private val gamificationToolbarRemoteDataSource: GamificationToolbarRemoteDataSource -) : GamificationToolbarRepository { - override suspend fun getGamificationToolbarData(): Result = +) : CurrentGamificationToolbarDataStateRepository, BaseStateRepository() { + override suspend fun loadState(): Result = gamificationToolbarRemoteDataSource.getGamificationToolbarData() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/CurrentGamificationToolbarDataStateRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/CurrentGamificationToolbarDataStateRepository.kt new file mode 100644 index 0000000000..cc7673dcf3 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/CurrentGamificationToolbarDataStateRepository.kt @@ -0,0 +1,6 @@ +package org.hyperskill.app.gamification_toolbar.domain.repository + +import org.hyperskill.app.core.domain.repository.StateRepository +import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarData + +interface CurrentGamificationToolbarDataStateRepository : StateRepository \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/GamificationToolbarRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/GamificationToolbarRepository.kt deleted file mode 100644 index 90cc8b051e..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/domain/repository/GamificationToolbarRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.hyperskill.app.gamification_toolbar.domain.repository - -import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarData - -interface GamificationToolbarRepository { - suspend fun getGamificationToolbarData(): Result -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt index a33b1393e2..bf5518252a 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt @@ -2,13 +2,9 @@ package org.hyperskill.app.gamification_toolbar.injection import org.hyperskill.app.core.injection.AppGraph import org.hyperskill.app.core.presentation.ActionDispatcherOptions -import org.hyperskill.app.gamification_toolbar.data.repository.GamificationToolbarRepositoryImpl -import org.hyperskill.app.gamification_toolbar.data.source.GamificationToolbarRemoteDataSource import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarScreen -import org.hyperskill.app.gamification_toolbar.domain.repository.GamificationToolbarRepository import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarActionDispatcher import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarReducer -import org.hyperskill.app.gamification_toolbar.remote.GamificationToolbarRemoteDataSourceImpl class GamificationToolbarComponentImpl( private val appGraph: AppGraph, @@ -17,12 +13,6 @@ class GamificationToolbarComponentImpl( override val gamificationToolbarReducer: GamificationToolbarReducer get() = GamificationToolbarReducer(screen) - private val gamificationToolbarRemoteDataSource: GamificationToolbarRemoteDataSource = - GamificationToolbarRemoteDataSourceImpl(appGraph.networkComponent.authorizedHttpClient) - - private val gamificationToolbarRepository: GamificationToolbarRepository - get() = GamificationToolbarRepositoryImpl(gamificationToolbarRemoteDataSource) - override val gamificationToolbarActionDispatcher: GamificationToolbarActionDispatcher get() { val profileComponent = appGraph.profileDataComponent @@ -33,7 +23,7 @@ class GamificationToolbarComponentImpl( profileComponent.currentProfileStateRepository, appGraph.stateRepositoriesComponent.currentStudyPlanStateRepository, appGraph.stepCompletionFlowDataComponent.topicCompletedFlow, - gamificationToolbarRepository, + appGraph.stateRepositoriesComponent.currentGamificationToolbarDataStateRepository, appGraph.analyticComponent.analyticInteractor, appGraph.sentryComponent.sentryInteractor ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarActionDispatcher.kt index d675708e9d..e90796de29 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarActionDispatcher.kt @@ -5,12 +5,15 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.core.presentation.ActionDispatcherOptions -import org.hyperskill.app.gamification_toolbar.domain.repository.GamificationToolbarRepository +import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Action +import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.InternalAction +import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.InternalMessage import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Message import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.profile.domain.repository.observeHypercoinsBalance import org.hyperskill.app.sentry.domain.interactor.SentryInteractor +import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.step_completion.domain.flow.TopicCompletedFlow import org.hyperskill.app.step_quiz.domain.repository.SubmissionRepository import org.hyperskill.app.streaks.domain.flow.StreakFlow @@ -24,68 +27,73 @@ class GamificationToolbarActionDispatcher( currentProfileStateRepository: CurrentProfileStateRepository, currentStudyPlanStateRepository: CurrentStudyPlanStateRepository, topicCompletedFlow: TopicCompletedFlow, - private val gamificationToolbarRepository: GamificationToolbarRepository, + private val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository, private val analyticInteractor: AnalyticInteractor, private val sentryInteractor: SentryInteractor ) : CoroutineActionDispatcher(config.createConfig()) { init { submissionRepository.solvedStepsSharedFlow - .onEach { onNewMessage(Message.StepSolved) } + .onEach { onNewMessage(InternalMessage.StepSolved) } .launchIn(actionScope) currentProfileStateRepository.observeHypercoinsBalance() .onEach { hypercoinsBalance -> - onNewMessage(Message.HypercoinsBalanceChanged(hypercoinsBalance)) + onNewMessage(InternalMessage.HypercoinsBalanceChanged(hypercoinsBalance)) } .launchIn(actionScope) streakFlow.observe() .distinctUntilChanged() .onEach { streak -> - onNewMessage(Message.StreakChanged(streak)) + onNewMessage(InternalMessage.StreakChanged(streak)) } .launchIn(actionScope) currentStudyPlanStateRepository.changes .distinctUntilChanged() .onEach { studyPlan -> - onNewMessage(Message.StudyPlanChanged(studyPlan)) + onNewMessage(InternalMessage.StudyPlanChanged(studyPlan)) } .launchIn(actionScope) topicCompletedFlow.observe() .distinctUntilChanged() .onEach { - onNewMessage(Message.TopicCompleted) + onNewMessage(InternalMessage.TopicCompleted) } .launchIn(actionScope) + + currentGamificationToolbarDataStateRepository.changes + .distinctUntilChanged() + .onEach { onNewMessage(InternalMessage.GamificationToolbarDataChanged(it)) } + .launchIn(actionScope) } override suspend fun doSuspendableAction(action: Action) { when (action) { - is Action.FetchGamificationToolbarData -> { - val sentryTransaction = action.screen.fetchContentSentryTransaction - sentryInteractor.startTransaction(sentryTransaction) - - val gamificationToolbarData = gamificationToolbarRepository - .getGamificationToolbarData() - .getOrElse { - sentryInteractor.finishTransaction(sentryTransaction, throwable = it) - return onNewMessage(Message.FetchGamificationToolbarDataError) - } - - sentryInteractor.finishTransaction(sentryTransaction) - - onNewMessage( - Message.FetchGamificationToolbarDataSuccess(gamificationToolbarData) - ) - } - is Action.LogAnalyticEvent -> + is InternalAction.FetchGamificationToolbarData -> + handleFetchGamificationToolbarDataAction(action, ::onNewMessage) + is InternalAction.LogAnalyticEvent -> analyticInteractor.logEvent(action.analyticEvent) else -> { // no op } } } + + private suspend fun handleFetchGamificationToolbarDataAction( + action: InternalAction.FetchGamificationToolbarData, + onNewMessage: (Message) -> Unit + ) { + sentryInteractor.withTransaction( + action.screen.fetchContentSentryTransaction, + onError = { InternalMessage.FetchGamificationToolbarDataError } + ) { + currentGamificationToolbarDataStateRepository + .getState(forceUpdate = action.forceUpdate) + .getOrThrow() + .let(InternalMessage::FetchGamificationToolbarDataSuccess) + }.let(onNewMessage) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt index 38a1a2315d..9b769173c3 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt @@ -26,46 +26,49 @@ object GamificationToolbarFeature { get() = this is State.Content && isRefreshing sealed interface Message { + object ClickedGems : Message + object ClickedStreak : Message + object ClickedProgress : Message + } + + internal sealed interface InternalMessage : Message { /** * Initialization */ - data class Initialize(val forceUpdate: Boolean = false) : Message - - object FetchGamificationToolbarDataError : Message + data class Initialize(val forceUpdate: Boolean = false) : InternalMessage + object FetchGamificationToolbarDataError : InternalMessage data class FetchGamificationToolbarDataSuccess( val gamificationToolbarData: GamificationToolbarData - ) : Message + ) : InternalMessage - object PullToRefresh : Message + object PullToRefresh : InternalMessage /** * Flow Messages */ - object StepSolved : Message - data class HypercoinsBalanceChanged(val hypercoinsBalance: Int) : Message - data class StreakChanged(val streak: Streak?) : Message - data class StudyPlanChanged(val studyPlan: StudyPlan) : Message - object TopicCompleted : Message - - /** - * Clicks - */ - object ClickedGems : Message - object ClickedStreak : Message - object ClickedProgress : Message + object StepSolved : InternalMessage + data class HypercoinsBalanceChanged(val hypercoinsBalance: Int) : InternalMessage + data class StreakChanged(val streak: Streak?) : InternalMessage + data class StudyPlanChanged(val studyPlan: StudyPlan) : InternalMessage + object TopicCompleted : InternalMessage + data class GamificationToolbarDataChanged( + val gamificationToolbarData: GamificationToolbarData + ) : InternalMessage } sealed interface Action { - data class FetchGamificationToolbarData( - val screen: GamificationToolbarScreen, - val forceUpdate: Boolean - ) : Action - - data class LogAnalyticEvent(val analyticEvent: AnalyticEvent) : Action - sealed interface ViewAction : Action { object ShowProfileTab : ViewAction object ShowProgressScreen : ViewAction } } + + internal sealed interface InternalAction : Action { + data class FetchGamificationToolbarData( + val screen: GamificationToolbarScreen, + val forceUpdate: Boolean + ) : InternalAction + + data class LogAnalyticEvent(val analyticEvent: AnalyticEvent) : InternalAction + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt index 22b9361eee..56f7186fae 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt @@ -3,8 +3,11 @@ package org.hyperskill.app.gamification_toolbar.presentation import org.hyperskill.app.gamification_toolbar.domain.analytic.GamificationToolbarClickedGemsHyperskillAnalyticEvent import org.hyperskill.app.gamification_toolbar.domain.analytic.GamificationToolbarClickedProgressHyperskillAnalyticEvent import org.hyperskill.app.gamification_toolbar.domain.analytic.GamificationToolbarClickedStreakHyperskillAnalyticEvent +import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarData import org.hyperskill.app.gamification_toolbar.domain.model.GamificationToolbarScreen import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Action +import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.InternalAction +import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.InternalMessage import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Message import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.State import org.hyperskill.app.streaks.domain.model.HistoricalStreak @@ -16,39 +19,34 @@ class GamificationToolbarReducer( ) : StateReducer { override fun reduce(state: State, message: Message): Pair> = when (message) { - is Message.Initialize -> + is InternalMessage.Initialize -> if (state is State.Idle || (message.forceUpdate && (state is State.Content || state is State.Error)) ) { - State.Loading to setOf(Action.FetchGamificationToolbarData(screen, message.forceUpdate)) + State.Loading to setOf(InternalAction.FetchGamificationToolbarData(screen, message.forceUpdate)) } else { null } - is Message.FetchGamificationToolbarDataError -> + is InternalMessage.FetchGamificationToolbarDataError -> State.Error to emptySet() - is Message.FetchGamificationToolbarDataSuccess -> - State.Content( - trackProgress = message.gamificationToolbarData.trackProgress, - currentStreak = message.gamificationToolbarData.currentStreak, - historicalStreak = HistoricalStreak(message.gamificationToolbarData.streakState), - hypercoinsBalance = message.gamificationToolbarData.hypercoinsBalance - ) to emptySet() - is Message.PullToRefresh -> + is InternalMessage.FetchGamificationToolbarDataSuccess -> + createContentState(message.gamificationToolbarData) to emptySet() + is InternalMessage.PullToRefresh -> when (state) { is State.Content -> if (state.isRefreshing) { null } else { state.copy(isRefreshing = true) to - setOf(Action.FetchGamificationToolbarData(screen, true)) + setOf(InternalAction.FetchGamificationToolbarData(screen, true)) } is State.Error -> - State.Loading to setOf(Action.FetchGamificationToolbarData(screen, true)) + State.Loading to setOf(InternalAction.FetchGamificationToolbarData(screen, true)) else -> null } // Flow Messages - is Message.StepSolved -> + is InternalMessage.StepSolved -> if (state is State.Content && state.historicalStreak.state == StreakState.NOTHING) { state.copy( historicalStreak = HistoricalStreak(StreakState.COMPLETED), @@ -57,7 +55,7 @@ class GamificationToolbarReducer( } else { null } - is Message.HypercoinsBalanceChanged -> + is InternalMessage.HypercoinsBalanceChanged -> if (state is State.Content) { state.copy( hypercoinsBalance = message.hypercoinsBalance @@ -65,7 +63,7 @@ class GamificationToolbarReducer( } else { null } - is Message.StreakChanged -> + is InternalMessage.StreakChanged -> if (state is State.Content && message.streak != null) { state.copy( currentStreak = message.streak.currentStreak, @@ -74,11 +72,11 @@ class GamificationToolbarReducer( } else { null } - is Message.StudyPlanChanged -> { + is InternalMessage.StudyPlanChanged -> { if (state is State.Content) { if (message.studyPlan.trackId != null) { state to setOf( - Action.FetchGamificationToolbarData(screen, forceUpdate = true) + InternalAction.FetchGamificationToolbarData(screen, forceUpdate = true) ) } else { state.copy(trackProgress = null) to emptySet() @@ -87,21 +85,36 @@ class GamificationToolbarReducer( null } } - is Message.TopicCompleted -> { + is InternalMessage.TopicCompleted -> { if (state is State.Content) { state to setOf( - Action.FetchGamificationToolbarData(screen, forceUpdate = true) + InternalAction.FetchGamificationToolbarData(screen, forceUpdate = true) ) } else { null } } + is InternalMessage.GamificationToolbarDataChanged -> { + when (state) { + is State.Content -> { + if (state.isRefreshing) { + null + } else { + createContentState(message.gamificationToolbarData) to emptySet() + } + } + State.Error -> + createContentState(message.gamificationToolbarData) to emptySet() + State.Idle, + State.Loading -> null + } + } // Click Messages is Message.ClickedGems -> if (state is State.Content) { state to setOf( Action.ViewAction.ShowProfileTab, - Action.LogAnalyticEvent(GamificationToolbarClickedGemsHyperskillAnalyticEvent(screen)) + InternalAction.LogAnalyticEvent(GamificationToolbarClickedGemsHyperskillAnalyticEvent(screen)) ) } else { null @@ -110,7 +123,9 @@ class GamificationToolbarReducer( if (state is State.Content) { state to setOf( Action.ViewAction.ShowProfileTab, - Action.LogAnalyticEvent(GamificationToolbarClickedStreakHyperskillAnalyticEvent(screen)) + InternalAction.LogAnalyticEvent( + GamificationToolbarClickedStreakHyperskillAnalyticEvent(screen) + ) ) } else { null @@ -119,10 +134,20 @@ class GamificationToolbarReducer( if (state is State.Content) { state to setOf( Action.ViewAction.ShowProgressScreen, - Action.LogAnalyticEvent(GamificationToolbarClickedProgressHyperskillAnalyticEvent(screen)) + InternalAction.LogAnalyticEvent( + GamificationToolbarClickedProgressHyperskillAnalyticEvent(screen) + ) ) } else { null } } ?: (state to emptySet()) + + private fun createContentState(gamificationToolbarData: GamificationToolbarData): State.Content = + State.Content( + trackProgress = gamificationToolbarData.trackProgress, + currentStreak = gamificationToolbarData.currentStreak, + historicalStreak = HistoricalStreak(gamificationToolbarData.streakState), + hypercoinsBalance = gamificationToolbarData.hypercoinsBalance + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt index 064dda8128..3ad66fd080 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt @@ -227,7 +227,7 @@ class HomeReducer( val (toolbarState, toolbarActions) = reduceGamificationToolbarMessage( state.toolbarState, - GamificationToolbarFeature.Message.Initialize(forceUpdate) + GamificationToolbarFeature.InternalMessage.Initialize(forceUpdate) ) return state.copy( @@ -250,7 +250,7 @@ class HomeReducer( val (toolbarState, toolbarActions) = reduceGamificationToolbarMessage( state.toolbarState, - GamificationToolbarFeature.Message.PullToRefresh + GamificationToolbarFeature.InternalMessage.PullToRefresh ) return state.copy( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/domain/interactor/AppInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/domain/interactor/AppInteractor.kt index fa7136bd15..224aea580b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/domain/interactor/AppInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/domain/interactor/AppInteractor.kt @@ -7,6 +7,7 @@ import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepositor import org.hyperskill.app.progresses.domain.repository.ProgressesRepository import org.hyperskill.app.projects.domain.repository.ProjectsRepository import org.hyperskill.app.providers.domain.repository.ProvidersRepository +import org.hyperskill.app.share_streak.domain.repository.ShareStreakRepository import org.hyperskill.app.track.domain.repository.TrackRepository import org.hyperskill.app.user_storage.domain.interactor.UserStorageInteractor @@ -19,6 +20,7 @@ class AppInteractor( private val trackRepository: TrackRepository, private val providersRepository: ProvidersRepository, private val projectsRepository: ProjectsRepository, + private val shareStreakRepository: ShareStreakRepository, private val pushNotificationsInteractor: PushNotificationsInteractor ) { suspend fun doCurrentUserSignedOutCleanUp() { @@ -36,5 +38,6 @@ class AppInteractor( trackRepository.clearCache() providersRepository.clearCache() projectsRepository.clearCache() + shareStreakRepository.clearCache() } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainDataComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainDataComponentImpl.kt index 6301b395a2..86039e5562 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainDataComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainDataComponentImpl.kt @@ -3,7 +3,7 @@ package org.hyperskill.app.main.injection import org.hyperskill.app.core.injection.AppGraph import org.hyperskill.app.main.domain.interactor.AppInteractor -class MainDataComponentImpl(private val appGraph: AppGraph) : MainDataComponent { +internal class MainDataComponentImpl(private val appGraph: AppGraph) : MainDataComponent { override val appInteractor: AppInteractor get() = AppInteractor( appGraph.authComponent.authInteractor, @@ -14,6 +14,7 @@ class MainDataComponentImpl(private val appGraph: AppGraph) : MainDataComponent appGraph.buildTrackDataComponent().trackRepository, appGraph.buildProvidersDataComponent().providersRepository, appGraph.buildProjectsDataComponent().projectsRepository, + appGraph.buildShareStreakDataComponent().shareStreakRepository, appGraph.buildPushNotificationsComponent().pushNotificationsInteractor ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt index 38b7dcf53c..1486ce51b4 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt @@ -2,7 +2,6 @@ package org.hyperskill.app.notification.local.domain.interactor import kotlin.time.DurationUnit import kotlin.time.toDuration -import kotlinx.coroutines.flow.SharedFlow import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.atTime @@ -29,8 +28,6 @@ class NotificationInteractor( private const val UTC_TIME_ZONE_ID = "UTC" } - val solvedStepsSharedFlow: SharedFlow = submissionRepository.solvedStepsMutableSharedFlow - fun isNotificationsPermissionGranted(): Boolean = notificationRepository.isNotificationsPermissionGranted() diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheDataSourceImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheDataSourceImpl.kt new file mode 100644 index 0000000000..3b3e12e726 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheDataSourceImpl.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.share_streak.cache + +import com.russhwolf.settings.Settings +import kotlinx.datetime.Instant +import org.hyperskill.app.share_streak.data.source.ShareStreakCacheDataSource + +internal class ShareStreakCacheDataSourceImpl(private val settings: Settings) : ShareStreakCacheDataSource { + override fun getLastTimeShareStreakShown(): Instant? = + settings + .getLongOrNull(ShareStreakCacheKeyValues.SHARE_STREAK_LAST_TIME_SHOWN) + ?.let(Instant.Companion::fromEpochMilliseconds) + + override fun setLastTimeShareStreakShown(instant: Instant) { + settings.putLong(ShareStreakCacheKeyValues.SHARE_STREAK_LAST_TIME_SHOWN, instant.toEpochMilliseconds()) + } + + override fun clearCache() { + settings.remove(ShareStreakCacheKeyValues.SHARE_STREAK_LAST_TIME_SHOWN) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheKeyValues.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheKeyValues.kt new file mode 100644 index 0000000000..876bbbf7df --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/cache/ShareStreakCacheKeyValues.kt @@ -0,0 +1,5 @@ +package org.hyperskill.app.share_streak.cache + +internal object ShareStreakCacheKeyValues { + const val SHARE_STREAK_LAST_TIME_SHOWN = "share_streak_last_time_shown" +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/repository/ShareStreakRepositoryImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/repository/ShareStreakRepositoryImpl.kt new file mode 100644 index 0000000000..2027dd3a3e --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/repository/ShareStreakRepositoryImpl.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.share_streak.data.repository + +import kotlinx.datetime.Instant +import org.hyperskill.app.share_streak.data.source.ShareStreakCacheDataSource +import org.hyperskill.app.share_streak.domain.repository.ShareStreakRepository + +internal class ShareStreakRepositoryImpl( + private val shareStreakCacheDataSource: ShareStreakCacheDataSource +) : ShareStreakRepository { + override fun getLastTimeShareStreakShown(): Instant? = + shareStreakCacheDataSource.getLastTimeShareStreakShown() + + override fun setLastTimeShareStreakShown(instant: Instant) { + shareStreakCacheDataSource.setLastTimeShareStreakShown(instant) + } + + override fun clearCache() { + shareStreakCacheDataSource.clearCache() + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/source/ShareStreakCacheDataSource.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/source/ShareStreakCacheDataSource.kt new file mode 100644 index 0000000000..ca3558d9f6 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/data/source/ShareStreakCacheDataSource.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.share_streak.data.source + +import kotlinx.datetime.Instant + +internal interface ShareStreakCacheDataSource { + fun getLastTimeShareStreakShown(): Instant? + fun setLastTimeShareStreakShown(instant: Instant) + fun clearCache() +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/interactor/ShareStreakInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/interactor/ShareStreakInteractor.kt new file mode 100644 index 0000000000..af06502074 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/interactor/ShareStreakInteractor.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.share_streak.domain.interactor + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.hyperskill.app.share_streak.domain.repository.ShareStreakRepository +import org.hyperskill.app.streaks.domain.model.StreakState + +class ShareStreakInteractor(private val shareStreakRepository: ShareStreakRepository) { + companion object { + private val SHAREABLE_STREAKS = setOf(1, 5, 10, 25, 50, 100) + } + + fun setLastTimeShareStreakShown() { + shareStreakRepository.setLastTimeShareStreakShown() + } + + fun shouldShareStreakAfterStepSolved(streak: Int, streakState: StreakState): Boolean { + if (isUserSawShareStreakToday()) { + return false + } + return when (streakState) { + StreakState.NOTHING, + StreakState.COMPLETED, + StreakState.MANUAL_COMPLETED -> + SHAREABLE_STREAKS.contains(streak) + else -> false + } + } + + private fun isUserSawShareStreakToday(): Boolean { + val lastTimeShownLocalDateTime = + shareStreakRepository.getLastTimeShareStreakShown()?.toLocalDateTime(TimeZone.UTC) ?: return false + + val nowLocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) + + return lastTimeShownLocalDateTime.year == nowLocalDateTime.year && + lastTimeShownLocalDateTime.monthNumber == nowLocalDateTime.monthNumber && + lastTimeShownLocalDateTime.dayOfMonth == nowLocalDateTime.dayOfMonth + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/repository/ShareStreakRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/repository/ShareStreakRepository.kt new file mode 100644 index 0000000000..800e15deeb --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/domain/repository/ShareStreakRepository.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.share_streak.domain.repository + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +interface ShareStreakRepository { + fun getLastTimeShareStreakShown(): Instant? + fun setLastTimeShareStreakShown(instant: Instant = Clock.System.now()) + fun clearCache() +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponent.kt new file mode 100644 index 0000000000..a815784281 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponent.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.share_streak.injection + +import org.hyperskill.app.share_streak.domain.interactor.ShareStreakInteractor +import org.hyperskill.app.share_streak.domain.repository.ShareStreakRepository + +interface ShareStreakDataComponent { + val shareStreakRepository: ShareStreakRepository + val shareStreakInteractor: ShareStreakInteractor +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponentImpl.kt new file mode 100644 index 0000000000..035e20ae2e --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/share_streak/injection/ShareStreakDataComponentImpl.kt @@ -0,0 +1,19 @@ +package org.hyperskill.app.share_streak.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.share_streak.cache.ShareStreakCacheDataSourceImpl +import org.hyperskill.app.share_streak.data.repository.ShareStreakRepositoryImpl +import org.hyperskill.app.share_streak.data.source.ShareStreakCacheDataSource +import org.hyperskill.app.share_streak.domain.interactor.ShareStreakInteractor +import org.hyperskill.app.share_streak.domain.repository.ShareStreakRepository + +internal class ShareStreakDataComponentImpl(appGraph: AppGraph) : ShareStreakDataComponent { + private val shareStreakCacheDataSource: ShareStreakCacheDataSource = + ShareStreakCacheDataSourceImpl(appGraph.commonComponent.settings) + + override val shareStreakRepository: ShareStreakRepository + get() = ShareStreakRepositoryImpl(shareStreakCacheDataSource) + + override val shareStreakInteractor: ShareStreakInteractor + get() = ShareStreakInteractor(shareStreakRepository) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent.kt new file mode 100644 index 0000000000..d3752926f8 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.step_completion.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents click on the "Share your streak" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "click", + * "part": "daily_step_completed_modal", + * "target": "share_your_streak", + * "context": + * { + * "streak": 1 + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + val streak: Int +) : HyperskillAnalyticEvent( + route, + HyperskillAnalyticAction.CLICK, + HyperskillAnalyticPart.DAILY_STEP_COMPLETED_MODAL, + HyperskillAnalyticTarget.SHARE_YOUR_STREAK +) { + override val params: Map + get() = super.params + mapOf( + PARAM_CONTEXT to mapOf(StepCompletionHyperskillAnalyticParams.PARAM_STREAK to streak) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionHyperskillAnalyticParams.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionHyperskillAnalyticParams.kt new file mode 100644 index 0000000000..54a687ae22 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionHyperskillAnalyticParams.kt @@ -0,0 +1,5 @@ +package org.hyperskill.app.step_completion.domain.analytic + +object StepCompletionHyperskillAnalyticParams { + const val PARAM_STREAK = "streak" +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent.kt new file mode 100644 index 0000000000..214d2e119f --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.step_completion.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents click on the "No thanks" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "click", + * "part": "share_streak_modal", + * "target": "no_thanks", + * "context": + * { + * "streak": 1 + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + val streak: Int +) : HyperskillAnalyticEvent( + route, + HyperskillAnalyticAction.CLICK, + HyperskillAnalyticPart.SHARE_STREAK_MODAL, + HyperskillAnalyticTarget.NO_THANKS +) { + override val params: Map + get() = super.params + mapOf( + PARAM_CONTEXT to mapOf(StepCompletionHyperskillAnalyticParams.PARAM_STREAK to streak) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent.kt new file mode 100644 index 0000000000..0a493f7545 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.step_completion.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents click on the "Share" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "click", + * "part": "share_streak_modal", + * "target": "share", + * "context": + * { + * "streak": 1 + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + val streak: Int +) : HyperskillAnalyticEvent( + route, + HyperskillAnalyticAction.CLICK, + HyperskillAnalyticPart.SHARE_STREAK_MODAL, + HyperskillAnalyticTarget.SHARE +) { + override val params: Map + get() = super.params + mapOf( + PARAM_CONTEXT to mapOf(StepCompletionHyperskillAnalyticParams.PARAM_STREAK to streak) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent.kt new file mode 100644 index 0000000000..314427ad87 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.step_completion.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents a hidden analytic event of the share streak modal. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "hidden", + * "part": "share_streak_modal", + * "target": "close", + * "context": + * { + * "streak": 1 + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + val streak: Int +) : HyperskillAnalyticEvent( + route, + HyperskillAnalyticAction.HIDDEN, + HyperskillAnalyticPart.SHARE_STREAK_MODAL, + HyperskillAnalyticTarget.CLOSE +) { + override val params: Map + get() = super.params + mapOf( + PARAM_CONTEXT to mapOf(StepCompletionHyperskillAnalyticParams.PARAM_STREAK to streak) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalShownHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalShownHyperskillAnalyticEvent.kt new file mode 100644 index 0000000000..cd8f5c8b07 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/domain/analytic/StepCompletionShareStreakModalShownHyperskillAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.step_completion.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents a shown analytic event of the share streak modal. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "shown", + * "part": "modal", + * "target": "share_streak_modal", + * "context": + * { + * "streak": 1 + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepCompletionShareStreakModalShownHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + val streak: Int +) : HyperskillAnalyticEvent( + route, + HyperskillAnalyticAction.SHOWN, + HyperskillAnalyticPart.MODAL, + HyperskillAnalyticTarget.SHARE_STREAK_MODAL +) { + override val params: Map + get() = super.params + mapOf( + PARAM_CONTEXT to mapOf(StepCompletionHyperskillAnalyticParams.PARAM_STREAK to streak) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt index d0e8aa4b85..3c459d139b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt @@ -6,7 +6,7 @@ import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.step_completion.presentation.StepCompletionActionDispatcher import org.hyperskill.app.step_completion.presentation.StepCompletionReducer -class StepCompletionComponentImpl( +internal class StepCompletionComponentImpl( private val appGraph: AppGraph, private val stepRoute: StepRoute ) : StepCompletionComponent { @@ -16,6 +16,7 @@ class StepCompletionComponentImpl( override val stepCompletionActionDispatcher: StepCompletionActionDispatcher get() = StepCompletionActionDispatcher( ActionDispatcherOptions(), + appGraph.submissionDataComponent.submissionRepository, appGraph.buildStepDataComponent().stepInteractor, appGraph.buildProgressesDataComponent().progressesInteractor, appGraph.buildTopicsDataComponent().topicsInteractor, @@ -23,8 +24,10 @@ class StepCompletionComponentImpl( appGraph.commonComponent.resourceProvider, appGraph.sentryComponent.sentryInteractor, appGraph.buildFreemiumDataComponent().freemiumInteractor, + appGraph.buildShareStreakDataComponent().shareStreakInteractor, appGraph.stateRepositoriesComponent.nextLearningActivityStateRepository, appGraph.profileDataComponent.currentProfileStateRepository, + appGraph.stateRepositoriesComponent.currentGamificationToolbarDataStateRepository, appGraph.stepCompletionFlowDataComponent.topicCompletedFlow, appGraph.progressesFlowDataComponent.topicProgressFlow, appGraph.buildNotificationComponent().notificationInteractor diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt index 5d38f70c6d..309f19268d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt @@ -9,6 +9,7 @@ import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.core.presentation.ActionDispatcherOptions import org.hyperskill.app.core.view.mapper.ResourceProvider import org.hyperskill.app.freemium.domain.interactor.FreemiumInteractor +import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository import org.hyperskill.app.notification.local.cache.NotificationCacheKeyValues import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor @@ -17,6 +18,7 @@ import org.hyperskill.app.progresses.domain.flow.TopicProgressFlow import org.hyperskill.app.progresses.domain.interactor.ProgressesInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder +import org.hyperskill.app.share_streak.domain.interactor.ShareStreakInteractor import org.hyperskill.app.step.domain.interactor.StepInteractor import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step.domain.model.StepRoute @@ -25,11 +27,14 @@ import org.hyperskill.app.step_completion.domain.analytic.StepCompletionTopicCom import org.hyperskill.app.step_completion.domain.flow.TopicCompletedFlow import org.hyperskill.app.step_completion.presentation.StepCompletionFeature.Action import org.hyperskill.app.step_completion.presentation.StepCompletionFeature.Message +import org.hyperskill.app.step_quiz.domain.repository.SubmissionRepository +import org.hyperskill.app.streaks.domain.model.StreakState import org.hyperskill.app.topics.domain.interactor.TopicsInteractor import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher class StepCompletionActionDispatcher( config: ActionDispatcherOptions, + submissionRepository: SubmissionRepository, private val stepInteractor: StepInteractor, private val progressesInteractor: ProgressesInteractor, private val topicsInteractor: TopicsInteractor, @@ -37,17 +42,18 @@ class StepCompletionActionDispatcher( private val resourceProvider: ResourceProvider, private val sentryInteractor: SentryInteractor, private val freemiumInteractor: FreemiumInteractor, + private val shareStreakInteractor: ShareStreakInteractor, private val nextLearningActivityStateRepository: NextLearningActivityStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, + private val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository, private val topicCompletedFlow: TopicCompletedFlow, private val topicProgressFlow: TopicProgressFlow, private val notificationInteractor: NotificationInteractor ) : CoroutineActionDispatcher(config.createConfig()) { + init { - notificationInteractor.solvedStepsSharedFlow - .onEach { solvedStepId -> - handleStepSolved(solvedStepId) - } + submissionRepository.solvedStepsSharedFlow + .onEach(::handleStepSolved) .launchIn(actionScope) } @@ -95,6 +101,9 @@ class StepCompletionActionDispatcher( is Action.PostponeDailyStudyReminder -> { handlePostponeDailyStudyReminderAction() } + is Action.UpdateLastTimeShareStreakShown -> { + shareStreakInteractor.setLastTimeShareStreakShown() + } is Action.LogAnalyticEvent -> { analyticInteractor.logEvent(action.analyticEvent) } @@ -187,8 +196,10 @@ class StepCompletionActionDispatcher( } private suspend fun handleStepSolved(stepId: Long) { - // we should load cached and current profile - // to update hypercoins balance in case of + // update problems limit + onNewMessage(Message.StepSolved(stepId)) + + // We should load cached and current profile to update hypercoins balance in case of // requesting study reminders permission val cachedProfile = currentProfileStateRepository .getState(forceUpdate = false) @@ -201,30 +212,80 @@ class StepCompletionActionDispatcher( ) ) - val currentProfileHypercoinsBalance = currentProfileStateRepository - .getState(forceUpdate = true) - .map { it.gamification.hypercoinsBalance } - .getOrElse { return } + val currentGamificationToolbarData = currentGamificationToolbarDataStateRepository + .getState(forceUpdate = false) + .getOrNull() + val streakToShare = currentGamificationToolbarData?.let { + if (it.streakState == StreakState.NOTHING) { + it.currentStreak + 1 + } else { + it.currentStreak + } + } + val shouldShareStreak = if (currentGamificationToolbarData != null && streakToShare != null) { + shareStreakInteractor.shouldShareStreakAfterStepSolved( + streak = streakToShare, + streakState = currentGamificationToolbarData.streakState + ) + } else { + false + } // TODO: ALTUX-2415 Enhance the user experience of "Daily Study Reminders" if (notificationInteractor.isRequiredToAskUserToEnableDailyReminders()) { onNewMessage(Message.RequestDailyStudyRemindersPermission) - } else { - if (cachedProfile.dailyStep == stepId) { + updateCurrentProfileHypercoinsBalanceRemotely() + return + } + + if (cachedProfile.dailyStep == stepId) { + val currentProfileHypercoinsBalance = updateCurrentProfileHypercoinsBalanceRemotely() + if (currentProfileHypercoinsBalance != null) { val gemsEarned = currentProfileHypercoinsBalance - cachedProfile.gamification.hypercoinsBalance + val earnedGemsText = resourceProvider.getQuantityString( + SharedResources.plurals.earned_gems, + gemsEarned, + gemsEarned + ) + + val shareStreakData = if (shouldShareStreak && streakToShare != null) { + val daysText = resourceProvider.getQuantityString( + SharedResources.plurals.days, + streakToShare, + streakToShare + ) + StepCompletionFeature.ShareStreakData.Content( + streakText = resourceProvider.getString( + SharedResources.strings.step_quiz_problem_of_day_solved_modal_streak_text, + daysText + ), + streak = streakToShare + ) + } else { + StepCompletionFeature.ShareStreakData.Empty + } + onNewMessage( Message.ProblemOfDaySolved( - // TODO move formatting to the view state mapper - earnedGemsText = resourceProvider.getQuantityString( - SharedResources.plurals.earned_gems, - gemsEarned, - gemsEarned - ) + earnedGemsText = earnedGemsText, + shareStreakData = shareStreakData ) ) - } else { - onNewMessage(Message.StepSolved(stepId)) + return } } + + if (shouldShareStreak && streakToShare != null) { + shareStreakInteractor.setLastTimeShareStreakShown() + onNewMessage(Message.ShareStreak(streak = streakToShare)) + + updateCurrentProfileHypercoinsBalanceRemotely() + } } + + private suspend fun updateCurrentProfileHypercoinsBalanceRemotely(): Int? = + currentProfileStateRepository + .getState(forceUpdate = true) + .map { it.gamification.hypercoinsBalance } + .getOrNull() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt index d65597ada0..ee9bf11855 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt @@ -47,6 +47,17 @@ interface StepCompletionFeature { object CheckTopicCompletion : ContinueButtonAction } + /** + * Helper class to show streak text and streak value + * + * @see Message.ProblemOfDaySolved + * @see Action.ViewAction.ShowProblemOfDaySolvedModal + */ + sealed interface ShareStreakData { + object Empty : ShareStreakData + data class Content(val streakText: String, val streak: Int) : ShareStreakData + } + sealed interface Message { object StartPracticingClicked : Message @@ -57,7 +68,6 @@ interface StepCompletionFeature { /** * Topic completed modal */ - sealed interface CheckTopicCompletionStatus : Message { data class Completed( val topicId: Long, @@ -79,8 +89,12 @@ interface StepCompletionFeature { /** * Show problem of day solve modal */ - data class ProblemOfDaySolved(val earnedGemsText: String) : Message + data class ProblemOfDaySolved( + val earnedGemsText: String, + val shareStreakData: ShareStreakData + ) : Message object ProblemOfDaySolvedModalGoBackClicked : Message + data class ProblemOfDaySolvedModalShareStreakClicked(val streak: Int) : Message /** * Daily study reminders @@ -88,6 +102,15 @@ interface StepCompletionFeature { object RequestDailyStudyRemindersPermission : Message data class RequestDailyStudyRemindersPermissionResult(val isGranted: Boolean) : Message + /** + * Share streak + */ + data class ShareStreak(val streak: Int) : Message + data class ShareStreakModalShownEventMessage(val streak: Int) : Message + data class ShareStreakModalHiddenEventMessage(val streak: Int) : Message + data class ShareStreakModalShareClickedEventMessage(val streak: Int) : Message + data class ShareStreakModalNoThanksClickedEventMessage(val streak: Int) : Message + /** * Analytic */ @@ -110,10 +133,20 @@ interface StepCompletionFeature { object TurnOnDailyStudyReminder : Action object PostponeDailyStudyReminder : Action + object UpdateLastTimeShareStreakShown : Action + sealed interface ViewAction : Action { - data class ShowTopicCompletedModal(val modalText: String, val isNextStepAvailable: Boolean) : ViewAction + data class ShowTopicCompletedModal( + val modalText: String, + val isNextStepAvailable: Boolean + ) : ViewAction + + data class ShowProblemOfDaySolvedModal( + val earnedGemsText: String, + val shareStreakData: ShareStreakData + ) : ViewAction - data class ShowProblemOfDaySolvedModal(val earnedGemsText: String) : ViewAction + data class ShowShareStreakModal(val streak: Int) : ViewAction object RequestDailyStudyRemindersPermission : ViewAction diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt index 3519de0fe2..6190a2f9bc 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt @@ -7,9 +7,14 @@ import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.step_completion.domain.analytic.StepCompletionClickedContinueHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionClickedStartPracticingHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionDailyStepCompletedModalClickedGoBackHyperskillAnalyticEvent +import org.hyperskill.app.step_completion.domain.analytic.StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionDailyStepCompletedModalHiddenHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionDailyStepCompletedModalShownHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionHiddenDailyNotificationsNoticeHyperskillAnalyticEvent +import org.hyperskill.app.step_completion.domain.analytic.StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent +import org.hyperskill.app.step_completion.domain.analytic.StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent +import org.hyperskill.app.step_completion.domain.analytic.StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent +import org.hyperskill.app.step_completion.domain.analytic.StepCompletionShareStreakModalShownHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionShownDailyNotificationsNoticeHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionTopicCompletedModalClickedContinueNextTopicHyperskillAnalyticEvent import org.hyperskill.app.step_completion.domain.analytic.StepCompletionTopicCompletedModalClickedGoToStudyPlanHyperskillAnalyticEvent @@ -117,7 +122,12 @@ class StepCompletionReducer(private val stepRoute: StepRoute) : StateReducer - state to setOf(Action.ViewAction.ShowProblemOfDaySolvedModal(message.earnedGemsText)) + state to setOf( + Action.ViewAction.ShowProblemOfDaySolvedModal( + earnedGemsText = message.earnedGemsText, + shareStreakData = message.shareStreakData + ) + ) is Message.ProblemOfDaySolvedModalGoBackClicked -> { state to setOf( Action.LogAnalyticEvent( @@ -128,6 +138,17 @@ class StepCompletionReducer(private val stepRoute: StepRoute) : StateReducer { + state to setOf( + Action.UpdateLastTimeShareStreakShown, + Action.LogAnalyticEvent( + StepCompletionDailyStepCompletedModalClickedShareStreakHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + streak = message.streak + ) + ) + ) + } is Message.StepSolved -> if (stepRoute is StepRoute.Learn && message.stepId == state.currentStep.id @@ -158,6 +179,49 @@ class StepCompletionReducer(private val stepRoute: StepRoute) : StateReducer { + state to setOf(Action.ViewAction.ShowShareStreakModal(streak = message.streak)) + } + is Message.ShareStreakModalShownEventMessage -> { + state to setOf( + Action.LogAnalyticEvent( + StepCompletionShareStreakModalShownHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + streak = message.streak + ) + ) + ) + } + is Message.ShareStreakModalHiddenEventMessage -> { + state to setOf( + Action.LogAnalyticEvent( + StepCompletionShareStreakModalHiddenHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + streak = message.streak + ) + ) + ) + } + is Message.ShareStreakModalShareClickedEventMessage -> { + state to setOf( + Action.LogAnalyticEvent( + StepCompletionShareStreakModalClickedShareHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + streak = message.streak + ) + ) + ) + } + is Message.ShareStreakModalNoThanksClickedEventMessage -> { + state to setOf( + Action.LogAnalyticEvent( + StepCompletionShareStreakModalClickedNoThanksHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + streak = message.streak + ) + ) + ) + } /** * Analytic * */ diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/screen/presentation/StudyPlanScreenReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/screen/presentation/StudyPlanScreenReducer.kt index 7386afd61d..6b58ab9ec5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/screen/presentation/StudyPlanScreenReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/screen/presentation/StudyPlanScreenReducer.kt @@ -35,7 +35,7 @@ internal class StudyPlanScreenReducer( val (toolbarState, toolbarActions) = reduceToolbarMessage( state.toolbarState, - GamificationToolbarFeature.Message.PullToRefresh + GamificationToolbarFeature.InternalMessage.PullToRefresh ) state.copy( @@ -88,7 +88,7 @@ internal class StudyPlanScreenReducer( val (toolbarState, toolbarActions) = reduceToolbarMessage( state.toolbarState, - GamificationToolbarFeature.Message.Initialize(forceUpdate = retryContentLoadingClicked) + GamificationToolbarFeature.InternalMessage.Initialize(forceUpdate = retryContentLoadingClicked) ) val (problemsLimitState, problemsLimitActions) = reduceProblemsLimitMessage( diff --git a/shared/src/commonMain/resources/MR/base/strings.xml b/shared/src/commonMain/resources/MR/base/strings.xml index d7c58d7453..f4564aee59 100644 --- a/shared/src/commonMain/resources/MR/base/strings.xml +++ b/shared/src/commonMain/resources/MR/base/strings.xml @@ -87,6 +87,8 @@ Current quiz type is not supported in mobile app yet Problem of the day solved! Solve another one tomorrow to get more + %s streak + Share your streak You completed %s Continue with next topic Description @@ -319,7 +321,6 @@ repeat each topic three times within a month after you first complete it. - What is your learning goal? @@ -494,6 +495,13 @@ You did a great job! gems for completing the whole project + + Share your progress + Share + No, thanks + Hyperskill – Level up your coding skills! + https://hi.hyperskill.org/my-hyperskill-app + Project Mastery Topic Mastery