Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALTAPPS-664: shared activities pagination inside section #457

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ data class LearningActivity(
private val targetTypeValue: String,
@SerialName("type")
private val typeValue: Int,
@SerialName("is_current")
val isCurrent: Boolean,
@SerialName("title")
val title: String = "",
@SerialName("hypercoins_award")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ data class StudyPlanSection(
val targetId: Long? = null,
@SerialName("target_type")
val targetType: String? = null,
@SerialName("next_activity_id")
val nextActivityId: Long? = null,
@SerialName("is_visible")
val isVisible: Boolean,
@SerialName("title")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
package org.hyperskill.app.study_plan.widget.presentation

import org.hyperskill.app.learning_activities.domain.model.LearningActivity
import org.hyperskill.app.learning_activities.domain.model.LearningActivityState
import org.hyperskill.app.study_plan.domain.model.StudyPlanSection
import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType

internal fun StudyPlanWidgetFeature.State.firstSection(): StudyPlanSection? =
/**
* Returns true if the section is supported in the study plan, when it is visible and its type is supported.
* @see StudyPlanSectionType
*/
internal val StudyPlanSection.isSupportedInStudyPlan: Boolean
get() = isVisible && StudyPlanSectionType.supportedTypes().contains(type)

/**
* Finds first supported section in the study plan.
*
* @return first [StudyPlanSection] if it is supported in the study plan, otherwise null.
* @see StudyPlanSection.isSupportedInStudyPlan
*/
internal fun StudyPlanWidgetFeature.State.getCurrentSection(): StudyPlanSection? =
studyPlanSections.values
.firstOrNull { it.studyPlanSection.isSupportedInStudyPlan }
?.studyPlanSection

/**
* Finds current activity in the study plan. If the current section is root topics, then the next activity is returned.
* Otherwise, the first activity with [LearningActivityState.TODO] state is returned.
*
* @return current [LearningActivity].
*/
internal fun StudyPlanWidgetFeature.State.getCurrentActivity(): LearningActivity? =
getCurrentSection()?.let { section ->
getSectionActivities(section.id)
.firstOrNull {
if (section.type == StudyPlanSectionType.ROOT_TOPICS) {
it.id == section.nextActivityId
} else {
it.state == LearningActivityState.TODO
}
}
}

/**
* @param sectionId target section id.
* @return list of [LearningActivity] for the given section with [sectionId].
*/
internal fun StudyPlanWidgetFeature.State.getSectionActivities(sectionId: Long): List<LearningActivity> =
studyPlanSections[sectionId]
?.studyPlanSection
?.activities
?.mapNotNull { id -> activities[id] } ?: emptyList()

internal val StudyPlanSection.isSupportedInStudyPlan: Boolean
get() = isVisible && StudyPlanSectionType.supportedTypes().contains(type)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.hyperskill.app.study_plan.widget.presentation

import kotlin.math.max
import kotlin.math.min
import org.hyperskill.app.learning_activities.domain.model.LearningActivityTargetType
import org.hyperskill.app.learning_activities.domain.model.LearningActivityType
import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder
Expand All @@ -11,6 +13,7 @@ import org.hyperskill.app.study_plan.domain.analytic.StudyPlanClickedSectionHype
import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalClickedGoToHomeScreenHyperskillAnalyticEvent
import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalHiddenHyperskillAnalyticEvent
import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalShownHyperskillAnalyticEvent
import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType
import org.hyperskill.app.study_plan.domain.model.StudyPlanStatus
import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.Action
import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.InternalAction
Expand All @@ -22,6 +25,10 @@ import ru.nobird.app.presentation.redux.reducer.StateReducer
internal typealias StudyPlanWidgetReducerResult = Pair<State, Set<Action>>

class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
companion object {
const val SECTION_ROOT_TOPICS_PAGE_SIZE = 10
}

override fun reduce(state: State, message: Message): StudyPlanWidgetReducerResult =
when (message) {
is Message.Initialize ->
Expand All @@ -38,7 +45,7 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
state.copy(
studyPlanSections = state.studyPlanSections.mapValues { (sectionId, sectionInfo) ->
sectionInfo.copy(
contentStatus = if (sectionId == state.firstSection()?.id) {
contentStatus = if (sectionId == state.getCurrentSection()?.id) {
sectionInfo.contentStatus
} else {
StudyPlanWidgetFeature.ContentStatus.IDLE
Expand Down Expand Up @@ -209,9 +216,9 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
) to setOf(
InternalAction.FetchActivities(
sectionId = message.sectionId,
activitiesIds = section.studyPlanSection.activities,
activitiesIds = getPaginatedActivitiesIds(section),
sentryTransaction = HyperskillSentryTransactionBuilder.buildStudyPlanWidgetFetchLearningActivities(
isCurrentSection = message.sectionId == state.firstSection()?.id
isCurrentSection = message.sectionId == state.getCurrentSection()?.id
)
),
InternalAction.LogAnalyticEvent(
Expand Down Expand Up @@ -250,9 +257,9 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
updateSectionState(StudyPlanWidgetFeature.ContentStatus.LOADING) to setOfNotNull(
InternalAction.FetchActivities(
sectionId = sectionId,
activitiesIds = section.studyPlanSection.activities,
activitiesIds = getPaginatedActivitiesIds(section),
sentryTransaction = HyperskillSentryTransactionBuilder.buildStudyPlanWidgetFetchLearningActivities(
isCurrentSection = sectionId == state.firstSection()?.id
isCurrentSection = sectionId == state.getCurrentSection()?.id
)
),
logAnalyticEventAction
Expand All @@ -270,6 +277,19 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
}
}

internal fun getPaginatedActivitiesIds(section: StudyPlanWidgetFeature.StudyPlanSectionInfo): List<Long> =
if (section.studyPlanSection.type == StudyPlanSectionType.ROOT_TOPICS &&
section.studyPlanSection.nextActivityId != null
) {
val startIndex =
max(0, section.studyPlanSection.activities.indexOf(section.studyPlanSection.nextActivityId))
val endIndex =
min(startIndex + (SECTION_ROOT_TOPICS_PAGE_SIZE - 1), section.studyPlanSection.activities.size - 1)
section.studyPlanSection.activities.slice(startIndex..endIndex)
} else {
section.studyPlanSection.activities
}

private fun handleActivityClicked(state: State, activityId: Long): StudyPlanWidgetReducerResult {
val activity = state.activities[activityId] ?: return state to emptySet()

Expand All @@ -282,7 +302,7 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
)
)

if (!activity.isCurrent) {
if (activity.id != state.getCurrentActivity()?.id) {
return state to setOf(logAnalyticEventAction)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import org.hyperskill.app.core.view.mapper.DateFormatter
import org.hyperskill.app.learning_activities.domain.model.LearningActivity
import org.hyperskill.app.learning_activities.domain.model.LearningActivityState
import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature
import org.hyperskill.app.study_plan.widget.presentation.firstSection
import org.hyperskill.app.study_plan.widget.presentation.getCurrentActivity
import org.hyperskill.app.study_plan.widget.presentation.getCurrentSection
import org.hyperskill.app.study_plan.widget.presentation.getSectionActivities
import org.hyperskill.app.study_plan.widget.view.StudyPlanWidgetViewState.SectionContent

Expand All @@ -19,22 +20,24 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) {
}

private fun getLoadedWidgetContent(state: StudyPlanWidgetFeature.State): StudyPlanWidgetViewState.Content {
val firstSectionId = state.firstSection()?.id
val currentSectionId = state.getCurrentSection()?.id
val currentActivityId = state.getCurrentActivity()?.id

return StudyPlanWidgetViewState.Content(
sections = state.studyPlan?.sections?.mapNotNull { sectionId ->
val sectionInfo = state.studyPlanSections[sectionId] ?: return@mapNotNull null
val section = sectionInfo.studyPlanSection

val shouldShowSectionStatistics = firstSectionId == section.id || sectionInfo.isExpanded
val shouldShowSectionStatistics = currentSectionId == section.id || sectionInfo.isExpanded

StudyPlanWidgetViewState.Section(
id = section.id,
title = section.title,
subtitle = section.subtitle.takeIf { it.isNotEmpty() },
content = getSectionContent(
state = state,
sectionInfo = sectionInfo
sectionInfo = sectionInfo,
currentActivityId = currentActivityId
),
formattedTopicsCount = if (shouldShowSectionStatistics) {
formatTopicsCount(
Expand All @@ -59,7 +62,8 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) {

private fun getSectionContent(
sectionInfo: StudyPlanWidgetFeature.StudyPlanSectionInfo,
state: StudyPlanWidgetFeature.State
state: StudyPlanWidgetFeature.State,
currentActivityId: Long?
): SectionContent =
if (sectionInfo.isExpanded) {
when (sectionInfo.contentStatus) {
Expand All @@ -69,14 +73,14 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) {
if (activities.isEmpty()) {
SectionContent.Loading
} else {
getContent(activities)
getContent(activities, currentActivityId)
}
}
StudyPlanWidgetFeature.ContentStatus.ERROR -> SectionContent.Error
StudyPlanWidgetFeature.ContentStatus.LOADED -> {
val activities = state.getSectionActivities(sectionInfo.studyPlanSection.id)
if (activities.isNotEmpty()) {
getContent(activities)
getContent(activities, currentActivityId)
} else {
SectionContent.Error
}
Expand All @@ -86,14 +90,14 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) {
SectionContent.Collapsed
}

private fun getContent(activities: List<LearningActivity>): SectionContent.Content =
private fun getContent(activities: List<LearningActivity>, currentActivityId: Long?): SectionContent.Content =
SectionContent.Content(
sectionItems = activities.map { activity ->
StudyPlanWidgetViewState.SectionItem(
id = activity.id,
title = activity.title.ifBlank { activity.id.toString() },
state = when (activity.state) {
LearningActivityState.TODO -> if (activity.isCurrent) {
LearningActivityState.TODO -> if (activity.id == currentActivityId) {
StudyPlanWidgetViewState.SectionItemState.NEXT
} else {
StudyPlanWidgetViewState.SectionItemState.LOCKED
Expand Down
Loading