From c1a160ebee90bd304fd614453dbff9748c87b48d Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 7 Sep 2022 14:37:55 +0200 Subject: [PATCH 01/20] LUN-407: add evaluations of levels (wip) --- assets/images/index.ts | 4 ++ assets/images/thumbs-down.svg | 7 +++ assets/images/thumbs-up.svg | 6 +++ .../LUN-407-add-evaluation-of-levels.yml | 6 +++ src/components/FeedbackBadge.tsx | 53 +++++++++++++++++++ src/components/ListItem.tsx | 23 ++++++-- src/constants/labels.json | 4 ++ src/routes/exercises/ExercisesScreen.tsx | 28 +++++----- src/services/helpers.ts | 16 ++++++ 9 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 assets/images/thumbs-down.svg create mode 100644 assets/images/thumbs-up.svg create mode 100644 release-notes/unreleased/LUN-407-add-evaluation-of-levels.yml create mode 100644 src/components/FeedbackBadge.tsx diff --git a/assets/images/index.ts b/assets/images/index.ts index 76b1d7e72..bfa06fc7a 100644 --- a/assets/images/index.ts +++ b/assets/images/index.ts @@ -61,6 +61,8 @@ import StarIconWhite from './star-icon-white.svg' import TrashIcon from './trash-bin-icon.svg' import TrophyIcon from './trophy-icon.svg' import VolumeUpCircleIcon from './volume-up-circle-icon.svg' +import ThumbsUpIcon from './thumbs-up.svg' +import ThumbsDownIcon from './thumbs-down.svg' export { AddCircleIcon, @@ -126,4 +128,6 @@ export { TrashIcon, TrophyIcon, VolumeUpCircleIcon, + ThumbsUpIcon, + ThumbsDownIcon, } diff --git a/assets/images/thumbs-down.svg b/assets/images/thumbs-down.svg new file mode 100644 index 000000000..1b00be3c4 --- /dev/null +++ b/assets/images/thumbs-down.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/thumbs-up.svg b/assets/images/thumbs-up.svg new file mode 100644 index 000000000..10785c304 --- /dev/null +++ b/assets/images/thumbs-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/release-notes/unreleased/LUN-407-add-evaluation-of-levels.yml b/release-notes/unreleased/LUN-407-add-evaluation-of-levels.yml new file mode 100644 index 000000000..2fb37e16c --- /dev/null +++ b/release-notes/unreleased/LUN-407-add-evaluation-of-levels.yml @@ -0,0 +1,6 @@ +issue_key: LUN-407 +show_in_stores: true +platforms: + - android + - ios +de: Zeige Feedback zu bereits abgeschlossenen Leveln diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx new file mode 100644 index 000000000..718e0b793 --- /dev/null +++ b/src/components/FeedbackBadge.tsx @@ -0,0 +1,53 @@ +import React, { ReactElement, useState, useCallback } from 'react' +import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' +import styled from 'styled-components/native' +// import { useFocusEffect } from '@react-navigation/native' +import { ThumbsDownIcon } from '../../assets/images' +import { ContentSecondaryLight } from './text/Content' +import { getLabels} from '../services/helpers' +// import { reportError } from '../services/sentry' +// import { getFeedback } from '../hooks/helpers' + +const BadgeContainer = styled.View` + display: flex; + flex-flow: row nowrap; + justify-content: center; + background-color: ${props => props.theme.colors.networkErrorBackground}; + width: 100%; +` +import Trophy from '../../components/Trophy' + +const BadgeText = styled(ContentSecondaryLight)` + font-style: italic; + margin-left: ${props => props.theme.spacings.xs}; +` + +interface FeedbackBadgeProps { + disciplineId: number + thisLevel: number + nextLevel: number | null +} + +const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => ( + // const [feedback, setFeedback] = useState(0) + // + // const {disciplineId, thisLevel, nextLevel} = props ?? {} + // + // useFocusEffect( + // useCallback(() => { + // if (disciplineId && nextLevel) { + // getFeedback(disciplineId, nextLevel) + // .then(setFeedback) + // .catch(reportError) + // } + // }, [disciplineId, nextLevel]) + // ) + // if (!nextLevel || thisLevel !== nextLevel) { return null } + + + + {getLabels().exercises.feedback.negative} + + ) + +export default FeedbackModal diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 26f50cf04..1f2179833 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -2,30 +2,38 @@ import React, { ReactElement, useCallback, useState } from 'react' import { GestureResponderEvent } from 'react-native' import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled, { useTheme } from 'styled-components/native' +import FeedbackBadge from './FeedbackBadge' import { ChevronRight } from '../../assets/images' import { ContentSecondaryLight } from './text/Content' export const GenericListItemContainer = styled.Pressable` margin-bottom: ${props => props.theme.spacings.xxs}; - align-self: center; flex-direction: row; justify-content: space-between; align-items: center; border-radius: 2px; ` - const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: boolean }>` + flex-direction: column; + border: 1px solid ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; + border-left-color: ${props => props.theme.colors.incorrect}; + border-left-radius: 0; + border-left-width: 6px; +` + +const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` min-height: ${hp('12%')}px; + display: flex; + flex-direction: row; + padding: ${props => + `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; background-color: ${prop => { if (prop.disabled) { return prop.theme.colors.disabled } return prop.pressed ? prop.theme.colors.primary : prop.theme.colors.backgroundAccent }}; - border: 1px solid ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; - padding: ${props => - `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; ` const Title = styled.Text<{ pressed: boolean }>` @@ -87,6 +95,7 @@ interface ListItemProps { hideRightChildren?: boolean arrowDisabled?: boolean disabled?: boolean + feedbackInfo?: {disciplineId: number, thisLevel: number, nextLevel: number | null} | null } const ListItem = ({ @@ -100,6 +109,7 @@ const ListItem = ({ hideRightChildren = false, arrowDisabled = false, disabled = false, + feedbackInfo = null, }: ListItemProps): ReactElement => { const [pressInY, setPressInY] = useState(null) const [pressed, setPressed] = useState(false) @@ -161,6 +171,8 @@ const ListItem = ({ pressed={pressed} delayLongPress={200} testID='list-item'> + + {iconToRender} {titleToRender} @@ -171,6 +183,7 @@ const ListItem = ({ {children} {rightChildrenToRender} + ) } diff --git a/src/constants/labels.json b/src/constants/labels.json index aae412ed2..526a86244 100644 --- a/src/constants/labels.json +++ b/src/constants/labels.json @@ -98,6 +98,10 @@ "cancelModal": { "cancelAsk": "Möchtest du diese Übung wirklich beenden?", "cancel": "Beenden" + }, + "feedback": { + "positive": "Weiter so, das war super!", + "negative": "Das geht besser. Probier es nochmal." } }, "results": { diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index aac37c605..b482591a1 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -10,7 +10,6 @@ import Modal from '../../components/Modal' import RouteWrapper from '../../components/RouteWrapper' import ServerResponseHandler from '../../components/ServerResponseHandler' import Title from '../../components/Title' -import Trophy from '../../components/Trophy' import { ContentTextBold, ContentTextLight } from '../../components/text/Content' import { Exercise, EXERCISES } from '../../constants/data' import useLoadDocuments from '../../hooks/useLoadDocuments' @@ -52,7 +51,7 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme disciplineId: discipline.id, apiKey: discipline.apiKey, }) - + useFocusEffect( useCallback(() => { getDoneExercises(disciplineId) @@ -83,19 +82,18 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme } const renderListItem = ({ item, index }: { item: Exercise; index: number }): JSX.Element | null => ( - - - - handleNavigation(item)} - arrowDisabled={nextExercise === null || item.level > nextExercise.level}> - - - - - ) + + + + handleNavigation(item)} + arrowDisabled={nextExercise === null || item.level > nextExercise.level} + feedbackInfo={{disciplineId, thisLevel: item.level, nextLevel: nextExercise?.level}}/> + + + ) const nextExercisePreposition = nextExercise?.key === 0 diff --git a/src/services/helpers.ts b/src/services/helpers.ts index bfceeb749..d1b16c84b 100644 --- a/src/services/helpers.ts +++ b/src/services/helpers.ts @@ -74,6 +74,22 @@ const getDoneExercisesByProgress = (disciplineId: number, progress: Progress): n export const getDoneExercises = (disciplineId: number): Promise => AsyncStorage.getExerciseProgress().then(progress => getDoneExercisesByProgress(disciplineId, progress)) +const getFeedbackFromScore = (score: number): number => { + // to be replaced by constants from LUN-362 + const SCORE_THRESHOLD_POSITIVE_FEEDBACK = 4 + const SCORE_THRESHOLD_NEGATIVE_FEEDBACK = 2 + + if (score >= SCORE_THRESHOLD_POSITIVE_FEEDBACK) { return 1 } + if (score <= SCORE_THRESHOLD_NEGATIVE_FEEDBACK) { return -1 } + return 0 + +} + +export const getFeedback = async (disciplineId: number, exerciseKey: number): number => { + const progress = await AsyncStorage.getExerciseProgress() + return getFeedbackFromScore(progress[disciplineId][exerciseKey]) +} + /* Calculates the next exercise that needs to be done for a profession (= second level discipline of lunes standard vocabulary) returns From b0bf29c8d8542f678efade9daa59952afcf2f5ea Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 20 Sep 2022 16:56:40 +0200 Subject: [PATCH 02/20] LUN-407: add feedback badge to ListItems --- assets/images/index.ts | 4 +- src/components/FeedbackBadge.tsx | 95 +++++++++++++++--------- src/components/ListItem.tsx | 56 +++++++++----- src/routes/exercises/ExercisesScreen.tsx | 27 +++---- src/services/helpers.ts | 16 ---- 5 files changed, 111 insertions(+), 87 deletions(-) diff --git a/assets/images/index.ts b/assets/images/index.ts index a93574a85..73719c033 100644 --- a/assets/images/index.ts +++ b/assets/images/index.ts @@ -59,11 +59,11 @@ import StarCircleIconGrey from './star-circle-icon-grey.svg' import StarIconGreyFilled from './star-icon-grey-filled.svg' import StarIconGrey from './star-icon-grey.svg' import StarIconWhite from './star-icon-white.svg' +import ThumbsDownIcon from './thumbs-down.svg' +import ThumbsUpIcon from './thumbs-up.svg' import TrashIcon from './trash-bin-icon.svg' import TrophyIcon from './trophy-icon.svg' import VolumeUpCircleIcon from './volume-up-circle-icon.svg' -import ThumbsUpIcon from './thumbs-up.svg' -import ThumbsDownIcon from './thumbs-down.svg' export { AddCircleIcon, diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 718e0b793..c16c5e63e 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -1,53 +1,74 @@ -import React, { ReactElement, useState, useCallback } from 'react' -import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' +import React, { ReactElement, useState, useEffect } from 'react' +import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' -// import { useFocusEffect } from '@react-navigation/native' -import { ThumbsDownIcon } from '../../assets/images' + +import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' +import { SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' +import useLoadAsync from '../hooks/useLoadAsync' +import AsyncStorage from '../services/AsyncStorage' +import { getLabels } from '../services/helpers' import { ContentSecondaryLight } from './text/Content' -import { getLabels} from '../services/helpers' -// import { reportError } from '../services/sentry' -// import { getFeedback } from '../hooks/helpers' const BadgeContainer = styled.View` - display: flex; - flex-flow: row nowrap; - justify-content: center; - background-color: ${props => props.theme.colors.networkErrorBackground}; - width: 100%; + display: flex; + flex-flow: row nowrap; + justify-content: center; + background-color: ${props => props.theme.colors.networkErrorBackground}; + width: 100%; ` -import Trophy from '../../components/Trophy' - const BadgeText = styled(ContentSecondaryLight)` - font-style: italic; - margin-left: ${props => props.theme.spacings.xs}; + font-style: italic; + margin-left: ${props => props.theme.spacings.xs}; ` interface FeedbackBadgeProps { disciplineId: number - thisLevel: number - nextLevel: number | null + level: number } -const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => ( - // const [feedback, setFeedback] = useState(0) - // - // const {disciplineId, thisLevel, nextLevel} = props ?? {} - // - // useFocusEffect( - // useCallback(() => { - // if (disciplineId && nextLevel) { - // getFeedback(disciplineId, nextLevel) - // .then(setFeedback) - // .catch(reportError) - // } - // }, [disciplineId, nextLevel]) - // ) - // if (!nextLevel || thisLevel !== nextLevel) { return null } - - - +export const enum FEEDBACK { + POSITIVE, + NONE, + NEGATIVE, +} + +const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => { + const [feedback, setFeedback] = useState(FEEDBACK.NONE) + const { setFeedback: setFeedbackOnParent, feedbackInfo } = props + const { disciplineId, level } = feedbackInfo ?? {} + const { data: scores } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + + useEffect(() => { + const updateFeedback = feedback => { + setFeedback(feedback) + setFeedbackOnParent(feedback) + } + if (scores && disciplineId && level && level !== 0) { + const score = scores[disciplineId]?.[level] + if (score) { + updateFeedback(score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? FEEDBACK.POSITIVE : FEEDBACK.NEGATIVE) + } + } + }, [scores, disciplineId, level, setFeedbackOnParent]) + + if (feedback === FEEDBACK.POSITIVE) { + return ( + + + {getLabels().exercises.feedback.positive} + + ) + } + + if (feedback === FEEDBACK.NEGATIVE) { + return ( + + {getLabels().exercises.feedback.negative} - + ) + } + return null +} export default FeedbackModal diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 1f2179833..1705ed731 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -2,9 +2,9 @@ import React, { ReactElement, useCallback, useState } from 'react' import { GestureResponderEvent } from 'react-native' import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled, { useTheme } from 'styled-components/native' -import FeedbackBadge from './FeedbackBadge' import { ChevronRight } from '../../assets/images' +import FeedbackBadge, { FEEDBACK } from './FeedbackBadge' import { ContentSecondaryLight } from './text/Content' export const GenericListItemContainer = styled.Pressable` @@ -14,18 +14,34 @@ export const GenericListItemContainer = styled.Pressable` align-items: center; border-radius: 2px; ` -const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: boolean }>` +const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: boolean; feedback: FEEDBACK }>` flex-direction: column; - border: 1px solid ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; - border-left-color: ${props => props.theme.colors.incorrect}; + border-top-width: 1px; + border-top-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; + border-right-width: 1px; + border-right-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; + border-bottom-width: 1px; + border-bottom-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; + border-left-color: ${props => { + if (props.feedback === FEEDBACK.POSITIVE) { + return props.theme.colors.correct + } + if (props.feedback === FEEDBACK.NEGATIVE) { + return props.theme.colors.incorrect + } + if (props.pressed) { + return props.theme.colors.primary + } + return props.theme.colors.disabled + }}; border-left-radius: 0; - border-left-width: 6px; + border-left-width: ${props => (props.feedback !== FEEDBACK.NONE ? '4px' : '1px')}; ` const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` min-height: ${hp('12%')}px; - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; padding: ${props => `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; background-color: ${prop => { @@ -95,7 +111,7 @@ interface ListItemProps { hideRightChildren?: boolean arrowDisabled?: boolean disabled?: boolean - feedbackInfo?: {disciplineId: number, thisLevel: number, nextLevel: number | null} | null + feedbackInfo?: { disciplineId: number; level: number } | null } const ListItem = ({ @@ -113,6 +129,7 @@ const ListItem = ({ }: ListItemProps): ReactElement => { const [pressInY, setPressInY] = useState(null) const [pressed, setPressed] = useState(false) + const [feedback, setFeedback] = useState(FEEDBACK.NONE) const updatePressed = useCallback((pressed: boolean): void => onPress && setPressed(pressed), [onPress]) const theme = useTheme() @@ -170,19 +187,20 @@ const ListItem = ({ onLongPress={() => updatePressed(true)} pressed={pressed} delayLongPress={200} + feedback={feedback} testID='list-item'> - + - {iconToRender} - - {titleToRender} - - {badgeLabel && {badgeLabel}} - {description && description.length > 0 && {description}} - - {children} - - {rightChildrenToRender} + {iconToRender} + + {titleToRender} + + {badgeLabel && {badgeLabel}} + {description && description.length > 0 && {description}} + + {children} + + {rightChildrenToRender} ) diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index b482591a1..e2a82b554 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -51,7 +51,7 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme disciplineId: discipline.id, apiKey: discipline.apiKey, }) - + useFocusEffect( useCallback(() => { getDoneExercises(disciplineId) @@ -82,18 +82,19 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme } const renderListItem = ({ item, index }: { item: Exercise; index: number }): JSX.Element | null => ( - - - - handleNavigation(item)} - arrowDisabled={nextExercise === null || item.level > nextExercise.level} - feedbackInfo={{disciplineId, thisLevel: item.level, nextLevel: nextExercise?.level}}/> - - - ) + + + + handleNavigation(item)} + arrowDisabled={nextExercise === null || item.level > nextExercise.level} + feedbackInfo={{ disciplineId, level: item.level }} + /> + + + ) const nextExercisePreposition = nextExercise?.key === 0 diff --git a/src/services/helpers.ts b/src/services/helpers.ts index 83ec2dcf8..83ad0c283 100644 --- a/src/services/helpers.ts +++ b/src/services/helpers.ts @@ -83,22 +83,6 @@ const getDoneExercisesByProgress = (disciplineId: number, progress: Progress): n export const getDoneExercises = (disciplineId: number): Promise => AsyncStorage.getExerciseProgress().then(progress => getDoneExercisesByProgress(disciplineId, progress)) -const getFeedbackFromScore = (score: number): number => { - // to be replaced by constants from LUN-362 - const SCORE_THRESHOLD_POSITIVE_FEEDBACK = 4 - const SCORE_THRESHOLD_NEGATIVE_FEEDBACK = 2 - - if (score >= SCORE_THRESHOLD_POSITIVE_FEEDBACK) { return 1 } - if (score <= SCORE_THRESHOLD_NEGATIVE_FEEDBACK) { return -1 } - return 0 - -} - -export const getFeedback = async (disciplineId: number, exerciseKey: number): number => { - const progress = await AsyncStorage.getExerciseProgress() - return getFeedbackFromScore(progress[disciplineId][exerciseKey]) -} - /* Calculates the next exercise that needs to be done for a profession (= second level discipline of lunes standard vocabulary) returns From 84c70ae5fec71a41a79d56e18806aaf4b9b7d37a Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 20 Sep 2022 17:03:15 +0200 Subject: [PATCH 03/20] LUN-407: keep to style guidelines --- src/components/FeedbackBadge.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index c16c5e63e..75dc2ba13 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -21,17 +21,17 @@ const BadgeText = styled(ContentSecondaryLight)` margin-left: ${props => props.theme.spacings.xs}; ` -interface FeedbackBadgeProps { - disciplineId: number - level: number -} - export const enum FEEDBACK { POSITIVE, NONE, NEGATIVE, } +interface FeedbackBadgeProps { + disciplineId: number + level: number +} + const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => { const [feedback, setFeedback] = useState(FEEDBACK.NONE) const { setFeedback: setFeedbackOnParent, feedbackInfo } = props From c8b406b49d5349a902f0bd72f2749d7bed1a26fe Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 20 Sep 2022 17:49:14 +0200 Subject: [PATCH 04/20] LUN-407: fix failing tests --- src/components/FeedbackBadge.tsx | 21 ++++++++----------- src/components/ListItem.tsx | 17 ++++++++------- src/constants/data.ts | 6 ++++++ .../__tests__/VocabularyListScreen.spec.tsx | 2 ++ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 75dc2ba13..26f9562ef 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -3,7 +3,7 @@ import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' -import { SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' +import { FEEDBACK, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' import useLoadAsync from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' import { getLabels } from '../services/helpers' @@ -21,25 +21,22 @@ const BadgeText = styled(ContentSecondaryLight)` margin-left: ${props => props.theme.spacings.xs}; ` -export const enum FEEDBACK { - POSITIVE, - NONE, - NEGATIVE, -} - interface FeedbackBadgeProps { - disciplineId: number - level: number + feedbackInfo: { + disciplineId: number + level: number + } | null + setFeedback: (feedback: FEEDBACK) => void } -const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => { +const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { const [feedback, setFeedback] = useState(FEEDBACK.NONE) const { setFeedback: setFeedbackOnParent, feedbackInfo } = props const { disciplineId, level } = feedbackInfo ?? {} const { data: scores } = useLoadAsync(AsyncStorage.getExerciseProgress, null) useEffect(() => { - const updateFeedback = feedback => { + const updateFeedback = (feedback: FEEDBACK) => { setFeedback(feedback) setFeedbackOnParent(feedback) } @@ -71,4 +68,4 @@ const FeedbackModal = (props: FeedbackBadgeProps | null): ReactElement => { return null } -export default FeedbackModal +export default FeedbackBadge diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 1705ed731..59b04d73a 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -4,7 +4,8 @@ import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-nat import styled, { useTheme } from 'styled-components/native' import { ChevronRight } from '../../assets/images' -import FeedbackBadge, { FEEDBACK } from './FeedbackBadge' +import { FEEDBACK } from '../constants/data' +import FeedbackBadge from './FeedbackBadge' import { ContentSecondaryLight } from './text/Content' export const GenericListItemContainer = styled.Pressable` @@ -36,6 +37,12 @@ const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: }}; border-left-radius: 0; border-left-width: ${props => (props.feedback !== FEEDBACK.NONE ? '4px' : '1px')}; + background-color: ${prop => { + if (prop.disabled) { + return prop.theme.colors.disabled + } + return prop.pressed ? prop.theme.colors.primary : prop.theme.colors.backgroundAccent + }}; ` const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` @@ -44,12 +51,6 @@ const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` flex-direction: row; padding: ${props => `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; - background-color: ${prop => { - if (prop.disabled) { - return prop.theme.colors.disabled - } - return prop.pressed ? prop.theme.colors.primary : prop.theme.colors.backgroundAccent - }}; ` const Title = styled.Text<{ pressed: boolean }>` @@ -190,7 +191,7 @@ const ListItem = ({ feedback={feedback} testID='list-item'> - + {iconToRender} {titleToRender} diff --git a/src/constants/data.ts b/src/constants/data.ts index c8c79d1c2..bb2a6663c 100644 --- a/src/constants/data.ts +++ b/src/constants/data.ts @@ -154,3 +154,9 @@ export const numberOfMaxRetries = 3 export const SCORE_THRESHOLD_POSITIVE_FEEDBACK = 4 export const SCORE_THRESHOLD_UNLOCK = 2 + +export const enum FEEDBACK { + POSITIVE, + NONE, + NEGATIVE, +} diff --git a/src/routes/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx index 81c32be45..7a6bc1719 100644 --- a/src/routes/__tests__/VocabularyListScreen.spec.tsx +++ b/src/routes/__tests__/VocabularyListScreen.spec.tsx @@ -24,6 +24,8 @@ jest.mock('../../components/AudioPlayer', () => { return () => AudioPlayer }) +jest.mock('../../components/FeedbackBadge', () => () => null) + describe('VocabularyListScreen', () => { const documents = new DocumentBuilder(2).build() const route: RouteProp = { From 462d7d755e080e5ce3e15482b3fbe21622034f31 Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 21 Sep 2022 09:53:20 +0200 Subject: [PATCH 05/20] LUN-407: add tests for FeedbackBadge --- src/components/ErrorMessage.tsx | 2 +- src/components/FeedbackBadge.tsx | 23 ++--- src/components/ListItem.tsx | 6 +- .../__tests__/FeedbackBadge.spec.tsx | 99 +++++++++++++++++++ src/constants/theme/colors.ts | 2 +- src/routes/exercises/ExercisesScreen.tsx | 2 +- 6 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 src/components/__tests__/FeedbackBadge.spec.tsx diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx index dffe9504c..6c1cbc8d3 100644 --- a/src/components/ErrorMessage.tsx +++ b/src/components/ErrorMessage.tsx @@ -65,7 +65,7 @@ const ErrorMessage = ({ error, refresh, contained }: ErrorMessageProps): JSX.Ele return ( - + {error.message === NetworkError && ( diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 26f9562ef..94fe74953 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' import { FEEDBACK, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' -import useLoadAsync from '../hooks/useLoadAsync' +import { useLoadAsync } from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' import { getLabels } from '../services/helpers' import { ContentSecondaryLight } from './text/Content' @@ -13,7 +13,7 @@ const BadgeContainer = styled.View` display: flex; flex-flow: row nowrap; justify-content: center; - background-color: ${props => props.theme.colors.networkErrorBackground}; + background-color: ${props => props.theme.colors.lightGreyBackground}; width: 100%; ` const BadgeText = styled(ContentSecondaryLight)` @@ -22,7 +22,7 @@ const BadgeText = styled(ContentSecondaryLight)` ` interface FeedbackBadgeProps { - feedbackInfo: { + levelIdentifier: { disciplineId: number level: number } | null @@ -31,26 +31,27 @@ interface FeedbackBadgeProps { const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { const [feedback, setFeedback] = useState(FEEDBACK.NONE) - const { setFeedback: setFeedbackOnParent, feedbackInfo } = props - const { disciplineId, level } = feedbackInfo ?? {} - const { data: scores } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + const { setFeedback: setFeedbackOnParent, levelIdentifier } = props + const { disciplineId, level } = levelIdentifier ?? {} + const { data: scores, loading } = useLoadAsync(AsyncStorage.getExerciseProgress, null) useEffect(() => { const updateFeedback = (feedback: FEEDBACK) => { setFeedback(feedback) setFeedbackOnParent(feedback) } - if (scores && disciplineId && level && level !== 0) { - const score = scores[disciplineId]?.[level] + if (!loading && disciplineId != null && level != null && level !== 0) { + /* eslint-disable @typescript-eslint/no-unnecessary-condition */ + const score = scores?.[disciplineId]?.[level] if (score) { updateFeedback(score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? FEEDBACK.POSITIVE : FEEDBACK.NEGATIVE) } } - }, [scores, disciplineId, level, setFeedbackOnParent]) + }, [loading, scores, disciplineId, level, setFeedbackOnParent]) if (feedback === FEEDBACK.POSITIVE) { return ( - + {getLabels().exercises.feedback.positive} @@ -59,7 +60,7 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { if (feedback === FEEDBACK.NEGATIVE) { return ( - + {getLabels().exercises.feedback.negative} diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 59b04d73a..a53ae0b02 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -112,7 +112,7 @@ interface ListItemProps { hideRightChildren?: boolean arrowDisabled?: boolean disabled?: boolean - feedbackInfo?: { disciplineId: number; level: number } | null + levelIdentifier?: { disciplineId: number; level: number } | null } const ListItem = ({ @@ -126,7 +126,7 @@ const ListItem = ({ hideRightChildren = false, arrowDisabled = false, disabled = false, - feedbackInfo = null, + levelIdentifier = null, }: ListItemProps): ReactElement => { const [pressInY, setPressInY] = useState(null) const [pressed, setPressed] = useState(false) @@ -190,7 +190,7 @@ const ListItem = ({ delayLongPress={200} feedback={feedback} testID='list-item'> - + {iconToRender} diff --git a/src/components/__tests__/FeedbackBadge.spec.tsx b/src/components/__tests__/FeedbackBadge.spec.tsx new file mode 100644 index 000000000..a18edd52b --- /dev/null +++ b/src/components/__tests__/FeedbackBadge.spec.tsx @@ -0,0 +1,99 @@ +import { waitFor } from '@testing-library/react-native' +import { mocked } from 'jest-mock' +import React from 'react' + +import { SCORE_THRESHOLD_POSITIVE_FEEDBACK, FEEDBACK } from '../../constants/data' +import { useLoadAsync } from '../../hooks/useLoadAsync' +import { getLabels } from '../../services/helpers' +import render from '../../testing/render' +import FeedbackBadge from '../FeedbackBadge' + +jest.mock('../../hooks/useLoadAsync', () => ({ + useLoadAsync: jest.fn(), +})) + +const mockSetFeedback = jest.fn() + +describe('FeedbackBadge', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + const renderFeedbackBadge = (levelIdentifier: { disciplineId: number; level: number } | null) => + render( + + ) + + it('should not show when no level info is provided', async () => { + mocked(useLoadAsync).mockImplementation(() => ({ + data: {}, + error: null, + loading: false, + refresh: () => null, + })) + const { queryByTestId } = renderFeedbackBadge(null) + + await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) + await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) + }) + + it('should not show when level is not done', async () => { + mocked(useLoadAsync).mockImplementation(() => ({ + data: { '0': { '1': undefined } }, + error: null, + loading: false, + refresh: () => null, + })) + const { queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) + + await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) + await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) + }) + + it('should not show for wordlist level', async () => { + mocked(useLoadAsync).mockImplementation(() => ({ + data: { '0': { '0': SCORE_THRESHOLD_POSITIVE_FEEDBACK + 1 } }, + error: null, + loading: false, + refresh: () => null, + })) + const { queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 0 }) + + await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) + await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) + }) + + it('should show positive feedback for scores above threshold', async () => { + mocked(useLoadAsync).mockImplementation(() => ({ + data: { '0': { '1': SCORE_THRESHOLD_POSITIVE_FEEDBACK + 1 } }, + error: null, + loading: false, + refresh: () => null, + })) + const { queryByText, queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) + + await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) + await waitFor(() => expect(queryByTestId('positive-badge')).not.toBeNull()) + await waitFor(() => expect(queryByText(getLabels().exercises.feedback.positive)).not.toBeNull()) + await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(FEEDBACK.POSITIVE)) + }) + + it('should show negative feedback for scores below threshold', async () => { + mocked(useLoadAsync).mockImplementation(() => ({ + data: { '0': { '1': SCORE_THRESHOLD_POSITIVE_FEEDBACK - 1 } }, + error: null, + loading: false, + refresh: () => null, + })) + const { queryByText, queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) + + await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) + await waitFor(() => expect(queryByTestId('negative-badge')).not.toBeNull()) + await waitFor(() => expect(queryByText(getLabels().exercises.feedback.negative)).not.toBeNull()) + await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(FEEDBACK.NEGATIVE)) + }) +}) diff --git a/src/constants/theme/colors.ts b/src/constants/theme/colors.ts index 4c38e5e28..d0e6f0b7d 100644 --- a/src/constants/theme/colors.ts +++ b/src/constants/theme/colors.ts @@ -22,7 +22,7 @@ export const COLORS = { articleFeminine: '#faa7a7', articleMasculine: '#8cc8f3', link: 'blue', - networkErrorBackground: '#e0e4ed', + lightGreyBackground: '#e0e4ed', } export type Color = typeof COLORS[keyof typeof COLORS] diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index e2a82b554..f63bbb838 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -90,7 +90,7 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme description={item.description} onPress={() => handleNavigation(item)} arrowDisabled={nextExercise === null || item.level > nextExercise.level} - feedbackInfo={{ disciplineId, level: item.level }} + levelIdentifier={{ disciplineId, level: item.level }} /> From bd3d7112082e0bd6d7e8fe0793172a1eb490e0d9 Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 21 Sep 2022 10:45:00 +0200 Subject: [PATCH 06/20] LUN-407: refresh feedback info on closeExerciseAction --- src/components/FeedbackBadge.tsx | 10 +++++++++- src/components/__tests__/FeedbackBadge.spec.tsx | 11 +++-------- src/components/__tests__/ListItem.spec.tsx | 1 + src/components/__tests__/VocabularyList.spec.tsx | 1 + src/components/__tests__/VocabularyListItem.spec.tsx | 1 + .../__tests__/UserVocabularyOverviewScreen.spec.tsx | 1 + .../dictionary/__tests__/DictionaryScreen.spec.tsx | 1 + .../components/__tests__/SeletionItem.spec.tsx | 1 + .../__tests__/UserVocabularyListScreen.spec.tsx | 1 + 9 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 94fe74953..c7a9476c0 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -1,3 +1,4 @@ +import { useIsFocused } from '@react-navigation/native' import React, { ReactElement, useState, useEffect } from 'react' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' @@ -33,7 +34,8 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { const [feedback, setFeedback] = useState(FEEDBACK.NONE) const { setFeedback: setFeedbackOnParent, levelIdentifier } = props const { disciplineId, level } = levelIdentifier ?? {} - const { data: scores, loading } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + const { data: scores, loading, refresh } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + const isFocused = useIsFocused() useEffect(() => { const updateFeedback = (feedback: FEEDBACK) => { @@ -49,6 +51,12 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { } }, [loading, scores, disciplineId, level, setFeedbackOnParent]) + useEffect(() => { + if (isFocused) { + refresh() + } + }, [isFocused, refresh]) + if (feedback === FEEDBACK.POSITIVE) { return ( diff --git a/src/components/__tests__/FeedbackBadge.spec.tsx b/src/components/__tests__/FeedbackBadge.spec.tsx index a18edd52b..487d42952 100644 --- a/src/components/__tests__/FeedbackBadge.spec.tsx +++ b/src/components/__tests__/FeedbackBadge.spec.tsx @@ -8,6 +8,7 @@ import { getLabels } from '../../services/helpers' import render from '../../testing/render' import FeedbackBadge from '../FeedbackBadge' +jest.mock('@react-navigation/native') jest.mock('../../hooks/useLoadAsync', () => ({ useLoadAsync: jest.fn(), })) @@ -18,15 +19,9 @@ describe('FeedbackBadge', () => { beforeEach(() => { jest.clearAllMocks() }) + const renderFeedbackBadge = (levelIdentifier: { disciplineId: number; level: number } | null) => - render( - - ) + render() it('should not show when no level info is provided', async () => { mocked(useLoadAsync).mockImplementation(() => ({ diff --git a/src/components/__tests__/ListItem.spec.tsx b/src/components/__tests__/ListItem.spec.tsx index 12eff6ded..7210a083f 100644 --- a/src/components/__tests__/ListItem.spec.tsx +++ b/src/components/__tests__/ListItem.spec.tsx @@ -5,6 +5,7 @@ import { COLORS } from '../../constants/theme/colors' import render from '../../testing/render' import ListItem from '../ListItem' +jest.mock('@react-navigation/native') describe('ListItem', () => { const onPress = jest.fn() const description = 'Wörter' diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx index 696faeed7..ade9545f0 100644 --- a/src/components/__tests__/VocabularyList.spec.tsx +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -7,6 +7,7 @@ import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' import render from '../../testing/render' import VocabularyList from '../VocabularyList' +jest.mock('@react-navigation/native') jest.mock('../FavoriteButton', () => () => { const { Text } = require('react-native') return FavoriteButton diff --git a/src/components/__tests__/VocabularyListItem.spec.tsx b/src/components/__tests__/VocabularyListItem.spec.tsx index d61aea728..1ebcb643a 100644 --- a/src/components/__tests__/VocabularyListItem.spec.tsx +++ b/src/components/__tests__/VocabularyListItem.spec.tsx @@ -7,6 +7,7 @@ import { Document } from '../../constants/endpoints' import render from '../../testing/render' import VocabularyListItem from '../VocabularyListItem' +jest.mock('@react-navigation/native') jest.mock('../FavoriteButton', () => () => { const { Text } = require('react-native') return FavoriteButton diff --git a/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx b/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx index 6e2ca6417..11a2bd6f2 100644 --- a/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx +++ b/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx @@ -5,6 +5,7 @@ import createNavigationMock from '../../testing/createNavigationPropMock' import render from '../../testing/render' import UserVocabularyOverviewScreen from '../UserVocabularyOverviewScreen' +jest.mock('@react-navigation/native') describe('UserVocabularyOverviewScreen', () => { const navigation = createNavigationMock<'UserVocabularyOverview'>() it('should show content', () => { diff --git a/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx b/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx index e09aa3957..b46e367cb 100644 --- a/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx +++ b/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx @@ -10,6 +10,7 @@ import { getReturnOf } from '../../../testing/helper' import render from '../../../testing/render' import DictionaryScreen from '../DictionaryScreen' +jest.mock('@react-navigation/native') jest.mock('../../../components/FavoriteButton', () => { const Text = require('react-native').Text return () => FavoriteButton diff --git a/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx b/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx index 6265ec5f6..f242d199a 100644 --- a/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx +++ b/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx @@ -12,6 +12,7 @@ import { import render from '../../../../testing/render' import SelectionItem from '../SelectionItem' +jest.mock('@react-navigation/native') describe('SelectionItem', () => { const deleteItem = jest.fn() const renderSelectionItem = (): RenderAPI => diff --git a/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx b/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx index cee4b6672..3aeefc395 100644 --- a/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx +++ b/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx @@ -9,6 +9,7 @@ import { getReturnOf } from '../../../testing/helper' import render from '../../../testing/render' import UserVocabularyListScreen from '../UserVocabularyListScreen' +jest.mock('@react-navigation/native') jest.mock('../../../hooks/useReadUserVocabulary') jest.mock('../../../components/FavoriteButton', () => () => { From 52760a5464d11db090088bab9422cb6fa8778eee Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 21 Sep 2022 16:41:09 +0200 Subject: [PATCH 07/20] LUN-407: rename FEEDBACK to EXERCISE_FEEDBACK --- src/components/FeedbackBadge.tsx | 16 +++++++++------- src/components/ListItem.tsx | 16 ++++++++++------ src/components/__tests__/FeedbackBadge.spec.tsx | 6 +++--- src/constants/data.ts | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index c7a9476c0..8409020c8 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -4,7 +4,7 @@ import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' -import { FEEDBACK, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' +import { EXERCISE_FEEDBACK, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' import { useLoadAsync } from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' import { getLabels } from '../services/helpers' @@ -27,18 +27,18 @@ interface FeedbackBadgeProps { disciplineId: number level: number } | null - setFeedback: (feedback: FEEDBACK) => void + setFeedback: (feedback: EXERCISE_FEEDBACK) => void } const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { - const [feedback, setFeedback] = useState(FEEDBACK.NONE) + const [feedback, setFeedback] = useState(EXERCISE_FEEDBACK.NONE) const { setFeedback: setFeedbackOnParent, levelIdentifier } = props const { disciplineId, level } = levelIdentifier ?? {} const { data: scores, loading, refresh } = useLoadAsync(AsyncStorage.getExerciseProgress, null) const isFocused = useIsFocused() useEffect(() => { - const updateFeedback = (feedback: FEEDBACK) => { + const updateFeedback = (feedback: EXERCISE_FEEDBACK) => { setFeedback(feedback) setFeedbackOnParent(feedback) } @@ -46,7 +46,9 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { /* eslint-disable @typescript-eslint/no-unnecessary-condition */ const score = scores?.[disciplineId]?.[level] if (score) { - updateFeedback(score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? FEEDBACK.POSITIVE : FEEDBACK.NEGATIVE) + updateFeedback( + score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? EXERCISE_FEEDBACK.POSITIVE : EXERCISE_FEEDBACK.NEGATIVE + ) } } }, [loading, scores, disciplineId, level, setFeedbackOnParent]) @@ -57,7 +59,7 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { } }, [isFocused, refresh]) - if (feedback === FEEDBACK.POSITIVE) { + if (feedback === EXERCISE_FEEDBACK.POSITIVE) { return ( @@ -66,7 +68,7 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { ) } - if (feedback === FEEDBACK.NEGATIVE) { + if (feedback === EXERCISE_FEEDBACK.NEGATIVE) { return ( diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index a53ae0b02..735c9e421 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -4,7 +4,7 @@ import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-nat import styled, { useTheme } from 'styled-components/native' import { ChevronRight } from '../../assets/images' -import { FEEDBACK } from '../constants/data' +import { EXERCISE_FEEDBACK } from '../constants/data' import FeedbackBadge from './FeedbackBadge' import { ContentSecondaryLight } from './text/Content' @@ -15,7 +15,11 @@ export const GenericListItemContainer = styled.Pressable` align-items: center; border-radius: 2px; ` -const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: boolean; feedback: FEEDBACK }>` +const Container = styled(GenericListItemContainer)<{ + pressed: boolean + disabled: boolean + feedback: EXERCISE_FEEDBACK +}>` flex-direction: column; border-top-width: 1px; border-top-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; @@ -24,10 +28,10 @@ const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: border-bottom-width: 1px; border-bottom-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; border-left-color: ${props => { - if (props.feedback === FEEDBACK.POSITIVE) { + if (props.feedback === EXERCISE_FEEDBACK.POSITIVE) { return props.theme.colors.correct } - if (props.feedback === FEEDBACK.NEGATIVE) { + if (props.feedback === EXERCISE_FEEDBACK.NEGATIVE) { return props.theme.colors.incorrect } if (props.pressed) { @@ -36,7 +40,7 @@ const Container = styled(GenericListItemContainer)<{ pressed: boolean; disabled: return props.theme.colors.disabled }}; border-left-radius: 0; - border-left-width: ${props => (props.feedback !== FEEDBACK.NONE ? '4px' : '1px')}; + border-left-width: ${props => (props.feedback !== EXERCISE_FEEDBACK.NONE ? '4px' : '1px')}; background-color: ${prop => { if (prop.disabled) { return prop.theme.colors.disabled @@ -130,7 +134,7 @@ const ListItem = ({ }: ListItemProps): ReactElement => { const [pressInY, setPressInY] = useState(null) const [pressed, setPressed] = useState(false) - const [feedback, setFeedback] = useState(FEEDBACK.NONE) + const [feedback, setFeedback] = useState(EXERCISE_FEEDBACK.NONE) const updatePressed = useCallback((pressed: boolean): void => onPress && setPressed(pressed), [onPress]) const theme = useTheme() diff --git a/src/components/__tests__/FeedbackBadge.spec.tsx b/src/components/__tests__/FeedbackBadge.spec.tsx index 487d42952..97108f4b9 100644 --- a/src/components/__tests__/FeedbackBadge.spec.tsx +++ b/src/components/__tests__/FeedbackBadge.spec.tsx @@ -2,7 +2,7 @@ import { waitFor } from '@testing-library/react-native' import { mocked } from 'jest-mock' import React from 'react' -import { SCORE_THRESHOLD_POSITIVE_FEEDBACK, FEEDBACK } from '../../constants/data' +import { SCORE_THRESHOLD_POSITIVE_FEEDBACK, EXERCISE_FEEDBACK } from '../../constants/data' import { useLoadAsync } from '../../hooks/useLoadAsync' import { getLabels } from '../../services/helpers' import render from '../../testing/render' @@ -74,7 +74,7 @@ describe('FeedbackBadge', () => { await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) await waitFor(() => expect(queryByTestId('positive-badge')).not.toBeNull()) await waitFor(() => expect(queryByText(getLabels().exercises.feedback.positive)).not.toBeNull()) - await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(FEEDBACK.POSITIVE)) + await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(EXERCISE_FEEDBACK.POSITIVE)) }) it('should show negative feedback for scores below threshold', async () => { @@ -89,6 +89,6 @@ describe('FeedbackBadge', () => { await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) await waitFor(() => expect(queryByTestId('negative-badge')).not.toBeNull()) await waitFor(() => expect(queryByText(getLabels().exercises.feedback.negative)).not.toBeNull()) - await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(FEEDBACK.NEGATIVE)) + await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(EXERCISE_FEEDBACK.NEGATIVE)) }) }) diff --git a/src/constants/data.ts b/src/constants/data.ts index bb2a6663c..7672548ca 100644 --- a/src/constants/data.ts +++ b/src/constants/data.ts @@ -155,7 +155,7 @@ export const numberOfMaxRetries = 3 export const SCORE_THRESHOLD_POSITIVE_FEEDBACK = 4 export const SCORE_THRESHOLD_UNLOCK = 2 -export const enum FEEDBACK { +export const enum EXERCISE_FEEDBACK { POSITIVE, NONE, NEGATIVE, From 0aa442c462547e9c16e0b025f31abf80a3f35198 Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 21 Sep 2022 16:45:19 +0200 Subject: [PATCH 08/20] LUN-407: remove Trophy icon and component --- assets/images/index.ts | 2 -- assets/images/trophy-icon.svg | 36 ------------------------ src/components/Trophy.tsx | 29 ------------------- src/components/__tests__/Trophy.spec.tsx | 25 ---------------- 4 files changed, 92 deletions(-) delete mode 100644 assets/images/trophy-icon.svg delete mode 100644 src/components/Trophy.tsx delete mode 100644 src/components/__tests__/Trophy.spec.tsx diff --git a/assets/images/index.ts b/assets/images/index.ts index 5dc559412..33faff798 100644 --- a/assets/images/index.ts +++ b/assets/images/index.ts @@ -64,7 +64,6 @@ import ThumbsDownIcon from './thumbs-down.svg' import ThumbsUpIcon from './thumbs-up.svg' import TrashIconWhite from './trash-bin-icon-white.svg' import TrashIcon from './trash-bin-icon.svg' -import TrophyIcon from './trophy-icon.svg' import VolumeUpCircleIcon from './volume-up-circle-icon.svg' export { @@ -132,7 +131,6 @@ export { StarIconWhite, TrashIcon, TrashIconWhite, - TrophyIcon, VolumeUpCircleIcon, ThumbsUpIcon, ThumbsDownIcon, diff --git a/assets/images/trophy-icon.svg b/assets/images/trophy-icon.svg deleted file mode 100644 index e13fb4b65..000000000 --- a/assets/images/trophy-icon.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/components/Trophy.tsx b/src/components/Trophy.tsx deleted file mode 100644 index 6b6b0b84e..000000000 --- a/src/components/Trophy.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { ReactElement } from 'react' -import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' -import styled from 'styled-components/native' - -import { TrophyIcon } from '../../assets/images' - -const TrophyContainer = styled.View` - margin-top: ${props => props.theme.spacings.xs}; - display: flex; - flex-direction: row; -` - -const TrophyIconStyled = styled(TrophyIcon)` - margin-right: ${props => props.theme.spacings.xxs}; -` - -interface PropsType { - level: number -} - -const Trophy = ({ level }: PropsType): ReactElement => { - const trophies = [] - for (let i = 0; i < level; i += 1) { - trophies.push() - } - return {trophies} -} - -export default Trophy diff --git a/src/components/__tests__/Trophy.spec.tsx b/src/components/__tests__/Trophy.spec.tsx deleted file mode 100644 index 16d5a67ad..000000000 --- a/src/components/__tests__/Trophy.spec.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' - -import renderWithTheme from '../../testing/render' -import Trophy from '../Trophy' - -describe('Trophy', () => { - it('should show no trophy when level is 0', () => { - const { queryByTestId } = renderWithTheme() - expect(queryByTestId('trophy-0')).toBeNull() - }) - - it('should show one trophy when level is 1', () => { - const { queryByTestId } = renderWithTheme() - expect(queryByTestId('trophy-0')).not.toBeNull() - expect(queryByTestId('trophy-1')).toBeNull() - }) - - it('should show three trophies when level is 3', () => { - const { queryByTestId } = renderWithTheme() - expect(queryByTestId('trophy-0')).not.toBeNull() - expect(queryByTestId('trophy-1')).not.toBeNull() - expect(queryByTestId('trophy-2')).not.toBeNull() - expect(queryByTestId('trophy-3')).toBeNull() - }) -}) From bfd1f15ad0f57e05fbe00fbb4303f24bacc401b0 Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 21 Sep 2022 16:50:34 +0200 Subject: [PATCH 09/20] LUN-407: use hp instead of wp --- src/components/FeedbackBadge.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 8409020c8..65befa650 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -1,6 +1,6 @@ import { useIsFocused } from '@react-navigation/native' import React, { ReactElement, useState, useEffect } from 'react' -import { widthPercentageToDP as wp } from 'react-native-responsive-screen' +import { heightPercentageToDP as hp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' @@ -62,7 +62,7 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { if (feedback === EXERCISE_FEEDBACK.POSITIVE) { return ( - + {getLabels().exercises.feedback.positive} ) @@ -71,7 +71,7 @@ const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { if (feedback === EXERCISE_FEEDBACK.NEGATIVE) { return ( - + {getLabels().exercises.feedback.negative} ) From d33b4cb595eb394cd3a66649b489e6dc56fc9303 Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 26 Sep 2022 07:13:19 +0200 Subject: [PATCH 10/20] LUN-407: fix alignment of rightChildren --- src/components/ListItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 735c9e421..93eeaf965 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -55,6 +55,7 @@ const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` flex-direction: row; padding: ${props => `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; + align-items: center; ` const Title = styled.Text<{ pressed: boolean }>` From b6f121d76089fdcd29162feb94601210c5594339 Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 26 Sep 2022 07:15:41 +0200 Subject: [PATCH 11/20] LUN-407: fix alignment of LockingLane icons --- src/routes/exercises/ExercisesScreen.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index f63bbb838..86f92ca9a 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -21,6 +21,7 @@ import LockingLane from './components/LockingLane' const Container = styled.View` display: flex; flex-direction: row; + align-items: center; ` const ListItemResizer = styled.View` From 61d2e99a5d28542e02518c7be5661e0b24b07bd9 Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 26 Sep 2022 11:17:07 +0200 Subject: [PATCH 12/20] LUN-407: move feedback logic to ExerciseScreen --- src/components/FeedbackBadge.tsx | 43 ++---------- src/components/ListItem.tsx | 7 +- .../__tests__/FeedbackBadge.spec.tsx | 70 ++----------------- src/routes/exercises/ExercisesScreen.tsx | 41 +++++++++-- .../__tests__/ExercisesScreen.spec.tsx | 22 +++++- 5 files changed, 70 insertions(+), 113 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index 65befa650..e6d720f97 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -1,12 +1,9 @@ -import { useIsFocused } from '@react-navigation/native' -import React, { ReactElement, useState, useEffect } from 'react' +import React, { ReactElement } from 'react' import { heightPercentageToDP as hp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' -import { EXERCISE_FEEDBACK, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../constants/data' -import { useLoadAsync } from '../hooks/useLoadAsync' -import AsyncStorage from '../services/AsyncStorage' +import { EXERCISE_FEEDBACK } from '../constants/data' import { getLabels } from '../services/helpers' import { ContentSecondaryLight } from './text/Content' @@ -23,42 +20,10 @@ const BadgeText = styled(ContentSecondaryLight)` ` interface FeedbackBadgeProps { - levelIdentifier: { - disciplineId: number - level: number - } | null - setFeedback: (feedback: EXERCISE_FEEDBACK) => void + feedback: EXERCISE_FEEDBACK } -const FeedbackBadge = (props: FeedbackBadgeProps): ReactElement | null => { - const [feedback, setFeedback] = useState(EXERCISE_FEEDBACK.NONE) - const { setFeedback: setFeedbackOnParent, levelIdentifier } = props - const { disciplineId, level } = levelIdentifier ?? {} - const { data: scores, loading, refresh } = useLoadAsync(AsyncStorage.getExerciseProgress, null) - const isFocused = useIsFocused() - - useEffect(() => { - const updateFeedback = (feedback: EXERCISE_FEEDBACK) => { - setFeedback(feedback) - setFeedbackOnParent(feedback) - } - if (!loading && disciplineId != null && level != null && level !== 0) { - /* eslint-disable @typescript-eslint/no-unnecessary-condition */ - const score = scores?.[disciplineId]?.[level] - if (score) { - updateFeedback( - score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? EXERCISE_FEEDBACK.POSITIVE : EXERCISE_FEEDBACK.NEGATIVE - ) - } - } - }, [loading, scores, disciplineId, level, setFeedbackOnParent]) - - useEffect(() => { - if (isFocused) { - refresh() - } - }, [isFocused, refresh]) - +const FeedbackBadge = ({ feedback }: FeedbackBadgeProps): ReactElement | null => { if (feedback === EXERCISE_FEEDBACK.POSITIVE) { return ( diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 93eeaf965..325121b7d 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -117,7 +117,7 @@ interface ListItemProps { hideRightChildren?: boolean arrowDisabled?: boolean disabled?: boolean - levelIdentifier?: { disciplineId: number; level: number } | null + feedback?: EXERCISE_FEEDBACK } const ListItem = ({ @@ -131,11 +131,10 @@ const ListItem = ({ hideRightChildren = false, arrowDisabled = false, disabled = false, - levelIdentifier = null, + feedback = EXERCISE_FEEDBACK.NONE, }: ListItemProps): ReactElement => { const [pressInY, setPressInY] = useState(null) const [pressed, setPressed] = useState(false) - const [feedback, setFeedback] = useState(EXERCISE_FEEDBACK.NONE) const updatePressed = useCallback((pressed: boolean): void => onPress && setPressed(pressed), [onPress]) const theme = useTheme() @@ -195,7 +194,7 @@ const ListItem = ({ delayLongPress={200} feedback={feedback} testID='list-item'> - + {iconToRender} diff --git a/src/components/__tests__/FeedbackBadge.spec.tsx b/src/components/__tests__/FeedbackBadge.spec.tsx index 97108f4b9..e978ad025 100644 --- a/src/components/__tests__/FeedbackBadge.spec.tsx +++ b/src/components/__tests__/FeedbackBadge.spec.tsx @@ -1,94 +1,38 @@ import { waitFor } from '@testing-library/react-native' -import { mocked } from 'jest-mock' import React from 'react' import { SCORE_THRESHOLD_POSITIVE_FEEDBACK, EXERCISE_FEEDBACK } from '../../constants/data' -import { useLoadAsync } from '../../hooks/useLoadAsync' import { getLabels } from '../../services/helpers' import render from '../../testing/render' import FeedbackBadge from '../FeedbackBadge' -jest.mock('@react-navigation/native') -jest.mock('../../hooks/useLoadAsync', () => ({ - useLoadAsync: jest.fn(), -})) - -const mockSetFeedback = jest.fn() - describe('FeedbackBadge', () => { beforeEach(() => { jest.clearAllMocks() }) - const renderFeedbackBadge = (levelIdentifier: { disciplineId: number; level: number } | null) => - render() - - it('should not show when no level info is provided', async () => { - mocked(useLoadAsync).mockImplementation(() => ({ - data: {}, - error: null, - loading: false, - refresh: () => null, - })) - const { queryByTestId } = renderFeedbackBadge(null) - - await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) - await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) - }) - - it('should not show when level is not done', async () => { - mocked(useLoadAsync).mockImplementation(() => ({ - data: { '0': { '1': undefined } }, - error: null, - loading: false, - refresh: () => null, - })) - const { queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) - - await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) - await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) - }) + const renderFeedbackBadge = (feedback: EXERCISE_FEEDBACK) => render() - it('should not show for wordlist level', async () => { - mocked(useLoadAsync).mockImplementation(() => ({ - data: { '0': { '0': SCORE_THRESHOLD_POSITIVE_FEEDBACK + 1 } }, - error: null, - loading: false, - refresh: () => null, - })) - const { queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 0 }) + it('should not show badge for no feedback', async () => { + const { queryByTestId } = renderFeedbackBadge(EXERCISE_FEEDBACK.NONE) await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) }) - it('should show positive feedback for scores above threshold', async () => { - mocked(useLoadAsync).mockImplementation(() => ({ - data: { '0': { '1': SCORE_THRESHOLD_POSITIVE_FEEDBACK + 1 } }, - error: null, - loading: false, - refresh: () => null, - })) - const { queryByText, queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) + it('should show badge for positive feedback', async () => { + const { queryByText, queryByTestId } = renderFeedbackBadge(EXERCISE_FEEDBACK.POSITIVE) await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) await waitFor(() => expect(queryByTestId('positive-badge')).not.toBeNull()) await waitFor(() => expect(queryByText(getLabels().exercises.feedback.positive)).not.toBeNull()) - await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(EXERCISE_FEEDBACK.POSITIVE)) }) - it('should show negative feedback for scores below threshold', async () => { - mocked(useLoadAsync).mockImplementation(() => ({ - data: { '0': { '1': SCORE_THRESHOLD_POSITIVE_FEEDBACK - 1 } }, - error: null, - loading: false, - refresh: () => null, - })) - const { queryByText, queryByTestId } = renderFeedbackBadge({ disciplineId: 0, level: 1 }) + it('should show badge for negative feedback', async () => { + const { queryByText, queryByTestId } = renderFeedbackBadge(EXERCISE_FEEDBACK.NEGATIVE) await waitFor(() => expect(queryByTestId('positive-badge')).toBeNull()) await waitFor(() => expect(queryByTestId('negative-badge')).not.toBeNull()) await waitFor(() => expect(queryByText(getLabels().exercises.feedback.negative)).not.toBeNull()) - await waitFor(() => expect(mockSetFeedback).toHaveBeenCalledWith(EXERCISE_FEEDBACK.NEGATIVE)) }) }) diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index 86f92ca9a..f0a6cc7c8 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -1,6 +1,6 @@ -import { CommonActions, RouteProp, useFocusEffect } from '@react-navigation/native' +import { CommonActions, RouteProp, useIsFocused, useFocusEffect } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { FlatList } from 'react-native' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' @@ -11,9 +11,11 @@ import RouteWrapper from '../../components/RouteWrapper' import ServerResponseHandler from '../../components/ServerResponseHandler' import Title from '../../components/Title' import { ContentTextBold, ContentTextLight } from '../../components/text/Content' -import { Exercise, EXERCISES } from '../../constants/data' +import { Exercise, EXERCISES, SCORE_THRESHOLD_POSITIVE_FEEDBACK, EXERCISE_FEEDBACK } from '../../constants/data' +import { useLoadAsync } from '../../hooks/useLoadAsync' import useLoadDocuments from '../../hooks/useLoadDocuments' import { RoutesParams } from '../../navigation/NavigationTypes' +import AsyncStorage from '../../services/AsyncStorage' import { getLabels, getDoneExercises, wordsDescription } from '../../services/helpers' import { reportError } from '../../services/sentry' import LockingLane from './components/LockingLane' @@ -42,7 +44,14 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme const { discipline, disciplineTitle, disciplineId } = route.params const [isModalVisible, setIsModalVisible] = useState(false) const [nextExercise, setNextExercise] = useState(EXERCISES[0]) - + const [feedback, setFeedback] = useState([]) + const [isFeedbackSet, setIsFeedbackSet] = useState(false) + const { + data: scores, + loading: loadingFeedback, + refresh: refreshFeedback, + } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + const isFocused = useIsFocused() const { data: documents, error, @@ -53,6 +62,21 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme apiKey: discipline.apiKey, }) + useEffect(() => { + if (!loadingFeedback && !isFeedbackSet) { + const exerciseScores = scores?.[disciplineId] ?? {} + const updatedFeedback: EXERCISE_FEEDBACK[] = Object.values(exerciseScores).map(score => { + if (!score) { + return EXERCISE_FEEDBACK.NONE + } + return score > SCORE_THRESHOLD_POSITIVE_FEEDBACK ? EXERCISE_FEEDBACK.POSITIVE : EXERCISE_FEEDBACK.NEGATIVE + }) + updatedFeedback[0] = EXERCISE_FEEDBACK.NONE + setFeedback(updatedFeedback) + setIsFeedbackSet(true) + } + }, [loadingFeedback, isFeedbackSet, disciplineId, scores]) + useFocusEffect( useCallback(() => { getDoneExercises(disciplineId) @@ -61,6 +85,13 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme }, [disciplineId]) ) + useEffect(() => { + if (isFocused) { + refreshFeedback() + setIsFeedbackSet(false) + } + }, [isFocused, refreshFeedback]) + const handleNavigation = (item: Exercise): void => { if (nextExercise && item.level > nextExercise.level) { setIsModalVisible(true) @@ -91,7 +122,7 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme description={item.description} onPress={() => handleNavigation(item)} arrowDisabled={nextExercise === null || item.level > nextExercise.level} - levelIdentifier={{ disciplineId, level: item.level }} + feedback={feedback[item.level] ?? EXERCISE_FEEDBACK.NONE} /> diff --git a/src/routes/exercises/__tests__/ExercisesScreen.spec.tsx b/src/routes/exercises/__tests__/ExercisesScreen.spec.tsx index 68d076575..5056a64af 100644 --- a/src/routes/exercises/__tests__/ExercisesScreen.spec.tsx +++ b/src/routes/exercises/__tests__/ExercisesScreen.spec.tsx @@ -1,16 +1,17 @@ import RNAsyncStorage from '@react-native-async-storage/async-storage' import { RouteProp } from '@react-navigation/native' -import { fireEvent } from '@testing-library/react-native' +import { fireEvent, waitFor } from '@testing-library/react-native' import { mocked } from 'jest-mock' import React from 'react' -import { EXERCISES } from '../../../constants/data' +import { EXERCISES, SCORE_THRESHOLD_POSITIVE_FEEDBACK } from '../../../constants/data' import useLoadDocuments from '../../../hooks/useLoadDocuments' import { RoutesParams } from '../../../navigation/NavigationTypes' import DocumentBuilder from '../../../testing/DocumentBuilder' import createNavigationMock from '../../../testing/createNavigationPropMock' import { getReturnOf } from '../../../testing/helper' import { mockDisciplines } from '../../../testing/mockDiscipline' +import { mockUseLoadAsyncWithData } from '../../../testing/mockUseLoadFromEndpoint' import render from '../../../testing/render' import ExercisesScreen from '../ExercisesScreen' @@ -37,6 +38,13 @@ describe('ExercisesScreen', () => { }, } + mockUseLoadAsyncWithData({ + [route.params.disciplineId]: { + '0': SCORE_THRESHOLD_POSITIVE_FEEDBACK - 1, + '1': SCORE_THRESHOLD_POSITIVE_FEEDBACK + 1, + }, + }) + it('should render correctly', () => { const { getAllByText } = render() EXERCISES.forEach(exercise => { @@ -65,4 +73,14 @@ describe('ExercisesScreen', () => { documents, }) }) + + it('should show feedback badge for done levels', async () => { + const { queryByTestId } = render() + await waitFor(() => expect(queryByTestId('positive-badge')).not.toBeNull()) + }) + + it('should not show feedback badge for wordlist level', async () => { + const { queryByTestId } = render() + await waitFor(() => expect(queryByTestId('negative-badge')).toBeNull()) + }) }) From 18aac959411d649b036fde6e790d9159517fed57 Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 26 Sep 2022 11:32:24 +0200 Subject: [PATCH 13/20] LUN-407: remove obsolete mocks --- src/components/__tests__/VocabularyList.spec.tsx | 1 - src/components/__tests__/VocabularyListItem.spec.tsx | 1 - src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx | 1 - src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx | 1 - .../manage-selections/components/__tests__/SeletionItem.spec.tsx | 1 - 5 files changed, 5 deletions(-) diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx index ade9545f0..696faeed7 100644 --- a/src/components/__tests__/VocabularyList.spec.tsx +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -7,7 +7,6 @@ import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' import render from '../../testing/render' import VocabularyList from '../VocabularyList' -jest.mock('@react-navigation/native') jest.mock('../FavoriteButton', () => () => { const { Text } = require('react-native') return FavoriteButton diff --git a/src/components/__tests__/VocabularyListItem.spec.tsx b/src/components/__tests__/VocabularyListItem.spec.tsx index 1ebcb643a..d61aea728 100644 --- a/src/components/__tests__/VocabularyListItem.spec.tsx +++ b/src/components/__tests__/VocabularyListItem.spec.tsx @@ -7,7 +7,6 @@ import { Document } from '../../constants/endpoints' import render from '../../testing/render' import VocabularyListItem from '../VocabularyListItem' -jest.mock('@react-navigation/native') jest.mock('../FavoriteButton', () => () => { const { Text } = require('react-native') return FavoriteButton diff --git a/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx b/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx index 11a2bd6f2..6e2ca6417 100644 --- a/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx +++ b/src/routes/__tests__/UserVocabularyOverviewScreen.spec.tsx @@ -5,7 +5,6 @@ import createNavigationMock from '../../testing/createNavigationPropMock' import render from '../../testing/render' import UserVocabularyOverviewScreen from '../UserVocabularyOverviewScreen' -jest.mock('@react-navigation/native') describe('UserVocabularyOverviewScreen', () => { const navigation = createNavigationMock<'UserVocabularyOverview'>() it('should show content', () => { diff --git a/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx b/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx index b46e367cb..e09aa3957 100644 --- a/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx +++ b/src/routes/dictionary/__tests__/DictionaryScreen.spec.tsx @@ -10,7 +10,6 @@ import { getReturnOf } from '../../../testing/helper' import render from '../../../testing/render' import DictionaryScreen from '../DictionaryScreen' -jest.mock('@react-navigation/native') jest.mock('../../../components/FavoriteButton', () => { const Text = require('react-native').Text return () => FavoriteButton diff --git a/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx b/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx index f242d199a..6265ec5f6 100644 --- a/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx +++ b/src/routes/manage-selections/components/__tests__/SeletionItem.spec.tsx @@ -12,7 +12,6 @@ import { import render from '../../../../testing/render' import SelectionItem from '../SelectionItem' -jest.mock('@react-navigation/native') describe('SelectionItem', () => { const deleteItem = jest.fn() const renderSelectionItem = (): RenderAPI => From f9ce1602a9e2774d62e8b69bd8e13640c947db1a Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 26 Sep 2022 11:37:57 +0200 Subject: [PATCH 14/20] LUN-407: remove obsolete mocks --- .../__tests__/UserVocabularyListScreen.spec.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx b/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx index 164eaefe8..03e5298e6 100644 --- a/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx +++ b/src/routes/user-vocabulary-list/__tests__/UserVocabularyListScreen.spec.tsx @@ -11,7 +11,6 @@ import { getReturnOf } from '../../../testing/helper' import render from '../../../testing/render' import UserVocabularyListScreen from '../UserVocabularyListScreen' -jest.mock('@react-navigation/native') jest.mock('../../../hooks/useReadUserVocabulary') jest.mock('../../../components/FavoriteButton', () => () => { From d47b38339198b4f6f44f28be873c1fd748cd51b4 Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 27 Sep 2022 07:01:58 +0200 Subject: [PATCH 15/20] LUN-407: use destructuring for labels --- src/components/FeedbackBadge.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index e6d720f97..f85d521c3 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -24,11 +24,12 @@ interface FeedbackBadgeProps { } const FeedbackBadge = ({ feedback }: FeedbackBadgeProps): ReactElement | null => { + const {positive, negative} = {...getLabels().exercises.feedback} if (feedback === EXERCISE_FEEDBACK.POSITIVE) { return ( - {getLabels().exercises.feedback.positive} + {positive} ) } @@ -37,7 +38,7 @@ const FeedbackBadge = ({ feedback }: FeedbackBadgeProps): ReactElement | null => return ( - {getLabels().exercises.feedback.negative} + {negative} ) } From 44b71453d072260ecb9ffcfe67fc4bb8fff8f3f9 Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 27 Sep 2022 07:52:43 +0200 Subject: [PATCH 16/20] LUN-407: fix styling --- src/components/FeedbackBadge.tsx | 20 +++++++++++++++----- src/components/ListItem.tsx | 8 +++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/FeedbackBadge.tsx b/src/components/FeedbackBadge.tsx index f85d521c3..518137e4c 100644 --- a/src/components/FeedbackBadge.tsx +++ b/src/components/FeedbackBadge.tsx @@ -1,5 +1,5 @@ import React, { ReactElement } from 'react' -import { heightPercentageToDP as hp } from 'react-native-responsive-screen' +import { heightPercentageToDP as hp, widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { ThumbsDownIcon, ThumbsUpIcon } from '../../assets/images' @@ -10,13 +10,19 @@ import { ContentSecondaryLight } from './text/Content' const BadgeContainer = styled.View` display: flex; flex-flow: row nowrap; - justify-content: center; + padding: ${props => props.theme.spacings.xxs} ${props => props.theme.spacings.sm}; background-color: ${props => props.theme.colors.lightGreyBackground}; width: 100%; ` const BadgeText = styled(ContentSecondaryLight)` font-style: italic; margin-left: ${props => props.theme.spacings.xs}; + align-self: center; +` + +const BadgeIcon = styled.View` + height: ${hp('3.5%')}px; + max-height: ${wp('5%')}px; ` interface FeedbackBadgeProps { @@ -24,11 +30,13 @@ interface FeedbackBadgeProps { } const FeedbackBadge = ({ feedback }: FeedbackBadgeProps): ReactElement | null => { - const {positive, negative} = {...getLabels().exercises.feedback} + const { positive, negative } = { ...getLabels().exercises.feedback } if (feedback === EXERCISE_FEEDBACK.POSITIVE) { return ( - + + + {positive} ) @@ -37,7 +45,9 @@ const FeedbackBadge = ({ feedback }: FeedbackBadgeProps): ReactElement | null => if (feedback === EXERCISE_FEEDBACK.NEGATIVE) { return ( - + + + {negative} ) diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 325121b7d..744b38e81 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -49,12 +49,14 @@ const Container = styled(GenericListItemContainer)<{ }}; ` -const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` +const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean; feedback: EXERCISE_FEEDBACK }>` min-height: ${hp('12%')}px; display: flex; flex-direction: row; padding: ${props => - `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; + props.feedback !== EXERCISE_FEEDBACK.NONE + ? `${props.theme.spacings.xxs} ${props.theme.spacings.xs} ${props.theme.spacings.xxs} ${props.theme.spacings.sm}` + : `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; align-items: center; ` @@ -195,7 +197,7 @@ const ListItem = ({ feedback={feedback} testID='list-item'> - + {iconToRender} {titleToRender} From 4e4bc101be32fc920e8e91a8690f2ec431cbe1e6 Mon Sep 17 00:00:00 2001 From: charludo Date: Tue, 27 Sep 2022 09:52:58 +0200 Subject: [PATCH 17/20] LUN-432: change console log linting rule to error --- .eslintrc.js | 1 + src/components/AudioPlayer.tsx | 2 ++ .../add-custom-discipline/components/NotAuthorisedView.tsx | 3 ++- .../add-custom-discipline/components/QRCodeReaderOverlay.tsx | 3 ++- src/services/AsyncStorage.ts | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f45636d57..4c79c9337 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,7 @@ module.exports = { ], 'prefer-destructuring': ['error', { array: false }], 'prefer-object-spread': 'error', + 'no-console': 'error', 'react/function-component-definition': [ 'error', diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx index 120cedbe2..99f987c4a 100644 --- a/src/components/AudioPlayer.tsx +++ b/src/components/AudioPlayer.tsx @@ -54,8 +54,10 @@ const AudioPlayer = ({ document, disabled, submittedAlternative }: AudioPlayerPr } }) .catch(async (error: TtsError) => { + /* eslint-disable-next-line no-console */ console.error(`Tts-Error: ${error.code}`) if (error.code === 'no_engine') { + /* eslint-disable-next-line no-console */ await Tts.requestInstallEngine().catch(e => console.error('Failed to install tts engine: ', e)) } }) diff --git a/src/routes/add-custom-discipline/components/NotAuthorisedView.tsx b/src/routes/add-custom-discipline/components/NotAuthorisedView.tsx index 33e3d9ce6..1d1047a84 100644 --- a/src/routes/add-custom-discipline/components/NotAuthorisedView.tsx +++ b/src/routes/add-custom-discipline/components/NotAuthorisedView.tsx @@ -6,6 +6,7 @@ import Button from '../../../components/Button' import { ContentSecondary } from '../../../components/text/Content' import { BUTTONS_THEME } from '../../../constants/data' import { getLabels } from '../../../services/helpers' +import { reportError } from '../../../services/sentry' const Container = styled.View` display: flex; @@ -25,7 +26,7 @@ interface Props { const NotAuthorisedView = ({ setVisible }: Props): ReactElement => { const openSettings = () => { - Linking.openSettings().catch(() => console.error('Unable to open Settings')) + Linking.openSettings().catch(reportError) } return ( diff --git a/src/routes/add-custom-discipline/components/QRCodeReaderOverlay.tsx b/src/routes/add-custom-discipline/components/QRCodeReaderOverlay.tsx index bbf3dd9a9..85952eb2f 100644 --- a/src/routes/add-custom-discipline/components/QRCodeReaderOverlay.tsx +++ b/src/routes/add-custom-discipline/components/QRCodeReaderOverlay.tsx @@ -6,6 +6,7 @@ import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { CloseCircleIconBlue, CloseCircleIconWhite } from '../../../../assets/images' +import { reportError } from '../../../services/sentry' import NotAuthorisedView from './NotAuthorisedView' const Container = styled.SafeAreaView` @@ -47,7 +48,7 @@ const AddCustomDisciplineScreen = ({ setVisible, setCode }: Props): ReactElement if (!permissionRequested) { request(Platform.OS === 'ios' ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA) .then(result => setPermissionGranted(result === RESULTS.GRANTED)) - .catch(e => console.error(e)) + .catch(reportError) .finally(() => setPermissionRequested(true)) } }, [permissionRequested]) diff --git a/src/services/AsyncStorage.ts b/src/services/AsyncStorage.ts index 3b91ceacb..a8d133bdb 100644 --- a/src/services/AsyncStorage.ts +++ b/src/services/AsyncStorage.ts @@ -172,7 +172,6 @@ const editUserDocument = async (oldUserDocument: Document, newUserDocument: Docu } const deleteUserDocument = async (userDocument: Document): Promise => { - console.log('hi') const userVocabulary = getUserVocabulary().then(vocab => vocab.filter(item => JSON.stringify(item) !== JSON.stringify(userDocument)) ) From f856b652a0da67f58907660ff0bea872dd8597c6 Mon Sep 17 00:00:00 2001 From: charludo Date: Wed, 28 Sep 2022 12:58:09 +0200 Subject: [PATCH 18/20] LUN-407: implement suggested styling improvements --- src/components/ListItem.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 744b38e81..d47a6d5ad 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -20,6 +20,8 @@ const Container = styled(GenericListItemContainer)<{ disabled: boolean feedback: EXERCISE_FEEDBACK }>` + min-height: ${hp('12%')}px; + justify-content: center; flex-direction: column; border-top-width: 1px; border-top-color: ${prop => (prop.pressed ? prop.theme.colors.primary : prop.theme.colors.disabled)}; @@ -49,14 +51,11 @@ const Container = styled(GenericListItemContainer)<{ }}; ` -const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean; feedback: EXERCISE_FEEDBACK }>` - min-height: ${hp('12%')}px; +const ContentContainer = styled.View<{ pressed: boolean; disabled: boolean }>` display: flex; flex-direction: row; padding: ${props => - props.feedback !== EXERCISE_FEEDBACK.NONE - ? `${props.theme.spacings.xxs} ${props.theme.spacings.xs} ${props.theme.spacings.xxs} ${props.theme.spacings.sm}` - : `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; + `${props.theme.spacings.sm} ${props.theme.spacings.xs} ${props.theme.spacings.sm} ${props.theme.spacings.sm}`}; align-items: center; ` @@ -197,7 +196,7 @@ const ListItem = ({ feedback={feedback} testID='list-item'> - + {iconToRender} {titleToRender} From fae3d43b595b324e9876a4a7552f85603caf73e2 Mon Sep 17 00:00:00 2001 From: charludo Date: Sun, 2 Oct 2022 10:04:18 +0200 Subject: [PATCH 19/20] LUN-432: fix linting error from merge --- src/services/AsyncStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/AsyncStorage.ts b/src/services/AsyncStorage.ts index 572571a60..7dfb3ba73 100644 --- a/src/services/AsyncStorage.ts +++ b/src/services/AsyncStorage.ts @@ -175,7 +175,7 @@ export const editUserDocument = async (oldUserDocument: Document, newUserDocumen return true } -const deleteUserDocument = async (userDocument: Document): Promise => { +export const deleteUserDocument = async (userDocument: Document): Promise => { const userVocabulary = getUserVocabulary().then(vocab => vocab.filter(item => JSON.stringify(item) !== JSON.stringify(userDocument)) ) From 152711e90d7aaf6aeb4afb1714a490fa2275e077 Mon Sep 17 00:00:00 2001 From: charludo Date: Sun, 2 Oct 2022 10:09:55 +0200 Subject: [PATCH 20/20] LUN-407: fix importing error from merge --- src/routes/exercises/ExercisesScreen.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/routes/exercises/ExercisesScreen.tsx b/src/routes/exercises/ExercisesScreen.tsx index f0a6cc7c8..9f5a0f09b 100644 --- a/src/routes/exercises/ExercisesScreen.tsx +++ b/src/routes/exercises/ExercisesScreen.tsx @@ -15,7 +15,7 @@ import { Exercise, EXERCISES, SCORE_THRESHOLD_POSITIVE_FEEDBACK, EXERCISE_FEEDBA import { useLoadAsync } from '../../hooks/useLoadAsync' import useLoadDocuments from '../../hooks/useLoadDocuments' import { RoutesParams } from '../../navigation/NavigationTypes' -import AsyncStorage from '../../services/AsyncStorage' +import { getExerciseProgress } from '../../services/AsyncStorage' import { getLabels, getDoneExercises, wordsDescription } from '../../services/helpers' import { reportError } from '../../services/sentry' import LockingLane from './components/LockingLane' @@ -46,11 +46,7 @@ const ExercisesScreen = ({ route, navigation }: ExercisesScreenProps): JSX.Eleme const [nextExercise, setNextExercise] = useState(EXERCISES[0]) const [feedback, setFeedback] = useState([]) const [isFeedbackSet, setIsFeedbackSet] = useState(false) - const { - data: scores, - loading: loadingFeedback, - refresh: refreshFeedback, - } = useLoadAsync(AsyncStorage.getExerciseProgress, null) + const { data: scores, loading: loadingFeedback, refresh: refreshFeedback } = useLoadAsync(getExerciseProgress, null) const isFocused = useIsFocused() const { data: documents,