Browse Source

FEATURE | Add voiceover text & fix bugs (#8)

https://jetup-digital.atlassian.net/jira/software/projects/TRUT/boards/90?selectedIssue=TRUT-18
Co-authored-by: Vlad <vlad960706@gmail.com>
Reviewed-on: #8
Co-authored-by: Vlad Narizhnyi <vlad960706@gmail.com>
Co-committed-by: Vlad Narizhnyi <vlad960706@gmail.com>
main^2
Vlad Narizhnyi 10 months ago committed by Vitalik Yatsenko
parent
commit
fde610608c
  1. BIN
      android/app/src/main/assets/fonts/fontello.ttf
  2. 2
      ios/GoogleService-Info.plist
  3. 6
      ios/Podfile.lock
  4. 4
      ios/Truth.xcodeproj/project.pbxproj
  5. 11
      package-lock.json
  6. 1
      package.json
  7. BIN
      src/assets/resources/fonts/fontello.ttf
  8. 6
      src/config/fontello.json
  9. 1
      src/i18n/interfaces/common.interface.ts
  10. 1
      src/i18n/interfaces/settings.types.interface.ts
  11. 1
      src/i18n/locales/en/common.translation.ts
  12. 1
      src/i18n/locales/en/settings.translation.ts
  13. 11
      src/i18n/locales/ua/common.translation.ts
  14. 36
      src/module/common/questions-dares-list/questions-dares-list.ts
  15. 1
      src/module/common/typing/enums/storage-key.enum.ts
  16. 42
      src/module/game/animations/use-animation-truth-or-dare.hook.ts
  17. 2
      src/module/game/components/index.ts
  18. 2
      src/module/game/components/player-field.component.tsx
  19. 53
      src/module/game/components/truth-or-dare-view.tsx
  20. 104
      src/module/game/components/truth-or-dare.tsx
  21. 1
      src/module/game/config/index.ts
  22. 12
      src/module/game/config/voice.config.ts
  23. 12
      src/module/game/hooks/get-current-truth-dares.hook.tsx
  24. 1
      src/module/game/hooks/index.ts
  25. 42
      src/module/game/hooks/use-voice.hook.tsx
  26. 4
      src/module/game/screens/game.screen.tsx
  27. 16
      src/module/game/screens/players.screen.tsx
  28. 10
      src/module/game/screens/truth-or-dare.screen.tsx
  29. 9
      src/module/packages/animation/use-animation-list.hook.ts
  30. 13
      src/module/packages/atoms/packages-page-separator.atom.tsx
  31. 1
      src/module/root/navigations-groups/user.group.tsx
  32. 9
      src/module/root/screens/loading-screen.tsx
  33. 1
      src/module/settings/atoms/index.ts
  34. 2
      src/module/settings/atoms/switch-notifications.atom.tsx
  35. 33
      src/module/settings/atoms/switch-voiceover.atom.tsx
  36. 11
      src/module/settings/config/settings.config.tsx
  37. 2
      src/module/settings/screens/settings.screen.tsx
  38. 15
      src/store/slices/players.slice.ts

BIN
android/app/src/main/assets/fonts/fontello.ttf

Binary file not shown.

2
ios/GoogleService-Info.plist

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
<string>truth-or-dare-fcc54.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>ITSAppUsesNonExemptEncryption</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>

6
ios/Podfile.lock

@ -1286,6 +1286,8 @@ PODS: @@ -1286,6 +1286,8 @@ PODS:
- RNVectorIcons (9.2.0):
- React-Core
- SocketRocket (0.6.1)
- TextToSpeech (4.1.0):
- React
- Yoga (1.14.0)
- YogaKit (1.18.1):
- Yoga (~> 1.14)
@ -1368,6 +1370,7 @@ DEPENDENCIES: @@ -1368,6 +1370,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- TextToSpeech (from `../node_modules/react-native-tts`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@ -1503,6 +1506,8 @@ EXTERNAL SOURCES: @@ -1503,6 +1506,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
TextToSpeech:
:path: "../node_modules/react-native-tts"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@ -1585,6 +1590,7 @@ SPEC CHECKSUMS: @@ -1585,6 +1590,7 @@ SPEC CHECKSUMS:
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
TextToSpeech: b3aa777ff5585705f179c0a2436bfd0926d1716e
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

4
ios/Truth.xcodeproj/project.pbxproj

@ -547,7 +547,7 @@ @@ -547,7 +547,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 7LY53JU2YB;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -616,7 +616,7 @@ @@ -616,7 +616,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 7LY53JU2YB;
HEADER_SEARCH_PATHS = (
"$(inherited)",

11
package-lock.json generated

@ -36,6 +36,7 @@ @@ -36,6 +36,7 @@
"react-native-splash-screen": "^3.3.0",
"react-native-svg": "^12.5.1",
"react-native-svg-transformer": "^1.0.0",
"react-native-tts": "^4.1.0",
"react-native-vector-icons": "^9.2.0",
"react-redux": "^9.0.4",
"validate.js": "^0.13.1"
@ -13517,6 +13518,11 @@ @@ -13517,6 +13518,11 @@
"react-native-svg": ">=12.0.0"
}
},
"node_modules/react-native-tts": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-native-tts/-/react-native-tts-4.1.0.tgz",
"integrity": "sha512-tvf3lQ6u9MqztUie37qoPw5YQPqi0ql1lPhDsBBs/RRA6A/H1J9X9H/qb1A0Hx0ZpjavrEdtVSqQQ2JDZvZCTQ=="
},
"node_modules/react-native-vector-icons": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",
@ -25487,6 +25493,11 @@ @@ -25487,6 +25493,11 @@
"path-dirname": "^1.0.2"
}
},
"react-native-tts": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-native-tts/-/react-native-tts-4.1.0.tgz",
"integrity": "sha512-tvf3lQ6u9MqztUie37qoPw5YQPqi0ql1lPhDsBBs/RRA6A/H1J9X9H/qb1A0Hx0ZpjavrEdtVSqQQ2JDZvZCTQ=="
},
"react-native-vector-icons": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",

1
package.json

@ -44,6 +44,7 @@ @@ -44,6 +44,7 @@
"react-native-splash-screen": "^3.3.0",
"react-native-svg": "^12.5.1",
"react-native-svg-transformer": "^1.0.0",
"react-native-tts": "^4.1.0",
"react-native-vector-icons": "^9.2.0",
"react-redux": "^9.0.4",
"validate.js": "^0.13.1"

BIN
src/assets/resources/fonts/fontello.ttf

Binary file not shown.

6
src/config/fontello.json

@ -293,6 +293,12 @@ @@ -293,6 +293,12 @@
"width": 1000
},
"search": ["clear"]
},
{
"uid": "572c9ded6a688698dc275b30ff30fefa",
"css": "music",
"code": 59406,
"src": "linecons"
}
]
}

1
src/i18n/interfaces/common.interface.ts

@ -14,4 +14,5 @@ export interface Common { @@ -14,4 +14,5 @@ export interface Common {
limitDesc: string
buyAfterBtn: string
no: string
placeholderPlayer: string
}

1
src/i18n/interfaces/settings.types.interface.ts

@ -8,5 +8,6 @@ export namespace SettingLocale { @@ -8,5 +8,6 @@ export namespace SettingLocale {
share: string
policy: string
label: string
voiceover: string
}
}

1
src/i18n/locales/en/common.translation.ts

@ -20,4 +20,5 @@ export const common: Common = { @@ -20,4 +20,5 @@ export const common: Common = {
'You have reached the limit of questions for this set. You need the full version to continue, but you can use “Under18” and “Light” packages. They are always open for you.',
buyAfterBtn: 'Buy',
no: 'Back',
placeholderPlayer: 'Name'
}

1
src/i18n/locales/en/settings.translation.ts

@ -9,4 +9,5 @@ export const settingTranslation: SettingLocale.Core = { @@ -9,4 +9,5 @@ export const settingTranslation: SettingLocale.Core = {
share: 'Share app',
policy: 'Privacy policy',
label: 'What can we do to help?',
voiceover: 'Voiceover',
}

11
src/i18n/locales/ua/common.translation.ts

@ -7,14 +7,17 @@ const Validation = { @@ -7,14 +7,17 @@ const Validation = {
export const common: Common = {
validate: Validation,
shareMessage: 'Поділіться цим додатком зі своїми друзями',
shareMessage: 'Поділись цим додатком зі своїми друзями',
notFillPlayerTitle: 'Ой',
notFillPlayerMessage: 'Якесь поле не заповнене. Перевір будь ласка!',
notFillPlayerMessage:
'Здається не всі імена заповнені. Перевір будь ласка!',
helpAlertTitle: 'Добре!',
helpAlertDesc: 'Ми розглянемо питання і якнайшвидше дамо відповідь.',
writeUsBtn: 'Відправити',
limitTitle: 'Ліміт вичерпано',
limitDesc: 'Ліміт питань для пакету "Crazy" вичерпано. Щоб продовжити, потрібна повна версія. Але ти можеш використовувати пакети «До 18» та «Легкий». Вони завжди відкриті для тебе.',
limitDesc:
'Ліміт питань для пакету "Crazy" вичерпано. Щоб продовжити, потрібна повна версія. Але ти можеш використовувати пакети «До 18» та «Легкий». Вони завжди відкриті для тебе.',
buyAfterBtn: 'До покупок',
no: 'Назад'
no: 'Назад',
placeholderPlayer: "Ім'я",
}

36
src/module/common/questions-dares-list/questions-dares-list.ts

@ -2,8 +2,8 @@ export const under18 = [ @@ -2,8 +2,8 @@ export const under18 = [
{
id: 1,
isDare: false,
en: 'Tell about your first fight?',
ua: 'Розкажи про свою першу бійку?',
en: 'Tell about your first fight.',
ua: 'Розкажи про свою першу бійку.',
},
{
id: 2,
@ -50,14 +50,14 @@ export const under18 = [ @@ -50,14 +50,14 @@ export const under18 = [
{
id: 9,
isDare: false,
en: 'Name a song that you are uncomfortable listening to in front of others',
ua: 'Назви пісню, яку тобі некомфортно слухати при інших',
en: 'Name a song that you are uncomfortable listening to in front of others.',
ua: 'Назви пісню, яку тобі некомфортно слухати при інших.',
},
{
id: 10,
isDare: true,
en: "Write 'Where's my beer' to your president on the social network ",
ua: 'Напиши "Де моє пиво" своєму президентові в соціальній мережі',
en: "Write 'Where's my beer' to your president on the social network.",
ua: 'Напиши "Де моє пиво" своєму президентові в соціальній мережі.',
},
{
id: 11,
@ -68,8 +68,8 @@ export const under18 = [ @@ -68,8 +68,8 @@ export const under18 = [
{
id: 12,
isDare: true,
en: 'Call someone on video, be silent for 10 seconds and knock out',
ua: 'Подзвони комусь по відео мовчи 10 секунд і вибий',
en: 'Call someone on video, be silent for 10 seconds and knock out.',
ua: 'Подзвони комусь по відео мовчи 10 секунд і вибий.',
},
{
id: 13,
@ -104,8 +104,8 @@ export const under18 = [ @@ -104,8 +104,8 @@ export const under18 = [
{
id: 18,
isDare: true,
en: 'Send a heartfelt message to the fifth person in your contact list, expressing gratitude for something',
ua: "Відправ сердечне повідомлення п'ятій людині у своєму списку контактів, висловлюючи вдячність за щось",
en: 'Send a heartfelt message to the fifth person in your contact list, expressing gratitude for something.',
ua: "Відправ сердечне повідомлення п'ятій людині у своєму списку контактів, висловлюючи вдячність за щось.",
},
{
id: 19,
@ -141,7 +141,7 @@ export const under18 = [ @@ -141,7 +141,7 @@ export const under18 = [
id: 24,
isDare: true,
en: 'Write an message with some hint to the person you like.',
ua: 'Напишіть повідомлення з натяком людині, яка вам подобається.',
ua: 'Напиши повідомлення з натяком людині, яка тобі подобається.',
},
{
id: 25,
@ -287,8 +287,8 @@ export const light = [ @@ -287,8 +287,8 @@ export const light = [
{
id: 7,
isDare: false,
en: 'If you could be invisible for a day, what would you do?',
ua: 'Якщо б ви могли бути невидимими на один день, Що б ти зробив(ла)?',
en: 'You have invisibility for one day. What would you do?',
ua: 'У тебе невидимість на один день. Що б ти зробив(ла)?',
},
{
id: 8,
@ -312,7 +312,7 @@ export const light = [ @@ -312,7 +312,7 @@ export const light = [
id: 11,
isDare: false,
en: 'What’s the most unusual place you’ve ever visited?',
ua: 'Яке найстрашніше місце, в якому ти був(ла)',
ua: 'Яке найстрашніше місце, в якому ти був(ла)?',
},
{
id: 12,
@ -347,8 +347,8 @@ export const light = [ @@ -347,8 +347,8 @@ export const light = [
{
id: 17,
isDare: false,
en: 'Tell the first impression when you saw the person on the left of you',
ua: 'Розкажи перше враження, коли ти побачив(ла) людину ліворуч віж тебе',
en: 'Tell the first impression when you saw the person on the left of you.',
ua: 'Розкажи перше враження, коли ти побачив(ла) людину ліворуч від тебе.',
},
{
id: 18,
@ -462,7 +462,7 @@ export const light = [ @@ -462,7 +462,7 @@ export const light = [
id: 34,
isDare: true,
en: 'Sit with an angry and sad face until the next round.',
ua: 'Сидіть зі злою та сумною міною до наступного раунда.',
ua: 'Сиди зі злою та сумною міною до наступного раунда.',
},
{
id: 35,
@ -603,7 +603,7 @@ export const crazy = [ @@ -603,7 +603,7 @@ export const crazy = [
id: 17,
isDare: true,
en: 'Act like a certain animal until your next turn. Others have to guess the animal.',
ua: 'Ведіть себе як собака до свого наступного ходу.',
ua: 'Веди себе як собака до свого наступного ходу.',
},
{
id: 18,

1
src/module/common/typing/enums/storage-key.enum.ts

@ -9,4 +9,5 @@ export enum StorageKey { @@ -9,4 +9,5 @@ export enum StorageKey {
Players = 'Players',
LimitForCrazy = 'limit',
SavedSteps = 'SavedSteps',
Voiceover = 'Voiceover',
}

42
src/module/game/animations/use-animation-truth-or-dare.hook.ts

@ -1,10 +1,42 @@ @@ -1,10 +1,42 @@
import { useRef } from 'react'
import { Animated } from 'react-native'
import { useTranslation } from 'react-i18next'
import { Animated, Platform } from 'react-native'
import { Language } from '~module/common'
export const useAnimationTruthOrDare = () => {
export const useAnimationTruthOrDare = (wordsArr: string[]) => {
const animContainer = useRef(new Animated.Value(400)).current
const { i18n } = useTranslation()
const startAnimation = () => {
const animValues: Animated.Value[] = []
wordsArr.forEach((_, index) => (animValues[index] = new Animated.Value(0)))
const startAnimTextByVoice = () => {
const animations = wordsArr.map((word, index) => {
const wordWithComma = word.includes(',')
const isEnLanguage = i18n.language === Language.EN
const duration = isEnLanguage ? 50 : 75
const durationWithComma = Platform.select({
android: 75,
default: isEnLanguage ? 175 : 100,
})
return Animated.timing(animValues[index], {
toValue: 1,
duration:
word.length *
(wordWithComma ? durationWithComma : duration),
useNativeDriver: true,
})
})
Animated.sequence(animations).start()
}
const startAnimationCard = () => {
Animated.timing(animContainer, {
toValue: 0,
duration: 400,
@ -21,7 +53,9 @@ export const useAnimationTruthOrDare = () => { @@ -21,7 +53,9 @@ export const useAnimationTruthOrDare = () => {
}
return {
startAnimation,
startAnimationCard,
startAnimTextByVoice,
animValues,
animStyle: animStyleContainer,
}
}

2
src/module/game/components/index.ts

@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
export * from './truth-or-dare-view'
export * from './truth-or-dare'
export * from './player-field.component';

2
src/module/game/components/player-field.component.tsx

@ -24,7 +24,7 @@ export const PlayerField: FC<IProps> = ({ value, onChange, onDelete }) => { @@ -24,7 +24,7 @@ export const PlayerField: FC<IProps> = ({ value, onChange, onDelete }) => {
onChange={onChange}
renderPostfix={renderClear}
inputProps={{
placeholder: t('customPack.placeholder'),
placeholder: t('common.placeholderPlayer'),
placeholderTextColor: colors.darkPurple,
}}
inputStyle={styles.input}

53
src/module/game/components/truth-or-dare-view.tsx

@ -1,53 +0,0 @@ @@ -1,53 +0,0 @@
import React, { useEffect } from 'react'
import { Font, Icon, colors } from '../../common'
import { Animated, StyleSheet } from 'react-native'
import { useAnimationTruthOrDare } from '../animations'
interface IProps {
item: string
}
export const TruthOrDareView: React.FC<IProps> = ({ item }) => {
const { animStyle, startAnimation } = useAnimationTruthOrDare()
useEffect(() => {
startAnimation()
}, [])
return (
<Animated.View style={[styles.container, animStyle]}>
<Icon
name="magic-star"
size={24}
color={colors.turquoise}
style={styles.starIcon}
/>
<Animated.Text style={[styles.text]}>{item}</Animated.Text>
</Animated.View>
)
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 32,
backgroundColor: colors.darkPurple,
borderRadius: 20,
alignItems: 'center',
shadowColor: '#190f42',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 1,
shadowRadius: 4,
elevation: 5,
marginTop: 88,
},
starIcon: {
marginBottom: 14,
},
text: {
color: colors.purple,
fontSize: 22,
lineHeight: 32,
textAlign: 'center',
fontFamily: Font.Roboto400,
},
})

104
src/module/game/components/truth-or-dare.tsx

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
import React, { useEffect, useState } from 'react'
import { Font, Icon, StorageKey, colors, storageService } from '../../common'
import { Animated, StyleSheet, View } from 'react-native'
import { useAnimationTruthOrDare } from '../animations'
import Tts from 'react-native-tts'
interface IProps {
item: string
}
export const TruthOrDare: React.FC<IProps> = ({ item }) => {
const wordsArr = item.trim().split(' ')
const [voiceover, setVoiceover] = useState(true)
const { animStyle, animValues, startAnimationCard, startAnimTextByVoice } =
useAnimationTruthOrDare(wordsArr)
useEffect(() => {
startAnimationCard()
checkAndStartVoiceover()
}, [item])
const checkAndStartVoiceover = async () => {
const isVoicer = await storageService.get(StorageKey.Voiceover)
setVoiceover(isVoicer)
if (isVoicer) {
Tts.stop()
Tts.speak(item)
Tts.addEventListener('tts-start', startAnimTextByVoice)
}
}
return (
<Animated.View style={[styles.container, animStyle]}>
<Icon
name="magic-star"
size={24}
color={colors.turquoise}
style={styles.starIcon}
/>
<View style={styles.textWrap}>
{wordsArr?.map((word, index) => {
return (
<Animated.Text
key={`${word} + ${index * word.length}`}
style={[
styles.text,
{
opacity: voiceover ? animValues[index] : 1,
transform: [
{
translateX: voiceover
? Animated.multiply(
animValues[index],
new Animated.Value(-5),
)
: 0,
},
],
},
]}>
{word + ' '}
</Animated.Text>
)
})}
</View>
</Animated.View>
)
}
const styles = StyleSheet.create({
container: {
paddingLeft: 21,
paddingRight: 11,
paddingVertical: 32,
backgroundColor: colors.darkPurple,
borderRadius: 20,
alignItems: 'center',
shadowColor: '#190f42',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 1,
shadowRadius: 4,
elevation: 5,
marginTop: 88,
},
textWrap: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
},
starIcon: {
marginBottom: 14,
},
text: {
color: colors.purple,
fontSize: 22,
lineHeight: 32,
textAlign: 'center',
fontFamily: Font.Roboto400,
},
})

1
src/module/game/config/index.ts

@ -1 +1,2 @@ @@ -1 +1,2 @@
export * from './package-name.config';
export * from './voice.config';

12
src/module/game/config/voice.config.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { Language } from '~module/common'
export const voiceConfig: any = {
[Language.UA]: {
language: 'uk-UA',
voice: 'com.apple.voice.compact.uk-UA.Lesya',
},
[Language.EN]: {
language: 'en-US',
voice: 'com.apple.ttsbundle.Samantha-compact',
},
}

12
src/module/game/hooks/get-current-truth-dares.hook.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import {
resetStepsByTruthOrDare,
selectCustomPackage,
@ -64,8 +64,12 @@ export const useGetCurrentTruthOrDare = ({ @@ -64,8 +64,12 @@ export const useGetCurrentTruthOrDare = ({
return packageTruthsOrDares?.[currentStep]?.[i18n.language as Language]
}
const customTruthsOrDares = getGameItemsByCustomPackage()
const packageTruthsOrDares = getGameItemsByPackage()
const customTruthsOrDares = useMemo(getGameItemsByCustomPackage, [
customPackageShuffle,
customType,
])
const packageTruthsOrDares = useMemo(getGameItemsByPackage, [gameItems])
const currentItem = getCurrentItem()
const shuffleAndSavePackage = async () => {
@ -136,7 +140,7 @@ export const useGetCurrentTruthOrDare = ({ @@ -136,7 +140,7 @@ export const useGetCurrentTruthOrDare = ({
useEffect(() => {
checkIsNeedShuffle()
}, [currentStep, customTruthsOrDares])
}, [currentStep])
return currentItem
}

1
src/module/game/hooks/index.ts

@ -1,2 +1,3 @@ @@ -1,2 +1,3 @@
export * from './get-current-truth-dares.hook'
export * from './use-set-steps-by-package';
export * from './use-voice.hook';

42
src/module/game/hooks/use-voice.hook.tsx

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
import { useEffect, useState } from 'react'
import Tts from 'react-native-tts'
import { voiceConfig } from '../config'
import { useTranslation } from 'react-i18next'
export const useVoice = () => {
const { i18n } = useTranslation()
const [ttsStatus, setTtsStatus] = useState('initiliazing')
useEffect(() => {
Tts.addEventListener('tts-finish', _event => setTtsStatus('finished'))
Tts.addEventListener('tts-cancel', _event => setTtsStatus('cancelled'))
Tts.getInitStatus().then(initTts)
return () => {
if (ttsStatus === 'finished') {
Tts.removeEventListener('tts-start', _event =>
setTtsStatus('started'),
)
}
Tts.removeEventListener('tts-finish', _event =>
setTtsStatus('finished'),
)
Tts.removeEventListener('tts-cancel', _event =>
setTtsStatus('cancelled'),
)
}
}, [])
const initTts = async () => {
try {
await Tts.setDefaultLanguage(voiceConfig[i18n.language].language)
await Tts.setDefaultVoice(voiceConfig[i18n.language].voice)
await Tts.setDefaultRate(0.5)
} catch (err) {
//Samsung S9 has always this error:
//"Language is not supported"
console.log(`setDefaultLanguage error `, err)
}
}
}

4
src/module/game/screens/game.screen.tsx

@ -17,7 +17,7 @@ import { useIsFocused, useRoute } from '@react-navigation/native' @@ -17,7 +17,7 @@ import { useIsFocused, useRoute } from '@react-navigation/native'
import { useAnimationButton } from '../animations'
import { PlayerName } from '../components/player-name.component'
import { packageNameConfig } from '../config'
import { useSetStepsByPackage } from '../hooks'
import { useSetStepsByPackage, useVoice } from '../hooks'
interface IRouteParams {
packageType?: PackageType
@ -33,6 +33,8 @@ export const GameScreen: FC = () => { @@ -33,6 +33,8 @@ export const GameScreen: FC = () => {
useSetStepsByPackage(packageType)
useVoice()
const randomGame = () => {
const isTruthRandom = Math.random() < 0.5
const customRandom = isTruthRandom

16
src/module/game/screens/players.screen.tsx

@ -37,7 +37,7 @@ export const PlayersScreen: FC = () => { @@ -37,7 +37,7 @@ export const PlayersScreen: FC = () => {
const onDeletePlayer = (index: number) => {
const updatedPlayers = [...players]
if (updatedPlayers.length === 1) return
if (updatedPlayers.length <= 2) return
updatedPlayers.splice(index, 1)
dispatch(setPlayers(updatedPlayers))
@ -66,16 +66,13 @@ export const PlayersScreen: FC = () => { @@ -66,16 +66,13 @@ export const PlayersScreen: FC = () => {
<ScreenLayout
needScroll
scrollStyle={{
flex: 1,
paddingBottom: 40,
flexGrow: 1,
}}
headerComponent={
<Header
title={t('pageTitles.players')}
onPressLeft={onSavePlayers}
/>
<Header title={t('pageTitles.players')} leftIcon="" />
}>
<View style={{ rowGap: 16 }}>
<View style={{ rowGap: 16, flex: 1 }}>
{players.map((player, index) => (
<PlayerField
key={index}
@ -84,13 +81,12 @@ export const PlayersScreen: FC = () => { @@ -84,13 +81,12 @@ export const PlayersScreen: FC = () => {
onDelete={() => onDeletePlayer(index)}
/>
))}
</View>
<ButtonWithIcon
styleBtn={styles.plusBtn}
iconName="add-plus"
onPress={onAddField}
/>
</View>
<ButtonPrimary
iconName="play"
@ -109,7 +105,7 @@ const styles = StyleSheet.create({ @@ -109,7 +105,7 @@ const styles = StyleSheet.create({
marginBottom: 122,
},
plusBtn: {
marginTop: 24,
marginBottom: 24,
borderRadius: 50,
width: 50,
height: 50,

10
src/module/game/screens/truth-or-dare.screen.tsx

@ -20,13 +20,14 @@ import { @@ -20,13 +20,14 @@ import {
onNextPlayer,
selectStep,
} from '../../../store/slices'
import { TruthOrDareView } from '../components'
import { TruthOrDare } from '../components'
import { useAnimationIconsButton } from '../animations'
import { useGetCurrentTruthOrDare } from '../hooks'
import { PlayerName } from '../components/player-name.component'
import { packageNameConfig } from '../config'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import Tts from 'react-native-tts'
interface IRouteParams {
packageType?: PackageType
@ -55,6 +56,7 @@ export const TruthOrDareScreen: React.FC = () => { @@ -55,6 +56,7 @@ export const TruthOrDareScreen: React.FC = () => {
dispatch(onNextPlayer())
dispatch(nextStep(choiceType))
saveLastStep()
Tts.stop()
}
const onAddGameItemToCustomPackage = () => {
@ -67,6 +69,7 @@ export const TruthOrDareScreen: React.FC = () => { @@ -67,6 +69,7 @@ export const TruthOrDareScreen: React.FC = () => {
}
const onPressAddPlus = () => {
Tts.stop()
const subtitleByChoice =
choiceType === ChoiceType.Truth
? t('customPack.addCustomTruth')
@ -157,7 +160,7 @@ export const TruthOrDareScreen: React.FC = () => { @@ -157,7 +160,7 @@ export const TruthOrDareScreen: React.FC = () => {
<View style={{ flex: 1 }}>
<PlayerName />
<TruthOrDareView item={currentItem} />
{currentItem ? <TruthOrDare item={currentItem} /> : null}
<View style={styles.buttons}>
<ButtonWithIcon
@ -175,7 +178,7 @@ export const TruthOrDareScreen: React.FC = () => { @@ -175,7 +178,7 @@ export const TruthOrDareScreen: React.FC = () => {
animation={animScale.func}
animStyle={animScale.style}
/>
{packageType !== PackageType.Custom && (
<ButtonWithIcon
styleBtn={{ width: 101 }}
iconName="add-plus"
@ -183,6 +186,7 @@ export const TruthOrDareScreen: React.FC = () => { @@ -183,6 +186,7 @@ export const TruthOrDareScreen: React.FC = () => {
animation={animScale.func}
animStyle={animScale.style}
/>
)}
</View>
</View>
</ScreenLayout>

9
src/module/packages/animation/use-animation-list.hook.ts

@ -1,10 +1,7 @@ @@ -1,10 +1,7 @@
import React, { useEffect, useRef } from 'react'
import { Animated } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
export const useAnimationList = (delay: number) => {
const isFocus = useIsFocused()
const animTransformX = useRef(new Animated.Value(0)).current
const animTransformY = useRef(new Animated.Value(0)).current
@ -33,12 +30,8 @@ export const useAnimationList = (delay: number) => { @@ -33,12 +30,8 @@ export const useAnimationList = (delay: number) => {
),
])
if (!isFocus) {
return animation.reset()
}
animation.start()
}, [isFocus])
}, [])
const animationStyleItem = {
opacity: animTransformX,

13
src/module/packages/atoms/packages-page-separator.atom.tsx

@ -8,20 +8,17 @@ export const PackagesPageSeparator = () => { @@ -8,20 +8,17 @@ export const PackagesPageSeparator = () => {
useEffect(() => {
Animated.spring(fadeInAnim, {
toValue: 1,
useNativeDriver: true,
delay: 1100,
useNativeDriver: false,
stiffness: 50,
delay: 1400,
}).start()
}, [])
const animStyle = {
transform: [
{
translateY: fadeInAnim.interpolate({
width: fadeInAnim.interpolate({
inputRange: [0, 1],
outputRange: [-900, 0],
outputRange: ['0%', '100%'],
}),
},
],
}
return (

1
src/module/root/navigations-groups/user.group.tsx

@ -35,6 +35,7 @@ export const UserNavigationGroup: FC = () => { @@ -35,6 +35,7 @@ export const UserNavigationGroup: FC = () => {
<UserStack.Screen
name={UserRouteKey.TruthOrDare}
component={TruthOrDareScreen}
options={{ animation: 'fade' }}
/>
<UserStack.Screen

9
src/module/root/screens/loading-screen.tsx

@ -31,6 +31,14 @@ export const LoadingScreen: FC = () => { @@ -31,6 +31,14 @@ export const LoadingScreen: FC = () => {
return await storageService.get(StorageKey.FinishOnBoarding)
}
const setVoiceover = async () => {
const isVoiceover = await storageService.get(StorageKey.Voiceover)
if (isVoiceover === null) {
await storageService.set(StorageKey.Voiceover, true)
}
}
const init = async () => {
const language = await getLanguage()
const isOnBoard = await getOnboardEnd()
@ -56,6 +64,7 @@ export const LoadingScreen: FC = () => { @@ -56,6 +64,7 @@ export const LoadingScreen: FC = () => {
useEffect(() => {
init()
setVoiceover()
}, [])
return (

1
src/module/settings/atoms/index.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
export * from './selected-language-in-settings.atom'
export * from './switch-notifications.atom'
export * from './purchases.atom'
export * from './switch-voiceover.atom';

2
src/module/settings/atoms/switch-notifications.atom.tsx

@ -6,7 +6,7 @@ export const SwitchNotificationsAtom = () => { @@ -6,7 +6,7 @@ export const SwitchNotificationsAtom = () => {
const [isEnabled, setIsEnabled] = useState(true)
const toggleSwitch = () => {
setIsEnabled(previousState => !previousState)
setIsEnabled(!isEnabled)
}
return (

33
src/module/settings/atoms/switch-voiceover.atom.tsx

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import React, { useEffect, useState } from 'react'
import { Switch } from 'react-native'
import { StorageKey, colors, storageService } from '../../common'
export const SwitchVoiceoverAtom = () => {
const [voiceover, setVoiceover] = useState(true)
const toggleSwitch = async () => {
setVoiceover(!voiceover)
await storageService.set(StorageKey.Voiceover, !voiceover)
}
const getIsVoiceover = async () => {
const isVoiceover = await storageService.get(StorageKey.Voiceover)
setVoiceover(isVoiceover)
}
useEffect(() => {
getIsVoiceover()
}, [])
return (
<Switch
trackColor={{ true: colors.purple }}
thumbColor={voiceover ? colors.turquoise : colors.darkPurple}
ios_backgroundColor={colors.purple}
onValueChange={toggleSwitch}
style={{ width: 51 }}
value={voiceover}
/>
)
}

11
src/module/settings/config/settings.config.tsx

@ -1,6 +1,10 @@ @@ -1,6 +1,10 @@
import React from 'react'
import { SettingLocale } from '../../../i18n/interfaces/settings.types.interface'
import { SelectedLanguage, SwitchNotificationsAtom } from '../atoms'
import {
SelectedLanguage,
SwitchNotificationsAtom,
SwitchVoiceoverAtom,
} from '../atoms'
const translatePath = (itemKey: keyof SettingLocale.Core) =>
`settingTranslation.${itemKey}`
@ -20,6 +24,11 @@ export const settingsConfig = [ @@ -20,6 +24,11 @@ export const settingsConfig = [
image: 'notification',
component: () => <SwitchNotificationsAtom />,
},
{
title: translatePath('voiceover'),
image: 'music',
component: () => <SwitchVoiceoverAtom />,
},
{
title: translatePath('write'),
image: 'message',

2
src/module/settings/screens/settings.screen.tsx

@ -67,6 +67,8 @@ export const SettingsScreen: FC = () => { @@ -67,6 +67,8 @@ export const SettingsScreen: FC = () => {
break
case 'notification':
break
case 'voice':
break
case 'message':
nav.navigate(UserRouteKey.WriteToUs)
break

15
src/store/slices/players.slice.ts

@ -7,22 +7,18 @@ export interface PlayersState { @@ -7,22 +7,18 @@ export interface PlayersState {
players: string[]
playerIndex: number
currentPlayer: string
loaded: boolean
hasError: boolean
}
const initialState: PlayersState = {
players: [],
playerIndex: 0,
currentPlayer: '',
loaded: false,
hasError: false,
}
export const getPlayersAsync = createAsyncThunk('get-players', async () => {
const savedPlayers = await storageService.get(StorageKey.Players)
return savedPlayers ? savedPlayers : ['']
return savedPlayers ? savedPlayers : ['', '']
})
export const playersSlice = createSlice({
@ -47,17 +43,10 @@ export const playersSlice = createSlice({ @@ -47,17 +43,10 @@ export const playersSlice = createSlice({
},
},
extraReducers(builder) {
builder
.addCase(getPlayersAsync.fulfilled, (state, action) => {
builder.addCase(getPlayersAsync.fulfilled, (state, action) => {
state.players = action.payload
state.currentPlayer = state.players[0]
})
.addCase(getPlayersAsync.pending, state => {
state.loaded = false
})
.addCase(getPlayersAsync.rejected, state => {
state.hasError = true
})
},
})

Loading…
Cancel
Save