Skip to content

ALTAPPS-1301: Shared, Android root topics section pagination #1117

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

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class StudyPlanWidgetDelegate(
}
)
addDelegate(sectionsLoadingAdapterDelegate())
addDelegate(loadAllTopicsButtonDelegate())
addDelegate(paywallAdapterDelegate())
addDelegate(ActivityLoadingAdapterDelegate())
addDelegate(
Expand Down Expand Up @@ -132,10 +133,12 @@ class StudyPlanWidgetDelegate(
private fun getTopMarginFor(item: StudyPlanRecyclerItem): Int =
when (item) {
is StudyPlanRecyclerItem.SectionLoading,
is StudyPlanRecyclerItem.Section -> sectionTopMargin
is StudyPlanRecyclerItem.Section,
is StudyPlanRecyclerItem.PaywallBanner -> sectionTopMargin
is StudyPlanRecyclerItem.ActivityLoading,
is StudyPlanRecyclerItem.Activity,
is StudyPlanRecyclerItem.ActivitiesError -> activityTopMargin
is StudyPlanRecyclerItem.ActivitiesError,
is StudyPlanRecyclerItem.LoadAllTopicsButton -> activityTopMargin
else -> 0
}

Expand Down Expand Up @@ -171,6 +174,20 @@ class StudyPlanWidgetDelegate(
}
}

private fun loadAllTopicsButtonDelegate() =
adapterDelegate<StudyPlanRecyclerItem, StudyPlanRecyclerItem.LoadAllTopicsButton>(
R.layout.item_study_plan_load_more_button
) {
itemView.setOnClickListener {
val sectionId = item?.sectionId
if (sectionId != null) {
onNewMessage(
StudyPlanWidgetFeature.Message.LoadMoreActivitiesClicked(sectionId)
)
}
}
}

private fun mapContentToRecyclerItems(
studyPlanContent: StudyPlanWidgetViewState.Content
): List<StudyPlanRecyclerItem> =
Expand All @@ -185,14 +202,16 @@ class StudyPlanWidgetDelegate(
// no op
}
StudyPlanWidgetViewState.SectionContent.Loading -> {
addAll(
List(ACTIVITIES_LOADING_ITEMS_COUNT) { index ->
StudyPlanRecyclerItem.ActivityLoading(section.id, index)
}
)
addAll(getActivitiesLoadingItems(section.id))
}
is StudyPlanWidgetViewState.SectionContent.Content -> {
addAll(mapSectionContentToActivityItems(section.id, sectionContent))
addAll(mapSectionItemsToActivityItems(section.id, sectionContent.sectionItems))
if (sectionContent.isLoadAllTopicsButtonShown) {
add(StudyPlanRecyclerItem.LoadAllTopicsButton(section.id))
}
if (sectionContent.isNextPageLoadingShowed) {
addAll(getActivitiesLoadingItems(section.id))
}
}
StudyPlanWidgetViewState.SectionContent.Error -> {
add(StudyPlanRecyclerItem.ActivitiesError(section.id))
Expand Down Expand Up @@ -220,11 +239,11 @@ class StudyPlanWidgetDelegate(
isCurrentBadgeShown = section.isCurrentBadgeShown
)

private fun mapSectionContentToActivityItems(
private fun mapSectionItemsToActivityItems(
sectionId: Long,
content: StudyPlanWidgetViewState.SectionContent.Content
sectionItems: List<StudyPlanWidgetViewState.SectionItem>
): List<StudyPlanRecyclerItem.Activity> =
content.sectionItems.map { item ->
sectionItems.map { item ->
StudyPlanRecyclerItem.Activity(
id = item.id,
sectionId = sectionId,
Expand All @@ -247,4 +266,9 @@ class StudyPlanWidgetDelegate(
isIdeRequired = item.isIdeRequired
)
}

private fun getActivitiesLoadingItems(sectionId: Long) =
List(ACTIVITIES_LOADING_ITEMS_COUNT) { index ->
StudyPlanRecyclerItem.ActivityLoading(sectionId, index)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,28 @@ interface StudyPlanRecyclerItem {
data class SectionLoading(
val index: Int
) : StudyPlanRecyclerItem, Identifiable<String> {
override val id: String
get() = "sections-list-loading-item-$index"
override val id: String = "sections-list-loading-item-$index"
}

data class LoadAllTopicsButton(
val sectionId: Long
) : StudyPlanRecyclerItem, Identifiable<String> {
override val id: String = "study-plan-$sectionId-load-all-topics"
}

object PaywallBanner : StudyPlanRecyclerItem, Identifiable<String> {
override val id: String
get() = "study-plan-paywall-banner"
override val id: String = "study-plan-paywall-banner"
}

data class ActivityLoading(
val sectionId: Long,
val index: Int
) : StudyPlanRecyclerItem, Identifiable<String> {
override val id: String
get() = "activity-loading-item-$sectionId-$index"
override val id: String = "activity-loading-item-$sectionId-$index"
}

data class ActivitiesError(val sectionId: Long) : StudyPlanRecyclerItem, Identifiable<String> {
override val id: String
get() = "section-content-error-$sectionId"
override val id: String = "section-content-error-$sectionId"
}

data class Activity(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppTheme.Button.TextButton"
android:text="@string/study_plan_load_more_button_text" />
6 changes: 6 additions & 0 deletions androidHyperskillApp/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
<item name="cornerRadius">8dp</item>
</style>

<style name="Widget.AppTheme.Button.TextButton" parent="@style/Widget.MaterialComponents.Button.TextButton">
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>

</style>

<style name="Widget.AppTheme.TextInputLayouts.InputText" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxCornerRadiusBottomStart">@dimen/corner_radius</item>
<item name="boxCornerRadiusBottomEnd">@dimen/corner_radius</item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.hyperskill.app.study_plan.domain.model

import kotlin.math.max
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -28,4 +29,16 @@ data class StudyPlanSection(
) {
val type: StudyPlanSectionType?
get() = StudyPlanSectionType.getByValue(typeValue)
}
}

internal val StudyPlanSection.firstRootTopicsActivityIndexToBeLoaded: Int
get() = if (nextActivityId != null) {
max(0, activities.indexOf(nextActivityId))
} else {
0
}

internal val StudyPlanSection.activitiesToBeLoaded: List<Long>
get() = activities.slice(
firstRootTopicsActivityIndexToBeLoaded..activities.lastIndex
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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
import org.hyperskill.app.study_plan.domain.model.activitiesToBeLoaded
import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType
import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType

Expand All @@ -27,7 +28,7 @@ internal fun StudyPlanWidgetFeature.State.getCurrentSection(): StudyPlanSection?
*/
internal fun StudyPlanWidgetFeature.State.getCurrentActivity(): LearningActivity? =
getCurrentSection()?.let { section ->
getSectionActivities(section.id)
getLoadedSectionActivities(section.id)
.firstOrNull {
if (section.type == StudyPlanSectionType.ROOT_TOPICS) {
it.id == section.nextActivityId
Expand All @@ -42,14 +43,34 @@ internal fun StudyPlanWidgetFeature.State.getCurrentActivity(): LearningActivity
* @return a sequence of [LearningActivity] for the given section with [sectionId]
* filtered by availability in [StudyPlanWidgetFeature.State.activities]
*/
internal fun StudyPlanWidgetFeature.State.getSectionActivities(sectionId: Long): Sequence<LearningActivity> =
internal fun StudyPlanWidgetFeature.State.getLoadedSectionActivities(sectionId: Long): Sequence<LearningActivity> =
studyPlanSections[sectionId]
?.studyPlanSection
?.activities
?.asSequence()
?.mapNotNull { id -> activities[id] }
?: emptySequence()

internal fun StudyPlanWidgetFeature.State.getActivitiesToBeLoaded(sectionId: Long): Set<Long> {
val sectionInfo = studyPlanSections[sectionId] ?: return emptySet()
val studyPlanSection = sectionInfo.studyPlanSection
return if (studyPlanSection.type == StudyPlanSectionType.ROOT_TOPICS) {
val sectionLoadedActivity = getLoadedSectionActivities(sectionId).map { it.id }.toSet()
studyPlanSection.activitiesToBeLoaded.subtract(sectionLoadedActivity)
} else {
emptySet()
}
}

internal fun StudyPlanSection.getActivitiesToBeLoaded(allLoadedActivities: Collection<LearningActivity>): Set<Long> {
return if (type == StudyPlanSectionType.ROOT_TOPICS) {
val sectionActivities = activities.intersect(allLoadedActivities.map { it.id }.toSet())
activitiesToBeLoaded.subtract(sectionActivities)
} else {
emptySet()
}
}

/**
* @param sectionId target section id.
* @param activityId target activity id in the section with id = [sectionId]
Expand All @@ -64,7 +85,7 @@ internal fun StudyPlanWidgetFeature.State.isActivityLocked(
): Boolean {
val unlockedActivitiesCount = getUnlockedActivitiesCount(sectionId)
return unlockedActivitiesCount != null &&
getSectionActivities(sectionId)
getLoadedSectionActivities(sectionId)
.take(unlockedActivitiesCount)
.map { it.id }
.contains(activityId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object StudyPlanWidgetFeature {
/**
* Describes status of sections loading
*/
val sectionsStatus: ContentStatus = ContentStatus.IDLE,
val sectionsStatus: SectionStatus = SectionStatus.IDLE,

/**
* Map of activity ids to activities
Expand Down Expand Up @@ -61,28 +61,40 @@ object StudyPlanWidgetFeature {
get() = profile?.features?.isMobileContentTrialEnabled ?: false
}

enum class ContentStatus {
enum class SectionStatus {
IDLE,
LOADING,
ERROR,
LOADED
}

enum class SectionContentStatus {
IDLE,
ERROR,

FIRST_PAGE_LOADING,
NEXT_PAGE_LOADING,
PAGE_LOADED,
ALL_PAGES_LOADED
}

data class StudyPlanSectionInfo(
val studyPlanSection: StudyPlanSection,
val isExpanded: Boolean,

/**
* Describes status of section's activities loading
* */
val contentStatus: ContentStatus
val sectionContentStatus: SectionContentStatus
)

sealed interface Message {
data class SectionClicked(val sectionId: Long) : Message

data class ActivityClicked(val activityId: Long, val sectionId: Long) : Message

data class LoadMoreActivitiesClicked(val sectionId: Long) : Message

data class RetryActivitiesLoading(val sectionId: Long) : Message

data object PullToRefresh : Message
Expand All @@ -92,15 +104,15 @@ object StudyPlanWidgetFeature {
/**
* Stage implementation unsupported modal
*/
object StageImplementUnsupportedModalGoToHomeClicked : Message
object StageImplementUnsupportedModalShownEventMessage : Message
object StageImplementUnsupportedModalHiddenEventMessage : Message
data object StageImplementUnsupportedModalGoToHomeClicked : Message
data object StageImplementUnsupportedModalShownEventMessage : Message
data object StageImplementUnsupportedModalHiddenEventMessage : Message
}

internal sealed interface InternalMessage : Message {
data class Initialize(val forceUpdate: Boolean = false) : InternalMessage

object ReloadContentInBackground : InternalMessage
data object ReloadContentInBackground : InternalMessage

data class ProfileChanged(val profile: Profile) : InternalMessage

Expand All @@ -116,7 +128,7 @@ object StudyPlanWidgetFeature {
val canMakePayments: Boolean
) : LearningActivitiesWithSectionsFetchResult

object Failed : LearningActivitiesWithSectionsFetchResult
data object Failed : LearningActivitiesWithSectionsFetchResult
}

internal sealed interface LearningActivitiesFetchResult : Message {
Expand All @@ -131,13 +143,13 @@ object StudyPlanWidgetFeature {
internal sealed interface ProfileFetchResult : Message {
data class Success(val profile: Profile) : ProfileFetchResult

object Failed : ProfileFetchResult
data object Failed : ProfileFetchResult
}

sealed interface Action {
sealed interface ViewAction : Action {
sealed interface NavigateTo : ViewAction {
object Home : NavigateTo
data object Home : NavigateTo
data class LearningActivityTarget(val viewAction: LearningActivityTargetViewAction) : NavigateTo
data class Paywall(val paywallTransitionSource: PaywallTransitionSource) : NavigateTo
}
Expand All @@ -159,14 +171,14 @@ object StudyPlanWidgetFeature {
val sentryTransaction: HyperskillSentryTransaction
) : InternalAction

object FetchProfile : InternalAction
data object FetchProfile : InternalAction

data class UpdateCurrentStudyPlanState(val forceUpdate: Boolean) : InternalAction
data class UpdateNextLearningActivityState(val learningActivity: LearningActivity?) : InternalAction

data class PutTopicsProgressesToCache(val topicsProgresses: List<TopicProgress>) : InternalAction

object FetchPaymentAbility : InternalAction
data object FetchPaymentAbility : InternalAction

data class CaptureSentryException(val throwable: Throwable) : InternalAction
data class LogAnalyticEvent(val analyticEvent: AnalyticEvent) : InternalAction
Expand Down
Loading
Loading