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

Bugfixes and improvements #70

Merged
merged 4 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -6,13 +6,11 @@ import data.CourseRepository
import ivy.content.EmptyContent
import ivy.data.source.model.CourseResponse
import ivy.model.*
import kotlinx.coroutines.delay

class FakeCourseRepository : CourseRepository {
override suspend fun fetchCourse(
courseId: CourseId
): Either<String, CourseResponse> {
delay(1_000)
return CourseResponse(
course = Course(
id = CourseId("1"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ class FakeLessonRepository : LessonRepository {
): Either<String, Unit> {
return Either.Right(Unit)
}

override suspend fun markLessonAsCompleted(
course: CourseId,
lesson: LessonId
): Either<String, Unit> {
return Either.Right(Unit)
}
}
23 changes: 22 additions & 1 deletion composeApp/src/commonMain/kotlin/data/lesson/LessonRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import data.lesson.mapper.LessonMapper
import domain.SessionManager
import domain.model.LessonProgress
import domain.model.LessonWithProgress
import ivy.data.source.CoursesDataSource
import ivy.data.source.LessonDataSource
import ivy.model.CourseId
import ivy.model.LessonId
Expand All @@ -18,6 +19,7 @@ class LessonRepositoryImpl(
private val datasource: LessonDataSource,
private val mapper: LessonMapper,
private val sessionManager: SessionManager,
private val coursesDataSource: CoursesDataSource,
) : LessonRepository {

override suspend fun fetchLesson(
Expand Down Expand Up @@ -56,7 +58,21 @@ class LessonRepositoryImpl(
answered = progress.answered,
completed = progress.completed,
)
)
).bind()
}
}

override suspend fun markLessonAsCompleted(
course: CourseId,
lesson: LessonId,
): Either<String, Unit> = withContext(dispatchers.io) {
either {
val session = sessionManager.getSession().bind()
coursesDataSource.saveProgress(
session = session,
course = course,
lesson = lesson
).bind()
}
}
}
Expand All @@ -72,4 +88,9 @@ interface LessonRepository {
lesson: LessonId,
progress: LessonProgress,
): Either<String, Unit>

suspend fun markLessonAsCompleted(
course: CourseId,
lesson: LessonId,
): Either<String, Unit>
}
8 changes: 5 additions & 3 deletions composeApp/src/commonMain/kotlin/ui/EventHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ package ui
import kotlinx.coroutines.CoroutineScope
import kotlin.reflect.KClass

interface EventHandler<Event : Any, State : Any> {
interface EventHandler<Event : Any, State : Any, Args : Any> {
val eventTypes: Set<KClass<*>>
suspend fun VmContext<State>.handleEvent(event: Event)
suspend fun VmContext<State, Args>.handleEvent(event: Event)
}

interface VmContext<State> {
interface VmContext<State : Any, Args : Any> {
@EventHandlerDsl
val state: State

@EventHandlerDsl
fun modifyState(transformation: (State) -> State)

val screenScope: CoroutineScope

val args: Args
}

@DslMarker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,15 @@ class LessonScreen(
autoWire(::OnFinishClickEventHandler)
autoWire(::OnChoiceClickEventHandler)
register {
LessonViewModel(
LessonViewModel.Args(
courseId = courseId,
lessonId = lessonId,
lessonName = lessonName,
)
}
register {
LessonViewModel(
args = Di.get(),
screenScope = screenScope,
repository = Di.get(),
viewStateMapper = Di.get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ import kotlinx.coroutines.launch
import ui.ComposeViewModel
import ui.EventHandler
import ui.VmContext
import ui.screen.lesson.LessonViewModel.LocalState
import ui.screen.lesson.mapper.LessonViewStateMapper
import util.Logger


class LessonViewModel(
private val courseId: CourseId,
private val lessonId: LessonId,
private val lessonName: String,
override val args: Args,
override val screenScope: CoroutineScope,
private val repository: LessonRepository,
private val viewStateMapper: LessonViewStateMapper,
private val eventHandlers: Set<EventHandler<*, LocalState>>,
private val eventHandlers: Set<LessonEventHandler<*>>,
private val analytics: Analytics,
private val logger: Logger,
) : ComposeViewModel<LessonViewState, LessonViewEvent>, LessonVmContext {
Expand All @@ -46,8 +45,8 @@ class LessonViewModel(
private fun saveLessonProgress(localState: LocalState) {
screenScope.launch {
repository.saveLessonProgress(
course = courseId,
lesson = lessonId,
course = args.courseId,
lesson = args.lessonId,
progress = LessonProgress(
selectedAnswers = localState.selectedAnswers,
openAnswersInput = localState.openAnswersInput,
Expand All @@ -69,9 +68,9 @@ class LessonViewModel(
source = Source.Lesson,
event = "view",
params = mapOf(
Param.CourseId to courseId.value,
Param.LessonId to lessonId.value,
Param.LessonName to lessonName,
Param.CourseId to args.courseId.value,
Param.LessonId to args.lessonId.value,
Param.LessonName to args.lessonName,
)
)
}
Expand All @@ -83,7 +82,7 @@ class LessonViewModel(
}

null, is Either.Left -> LessonViewState(
title = lessonName,
title = args.lessonName,
items = persistentListOf(),
cta = null,
progress = LessonProgressViewState(0, 1),
Expand All @@ -94,8 +93,8 @@ class LessonViewModel(

private suspend fun loadLesson() {
lessonResponse = repository.fetchLesson(
course = courseId,
lesson = lessonId
course = args.courseId,
lesson = args.lessonId
).onRight {
localState = LocalState(
selectedAnswers = it.progress.selectedAnswers,
Expand All @@ -115,11 +114,12 @@ class LessonViewModel(

screenScope.launch {
@Suppress("UNCHECKED_CAST")
val typedEventHandler = eventHandler as EventHandler<LessonViewEvent, LocalState>
val typedEventHandler = eventHandler as LessonEventHandler<LessonViewEvent>
with(typedEventHandler) { handleEvent(event) }
}
}

@Immutable
@optics
data class LocalState(
val selectedAnswers: Map<LessonItemId, Set<AnswerId>>,
Expand All @@ -138,6 +138,13 @@ class LessonViewModel(
)
}
}

data class Args(
val courseId: CourseId,
val lessonId: LessonId,
val lessonName: String,
)
}

typealias LessonVmContext = VmContext<LessonViewModel.LocalState>
typealias LessonVmContext = VmContext<LocalState, LessonViewModel.Args>
typealias LessonEventHandler<E> = EventHandler<E, LocalState, LessonViewModel.Args>
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package ui.screen.lesson.handler

import navigation.Navigation
import ui.EventHandler
import ui.screen.lesson.LessonEventHandler
import ui.screen.lesson.LessonViewEvent
import ui.screen.lesson.LessonViewModel
import ui.screen.lesson.LessonVmContext

class OnBackClickEventHandler(
private val navigation: Navigation
) : EventHandler<LessonViewEvent.OnBackClick, LessonViewModel.LocalState> {
) : LessonEventHandler<LessonViewEvent.OnBackClick> {
override val eventTypes = setOf(LessonViewEvent.OnBackClick::class)

override suspend fun LessonVmContext.handleEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import arrow.optics.copy
import domain.SoundUseCase
import ivy.content.SoundsUrls
import ivy.model.ChoiceOptionId
import ui.EventHandler
import ui.screen.lesson.LessonViewEvent
import ui.screen.lesson.*
import ui.screen.lesson.LessonViewModel.LocalState
import ui.screen.lesson.LessonVmContext
import ui.screen.lesson.chosen
import ui.screen.lesson.completed
import ui.screen.lesson.mapper.toDomain

class OnChoiceClickEventHandler(
private val soundUseCase: SoundUseCase
) : EventHandler<LessonViewEvent.OnChoiceClick, LocalState> {
) : LessonEventHandler<LessonViewEvent.OnChoiceClick> {
override val eventTypes = setOf(LessonViewEvent.OnChoiceClick::class)

override suspend fun LessonVmContext.handleEvent(event: LessonViewEvent.OnChoiceClick) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ui.screen.lesson.handler

import domain.SoundUseCase
import ivy.content.SoundsUrls
import ui.EventHandler
import ui.screen.lesson.LessonEventHandler
import ui.screen.lesson.LessonViewEvent
import ui.screen.lesson.LessonViewModel.LocalState
import ui.screen.lesson.LessonVmContext
Expand All @@ -11,7 +11,7 @@ import ui.screen.lesson.mapper.toDomain

class OnContinueClickEventHandler(
private val soundUseCase: SoundUseCase
) : EventHandler<LessonViewEvent.OnContinueClick, LocalState> {
) : LessonEventHandler<LessonViewEvent.OnContinueClick> {
override val eventTypes = setOf(LessonViewEvent.OnContinueClick::class)

override suspend fun LessonVmContext.handleEvent(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
package ui.screen.lesson.handler

import data.lesson.LessonRepository
import domain.SoundUseCase
import ivy.content.SoundsUrls
import navigation.Navigation
import ui.EventHandler
import ui.screen.lesson.LessonEventHandler
import ui.screen.lesson.LessonViewEvent
import ui.screen.lesson.LessonViewModel.LocalState
import ui.screen.lesson.LessonVmContext
import util.Logger

class OnFinishClickEventHandler(
private val navigation: Navigation,
private val soundUseCase: SoundUseCase
) :
EventHandler<LessonViewEvent.OnFinishClick, LocalState> {
private val soundUseCase: SoundUseCase,
private val lessonRepository: LessonRepository,
private val logger: Logger,
) : LessonEventHandler<LessonViewEvent.OnFinishClick> {
override val eventTypes = setOf(LessonViewEvent.OnFinishClick::class)

override suspend fun LessonVmContext.handleEvent(
event: LessonViewEvent.OnFinishClick
) {
navigation.navigateBack()
soundUseCase.playSound(SoundsUrls.CompleteLesson)
lessonRepository.markLessonAsCompleted(
course = args.courseId,
lesson = args.lessonId,
).onLeft { errMsg ->
logger.error("Failed to mark lesson as completed because: $errMsg")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package ui.screen.lesson.handler

import domain.SoundUseCase
import ui.EventHandler
import ui.screen.lesson.LessonEventHandler
import ui.screen.lesson.LessonViewEvent
import ui.screen.lesson.LessonViewModel
import ui.screen.lesson.LessonVmContext

class OnSoundClickEventHandler(
private val soundUseCase: SoundUseCase
) : EventHandler<LessonViewEvent.OnSoundClick, LessonViewModel.LocalState> {
) : LessonEventHandler<LessonViewEvent.OnSoundClick> {
override val eventTypes = setOf(LessonViewEvent.OnSoundClick::class)

override suspend fun LessonVmContext.handleEvent(event: LessonViewEvent.OnSoundClick) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,15 @@ import arrow.optics.typeclasses.Index
import domain.SoundUseCase
import ivy.content.SoundsUrls
import ivy.model.AnswerId
import ui.EventHandler
import ui.screen.lesson.*
import ui.screen.lesson.LessonViewModel.LocalState
import ui.screen.lesson.LessonVmContext
import ui.screen.lesson.QuestionTypeViewState.MultipleChoice
import ui.screen.lesson.QuestionTypeViewState.SingleChoice
import ui.screen.lesson.QuestionViewEvent
import ui.screen.lesson.answered
import ui.screen.lesson.mapper.toDomain
import ui.screen.lesson.selectedAnswers

class QuestionEventHandler(
private val soundUseCase: SoundUseCase
) : EventHandler<QuestionViewEvent, LocalState> {
) : LessonEventHandler<QuestionViewEvent> {
override val eventTypes = setOf(
QuestionViewEvent.OnAnswerCheckChange::class,
QuestionViewEvent.OnCheckClick::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class WebSystemNavigation : SystemNavigation {
}

override fun navigateBack(): Boolean {
if (window.window.length <= 1) return false
if (window.history.length <= 1) return false

window.history.back() // Let the browser handle back navigation
// No need to call emitCurrentRoute() here; the listener will handle it
Expand Down
Loading