From 30ab80e6b8d37a0a3680c2e56fa9533beb1e670e Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 15:00:15 +0200 Subject: [PATCH 01/17] LUN-132: Add assets and fix names --- assets/images/index.ts | 14 ++++++++++---- assets/images/star-circle-icon-grey-filled.svg | 6 ++++++ assets/images/star-circle-icon-grey.svg | 6 ++++++ assets/images/star-icon-grey-filled.svg | 3 +++ src/navigation/BottomTabNavigator.tsx | 4 ++-- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 assets/images/star-circle-icon-grey-filled.svg create mode 100644 assets/images/star-circle-icon-grey.svg create mode 100644 assets/images/star-icon-grey-filled.svg diff --git a/assets/images/index.ts b/assets/images/index.ts index c34ae8570..fecdfa7d2 100644 --- a/assets/images/index.ts +++ b/assets/images/index.ts @@ -44,8 +44,11 @@ import OpenLockIcon from './open-lock-icon.svg' import QRCodeIcon from './qr-code-icon.svg' import RepeatIcon from './repeat-icon.svg' import ShareIcon from './share-icon.svg' -import StartIconGrey from './star-icon-grey.svg' -import StartIconWhite from './star-icon-white.svg' +import StarCircleIconGreyFilled from './star-circle-icon-grey-filled.svg' +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 TrashIcon from './trash-bin-icon.svg' import TrophyIcon from './trophy-icon.svg' import VolumeUpCircleIcon from './volume-up-circle-icon.svg' @@ -97,8 +100,11 @@ export { QRCodeIcon, RepeatIcon, ShareIcon, - StartIconGrey, - StartIconWhite, + StarCircleIconGrey, + StarCircleIconGreyFilled, + StarIconGrey, + StarIconGreyFilled, + StarIconWhite, TrashIcon, TrophyIcon, VolumeUpCircleIcon diff --git a/assets/images/star-circle-icon-grey-filled.svg b/assets/images/star-circle-icon-grey-filled.svg new file mode 100644 index 000000000..814d1651d --- /dev/null +++ b/assets/images/star-circle-icon-grey-filled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/star-circle-icon-grey.svg b/assets/images/star-circle-icon-grey.svg new file mode 100644 index 000000000..ed2857cae --- /dev/null +++ b/assets/images/star-circle-icon-grey.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/star-icon-grey-filled.svg b/assets/images/star-icon-grey-filled.svg new file mode 100644 index 000000000..ca8713c5f --- /dev/null +++ b/assets/images/star-icon-grey-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/navigation/BottomTabNavigator.tsx b/src/navigation/BottomTabNavigator.tsx index 8311dc2ab..f993151fd 100644 --- a/src/navigation/BottomTabNavigator.tsx +++ b/src/navigation/BottomTabNavigator.tsx @@ -49,9 +49,9 @@ const BottomTabNavigator = (): JSX.Element | null => { {/* options={{ */} {/* tabBarIcon: ({ focused }) => */} {/* focused ? ( */} - {/* */} + {/* */} {/* ) : ( */} - {/* */} + {/* */} {/* ), */} {/* title: labels.general.favorites */} {/* }} */} From 230edbe7a24a7cccc4e480c5bfc0d027e3e70b9f Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 15:09:27 +0200 Subject: [PATCH 02/17] LUN-132: Add async storage methods and refactor usages --- .../ManageSelectionsScreen.tsx | 6 +- src/services/AsyncStorage.ts | 65 +++++-- src/services/__tests__/AsyncStorage.spec.ts | 178 ++++++++++-------- 3 files changed, 155 insertions(+), 94 deletions(-) diff --git a/src/routes/manage-selections/ManageSelectionsScreen.tsx b/src/routes/manage-selections/ManageSelectionsScreen.tsx index 905db52c2..0a6fd02b0 100644 --- a/src/routes/manage-selections/ManageSelectionsScreen.tsx +++ b/src/routes/manage-selections/ManageSelectionsScreen.tsx @@ -11,7 +11,7 @@ import labels from '../../constants/labels.json' import useReadCustomDisciplines from '../../hooks/useReadCustomDisciplines' import useReadSelectedProfessions from '../../hooks/useReadSelectedProfessions' import { RoutesParams } from '../../navigation/NavigationTypes' -import { removeCustomDiscipline, removeSelectedProfession } from '../../services/AsyncStorage' +import AsyncStorage from '../../services/AsyncStorage' import { reportError } from '../../services/sentry' import SelectionItem from './components/SelectionItem' @@ -42,14 +42,14 @@ const ManageSelectionsScreen = ({ navigation }: Props): ReactElement => { const professionItems = selectedProfessions?.map(id => { const unselectProfessionAndRefresh = () => { - removeSelectedProfession(id).then(refreshSelectedProfessions).catch(reportError) + AsyncStorage.removeSelectedProfession(id).then(refreshSelectedProfessions).catch(reportError) } return }) const customDisciplineItems = customDisciplines?.map(apiKey => { const deleteCustomDisciplineAndRefresh = () => { - removeCustomDiscipline(apiKey).then(refreshCustomDisciplines).catch(reportError) + AsyncStorage.removeCustomDiscipline(apiKey).then(refreshCustomDisciplines).catch(reportError) } return }) diff --git a/src/services/AsyncStorage.ts b/src/services/AsyncStorage.ts index b57d325a4..074178880 100644 --- a/src/services/AsyncStorage.ts +++ b/src/services/AsyncStorage.ts @@ -1,21 +1,25 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { ExerciseKey, Progress } from '../constants/data' +import { Document } from '../constants/endpoints' import { DocumentResult } from '../navigation/NavigationTypes' -const progressKey = 'progress' +const SELECTED_PROFESSIONS_KEY = 'selectedProfessions' +const CUSTOM_DISCIPLINES_KEY = 'customDisciplines' +const FAVORITES_KEY = 'favorites' +const PROGRESS_KEY = 'progress' // return value of null means the selected profession was never set before, therefore the intro screen must be shown -export const getSelectedProfessions = async (): Promise => { - const professions = await AsyncStorage.getItem('selectedProfessions') +const getSelectedProfessions = async (): Promise => { + const professions = await AsyncStorage.getItem(SELECTED_PROFESSIONS_KEY) return professions ? JSON.parse(professions) : null } -export const setSelectedProfessions = async (selectedProfessions: number[]): Promise => { - await AsyncStorage.setItem('selectedProfessions', JSON.stringify(selectedProfessions)) +const setSelectedProfessions = async (selectedProfessions: number[]): Promise => { + await AsyncStorage.setItem(SELECTED_PROFESSIONS_KEY, JSON.stringify(selectedProfessions)) } -export const pushSelectedProfession = async (professionId: number): Promise => { +const pushSelectedProfession = async (professionId: number): Promise => { let professions = await getSelectedProfessions() if (professions === null) { professions = [professionId] @@ -26,7 +30,7 @@ export const pushSelectedProfession = async (professionId: number): Promise => { +const removeSelectedProfession = async (professionId: number): Promise => { const professions = await getSelectedProfessions() if (professions === null) { throw new Error('professions not set') @@ -36,16 +40,16 @@ export const removeSelectedProfession = async (professionId: number): Promise => { - const disciplines = await AsyncStorage.getItem('customDisciplines') +const getCustomDisciplines = async (): Promise => { + const disciplines = await AsyncStorage.getItem(CUSTOM_DISCIPLINES_KEY) return disciplines ? JSON.parse(disciplines) : [] } -export const setCustomDisciplines = async (customDisciplines: string[]): Promise => { - await AsyncStorage.setItem('customDisciplines', JSON.stringify(customDisciplines)) +const setCustomDisciplines = async (customDisciplines: string[]): Promise => { + await AsyncStorage.setItem(CUSTOM_DISCIPLINES_KEY, JSON.stringify(customDisciplines)) } -export const removeCustomDiscipline = async (customDiscipline: string): Promise => { +const removeCustomDiscipline = async (customDiscipline: string): Promise => { const disciplines = await getCustomDisciplines() const index = disciplines.indexOf(customDiscipline) if (index === -1) { @@ -56,7 +60,7 @@ export const removeCustomDiscipline = async (customDiscipline: string): Promise< } export const getExerciseProgress = async (): Promise => { - const progress = await AsyncStorage.getItem(progressKey) + const progress = await AsyncStorage.getItem(PROGRESS_KEY) return progress ? JSON.parse(progress) : {} } @@ -64,7 +68,7 @@ const setExerciseProgress = async (disciplineId: number, exerciseKey: ExerciseKe const savedProgress = await getExerciseProgress() const newScore = Math.max(savedProgress[disciplineId]?.[exerciseKey] ?? score, score) savedProgress[disciplineId] = { ...(savedProgress[disciplineId] ?? {}), [exerciseKey]: newScore } - await AsyncStorage.setItem(progressKey, JSON.stringify(savedProgress)) + await AsyncStorage.setItem(PROGRESS_KEY, JSON.stringify(savedProgress)) } export const saveExerciseProgress = async ( @@ -76,6 +80,32 @@ export const saveExerciseProgress = async ( await setExerciseProgress(disciplineId, exerciseKey, score) } +const getFavorites = async (): Promise => { + const documents = await AsyncStorage.getItem(FAVORITES_KEY) + return documents ? JSON.parse(documents) : [] +} + +const setFavorites = async (favorites: Document[]): Promise => { + await AsyncStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites)) +} + +const addFavorite = async (favorite: Document): Promise => { + const favorites = await getFavorites() + const newFavorites = [...favorites, favorite] + await setFavorites(newFavorites) +} + +const removeFavorite = async (favorite: Document): Promise => { + const favorites = await getFavorites() + const newFavorites = favorites.filter(it => it.id !== favorite.id) + await setFavorites(newFavorites) +} + +const isFavorite = async (favorite: Document): Promise => { + const favorites = await getFavorites() + return favorites.some(it => it.id === favorite.id) +} + export default { getCustomDisciplines, setCustomDisciplines, @@ -86,5 +116,10 @@ export default { removeSelectedProfession, saveExerciseProgress, setExerciseProgress, - getExerciseProgress + getExerciseProgress, + getFavorites, + setFavorites, + addFavorite, + removeFavorite, + isFavorite } diff --git a/src/services/__tests__/AsyncStorage.spec.ts b/src/services/__tests__/AsyncStorage.spec.ts index 0a254c0da..9a6105f0f 100644 --- a/src/services/__tests__/AsyncStorage.spec.ts +++ b/src/services/__tests__/AsyncStorage.spec.ts @@ -7,97 +7,123 @@ import { mockDisciplines } from '../../testing/mockDiscipline' import AsyncStorage, { saveExerciseProgress } from '../AsyncStorage' describe('AsyncStorage', () => { - const customDisciplines = ['first', 'second', 'third'] + describe('customDisciplines', () => { + const customDisciplines = ['first', 'second', 'third'] - it('should delete customDisicpline from array if exists', async () => { - await AsyncStorage.setCustomDisciplines(customDisciplines) - await expect(AsyncStorage.getCustomDisciplines()).resolves.toHaveLength(3) - await AsyncStorage.removeCustomDiscipline('first') - await expect(AsyncStorage.getCustomDisciplines()).resolves.toStrictEqual(['second', 'third']) - }) - - it('should not delete customDisicpline from array if not exists', async () => { - await AsyncStorage.setCustomDisciplines(customDisciplines) - await expect(AsyncStorage.getCustomDisciplines()).resolves.toHaveLength(3) - await expect(AsyncStorage.removeCustomDiscipline('fourth')).rejects.toThrow('customDiscipline not available') - await expect(AsyncStorage.getCustomDisciplines()).resolves.toStrictEqual(['first', 'second', 'third']) - }) - - const selectedProfessions = mockDisciplines() - - it('should delete selectedProfession from array if exists', async () => { - await AsyncStorage.setSelectedProfessions(selectedProfessions.map(item => item.id)) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(selectedProfessions.length) - await AsyncStorage.removeSelectedProfession(mockDisciplines()[0].id) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(selectedProfessions.length - 1) - }) + it('should delete customDisicpline from array if exists', async () => { + await AsyncStorage.setCustomDisciplines(customDisciplines) + await expect(AsyncStorage.getCustomDisciplines()).resolves.toHaveLength(3) + await AsyncStorage.removeCustomDiscipline('first') + await expect(AsyncStorage.getCustomDisciplines()).resolves.toStrictEqual(['second', 'third']) + }) - it('should not delete selectedProfession from array if not exists', async () => { - await AsyncStorage.setSelectedProfessions([mockDisciplines()[1].id]) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) - await AsyncStorage.removeSelectedProfession(mockDisciplines()[0].id) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) + it('should not delete customDiscipline from array if not exists', async () => { + await AsyncStorage.setCustomDisciplines(customDisciplines) + await expect(AsyncStorage.getCustomDisciplines()).resolves.toHaveLength(3) + await expect(AsyncStorage.removeCustomDiscipline('fourth')).rejects.toThrow('customDiscipline not available') + await expect(AsyncStorage.getCustomDisciplines()).resolves.toStrictEqual(['first', 'second', 'third']) + }) }) - it('should push selectedProfession to array', async () => { - await AsyncStorage.setSelectedProfessions([mockDisciplines()[0].id]) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) - await AsyncStorage.pushSelectedProfession(mockDisciplines()[1].id) - await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(2) - }) + describe('selectedProfessions', () => { + const selectedProfessions = mockDisciplines() - describe('ExerciseProgress', () => { - beforeEach(() => { - jest.clearAllMocks() - RNAsyncStorage.clear() + it('should delete selectedProfession from array if exists', async () => { + await AsyncStorage.setSelectedProfessions(selectedProfessions.map(item => item.id)) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(selectedProfessions.length) + await AsyncStorage.removeSelectedProfession(mockDisciplines()[0].id) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(selectedProfessions.length - 1) }) - it('should save progress for not yet done discipline', async () => { - const progressOneExercise: Progress = { - 1: { [ExerciseKeys.wordChoiceExercise]: 0.5 } - } - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) - await expect(AsyncStorage.getExerciseProgress()).resolves.toStrictEqual(progressOneExercise) + it('should not delete selectedProfession from array if not exists', async () => { + await AsyncStorage.setSelectedProfessions([mockDisciplines()[1].id]) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) + await AsyncStorage.removeSelectedProfession(mockDisciplines()[0].id) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) }) - it('should save progress for done discipline but not yet done exercise', async () => { - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.writeExercise, 0.6) - const progress = await AsyncStorage.getExerciseProgress() - expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5, [ExerciseKeys.writeExercise]: 0.6 }) + it('should push selectedProfession to array', async () => { + await AsyncStorage.setSelectedProfessions([mockDisciplines()[0].id]) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(1) + await AsyncStorage.pushSelectedProfession(mockDisciplines()[1].id) + await expect(AsyncStorage.getSelectedProfessions()).resolves.toHaveLength(2) }) - it('should save progress for done exercise with improvement', async () => { - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.8) - const progress = await AsyncStorage.getExerciseProgress() - expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.8 }) + describe('ExerciseProgress', () => { + beforeEach(() => { + jest.clearAllMocks() + RNAsyncStorage.clear() + }) + + it('should save progress for not yet done discipline', async () => { + const progressOneExercise: Progress = { + 1: { [ExerciseKeys.wordChoiceExercise]: 0.5 } + } + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) + await expect(AsyncStorage.getExerciseProgress()).resolves.toStrictEqual(progressOneExercise) + }) + + it('should save progress for done discipline but not yet done exercise', async () => { + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.writeExercise, 0.6) + const progress = await AsyncStorage.getExerciseProgress() + expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5, [ExerciseKeys.writeExercise]: 0.6 }) + }) + + it('should save progress for done exercise with improvement', async () => { + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.8) + const progress = await AsyncStorage.getExerciseProgress() + expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.8 }) + }) + + it('should not save progress for done exercise without improvement', async () => { + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) + await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.4) + const progress = await AsyncStorage.getExerciseProgress() + expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5 }) + }) + + it('should calculate and save exercise progress correctly', async () => { + const documents = new DocumentBuilder(2).build() + const documentsWithResults: DocumentResult[] = [ + { + document: documents[0], + result: SIMPLE_RESULTS.correct, + numberOfTries: 1 + }, + { + document: documents[0], + result: SIMPLE_RESULTS.incorrect, + numberOfTries: 3 + } + ] + await saveExerciseProgress(1, 1, documentsWithResults) + const progress = await AsyncStorage.getExerciseProgress() + expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5 }) + }) }) + }) - it('should not save progress for done exercise without improvement', async () => { - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.5) - await AsyncStorage.setExerciseProgress(1, ExerciseKeys.wordChoiceExercise, 0.4) - const progress = await AsyncStorage.getExerciseProgress() - expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5 }) + describe('favorites', () => { + const documents = new DocumentBuilder(4).build() + + it('should add favorites', async () => { + await AsyncStorage.setFavorites(documents.slice(0, 2)) + await expect(AsyncStorage.getFavorites()).resolves.toEqual(documents.slice(0, 2)) + await AsyncStorage.addFavorite(documents[2]) + await expect(AsyncStorage.getFavorites()).resolves.toEqual(documents.slice(0, 3)) + await AsyncStorage.addFavorite(documents[3]) + await expect(AsyncStorage.getFavorites()).resolves.toEqual(documents) }) - it('should calculate and save exercise progress correctly', async () => { - const documents = new DocumentBuilder(2).build() - const documentsWithResults: DocumentResult[] = [ - { - document: documents[0], - result: SIMPLE_RESULTS.correct, - numberOfTries: 1 - }, - { - document: documents[0], - result: SIMPLE_RESULTS.incorrect, - numberOfTries: 3 - } - ] - await saveExerciseProgress(1, 1, documentsWithResults) - const progress = await AsyncStorage.getExerciseProgress() - expect(progress[1]).toStrictEqual({ [ExerciseKeys.wordChoiceExercise]: 0.5 }) + it('should remove favorites', async () => { + await AsyncStorage.setFavorites(documents) + await expect(AsyncStorage.getFavorites()).resolves.toEqual(documents) + await AsyncStorage.removeFavorite(documents[2]) + await expect(AsyncStorage.getFavorites()).resolves.toEqual([documents[0], documents[1], documents[3]]) + await AsyncStorage.removeFavorite(documents[0]) + await expect(AsyncStorage.getFavorites()).resolves.toEqual([documents[1], documents[3]]) }) }) }) From 596f9a5244aba0d2dcc9638371468b78a66b0540 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 15:09:55 +0200 Subject: [PATCH 03/17] LUN-132: Add components and refactor --- src/components/AudioPlayer.tsx | 23 ++--- src/components/DocumentImageSection.tsx | 49 ++++++++++ src/components/FavoriteButton.tsx | 67 +++++++++++++ src/components/VocabularyListItem.tsx | 12 ++- .../__tests__/FavoriteButton.spec.tsx | 38 +++++++ src/constants/labels.json | 5 + .../components/SingleChoiceExercise.tsx | 6 +- .../write-exercise/WriteExerciseScreen.tsx | 25 ++--- .../__tests__/WriteExerciseScreen.spec.tsx | 5 + .../components/InteractionSection.tsx | 98 ++++++++++--------- .../__tests__/InteractionSection.spec.tsx | 5 + 11 files changed, 245 insertions(+), 88 deletions(-) create mode 100644 src/components/DocumentImageSection.tsx create mode 100644 src/components/FavoriteButton.tsx create mode 100644 src/components/__tests__/FavoriteButton.spec.tsx diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx index 0af8e23c3..95af7cb65 100644 --- a/src/components/AudioPlayer.tsx +++ b/src/components/AudioPlayer.tsx @@ -15,14 +15,7 @@ export interface AudioPlayerProps { submittedAlternative?: string | null } -const StyledView = styled.View` - align-items: center; - margin-bottom: ${props => props.theme.spacings.sm}; -` - const VolumeIcon = styled.TouchableOpacity<{ disabled: boolean; isActive: boolean }>` - position: absolute; - top: ${wp('-4.5%')}px; width: ${wp('9%')}px; height: ${wp('9%')}px; border-radius: 50px; @@ -107,15 +100,13 @@ const AudioPlayer = ({ document, disabled, submittedAlternative }: AudioPlayerPr } return ( - - - - - + + + ) } diff --git a/src/components/DocumentImageSection.tsx b/src/components/DocumentImageSection.tsx new file mode 100644 index 000000000..21d0766b7 --- /dev/null +++ b/src/components/DocumentImageSection.tsx @@ -0,0 +1,49 @@ +import React, { ReactElement } from 'react' +import { widthPercentageToDP as wp } from 'react-native-responsive-screen' +import styled from 'styled-components/native' + +import { Document } from '../constants/endpoints' +import AudioPlayer from './AudioPlayer' +import FavoriteButton from './FavoriteButton' +import ImageCarousel from './ImageCarousel' + +const AudioContainer = styled.View` + position: absolute; + bottom: ${wp('-4.5%')}px; + align-self: center; +` +const FavoriteContainer = styled.View` + position: absolute; + top: 0; + right: ${props => props.theme.spacings.md}; +` + +const Container = styled.View` + margin-bottom: ${props => props.theme.spacings.md}; +` + +interface Props { + document: Document + audioDisabled?: boolean + minimized?: boolean + submittedAlternative?: string | null +} + +const DocumentImageSection = ({ + document, + audioDisabled = false, + minimized = false, + submittedAlternative +}: Props): ReactElement => ( + + + + + + + + + +) + +export default DocumentImageSection diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx new file mode 100644 index 000000000..e7cd0a8f3 --- /dev/null +++ b/src/components/FavoriteButton.tsx @@ -0,0 +1,67 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import { widthPercentageToDP as wp } from 'react-native-responsive-screen' +import styled from 'styled-components/native' + +import { StarCircleIconGrey, StarCircleIconGreyFilled } from '../../assets/images' +import { Document } from '../constants/endpoints' +import labels from '../constants/labels.json' +import AsyncStorage from '../services/AsyncStorage' + +const Container = styled.View` + padding: ${props => `${props.theme.spacings.xs} 0 ${props.theme.spacings.xs} ${props.theme.spacings.sm}`}; + align-self: center; +` + +const Icon = styled(StarCircleIconGreyFilled)` + min-width: ${wp('9%')}px; + min-height: ${wp('9%')}px; +` +const IconOutline = styled(StarCircleIconGrey)` + min-width: ${wp('9%')}px; + min-height: ${wp('9%')}px; +` +const Button = styled.TouchableOpacity` + justify-content: center; + align-items: center; + shadow-radius: 5px; + shadow-offset: 1px 1px; + shadow-opacity: 0.5; +` + +interface Props { + document: Document +} + +const FavoriteButton = ({ document }: Props): ReactElement | null => { + const [isFavorite, setIsFavorite] = useState(null) + + useEffect(() => { + AsyncStorage.isFavorite(document) + .then(it => setIsFavorite(it)) + .catch(() => undefined) + }, [document]) + + const setFavorite = () => { + if (isFavorite) { + setIsFavorite(false) + AsyncStorage.removeFavorite(document).catch(() => setIsFavorite(true)) + } else { + setIsFavorite(true) + AsyncStorage.addFavorite(document).catch(() => setIsFavorite(false)) + } + } + + if (isFavorite === null) { + return null + } + + return ( + + + + ) +} + +export default FavoriteButton diff --git a/src/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx index 287ead256..61a0ea36b 100644 --- a/src/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components/native' import { Document } from '../constants/endpoints' import { getArticleColor } from '../services/helpers' import AudioPlayer from './AudioPlayer' +import FavoriteButton from '../../../components/FavoriteButton' import ListItem from './ListItem' import { ContentTextLight } from './text/Content' @@ -24,9 +25,9 @@ const StyledTitle = styled(ContentTextLight)<{ articleColor: string }>` height: ${wp('5%')}px; text-align: center; ` -const Speaker = styled.View` - padding-right: ${props => props.theme.spacings.xl}; - padding-top: ${props => props.theme.spacings.sm}; +const RightChildrenContainer = styled.View` + flex-direction: row; + justify-content: space-between; ` export interface VocabularyListItemProp { @@ -50,9 +51,10 @@ const VocabularyListItem = ({ document, onPress }: VocabularyListItemProp): Reac onPress={onPress} icon={icon} rightChildren={ - + - + + } /> ) diff --git a/src/components/__tests__/FavoriteButton.spec.tsx b/src/components/__tests__/FavoriteButton.spec.tsx new file mode 100644 index 000000000..9aea5458a --- /dev/null +++ b/src/components/__tests__/FavoriteButton.spec.tsx @@ -0,0 +1,38 @@ +import { fireEvent, waitFor } from '@testing-library/react-native' +import React from 'react' + +import labels from '../../constants/labels.json' +import AsyncStorage from '../../services/AsyncStorage' +import DocumentBuilder from '../../testing/DocumentBuilder' +import render from '../../testing/render' +import FavoriteButton from '../FavoriteButton' + +describe('FavoriteButton', () => { + const document = new DocumentBuilder(1).build()[0] + + it('should add favorite on click', async () => { + await AsyncStorage.setFavorites([]) + await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) + + const { getByA11yLabel } = render() + + await waitFor(() => expect(getByA11yLabel(labels.favorites.add)).toBeTruthy()) + fireEvent.press(getByA11yLabel(labels.favorites.add)) + + expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy() + await waitFor(() => expect(AsyncStorage.isFavorite(document)).resolves.toBe(true)) + }) + + it('should remove favorite on click', async () => { + await AsyncStorage.setFavorites([document]) + await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) + + const { getByA11yLabel } = render() + + await waitFor(() => expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy()) + fireEvent.press(getByA11yLabel(labels.favorites.remove)) + + expect(getByA11yLabel(labels.favorites.add)).toBeTruthy() + await waitFor(() => expect(AsyncStorage.isFavorite(document)).resolves.toBe(false)) + }) +}) diff --git a/src/constants/labels.json b/src/constants/labels.json index 75f2639b4..2a5bc259a 100644 --- a/src/constants/labels.json +++ b/src/constants/labels.json @@ -141,5 +141,10 @@ "disciplines": "Module", "word": "Wort", "words": "Wörter" + }, + "favorites": { + "favorites": "Favoriten", + "add": "Favorit hinzufügen", + "remove": "Favorit entfernen" } } diff --git a/src/routes/choice-exercises/components/SingleChoiceExercise.tsx b/src/routes/choice-exercises/components/SingleChoiceExercise.tsx index 4ef7dc239..231791891 100644 --- a/src/routes/choice-exercises/components/SingleChoiceExercise.tsx +++ b/src/routes/choice-exercises/components/SingleChoiceExercise.tsx @@ -4,10 +4,9 @@ import React, { ReactElement, useEffect, useState, useCallback } from 'react' import styled from 'styled-components/native' import { ArrowRightIcon } from '../../../../assets/images' -import AudioPlayer from '../../../components/AudioPlayer' import Button from '../../../components/Button' +import DocumentImageSection from '../../../components/DocumentImageSection' import ExerciseHeader from '../../../components/ExerciseHeader' -import ImageCarousel from '../../../components/ImageCarousel' import { Answer, BUTTONS_THEME, numberOfMaxRetries, SIMPLE_RESULTS, SimpleResult } from '../../../constants/data' import { AlternativeWord, Document } from '../../../constants/endpoints' import labels from '../../../constants/labels.json' @@ -138,8 +137,7 @@ const ChoiceExerciseScreen = ({ <> - {document.document_image.length > 0 && } - + props.theme.spacings.md}; - padding-bottom: ${props => props.theme.spacings.lg}; +const ButtonContainer = styled.View` align-items: center; - position: relative; - width: 100%; - height: 85%; ` export interface WriteExerciseScreenProps { @@ -120,15 +114,12 @@ const WriteExerciseScreen = ({ route, navigation }: WriteExerciseScreenProps): R - - - - - + + {isAnswerSubmitted && current.result !== 'similar' ? ( diff --git a/src/components/VocabularyList.tsx b/src/components/VocabularyList.tsx index de7cefa6d..488163c93 100644 --- a/src/components/VocabularyList.tsx +++ b/src/components/VocabularyList.tsx @@ -17,11 +17,12 @@ const Root = styled.View` interface VocabularyListScreenProps { documents: Document[] onItemPress: (index: number) => void + refreshFavorites?: () => void } -const VocabularyList = ({ documents, onItemPress }: VocabularyListScreenProps): JSX.Element => { +const VocabularyList = ({ documents, onItemPress, refreshFavorites }: VocabularyListScreenProps): JSX.Element => { const renderItem = ({ item, index }: { item: Document; index: number }): JSX.Element => ( - onItemPress(index)} /> + onItemPress(index)} refreshFavorites={refreshFavorites} /> ) return ( diff --git a/src/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx index c390dc050..147d74a74 100644 --- a/src/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -33,9 +33,10 @@ const RightChildrenContainer = styled.View` export interface VocabularyListItemProp { document: Document onPress: () => void + refreshFavorites?: () => void } -const VocabularyListItem = ({ document, onPress }: VocabularyListItemProp): ReactElement => { +const VocabularyListItem = ({ document, onPress, refreshFavorites }: VocabularyListItemProp): ReactElement => { const { article, word, document_image: documentImage } = document const title = {article.value} @@ -53,7 +54,7 @@ const VocabularyListItem = ({ document, onPress }: VocabularyListItemProp): Reac rightChildren={ - + } /> diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index 5cc296e12..fc0b6d4f0 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -15,15 +15,16 @@ interface FavoritesScreenProps { } const FavoritesScreen = ({ navigation }: FavoritesScreenProps): JSX.Element => { - const { data, refresh, error, loading } = useLoadAsync(AsyncStorage.getFavorites, {}) + const { data, refresh, error } = useLoadAsync(AsyncStorage.getFavorites, {}) useFocusEffect(refresh) return ( - + {data && ( navigation.navigate('VocabularyDetail', { disciplineId: 0, diff --git a/src/services/AsyncStorage.ts b/src/services/AsyncStorage.ts index 074178880..c4333db03 100644 --- a/src/services/AsyncStorage.ts +++ b/src/services/AsyncStorage.ts @@ -91,6 +91,9 @@ const setFavorites = async (favorites: Document[]): Promise => { const addFavorite = async (favorite: Document): Promise => { const favorites = await getFavorites() + if (favorites.includes(favorite)) { + return + } const newFavorites = [...favorites, favorite] await setFavorites(newFavorites) } From 2ca1739736826362360e602b4c6e14d25f02da8f Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 18:19:26 +0200 Subject: [PATCH 07/17] LUN-132: Simplify and fix cannot update on unmounted component --- src/components/FavoriteButton.tsx | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index a51aff118..58e1765bf 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -1,12 +1,14 @@ import { useFocusEffect } from '@react-navigation/native' -import React, { ReactElement, useCallback, useState } from 'react' +import React, { ReactElement } from 'react' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' import { StarCircleIconGrey, StarCircleIconGreyFilled } from '../../assets/images' import { Document } from '../constants/endpoints' import labels from '../constants/labels.json' +import useLoadAsync from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' +import { reportError } from '../services/sentry' const Container = styled.View` padding: ${props => `${props.theme.spacings.xs} 0 ${props.theme.spacings.xs} ${props.theme.spacings.sm}`}; @@ -35,24 +37,17 @@ interface Props { } const FavoriteButton = ({ document, refreshFavorites }: Props): ReactElement | null => { - const [isFavorite, setIsFavorite] = useState(null) + const { data: isFavorite, refresh } = useLoadAsync(AsyncStorage.isFavorite, document) - const loadIsFavorite = useCallback(() => { - AsyncStorage.isFavorite(document) - .then(it => setIsFavorite(it)) - .catch(() => undefined) - }, [document]) - - useFocusEffect(loadIsFavorite) + useFocusEffect(refresh) const onPress = async () => { if (isFavorite) { - setIsFavorite(false) - await AsyncStorage.removeFavorite(document).catch(() => setIsFavorite(true)) + await AsyncStorage.removeFavorite(document).catch(reportError) } else { - setIsFavorite(true) - await AsyncStorage.addFavorite(document).catch(() => setIsFavorite(false)) + await AsyncStorage.addFavorite(document).catch(reportError) } + refresh() if (refreshFavorites) { refreshFavorites() } From ca2854eca571ae8e5927669b0a83ee2a189287f1 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 18:27:18 +0200 Subject: [PATCH 08/17] LUN-132: Fix label --- src/components/VocabularyList.tsx | 10 ++++++++-- src/routes/FavoritesScreen.tsx | 24 +++++++++++++++--------- src/routes/VocabularyListScreen.tsx | 10 +++++++++- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/components/VocabularyList.tsx b/src/components/VocabularyList.tsx index 488163c93..17da0144e 100644 --- a/src/components/VocabularyList.tsx +++ b/src/components/VocabularyList.tsx @@ -18,9 +18,15 @@ interface VocabularyListScreenProps { documents: Document[] onItemPress: (index: number) => void refreshFavorites?: () => void + title: string } -const VocabularyList = ({ documents, onItemPress, refreshFavorites }: VocabularyListScreenProps): JSX.Element => { +const VocabularyList = ({ + documents, + onItemPress, + refreshFavorites, + title +}: VocabularyListScreenProps): JSX.Element => { const renderItem = ({ item, index }: { item: Document; index: number }): JSX.Element => ( onItemPress(index)} refreshFavorites={refreshFavorites} /> ) @@ -28,7 +34,7 @@ const VocabularyList = ({ documents, onItemPress, refreshFavorites }: Vocabulary return ( diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index fc0b6d4f0..67a6cb912 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -19,21 +19,27 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): JSX.Element => { useFocusEffect(refresh) + const onItemPress = (index: number) => { + if (!data) { + return + } + navigation.navigate('VocabularyDetail', { + disciplineId: 0, + disciplineTitle: labels.general.favorites, + documents: data, + documentIndex: index, + closeExerciseAction: CommonActions.goBack() + }) + } + return ( <ServerResponseHandler error={error} loading={false} refresh={refresh}> {data && ( <VocabularyList + title={labels.favorites.favorites} documents={data} refreshFavorites={refresh} - onItemPress={(index: number) => - navigation.navigate('VocabularyDetail', { - disciplineId: 0, - disciplineTitle: labels.general.favorites, - documents: data, - documentIndex: index, - closeExerciseAction: CommonActions.goBack() - }) - } + onItemPress={onItemPress} /> )} </ServerResponseHandler> diff --git a/src/routes/VocabularyListScreen.tsx b/src/routes/VocabularyListScreen.tsx index 80e9fce0d..f3ef8d096 100644 --- a/src/routes/VocabularyListScreen.tsx +++ b/src/routes/VocabularyListScreen.tsx @@ -3,6 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack' import React from 'react' import VocabularyList from '../components/VocabularyList' +import labels from '../constants/labels.json' import { RoutesParams } from '../navigation/NavigationTypes' interface VocabularyListScreenProps { @@ -13,7 +14,14 @@ interface VocabularyListScreenProps { const VocabularyListScreen = ({ route, navigation }: VocabularyListScreenProps): JSX.Element => { const onItemPress = (index: number) => navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index }) - return <VocabularyList documents={route.params.documents} onItemPress={onItemPress} /> + + return ( + <VocabularyList + documents={route.params.documents} + onItemPress={onItemPress} + title={labels.exercises.vocabularyList.title} + /> + ) } export default VocabularyListScreen From 6c7e40f73130b1c4c8e4339247a120260aa0088f Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Wed, 1 Jun 2022 15:58:15 +0200 Subject: [PATCH 09/17] LUN-357: Fix tests --- .../__tests__/FavoriteButton.spec.tsx | 20 +++++++++++++------ src/constants/labels.json | 4 +++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/__tests__/FavoriteButton.spec.tsx b/src/components/__tests__/FavoriteButton.spec.tsx index 9aea5458a..8015b04ce 100644 --- a/src/components/__tests__/FavoriteButton.spec.tsx +++ b/src/components/__tests__/FavoriteButton.spec.tsx @@ -1,3 +1,4 @@ +import { NavigationContainer } from '@react-navigation/native' import { fireEvent, waitFor } from '@testing-library/react-native' import React from 'react' @@ -10,29 +11,36 @@ import FavoriteButton from '../FavoriteButton' describe('FavoriteButton', () => { const document = new DocumentBuilder(1).build()[0] + const renderFavoriteButton = () => + render( + <NavigationContainer> + <FavoriteButton document={document} /> + </NavigationContainer> + ) + it('should add favorite on click', async () => { await AsyncStorage.setFavorites([]) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) - const { getByA11yLabel } = render(<FavoriteButton document={document} />) + const { getByA11yLabel } = renderFavoriteButton() await waitFor(() => expect(getByA11yLabel(labels.favorites.add)).toBeTruthy()) fireEvent.press(getByA11yLabel(labels.favorites.add)) - expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy() - await waitFor(() => expect(AsyncStorage.isFavorite(document)).resolves.toBe(true)) + await waitFor(() => expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy()) + await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) }) it('should remove favorite on click', async () => { await AsyncStorage.setFavorites([document]) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) - const { getByA11yLabel } = render(<FavoriteButton document={document} />) + const { getByA11yLabel } = renderFavoriteButton() await waitFor(() => expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy()) fireEvent.press(getByA11yLabel(labels.favorites.remove)) - expect(getByA11yLabel(labels.favorites.add)).toBeTruthy() - await waitFor(() => expect(AsyncStorage.isFavorite(document)).resolves.toBe(false)) + await waitFor(() => expect(getByA11yLabel(labels.favorites.add)).toBeTruthy()) + await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) }) }) diff --git a/src/constants/labels.json b/src/constants/labels.json index 84ad40bac..f49566d3d 100644 --- a/src/constants/labels.json +++ b/src/constants/labels.json @@ -148,6 +148,8 @@ "customModalCancel": "Zurück" }, "favorites": { - "favorites": "Favoriten" + "favorites": "Favoriten", + "add": "Favorit hinzufügen", + "remove": "Favorit entfernen" } } From 70500a242116c13ebb0e51464023772fc6eda8bb Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Wed, 1 Jun 2022 16:03:04 +0200 Subject: [PATCH 10/17] LUN-357: Rename onFavoritesChanged --- src/components/FavoriteButton.tsx | 8 ++++---- src/components/VocabularyList.tsx | 6 +++--- src/components/VocabularyListItem.tsx | 6 +++--- src/routes/FavoritesScreen.tsx | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index 58e1765bf..2df77def4 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -33,10 +33,10 @@ const Button = styled.TouchableOpacity` interface Props { document: Document - refreshFavorites?: () => void + onFavoritesChanged?: () => void } -const FavoriteButton = ({ document, refreshFavorites }: Props): ReactElement | null => { +const FavoriteButton = ({ document, onFavoritesChanged }: Props): ReactElement | null => { const { data: isFavorite, refresh } = useLoadAsync(AsyncStorage.isFavorite, document) useFocusEffect(refresh) @@ -48,8 +48,8 @@ const FavoriteButton = ({ document, refreshFavorites }: Props): ReactElement | n await AsyncStorage.addFavorite(document).catch(reportError) } refresh() - if (refreshFavorites) { - refreshFavorites() + if (onFavoritesChanged) { + onFavoritesChanged() } } diff --git a/src/components/VocabularyList.tsx b/src/components/VocabularyList.tsx index 0494d64af..68d53fb61 100644 --- a/src/components/VocabularyList.tsx +++ b/src/components/VocabularyList.tsx @@ -17,18 +17,18 @@ const Root = styled.View` interface VocabularyListScreenProps { documents: Document[] onItemPress: (index: number) => void - refreshFavorites?: () => void + onFavoritesChanged?: () => void title: string } const VocabularyList = ({ documents, onItemPress, - refreshFavorites, + onFavoritesChanged, title }: VocabularyListScreenProps): JSX.Element => { const renderItem = ({ item, index }: { item: Document; index: number }): JSX.Element => ( - <VocabularyListItem document={item} onPress={() => onItemPress(index)} refreshFavorites={refreshFavorites} /> + <VocabularyListItem document={item} onPress={() => onItemPress(index)} onFavoritesChanged={onFavoritesChanged} /> ) return ( diff --git a/src/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx index b90974915..03e8d0d1b 100644 --- a/src/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -33,10 +33,10 @@ const RightChildrenContainer = styled.View` interface VocabularyListItemProps { document: Document onPress: () => void - refreshFavorites?: () => void + onFavoritesChanged?: () => void } -const VocabularyListItem = ({ document, onPress, refreshFavorites }: VocabularyListItemProps): ReactElement => { +const VocabularyListItem = ({ document, onPress, onFavoritesChanged }: VocabularyListItemProps): ReactElement => { const { article, word, document_image: documentImage } = document const title = <StyledTitle articleColor={getArticleColor(article)}>{article.value}</StyledTitle> @@ -54,7 +54,7 @@ const VocabularyListItem = ({ document, onPress, refreshFavorites }: VocabularyL rightChildren={ <RightChildrenContainer> <AudioPlayer document={document} disabled={false} /> - <FavoriteButton document={document} refreshFavorites={refreshFavorites} /> + <FavoriteButton document={document} onFavoritesChanged={onFavoritesChanged} /> </RightChildrenContainer> } /> diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index 67a6cb912..82848bf79 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -38,7 +38,7 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): JSX.Element => { <VocabularyList title={labels.favorites.favorites} documents={data} - refreshFavorites={refresh} + onFavoritesChanged={refresh} onItemPress={onItemPress} /> )} From ffcb8f6855a554599621969bb5c2a3f111c8641d Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Wed, 1 Jun 2022 16:06:59 +0200 Subject: [PATCH 11/17] LUN-132: Remove server response handling --- .../__tests__/VocabularyList.spec.tsx | 4 +-- src/routes/FavoritesScreen.tsx | 27 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx index 2f5191222..e4f4c0cd8 100644 --- a/src/components/__tests__/VocabularyList.spec.tsx +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -28,7 +28,7 @@ describe('VocabularyList', () => { mockUseLoadAsyncWithData(documents) const { getByText, getAllByText, getAllByTestId } = render( - <VocabularyList documents={documents} onItemPress={onItemPress} /> + <VocabularyList title='Title' documents={documents} onItemPress={onItemPress} /> ) expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() @@ -42,7 +42,7 @@ describe('VocabularyList', () => { }) it('should call onItemPress with correct index', () => { - const { getByText } = render(<VocabularyList documents={documents} onItemPress={onItemPress} />) + const { getByText } = render(<VocabularyList title='Title' documents={documents} onItemPress={onItemPress} />) expect(onItemPress).toHaveBeenCalledTimes(0) diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index 82848bf79..3a642f161 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -1,8 +1,7 @@ import { CommonActions, RouteProp, useFocusEffect } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' -import React from 'react' +import React, { ReactNode } from 'react' -import ServerResponseHandler from '../components/ServerResponseHandler' import VocabularyList from '../components/VocabularyList' import labels from '../constants/labels.json' import useLoadAsync from '../hooks/useLoadAsync' @@ -14,8 +13,8 @@ interface FavoritesScreenProps { navigation: StackNavigationProp<RoutesParams, 'Favorites'> } -const FavoritesScreen = ({ navigation }: FavoritesScreenProps): JSX.Element => { - const { data, refresh, error } = useLoadAsync(AsyncStorage.getFavorites, {}) +const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactNode => { + const { data, refresh } = useLoadAsync(AsyncStorage.getFavorites, {}) useFocusEffect(refresh) @@ -32,17 +31,17 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): JSX.Element => { }) } + if (!data) { + return null + } + return ( - <ServerResponseHandler error={error} loading={false} refresh={refresh}> - {data && ( - <VocabularyList - title={labels.favorites.favorites} - documents={data} - onFavoritesChanged={refresh} - onItemPress={onItemPress} - /> - )} - </ServerResponseHandler> + <VocabularyList + title={labels.favorites.favorites} + documents={data} + onFavoritesChanged={refresh} + onItemPress={onItemPress} + /> ) } From f40c041f5b68cb0c8246aafbf340c7edd62b7ed9 Mon Sep 17 00:00:00 2001 From: steffi <metzger@integreat-app.de> Date: Thu, 2 Jun 2022 08:44:15 +0200 Subject: [PATCH 12/17] LUN-132: Change FavoriteButton --- src/components/FavoriteButton.tsx | 3 +-- .../__tests__/FavoriteButton.spec.tsx | 17 ++++++++--------- src/constants/labels.json | 6 +----- src/routes/FavoritesScreen.tsx | 7 +------ 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index 2df77def4..8bd9eb312 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -5,7 +5,6 @@ import styled from 'styled-components/native' import { StarCircleIconGrey, StarCircleIconGreyFilled } from '../../assets/images' import { Document } from '../constants/endpoints' -import labels from '../constants/labels.json' import useLoadAsync from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' import { reportError } from '../services/sentry' @@ -59,7 +58,7 @@ const FavoriteButton = ({ document, onFavoritesChanged }: Props): ReactElement | return ( <Container> - <Button accessibilityLabel={isFavorite ? labels.favorites.remove : labels.favorites.add} onPress={onPress}> + <Button testID={isFavorite ? 'remove' : 'add'} onPress={onPress}> {isFavorite ? <Icon /> : <IconOutline />} </Button> </Container> diff --git a/src/components/__tests__/FavoriteButton.spec.tsx b/src/components/__tests__/FavoriteButton.spec.tsx index 8015b04ce..23cfa9db5 100644 --- a/src/components/__tests__/FavoriteButton.spec.tsx +++ b/src/components/__tests__/FavoriteButton.spec.tsx @@ -2,7 +2,6 @@ import { NavigationContainer } from '@react-navigation/native' import { fireEvent, waitFor } from '@testing-library/react-native' import React from 'react' -import labels from '../../constants/labels.json' import AsyncStorage from '../../services/AsyncStorage' import DocumentBuilder from '../../testing/DocumentBuilder' import render from '../../testing/render' @@ -22,12 +21,12 @@ describe('FavoriteButton', () => { await AsyncStorage.setFavorites([]) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) - const { getByA11yLabel } = renderFavoriteButton() + const { getByTestId } = renderFavoriteButton() - await waitFor(() => expect(getByA11yLabel(labels.favorites.add)).toBeTruthy()) - fireEvent.press(getByA11yLabel(labels.favorites.add)) + await waitFor(() => expect(getByTestId('add')).toBeTruthy()) + fireEvent.press(getByTestId('add')) - await waitFor(() => expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy()) + await waitFor(() => expect(getByTestId('remove')).toBeTruthy()) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) }) @@ -35,12 +34,12 @@ describe('FavoriteButton', () => { await AsyncStorage.setFavorites([document]) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) - const { getByA11yLabel } = renderFavoriteButton() + const { getByTestId } = renderFavoriteButton() - await waitFor(() => expect(getByA11yLabel(labels.favorites.remove)).toBeTruthy()) - fireEvent.press(getByA11yLabel(labels.favorites.remove)) + await waitFor(() => expect(getByTestId('remove')).toBeTruthy()) + fireEvent.press(getByTestId('remove')) - await waitFor(() => expect(getByA11yLabel(labels.favorites.add)).toBeTruthy()) + await waitFor(() => expect(getByTestId('add')).toBeTruthy()) await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) }) }) diff --git a/src/constants/labels.json b/src/constants/labels.json index f49566d3d..4a5eea13b 100644 --- a/src/constants/labels.json +++ b/src/constants/labels.json @@ -147,9 +147,5 @@ "words": "Wörter", "customModalCancel": "Zurück" }, - "favorites": { - "favorites": "Favoriten", - "add": "Favorit hinzufügen", - "remove": "Favorit entfernen" - } + "favorites": "Favoriten" } diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index 3a642f161..925d126e4 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -36,12 +36,7 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactNode => { } return ( - <VocabularyList - title={labels.favorites.favorites} - documents={data} - onFavoritesChanged={refresh} - onItemPress={onItemPress} - /> + <VocabularyList title={labels.favorites} documents={data} onFavoritesChanged={refresh} onItemPress={onItemPress} /> ) } From 5ad0d5a85d21daebbbd70d95a962d5c05acfa688 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 7 Jun 2022 12:41:43 +0200 Subject: [PATCH 13/17] LUN-132: Fix tests and type --- src/components/__tests__/VocabularyList.spec.tsx | 2 +- src/routes/FavoritesScreen.tsx | 6 +++--- src/routes/__tests__/VocabularyListScreen.spec.tsx | 5 +++++ .../__tests__/WordChoiceExerciseScreen.spec.tsx | 4 ++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx index e4f4c0cd8..54f53a576 100644 --- a/src/components/__tests__/VocabularyList.spec.tsx +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -31,7 +31,7 @@ describe('VocabularyList', () => { <VocabularyList title='Title' documents={documents} onItemPress={onItemPress} /> ) - expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() + expect(getByText('Title')).toBeTruthy() expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() expect(getByText('Der')).toBeTruthy() expect(getByText('Spachtel')).toBeTruthy() diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index 3a642f161..ea0b03dc0 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -1,6 +1,6 @@ import { CommonActions, RouteProp, useFocusEffect } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' -import React, { ReactNode } from 'react' +import React, { ReactElement } from 'react' import VocabularyList from '../components/VocabularyList' import labels from '../constants/labels.json' @@ -13,7 +13,7 @@ interface FavoritesScreenProps { navigation: StackNavigationProp<RoutesParams, 'Favorites'> } -const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactNode => { +const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactElement => { const { data, refresh } = useLoadAsync(AsyncStorage.getFavorites, {}) useFocusEffect(refresh) @@ -32,7 +32,7 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactNode => { } if (!data) { - return null + return <></> } return ( diff --git a/src/routes/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx index 56cf03483..562861754 100644 --- a/src/routes/__tests__/VocabularyListScreen.spec.tsx +++ b/src/routes/__tests__/VocabularyListScreen.spec.tsx @@ -10,6 +10,11 @@ import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' import render from '../../testing/render' import VocabularyListScreen from '../VocabularyListScreen' +jest.mock('../../../components/FavoriteButton', () => { + const Text = require('react-native').Text + return () => <Text>FavoriteButton</Text> +}) + jest.mock('../../services/AsyncStorage', () => ({ setExerciseProgress: jest.fn(() => Promise.resolve()) })) diff --git a/src/routes/choice-exercises/__tests__/WordChoiceExerciseScreen.spec.tsx b/src/routes/choice-exercises/__tests__/WordChoiceExerciseScreen.spec.tsx index 6ed91626c..27c2453d0 100644 --- a/src/routes/choice-exercises/__tests__/WordChoiceExerciseScreen.spec.tsx +++ b/src/routes/choice-exercises/__tests__/WordChoiceExerciseScreen.spec.tsx @@ -11,6 +11,10 @@ import WordChoiceExerciseScreen from '../WordChoiceExerciseScreen' jest.useFakeTimers() +jest.mock('../../../components/FavoriteButton', () => { + const Text = require('react-native').Text + return () => <Text>FavoriteButton</Text> +}) jest.mock('../../../services/helpers', () => ({ ...jest.requireActual('../../../services/helpers'), shuffleArray: jest.fn(it => it) From 26eda47f35f8d8d0b4789228b6e56c881e0a9116 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 7 Jun 2022 13:08:12 +0200 Subject: [PATCH 14/17] LUN-132: Change disciplineId to null if not available --- src/navigation/NavigationTypes.ts | 5 +++-- src/routes/FavoritesScreen.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/navigation/NavigationTypes.ts b/src/navigation/NavigationTypes.ts index 10792b0c2..1c574ea87 100644 --- a/src/navigation/NavigationTypes.ts +++ b/src/navigation/NavigationTypes.ts @@ -16,8 +16,9 @@ interface ExerciseParams { closeExerciseAction: CommonNavigationAction } -type DetailExerciseParams = ExerciseParams & { +interface VocabularyDetailExerciseParams extends Omit<ExerciseParams, 'disciplineId'> { documentIndex: number + disciplineId: number | null } export interface ExercisesParams extends Omit<ExerciseParams, 'documents' | 'closeExerciseAction'> { @@ -51,7 +52,7 @@ export type RoutesParams = { discipline: Discipline initialSelection: boolean } - VocabularyDetail: DetailExerciseParams + VocabularyDetail: VocabularyDetailExerciseParams Exercises: ExercisesParams VocabularyList: ExerciseParams WordChoiceExercise: ExerciseParams diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index f54ecac68..a25855ace 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -23,7 +23,7 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactElement => return } navigation.navigate('VocabularyDetail', { - disciplineId: 0, + disciplineId: null, disciplineTitle: labels.general.favorites, documents: data, documentIndex: index, From 3df04ca4f7f47e3cf590203f8f7ed713a32a4181 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 7 Jun 2022 13:32:48 +0200 Subject: [PATCH 15/17] LUN-132: Only save ids --- src/components/FavoriteButton.tsx | 6 +++--- .../__tests__/FavoriteButton.spec.tsx | 10 ++++----- src/constants/endpoints.ts | 3 ++- src/hooks/useLoadDocuments.ts | 2 +- src/hooks/useLoadFavorites.ts | 21 +++++++++++++++++++ src/routes/FavoritesScreen.tsx | 21 ++++++++++++------- src/services/AsyncStorage.ts | 15 +++++++------ src/services/__tests__/AsyncStorage.spec.ts | 2 +- 8 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useLoadFavorites.ts diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index 8bd9eb312..9dabfbbfc 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -36,15 +36,15 @@ interface Props { } const FavoriteButton = ({ document, onFavoritesChanged }: Props): ReactElement | null => { - const { data: isFavorite, refresh } = useLoadAsync(AsyncStorage.isFavorite, document) + const { data: isFavorite, refresh } = useLoadAsync(AsyncStorage.isFavorite, document.id) useFocusEffect(refresh) const onPress = async () => { if (isFavorite) { - await AsyncStorage.removeFavorite(document).catch(reportError) + await AsyncStorage.removeFavorite(document.id).catch(reportError) } else { - await AsyncStorage.addFavorite(document).catch(reportError) + await AsyncStorage.addFavorite(document.id).catch(reportError) } refresh() if (onFavoritesChanged) { diff --git a/src/components/__tests__/FavoriteButton.spec.tsx b/src/components/__tests__/FavoriteButton.spec.tsx index 23cfa9db5..7bf6dbc6d 100644 --- a/src/components/__tests__/FavoriteButton.spec.tsx +++ b/src/components/__tests__/FavoriteButton.spec.tsx @@ -19,7 +19,7 @@ describe('FavoriteButton', () => { it('should add favorite on click', async () => { await AsyncStorage.setFavorites([]) - await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) + await expect(AsyncStorage.isFavorite(document.id)).resolves.toBe(false) const { getByTestId } = renderFavoriteButton() @@ -27,12 +27,12 @@ describe('FavoriteButton', () => { fireEvent.press(getByTestId('add')) await waitFor(() => expect(getByTestId('remove')).toBeTruthy()) - await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) + await expect(AsyncStorage.isFavorite(document.id)).resolves.toBe(true) }) it('should remove favorite on click', async () => { - await AsyncStorage.setFavorites([document]) - await expect(AsyncStorage.isFavorite(document)).resolves.toBe(true) + await AsyncStorage.setFavorites([document.id]) + await expect(AsyncStorage.isFavorite(document.id)).resolves.toBe(true) const { getByTestId } = renderFavoriteButton() @@ -40,6 +40,6 @@ describe('FavoriteButton', () => { fireEvent.press(getByTestId('remove')) await waitFor(() => expect(getByTestId('add')).toBeTruthy()) - await expect(AsyncStorage.isFavorite(document)).resolves.toBe(false) + await expect(AsyncStorage.isFavorite(document.id)).resolves.toBe(false) }) }) diff --git a/src/constants/endpoints.ts b/src/constants/endpoints.ts index a8b50c551..bf684e7a9 100644 --- a/src/constants/endpoints.ts +++ b/src/constants/endpoints.ts @@ -40,7 +40,8 @@ export const ENDPOINTS = { disciplinesByGroup: 'disciplines_by_group', groupInfo: 'group_info', trainingSet: 'training_set', - documents: 'documents/:id' + documents: 'documents/:id', + document: 'words' } export const ForbiddenError = 'Request failed with status code 403' diff --git a/src/hooks/useLoadDocuments.ts b/src/hooks/useLoadDocuments.ts index ee2c635e8..70b631a69 100644 --- a/src/hooks/useLoadDocuments.ts +++ b/src/hooks/useLoadDocuments.ts @@ -17,7 +17,7 @@ export interface DocumentFromServer { alternatives: AlternativeWordFromServer[] } -const formatServerResponse = (documents: DocumentFromServer[]): Document[] => +export const formatServerResponse = (documents: DocumentFromServer[]): Document[] => documents.map(document => ({ ...document, article: ARTICLES[document.article], diff --git a/src/hooks/useLoadFavorites.ts b/src/hooks/useLoadFavorites.ts new file mode 100644 index 000000000..58dce9dbe --- /dev/null +++ b/src/hooks/useLoadFavorites.ts @@ -0,0 +1,21 @@ +import { Document, ENDPOINTS } from '../constants/endpoints' +import AsyncStorage from '../services/AsyncStorage' +import { getFromEndpoint } from '../services/axios' +import useLoadAsync, { Return } from './useLoadAsync' +import { DocumentFromServer, formatServerResponse } from './useLoadDocuments' + +export const loadFavorites = async (): Promise<Document[]> => { + const favoriteIds = await AsyncStorage.getFavorites() + const documents = await Promise.all( + favoriteIds.map(id => { + const url = `${ENDPOINTS.document}/${id}` + return getFromEndpoint<DocumentFromServer>(url) + }) + ) + + return formatServerResponse(documents) +} + +const useLoadFavorites = (): Return<Document[]> => useLoadAsync(loadFavorites, {}) + +export default useLoadFavorites diff --git a/src/routes/FavoritesScreen.tsx b/src/routes/FavoritesScreen.tsx index a25855ace..7af95de6e 100644 --- a/src/routes/FavoritesScreen.tsx +++ b/src/routes/FavoritesScreen.tsx @@ -2,11 +2,11 @@ import { CommonActions, RouteProp, useFocusEffect } from '@react-navigation/nati import { StackNavigationProp } from '@react-navigation/stack' import React, { ReactElement } from 'react' +import ServerResponseHandler from '../components/ServerResponseHandler' import VocabularyList from '../components/VocabularyList' import labels from '../constants/labels.json' -import useLoadAsync from '../hooks/useLoadAsync' +import useLoadFavorites from '../hooks/useLoadFavorites' import { RoutesParams } from '../navigation/NavigationTypes' -import AsyncStorage from '../services/AsyncStorage' interface FavoritesScreenProps { route: RouteProp<RoutesParams, 'Favorites'> @@ -14,7 +14,7 @@ interface FavoritesScreenProps { } const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactElement => { - const { data, refresh } = useLoadAsync(AsyncStorage.getFavorites, {}) + const { data, error, refresh } = useLoadFavorites() useFocusEffect(refresh) @@ -31,12 +31,17 @@ const FavoritesScreen = ({ navigation }: FavoritesScreenProps): ReactElement => }) } - if (!data) { - return <></> - } - return ( - <VocabularyList title={labels.favorites} documents={data} onFavoritesChanged={refresh} onItemPress={onItemPress} /> + <ServerResponseHandler error={error} loading={false} refresh={refresh}> + {data && ( + <VocabularyList + title={labels.favorites} + documents={data} + onFavoritesChanged={refresh} + onItemPress={onItemPress} + /> + )} + </ServerResponseHandler> ) } diff --git a/src/services/AsyncStorage.ts b/src/services/AsyncStorage.ts index c4333db03..3a472abe3 100644 --- a/src/services/AsyncStorage.ts +++ b/src/services/AsyncStorage.ts @@ -1,7 +1,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { ExerciseKey, Progress } from '../constants/data' -import { Document } from '../constants/endpoints' import { DocumentResult } from '../navigation/NavigationTypes' const SELECTED_PROFESSIONS_KEY = 'selectedProfessions' @@ -80,16 +79,16 @@ export const saveExerciseProgress = async ( await setExerciseProgress(disciplineId, exerciseKey, score) } -const getFavorites = async (): Promise<Document[]> => { +const getFavorites = async (): Promise<number[]> => { const documents = await AsyncStorage.getItem(FAVORITES_KEY) return documents ? JSON.parse(documents) : [] } -const setFavorites = async (favorites: Document[]): Promise<void> => { +const setFavorites = async (favorites: number[]): Promise<void> => { await AsyncStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites)) } -const addFavorite = async (favorite: Document): Promise<void> => { +const addFavorite = async (favorite: number): Promise<void> => { const favorites = await getFavorites() if (favorites.includes(favorite)) { return @@ -98,15 +97,15 @@ const addFavorite = async (favorite: Document): Promise<void> => { await setFavorites(newFavorites) } -const removeFavorite = async (favorite: Document): Promise<void> => { +const removeFavorite = async (favoriteId: number): Promise<void> => { const favorites = await getFavorites() - const newFavorites = favorites.filter(it => it.id !== favorite.id) + const newFavorites = favorites.filter(it => it !== favoriteId) await setFavorites(newFavorites) } -const isFavorite = async (favorite: Document): Promise<boolean> => { +const isFavorite = async (favoriteId: number): Promise<boolean> => { const favorites = await getFavorites() - return favorites.some(it => it.id === favorite.id) + return favorites.includes(favoriteId) } export default { diff --git a/src/services/__tests__/AsyncStorage.spec.ts b/src/services/__tests__/AsyncStorage.spec.ts index 9a6105f0f..b330a44c4 100644 --- a/src/services/__tests__/AsyncStorage.spec.ts +++ b/src/services/__tests__/AsyncStorage.spec.ts @@ -106,7 +106,7 @@ describe('AsyncStorage', () => { }) describe('favorites', () => { - const documents = new DocumentBuilder(4).build() + const documents = new DocumentBuilder(4).build().map(it => it.id) it('should add favorites', async () => { await AsyncStorage.setFavorites(documents.slice(0, 2)) From 09b44c8ca96a2708a5758b7c3c5f4df9ac2e4e2c Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 7 Jun 2022 13:39:57 +0200 Subject: [PATCH 16/17] LUN-132: Fix test --- release-notes/unreleased/LUN-132-favorites.yml | 6 ++++++ src/routes/__tests__/VocabularyListScreen.spec.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 release-notes/unreleased/LUN-132-favorites.yml diff --git a/release-notes/unreleased/LUN-132-favorites.yml b/release-notes/unreleased/LUN-132-favorites.yml new file mode 100644 index 000000000..5f8ff2a60 --- /dev/null +++ b/release-notes/unreleased/LUN-132-favorites.yml @@ -0,0 +1,6 @@ +issue_key: LUN-132 +show_in_stores: true +platforms: + - android + - ios +de: Favoriten können nun gespeichert werden. diff --git a/src/routes/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx index 562861754..e065c9114 100644 --- a/src/routes/__tests__/VocabularyListScreen.spec.tsx +++ b/src/routes/__tests__/VocabularyListScreen.spec.tsx @@ -10,7 +10,7 @@ import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' import render from '../../testing/render' import VocabularyListScreen from '../VocabularyListScreen' -jest.mock('../../../components/FavoriteButton', () => { +jest.mock('../../components/FavoriteButton', () => { const Text = require('react-native').Text return () => <Text>FavoriteButton</Text> }) From 5d1959c834ec3665099e90f34c9e8b4d835ba956 Mon Sep 17 00:00:00 2001 From: Andy <fischer@integreat-app.de> Date: Tue, 7 Jun 2022 16:59:18 +0200 Subject: [PATCH 17/17] LUN-132: fix shadow and alignment --- src/components/FavoriteButton.tsx | 14 ++++---------- src/components/VocabularyListItem.tsx | 9 ++++++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/FavoriteButton.tsx b/src/components/FavoriteButton.tsx index 9dabfbbfc..e6e5779dd 100644 --- a/src/components/FavoriteButton.tsx +++ b/src/components/FavoriteButton.tsx @@ -9,11 +9,6 @@ import useLoadAsync from '../hooks/useLoadAsync' import AsyncStorage from '../services/AsyncStorage' import { reportError } from '../services/sentry' -const Container = styled.View` - padding: ${props => `${props.theme.spacings.xs} 0 ${props.theme.spacings.xs} ${props.theme.spacings.sm}`}; - align-self: center; -` - const Icon = styled(StarCircleIconGreyFilled)` min-width: ${wp('9%')}px; min-height: ${wp('9%')}px; @@ -25,6 +20,7 @@ const IconOutline = styled(StarCircleIconGrey)` const Button = styled.TouchableOpacity` justify-content: center; align-items: center; + shadow-color: ${props => props.theme.colors.shadow}; shadow-radius: 5px; shadow-offset: 1px 1px; shadow-opacity: 0.5; @@ -57,11 +53,9 @@ const FavoriteButton = ({ document, onFavoritesChanged }: Props): ReactElement | } return ( - <Container> - <Button testID={isFavorite ? 'remove' : 'add'} onPress={onPress}> - {isFavorite ? <Icon /> : <IconOutline />} - </Button> - </Container> + <Button testID={isFavorite ? 'remove' : 'add'} onPress={onPress}> + {isFavorite ? <Icon /> : <IconOutline />} + </Button> ) } diff --git a/src/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx index 03e8d0d1b..b2baf8aff 100644 --- a/src/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -30,6 +30,11 @@ const RightChildrenContainer = styled.View` justify-content: space-between; ` +const FavButtonContainer = styled.View` + padding: ${props => `0 ${props.theme.spacings.xs} 0 ${props.theme.spacings.sm}`}; + align-self: center; +` + interface VocabularyListItemProps { document: Document onPress: () => void @@ -54,7 +59,9 @@ const VocabularyListItem = ({ document, onPress, onFavoritesChanged }: Vocabular rightChildren={ <RightChildrenContainer> <AudioPlayer document={document} disabled={false} /> - <FavoriteButton document={document} onFavoritesChanged={onFavoritesChanged} /> + <FavButtonContainer> + <FavoriteButton document={document} onFavoritesChanged={onFavoritesChanged} /> + </FavButtonContainer> </RightChildrenContainer> } />