Skip to content

Commit

Permalink
Merge pull request #829 from hyperskill/feature/interview_preparation
Browse files Browse the repository at this point in the history
Interview preparation
  • Loading branch information
ivan-magda authored Jan 4, 2024
2 parents e891f37 + f8e974a commit f8e3de6
Show file tree
Hide file tree
Showing 127 changed files with 3,025 additions and 177 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.MutableStateFlow
import org.hyperskill.app.R
Expand All @@ -27,13 +29,17 @@ class ChallengeCardDelegate {

fun setup(
composeView: ComposeView,
viewLifecycleOwner: LifecycleOwner,
onNewMessage: (ChallengeWidgetFeature.Message) -> Unit
) {
composeView.setContent {
HyperskillTheme {
val viewState by stateFlow.collectAsStateWithLifecycle()
viewState?.let { actualViewState ->
ChallengeCard(viewState = actualViewState, onNewMessage = onNewMessage)
composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
setContent {
HyperskillTheme {
val viewState by stateFlow.collectAsStateWithLifecycle()
viewState?.let { actualViewState ->
ChallengeCard(viewState = actualViewState, onNewMessage = onNewMessage)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ fun HyperskillCard(
modifier = modifier
.clip(RoundedCornerShape(cornerRadius))
.background(MaterialTheme.colors.surface)
.apply {
.let {
if (onClick != null) {
clickable(
it.clickable(
interactionSource = interactionSource,
indication = rememberRipple(),
onClick = onClick
)
} else {
it
}
}
.padding(contentPadding),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ import org.hyperskill.app.android.core.view.ui.setHyperskillColors
import org.hyperskill.app.android.core.view.ui.updateIsRefreshing
import org.hyperskill.app.android.databinding.FragmentHomeBinding
import org.hyperskill.app.android.gamification_toolbar.view.ui.delegate.GamificationToolbarDelegate
import org.hyperskill.app.android.interview_preparation.delegate.InterviewPreparationCardDelegate
import org.hyperskill.app.android.interview_preparation_onboarding.screen.InterviewPreparationOnboardingScreen
import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter
import org.hyperskill.app.android.problem_of_day.view.delegate.ProblemOfDayCardFormDelegate
import org.hyperskill.app.android.step.view.screen.StepScreen
import org.hyperskill.app.android.topics_repetitions.view.delegate.TopicsRepetitionCardFormDelegate
import org.hyperskill.app.android.topics_repetitions.view.screen.TopicsRepetitionScreen
import org.hyperskill.app.android.view.base.ui.extension.snackbar
import org.hyperskill.app.challenges.widget.view.model.ChallengeWidgetViewState
import org.hyperskill.app.core.view.mapper.date.SharedDateFormatter
import org.hyperskill.app.home.presentation.HomeFeature
import org.hyperskill.app.home.presentation.HomeViewModel
import org.hyperskill.app.interview_preparation.presentation.InterviewPreparationWidgetFeature
import org.hyperskill.app.interview_preparation.view.model.InterviewPreparationWidgetViewState
import org.hyperskill.app.step.domain.model.StepRoute
import ru.nobird.android.view.base.ui.delegate.ViewStateDelegate
import ru.nobird.android.view.redux.ui.extension.reduxViewModel
Expand Down Expand Up @@ -62,6 +67,9 @@ class HomeFragment :
private val challengeCardDelegate: ChallengeCardDelegate by lazy(LazyThreadSafetyMode.NONE) {
ChallengeCardDelegate()
}
private val interviewPreparationCardDelegate: InterviewPreparationCardDelegate by lazy(LazyThreadSafetyMode.NONE) {
InterviewPreparationCardDelegate()
}

private var gamificationToolbarDelegate: GamificationToolbarDelegate? = null

Expand Down Expand Up @@ -89,10 +97,18 @@ class HomeFragment :
problemOfDayCardFormDelegate.setup(viewBinding.homeScreenProblemOfDayCard)
challengeCardDelegate.setup(
viewBinding.homeScreenChallengeCard,
viewLifecycleOwner = viewLifecycleOwner,
onNewMessage = {
homeViewModel.onNewMessage(HomeFeature.Message.ChallengeWidgetMessage(it))
}
)
interviewPreparationCardDelegate.setup(
viewBinding.homeScreenInterviewPreparationCard,
viewLifecycleOwner = viewLifecycleOwner,
onNewMessage = {
homeViewModel.onNewMessage(HomeFeature.Message.InterviewPreparationWidgetMessage(it))
}
)
with(viewBinding) {
homeScreenSwipeRefreshLayout.setHyperskillColors()
homeScreenSwipeRefreshLayout.setOnRefreshListener {
Expand Down Expand Up @@ -177,9 +193,7 @@ class HomeFragment :
router = requireRouter()
)
is HomeFeature.Action.ViewAction.NavigateTo.StepScreen -> {
requireRouter().navigateTo(
StepScreen(action.stepRoute)
)
navigateToStepScreen(action.stepRoute)
}
is HomeFeature.Action.ViewAction.ChallengeWidgetViewAction -> {
challengeCardDelegate.handleAction(
Expand All @@ -188,9 +202,23 @@ class HomeFragment :
action = action.viewAction
)
}
is HomeFeature.Action.ViewAction.InterviewPreparationWidgetViewAction -> {
when (val viewAction = action.viewAction) {
is InterviewPreparationWidgetFeature.Action.ViewAction.NavigateTo.Step ->
navigateToStepScreen(viewAction.stepRoute)
is InterviewPreparationWidgetFeature.Action.ViewAction.ShowOpenStepError ->
viewBinding.root.snackbar(viewAction.errorMessage)
is InterviewPreparationWidgetFeature.Action.ViewAction.NavigateTo.InterviewPreparationOnboarding ->
requireRouter().navigateTo(InterviewPreparationOnboardingScreen(viewAction.stepRoute))
}
}
}
}

private fun navigateToStepScreen(stepRoute: StepRoute) {
requireRouter().navigateTo(StepScreen(stepRoute))
}

override fun render(state: HomeFeature.ViewState) {
homeViewStateDelegate.switchState(state.homeState)

Expand All @@ -202,11 +230,17 @@ class HomeFragment :
renderTopicsRepetition(homeState.repetitionsState, homeState.isFreemiumEnabled)
}

val challengeState = state.challengeWidgetViewState
challengeCardDelegate.render(childFragmentManager, challengeState)
renderChallengeCard(state.challengeWidgetViewState)
renderInterviewPreparationCard(state.interviewPreparationWidgetViewState)

gamificationToolbarDelegate?.render(state.toolbarViewState)
}

private fun renderChallengeCard(challengeWidgetViewState: ChallengeWidgetViewState) {
challengeCardDelegate.render(childFragmentManager, challengeWidgetViewState)
viewBinding.homeScreenChallengeCard.updateLayoutParams<MarginLayoutParams> {
updateMargins(
top = when (challengeState) {
top = when (challengeWidgetViewState) {
ChallengeWidgetViewState.Idle, ChallengeWidgetViewState.Empty -> 0
else -> {
requireContext()
Expand All @@ -216,8 +250,25 @@ class HomeFragment :
}
)
}
}

gamificationToolbarDelegate?.render(state.toolbarViewState)
private fun renderInterviewPreparationCard(
interviewPreparationWidgetViewState: InterviewPreparationWidgetViewState
) {
interviewPreparationCardDelegate.render(interviewPreparationWidgetViewState)
viewBinding.homeScreenInterviewPreparationCard.updateLayoutParams<MarginLayoutParams> {
updateMargins(
top = when (interviewPreparationWidgetViewState) {
InterviewPreparationWidgetViewState.Idle,
InterviewPreparationWidgetViewState.Empty -> 0
else -> {
requireContext()
.resources
.getDimensionPixelOffset(R.dimen.home_screen_interview_card_top_margin)
}
}
)
}
}

private fun renderSwipeRefresh(state: HomeFeature.ViewState) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.hyperskill.app.android.interview_preparation.delegate

import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.MutableStateFlow
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.android.interview_preparation.ui.InterviewPreparationCard
import org.hyperskill.app.interview_preparation.presentation.InterviewPreparationWidgetFeature
import org.hyperskill.app.interview_preparation.view.model.InterviewPreparationWidgetViewState

class InterviewPreparationCardDelegate {
private val stateFlow: MutableStateFlow<InterviewPreparationWidgetViewState?> = MutableStateFlow(null)
fun setup(
composeView: ComposeView,
viewLifecycleOwner: LifecycleOwner,
onNewMessage: (InterviewPreparationWidgetFeature.Message) -> Unit
) {
composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
setContent {
HyperskillTheme {
val viewState by stateFlow.collectAsStateWithLifecycle()
DisposableEffect(viewLifecycleOwner) {
onNewMessage(InterviewPreparationWidgetFeature.Message.ViewedEventMessage)
onDispose {
// no op
}
}
viewState?.let { actualViewState ->
InterviewPreparationCard(
viewState = actualViewState,
onNewMessage = onNewMessage
)
}
}
}
}
}

fun render(
viewState: InterviewPreparationWidgetViewState
) {
stateFlow.value = viewState
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.hyperskill.app.android.interview_preparation.dialog

import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.FragmentInterviewPreparationFinishedBinding
import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme

class InterviewPreparationFinishedDialogFragment : BottomSheetDialogFragment() {
companion object {
const val TAG = "InterviewPreparationFinishedBottomSheetTag"

fun newInstance(): InterviewPreparationFinishedDialogFragment =
InterviewPreparationFinishedDialogFragment()
}

private val binding: FragmentInterviewPreparationFinishedBinding by viewBinding(
FragmentInterviewPreparationFinishedBinding::bind
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TopCornersRoundedBottomSheetDialog)
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
BottomSheetDialog(requireContext(), theme).also { dialog ->
dialog.setOnShowListener {
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
if (savedInstanceState == null) {
(parentFragment as? Callback)?.onInterviewPreparationFinishedDialogShown()
}
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? =
inflater.wrapWithTheme(requireActivity())
.inflate(R.layout.fragment_interview_preparation_finished, container, false)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.interviewFinishedGoTrainingButton.setOnClickListener {
(parentFragment as? Callback)
?.onInterviewPreparationFinishedDialogGoTrainingClicked()
}
}

override fun onDismiss(dialog: DialogInterface) {
(parentFragment as? Callback)?.onInterviewPreparationFinishedDialogHidden()
}

interface Callback {
fun onInterviewPreparationFinishedDialogShown()

fun onInterviewPreparationFinishedDialogHidden()

fun onInterviewPreparationFinishedDialogGoTrainingClicked()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.hyperskill.app.android.interview_preparation.ui

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.hyperskill.app.R
import org.hyperskill.app.android.core.view.ui.widget.compose.ShimmerLoading
import org.hyperskill.app.android.core.view.ui.widget.compose.WidgetDataLoadingError
import org.hyperskill.app.interview_preparation.presentation.InterviewPreparationWidgetFeature.Message
import org.hyperskill.app.interview_preparation.view.model.InterviewPreparationWidgetViewState

@Composable
fun InterviewPreparationCard(
viewState: InterviewPreparationWidgetViewState,
onNewMessage: (Message) -> Unit
) {
when (viewState) {
InterviewPreparationWidgetViewState.Idle,
InterviewPreparationWidgetViewState.Empty -> {
// no op
}
is InterviewPreparationWidgetViewState.Loading -> {
if (viewState.shouldShowSkeleton) {
ShimmerLoading(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
)
}
}
InterviewPreparationWidgetViewState.Error -> {
WidgetDataLoadingError(
onRetryClick = {
onNewMessage(Message.RetryContentLoading)
},
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = R.string.interview_preparation_widget_network_error_text)
)
}
is InterviewPreparationWidgetViewState.Content -> {
InterviewPreparationContent(
content = viewState,
onClick = {
onNewMessage(Message.WidgetClicked)
}
)
}
}
}
Loading

0 comments on commit f8e3de6

Please sign in to comment.