openLink(url)}\n />\n );\n }\n};\n\nconst ResponseLinks = ({query, response, handleReprocessQuery, disabled}) => {\n const results = [];\n\n for (let i = 1; i < 5; i++) {\n if (response[`link_${i}_type`]) {\n results.push(\n \n );\n }\n }\n\n if (!results.length) return null;\n\n return (\n \n {results}\n \n );\n};\n\nexport default ResponseLinks;\n","import React, {useEffect, useRef, useState} from 'react';\nimport {\n View,\n Platform,\n ActivityIndicator,\n} from 'react-native';\n\nimport * as Location from 'expo-location';\n\nlet Maps;\nlet PlacesMap;\n\nif (Platform.OS !== 'web') {\n Maps = require('react-native-maps');\n\n // eslint-disable-next-line react/display-name\n PlacesMap = ({cards}) => {\n const mapViewRef = useRef();\n\n const [showUserLocation, setShowUserLocation] = useState(false);\n\n useEffect(() => {\n const checkLocationPermission = async () => {\n const locationPerm = await Location.getForegroundPermissionsAsync();\n\n if (locationPerm.granted) {\n setShowUserLocation(true);\n }\n };\n\n checkLocationPermission();\n }, []);\n\n return (\n \n {\n setTimeout(() => mapViewRef.current && mapViewRef.current.fitToSuppliedMarkers(\n cards.map(({id}) => id.toString()),\n {\n animated: false,\n edgePadding: Platform.OS === 'ios'\n ? {\n top: 40,\n right: 15,\n bottom: 15,\n left: 15,\n }\n : {\n top: 150,\n right: 50,\n bottom: 50,\n left: 50,\n },\n }\n ), 0);\n }}\n >\n {cards.map(card => card.card_type === 'place' && (\n \n ))}\n \n \n );\n };\n} else {\n Maps = require('@react-google-maps/api');\n\n // eslint-disable-next-line react/display-name\n PlacesMap = ({cards}) => {\n const {isLoaded, loadError} = Maps.useLoadScript({\n googleMapsApiKey: 'AIzaSyC6fgqbEA1FjrlKFs1Xz85Fps8tm_enBvI',\n });\n\n const renderMap = () => {\n // wrapping to a function is useful in case you want to access `window.google`\n // to eg. setup options or create latLng object, it won't be available otherwise\n // feel free to render directly if you don't need that\n\n function onLoad (mapInstance) {\n // do something with map Instance\n const bounds = new window.google.maps.LatLngBounds();\n\n cards.forEach(card => {\n if (card.card_type === 'place') {\n bounds.extend(new window.google.maps.LatLng(card.latitude, card.longitude));\n }\n });\n\n mapInstance.fitBounds(bounds);\n }\n\n return (\n \n \n {cards.map(card => card.card_type === 'place' && (\n \n ))}\n \n \n );\n };\n\n if (loadError) {\n return Map cannot be loaded right now, sorry.
;\n }\n\n return isLoaded ? renderMap() : ;\n };\n}\n\nexport default PlacesMap;\n","import React, {useCallback, useContext, useMemo} from 'react';\nimport {\n Text,\n View,\n ScrollView,\n StyleSheet,\n TouchableOpacity,\n Share,\n Platform,\n Dimensions,\n} from 'react-native';\n\nimport {FontAwesome, MaterialIcons} from '@expo/vector-icons';\nimport {useTranslation} from 'react-i18next';\nimport * as Sentry from 'sentry-expo';\nimport {useNavigation} from '@react-navigation/native';\n\nimport Api from '../../../constants/Api';\nimport {ThemeContext, WEB_MAX_WIDTH} from '../../../contexts/ThemeContext';\nimport {formatDate, formatTime} from '../../../helpers/dateHelper';\nimport {debounce} from '../../../utils/debounce';\n\nimport Card from './Card';\nimport Bubble from './Bubble';\nimport ResponseLinks from './ResponseLinks';\nimport PlacesMap from './PlacesMap';\nimport {UserContext} from '../../../contexts/UserContext';\n\nconst CHANNEL_ICONS = {\n Recording: ,\n recording: ,\n Web: ,\n web: ,\n Whatsapp: ,\n whatsapp: ,\n Email: ,\n email: ,\n Slack: ,\n slack: ,\n Telegram: ,\n telegram: ,\n Keyboard: ,\n keyboard: ,\n Sms: ,\n sms: ,\n Message: ,\n message: ,\n};\n\nconst Chat = ({\n scrollViewRef,\n onScrollBeginDrag,\n queries,\n setQueryText,\n handleReprocessQuery,\n exampleSections,\n canStopGenerating,\n disableResponseLinks = false,\n disableQueryLinks = false,\n}) => {\n const {t} = useTranslation();\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n const {navigate} = useNavigation();\n const {user} = useContext(UserContext);\n\n let currentDate = queries.length ? new Date(queries[0].created_at) : null;\n\n const handleShare = useCallback((query, response) => debounce(async () => {\n try {\n let cardsText = '';\n\n if (response.cards.length) {\n if (response.cards.length === 1) {\n // TODO: Handle weather and shazam cards\n const card = response.cards[0];\n\n if (card.card_type === 'image') {\n cardsText = `\\n${card.image_url}`;\n }\n } else {\n const texts = response.cards.reduce((result, card) => {\n if (card.card_type === 'place') {\n result.push(`- ${card.title} (${card.link_1_url})`);\n }\n\n return result;\n }, []);\n\n if (texts.length) {\n cardsText = `\\n\\n${texts.join('\\n')}`;\n }\n }\n }\n\n const result = await Share.share({\n message: `${response.message}${cardsText}\\n\\n${t('MainScreen.ChatView.Chat.sharedByText', 'Message shared via {{appName}}.', {appName: Api.appName})}`,\n }, {\n subject: query.text,\n });\n\n if (result.action === Share.sharedAction) {\n if (result.activityType) {\n // shared with activity type of result.activityType\n } else {\n // shared\n }\n } else if (result.action === Share.dismissedAction) {\n // dismissed\n }\n } catch (error) {\n // TODO: Handle errors\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n }, 300, true), [t]);\n\n return (\n \n {!(queries || []).length && (exampleSections || []).length ? (\n \n \n {t(\n 'MainScreen.ChatView.Chat.exampleIntroduction',\n 'Here are some examples of the things you can ask :'\n )}\n \n\n {exampleSections.map(({title, examples}) => (\n \n \n {title}\n \n\n {(examples || []).length > 0 && (\n \n {examples.map((example, i) => (\n setQueryText(example)}>\n 600 ? 3 : 2) - 18,\n height: 100,\n justifyContent: 'center',\n paddingHorizontal: 12,\n }, {\n marginRight: i === examples.length - 1 ? 0 : 12,\n }]}\n >\n \n {example}\n \n \n \n ))}\n \n )}\n \n ))}\n \n ) : null}\n\n {queries.map((query, i) => {\n if (Object.keys(query).length === 0) {\n return null;\n }\n\n let shouldDisplayDate = i === 0;\n\n const queryDate = new Date(query.created_at);\n\n if (\n queryDate.getDate() !== currentDate.getDate()\n || queryDate.getMonth() !== currentDate.getMonth()\n || queryDate.getFullYear() !== currentDate.getFullYear()\n ) {\n currentDate = queryDate;\n shouldDisplayDate = true;\n }\n\n // if (query.status === 'pending') {\n // const fakeResponseId = uuid.v4();\n //\n // query.responses.push({\n // id: fakeResponseId,\n // message: null,\n // created_at: Date.now(),\n // channel: query.channel,\n // source: undefined,\n // cards: [],\n // });\n // }\n\n return (\n \n {shouldDisplayDate && (\n \n )}\n\n {!user.expert_mode || disableQueryLinks || query.status === 'processing'\n ? (\n \n )\n : (\n navigate('Query', {id: query.id})}>\n \n \n )}\n\n\n \n {CHANNEL_ICONS[query.channel]} {formatTime(queryDate)}\n \n\n {query.status === 'empty'\n ? null\n : !query.responses || !query.responses.length\n ? queryDate.getTime() > Date.now() - 60000\n // Note: Loading\n ? query.text ? : null\n // Note: Timed out\n : (\n \n )\n // Note: Actual responses\n : query.responses.sort((a, b) => a.id - b.id).map(response => (\n \n \n \n \n \n\n \n \n \n \n \n \n\n {(response.cards || []).length > 0 && response.cards.find(({card_type}) => card_type === 'place') && (\n \n )}\n\n {(response.cards || []).length > 0 && (\n \n {response.cards.map((card, i) => (\n \n ))}\n \n )}\n\n \n <>{CHANNEL_ICONS[response.channel]} {formatTime(new Date(response.created_at))}{response.source ? ` ${t('MainScreen.ChatView.Chat.source', '- Source: {{source}}', {source: response.source})}`: ''}>\n \n \n ))\n }\n \n );\n })}\n \n );\n};\n\nconst getStyles = theme => StyleSheet.create({\n examplesContainer: {\n flex: 1,\n paddingTop: 30,\n },\n text: {\n textAlign: 'center',\n color: theme.textPrimary,\n fontSize: 16,\n },\n});\n\nexport default Chat;\n","export function debounce(func, wait, immediate, context) {\n\tlet result;\n\tlet timeout = null;\n\n\treturn function() {\n\t\tconst ctx = context || this, args = arguments;\n\t\tconst later = function () {\n\t\t\ttimeout = null;\n\t\t\tif (!immediate) result = func.apply(ctx, args);\n\t\t};\n\t\tconst callNow = immediate && !timeout;\n\t\tclearTimeout(timeout);\n\t\ttimeout = setTimeout(later, wait);\n\t\tif (callNow) result = func.apply(ctx, args);\n\t\treturn result;\n\t};\n}\n","import {useState, useCallback} from 'react';\n\nimport {Alert, Linking} from 'react-native';\nimport {Audio, InterruptionModeAndroid, InterruptionModeIOS} from 'expo-av';\nimport {useTranslation} from 'react-i18next';\n\nconst recordingOptions = {\n isMeteringEnabled: true,\n android: {\n extension: '.m4a',\n outputFormat: Audio.AndroidOutputFormat.AMR_WB,\n audioEncoder: Audio.AndroidAudioEncoder.AMR_WB,\n sampleRate: 16000,\n numberOfChannels: 1,\n bitRate: 16000,\n },\n ios: {\n extension: '.wav',\n // audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH,\n audioQuality: Audio.IOSAudioQuality.HIGH,\n sampleRate: 44100,\n numberOfChannels: 1,\n bitRate: 128000,\n linearPCMBitDepth: 16,\n linearPCMIsBigEndian: false,\n linearPCMIsFloat: false,\n },\n web: {\n mimeType: 'audio/webm',\n bitsPerSecond: 128000,\n },\n};\n\nconst INITIAL_STATE = {\n status: 'initial',\n recordingFile: null,\n startingPromise: null,\n};\n\nconst useRecorder = () => {\n const {t} = useTranslation();\n const [state, setState] = useState(INITIAL_STATE);\n\n const requestPermissions = useCallback(async beforeRequestingCallback => {\n const audioPerm = await Audio.getPermissionsAsync();\n console.log('Audio perm before request', audioPerm);\n\n if (audioPerm.granted === false) {\n beforeRequestingCallback();\n\n if (audioPerm.canAskAgain === false) {\n return Alert.alert(\n t('useRecorder.alert.title', 'Microphone permission needed'),\n t('useRecorder.alert.message', 'Microphone permission is denied, please go to settings to enable it.'),\n [\n {text: t('useRecorder.alert.buttons.cancel', 'Cancel'), style: 'cancel'},\n {text: t('useRecorder.alert.buttons.confirm', 'Open settings'), onPress: () => Linking.openSettings()},\n ]\n );\n }\n\n console.log('Requesting permissions..');\n return Audio.requestPermissionsAsync();\n }\n }, []);\n\n const start = useCallback(async () => {\n if (state.status !== 'initial') {\n return ;\n }\n\n const audioRecording = new Audio.Recording();\n\n // eslint-disable-next-line no-async-promise-executor\n const startingPromise = new Promise(async (resolve) => {\n await Audio.setAudioModeAsync({\n allowsRecordingIOS: true,\n interruptionModeIOS: InterruptionModeIOS.MixWithOthers,\n playsInSilentModeIOS: true,\n shouldDuckAndroid: true,\n interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,\n playThroughEarpieceIOS: false,\n playThroughEarpieceAndroid: false,\n });\n\n const beforePrep = Date.now();\n await audioRecording.prepareToRecordAsync(recordingOptions);\n console.log('prepareToRecordAsync duration', Date.now() - beforePrep);\n\n const beforeStart = Date.now();\n await audioRecording.startAsync();\n console.log('startAsync duration', Date.now() - beforeStart);\n\n console.log('Recording started: ', audioRecording);\n\n setState({status: 'recording', recordingFile: audioRecording, startingPromise});\n\n resolve(audioRecording);\n });\n\n setState({status: 'starting', recordingFile: audioRecording, startingPromise});\n\n return startingPromise;\n }, [state]);\n\n const stop = useCallback(async () => {\n if (state.status === 'initial') {\n return ;\n }\n\n await state.startingPromise;\n\n try {\n await state.recordingFile.stopAndUnloadAsync();\n\n console.log('Recording stopped');\n\n setState(INITIAL_STATE);\n\n return state.recordingFile.getURI();\n } catch (error) {\n if (error.code === 'E_AUDIO_NODATA') {\n // Note: This error happen on android when recording is stopped too quickly, in this case\n // the recording file should be discarded.\n // See: https://docs.expo.dev/versions/v47.0.0/sdk/audio/#stopandunloadasync\n\n setState(INITIAL_STATE);\n }\n\n // TODO: Handle error\n console.log('Error stopping the recording', error);\n }\n }, [state]);\n\n return {\n state,\n requestPermissions,\n start,\n stop,\n };\n};\n\nexport default useRecorder;\n","import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';\n\nimport {Dimensions, Pressable, StyleSheet, View} from 'react-native';\n\nimport {ThemeContext} from '../contexts/ThemeContext';\n\nconst MODAL_HORIZONTAL_MARGIN = 12;\nconst MODAL_PADDING = 10;\nconst BORDER_WIDTH = 1;\nconst TRIANGLE_SIZE = 10;\n\nconst Menu = ({\n style,\n anchor,\n open,\n close,\n position = 'top',\n variant = 'regular',\n children,\n}) => {\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme, {position, variant}), [theme]);\n\n const menuRef = useRef(null);\n\n const [menuMeasurement, setMenuMeasurement] = useState({height: 0, x: -1000, opacity: 0});\n const [anchorMeasurement, setAnchorMeasurement] = useState({width: 0, x: -1000});\n\n useEffect(() => {\n if (menuRef.current) {\n setTimeout(() => menuRef.current.measure((fx, fy, width, height, px) => {\n setMenuMeasurement({height, x: px, opacity: 1});\n }), 0);\n }\n }, [open]);\n\n useEffect(() => {\n if (anchor.current) {\n setTimeout(() => anchor && anchor.current && anchor.current.measure((fx, fy, width, height, px) => {\n setAnchorMeasurement({width, x: px});\n }), 0);\n }\n }, [anchor, open]);\n\n if (!open) return null;\n\n return (\n \n \n {children}\n \n\n {variant === 'regular' && (\n \n )}\n\n \n \n );\n};\n\nconst getStyles = (theme, {position, variant}) => StyleSheet.create({\n modalView: {\n position: 'absolute',\n right: MODAL_HORIZONTAL_MARGIN,\n backgroundColor: variant === 'regular' ? theme.headerBackground : theme[variant],\n borderRadius: 10,\n ...(variant === 'regular'\n ? {\n borderStyle: 'solid',\n borderColor: theme.colorSecondary,\n borderWidth: BORDER_WIDTH,\n }\n : {}\n ),\n padding: MODAL_PADDING,\n alignItems: 'center',\n },\n modalText: {\n color: theme.textPrimary,\n fontSize: 16,\n textAlign: 'center',\n },\n triangle: {\n position: 'absolute',\n width: 0,\n height: 0,\n borderStyle: 'solid',\n borderLeftWidth: TRIANGLE_SIZE,\n borderLeftColor: 'transparent',\n borderRightWidth: TRIANGLE_SIZE,\n borderRightColor: 'transparent',\n },\n innerTriangle: {\n ...(position === 'top'\n ? {\n bottom: -(TRIANGLE_SIZE - BORDER_WIDTH),\n borderTopWidth: TRIANGLE_SIZE,\n borderTopColor: variant === 'regular' ? theme.headerBackground : theme[variant],\n }\n : {\n top: -(TRIANGLE_SIZE - BORDER_WIDTH),\n borderBottomWidth: TRIANGLE_SIZE,\n borderBottomColor: variant === 'regular' ? theme.headerBackground : theme[variant],\n }\n ),\n },\n outerTriangle: {\n ...(position === 'top'\n ? {\n bottom: -TRIANGLE_SIZE,\n borderTopWidth: TRIANGLE_SIZE,\n borderTopColor: variant === 'regular' ? theme.colorSecondary : undefined,\n }\n : {\n top: -TRIANGLE_SIZE,\n borderBottomWidth: TRIANGLE_SIZE,\n borderBottomColor: variant === 'regular' ? theme.colorSecondary : undefined,\n }\n ),\n },\n});\n\nexport default Menu;\n","import React, {forwardRef, useContext, useMemo} from 'react';\nimport {StyleSheet, TouchableOpacity, View} from 'react-native';\n\nimport {ThemeContext} from '../../contexts/ThemeContext';\n\nexport const BUTTON_SIZE = 47;\nexport const BUTTON_MARGIN_LEFT = 8;\n\n// eslint-disable-next-line react/display-name\nconst FooterButton = forwardRef(({disabled, iconBackgroundColor, icon, ...props}, ref) => {\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n\n return (\n \n \n {icon}\n \n \n );\n});\n\nconst getStyles = () => StyleSheet.create({\n touch: {\n height: '100%',\n justifyContent: 'center',\n },\n icon: {\n width: BUTTON_SIZE,\n height: BUTTON_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n marginLeft: BUTTON_MARGIN_LEFT,\n marginRight: 12,\n borderRadius: BUTTON_SIZE / 2,\n },\n});\n\nexport default FooterButton;\n","import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';\n\nimport {Animated, Dimensions, Image, Platform, StyleSheet, Text} from 'react-native';\nimport uuid from 'react-native-uuid';\nimport {useTranslation} from 'react-i18next';\nimport * as Device from 'expo-device';\nimport * as Location from 'expo-location';\nimport * as Haptics from 'expo-haptics';\nimport * as Sentry from 'sentry-expo';\nimport Constants from 'expo-constants';\n\nimport Api from '../../constants/Api';\nimport {UserContext} from '../../contexts/UserContext';\nimport {SettingsContext} from '../../contexts/SettingsContext';\nimport {ThemeContext} from '../../contexts/ThemeContext';\nimport {formatHoursAndMinutes} from '../../helpers/dateHelper';\nimport useRecorder from '../../hooks/useRecorder';\nimport useSpeaker from '../../hooks/useSpeaker';\nimport Menu from '../../components/Menu';\n\nimport FooterButton from './FooterButton';\n\nconst LONG_PRESS_TIME = 300;\n\nlet alertCloseTimeout;\nlet timerInterval;\nlet microphonePermissionsRequested = false;\n\nconst Recorder = ({disabled, pushLocalQuery, updateCurrentQuery, createQuery, scrollViewRef}) => {\n const {t} = useTranslation();\n const {userToken} = useContext(UserContext);\n const {settings: {language}} = useContext(SettingsContext);\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n\n const recordButtonRef = useRef(null);\n\n const recorder = useRecorder();\n const speaker = useSpeaker();\n const [shortPressAlertVisible, setShortPressAlertVisible] = useState(false);\n const [displayTime, setDisplayTime] = useState(0);\n const [uploading, setUploading] = useState(false);\n\n const handleUploadRecording = useCallback(async (uri, ref) => {\n try {\n const formData = new FormData();\n\n formData.append('user_token', userToken);\n formData.append('recording[ref]', ref);\n\n if(Platform.OS === 'web') {\n const blob = await (await fetch(uri)).blob();\n\n formData.append('recording[file]', blob, 'recording_' + Constants.sessionId + '_' + Date.now() + '.wav');\n } else {\n\n formData.append('recording[file]', {\n uri,\n type: 'audio/x-wav',\n name: 'recording_' + Constants.sessionId + '_' + Date.now() + '.wav',\n });\n }\n\n formData.append('recording[language]', language); // fr-FR\n formData.append('recording[device_brand]', Device.brand); // Android: \"google\", \"xiaomi\"; iOS: \"Apple\"; web: null\n formData.append('recording[device_name]', Device.deviceName); // \"Vivian's iPhone XS\"\n formData.append('recording[device_manufacturer]', Device.manufacturer); // Android: \"Google\", \"xiaomi\"; iOS: \"Apple\"; web: \"Google\", null\n formData.append('recording[device_os_name]', Platform.OS);\n formData.append('recording[device_os_version]', Device.osVersion);\n formData.append('recording[device_model_name]', Device.modelName);\n formData.append('recording[expo_session_id]', Constants.sessionId);\n formData.append('recording[expo_installation_id]', Constants.installationId);\n\n if(Platform.OS === 'web') {\n formData.append('recording[encoding]', 'WEBM_OPUS');\n formData.append('recording[sample_rate]', '48000');\n formData.append('recording[bit_rate]', '128000');\n } else if(Platform.OS === 'ios') {\n formData.append('recording[encoding]', 'LINEAR16');\n formData.append('recording[sample_rate]', '48000');\n formData.append('recording[bit_rate]', '128000');\n } else {\n formData.append('recording[encoding]', 'AMR_WB');\n formData.append('recording[sample_rate]', '16000');\n formData.append('recording[bit_rate]', '16000');\n }\n\n const deviceType = await Device.getDeviceTypeAsync();\n if(deviceType === 0) { formData.append('recording[device_type]', 'unknown'); }\n else if(deviceType === 1) { formData.append('recording[device_type]', 'phone'); }\n else if(deviceType === 2) { formData.append('recording[device_type]', 'tablet'); }\n else if(deviceType === 3) { formData.append('recording[device_type]', 'desktop'); }\n else if(deviceType === 4) { formData.append('recording[device_type]', 'tv'); }\n else { formData.append('recording[device_type]', 'UNKNOWN'); }\n\n let locationPerm = {};\n\n if (Platform.OS === 'web') {\n const {state} = await navigator.permissions.query({name: 'geolocation'});\n\n locationPerm = {granted: state === 'granted'};\n } else {\n locationPerm = await Location.getForegroundPermissionsAsync();\n }\n\n if (locationPerm.granted) {\n let location = await Location.getLastKnownPositionAsync({\n requiredAccuracy: 300, // Note: Return null if ~300 meter away from last known location\n maxAge: 600000, // Note: Return null if last known location is more than 10m old\n });\n\n if (!location) {\n location = await Location.getCurrentPositionAsync({\n accuracy: Location.Accuracy.High,\n mayShowUserSettingsDialog: false,\n });\n }\n\n formData.append('recording[latitude]', location.coords.latitude);\n formData.append('recording[longitude]', location.coords.longitude);\n }\n\n const response = await fetch(`${Api.apiBaseUrl}/recordings.json`, {\n method: 'POST',\n body: formData,\n });\n\n return await response.json();\n } catch (error) {\n // TODO: Handle errors\n console.log('There was an error during upload.', error);\n throw error;\n }\n }, [userToken, language]);\n\n const scaleAnim = useRef(new Animated.Value(1.0)).current;\n const translateAnim = useRef(new Animated.Value(Dimensions.get('window').width)).current;\n\n const recorderAnimation = useMemo(() => (\n Animated.stagger(400, [\n Animated.timing(translateAnim, {toValue: 0, duration: 400, useNativeDriver: true}),\n Animated.loop(\n Animated.sequence([\n Animated.timing(scaleAnim, {toValue: 1.0, duration: 400, useNativeDriver: true}),\n Animated.timing(scaleAnim, {toValue: 0.6, duration: 400, useNativeDriver: true}),\n Animated.timing(scaleAnim, {toValue: 1.0, duration: 400, useNativeDriver: true}),\n ])\n ),\n ])\n ), [scaleAnim, translateAnim]);\n\n const handleLongPress = useCallback(async () => {\n // Note: If this press triggered a permission request we don't want to do anything else\n if (microphonePermissionsRequested) {\n microphonePermissionsRequested = false;\n return;\n }\n\n speaker.stop();\n\n if (Platform.OS !== 'web') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle.High);\n }\n\n await recorder.start();\n\n const startTime = Date.now();\n\n timerInterval = setInterval(() => {\n setDisplayTime(Math.round((Date.now() - startTime) / 1000));\n }, 1000);\n\n recorderAnimation.start();\n }, [recorder, recorderAnimation, speaker]);\n\n const handlePress = useCallback(async () => {\n // Note: If this press triggered a permission request we don't want to do anything else\n if (microphonePermissionsRequested) {\n microphonePermissionsRequested = false;\n return;\n }\n\n if (Platform.OS !== 'web') {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n }\n\n setShortPressAlertVisible(true);\n\n // Note: We always clear the last alertCloseTimeout to replace it with the new one\n clearTimeout(alertCloseTimeout);\n alertCloseTimeout = setTimeout(() => setShortPressAlertVisible(false), 2000);\n }, []);\n\n const handlePressIn = useCallback(async () => {\n await recorder.requestPermissions(() => {\n microphonePermissionsRequested = true;\n });\n }, [recorder]);\n\n const handlePressOut = useCallback(async () => {\n if (recorder.state.status === 'starting') {\n recorder.stop();\n } else if (recorder.state.status === 'recording') {\n try {\n setUploading(true);\n\n if (Platform.OS !== 'web') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle.High);\n }\n\n const uri = await recorder.stop();\n\n const ref = uuid.v4();\n\n const localQuery = {\n ref,\n status: 'processing',\n text: null,\n channel: 'recording',\n created_at: new Date(),\n updated_at: new Date(),\n responses: [],\n };\n\n // Note: Optimistically add a fake query until the server return the result\n const localQueries = pushLocalQuery(localQuery);\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n\n let recording;\n\n try {\n recording = await handleUploadRecording(uri, ref);\n\n // Note: Update localQuery text with the transcription of the recording\n localQuery.text = recording.transcription;\n\n if (recording.status === 'failed') {\n localQuery.text = t('MainScreen.ChatView.Recorder.errors.noAudio', 'No audio detected');\n localQuery.status = 'empty';\n }\n } catch (error) {\n localQuery.text = t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.');\n localQuery.status = 'empty';\n\n recording = null;\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n\n updateCurrentQuery(localQueries, localQuery);\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n\n Animated.timing(translateAnim, {\n toValue: Dimensions.get('window').width,\n duration: 300,\n useNativeDriver: true,\n }).start(async () => {\n setUploading(false);\n\n if (Platform.OS !== 'web') {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);\n }\n\n clearInterval(timerInterval);\n recorderAnimation.reset();\n setDisplayTime(0);\n\n if (recording && recording.status !== 'failed') {\n await createQuery({\n ref,\n channel: 'recording',\n text: recording.transcription,\n recordingId: recording.id,\n }, localQueries, localQuery);\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }\n });\n } catch (error) {\n console.log('There was an error during upload.', error);\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n }\n }, [\n recorder,\n pushLocalQuery,\n updateCurrentQuery,\n translateAnim,\n scrollViewRef,\n handleUploadRecording,\n t,\n recorderAnimation,\n createQuery,\n ]);\n\n return (\n <>\n \n {uploading\n ? (\n \n {t('MainScreen.ChatView.Recorder.uploading', 'Uploading...')}\n \n )\n : (\n <>\n \n\n \n {formatHoursAndMinutes(Math.floor(displayTime / 60), (displayTime % 60))}\n \n >\n )}\n \n\n {!uploading && (\n \n }\n />\n )}\n\n \n >\n );\n};\n\nconst getStyles = () => StyleSheet.create({\n centeredView: {\n flex: 1,\n },\n modalText: {\n color: '#fff',\n fontSize: 16,\n textAlign: 'center',\n },\n});\n\nexport default Recorder;\n","import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';\n\nimport {\n ActivityIndicator,\n AppState,\n Dimensions,\n Keyboard,\n Platform,\n SafeAreaView,\n StyleSheet,\n View,\n} from 'react-native';\nimport uuid from 'react-native-uuid';\n\nimport * as Device from 'expo-device';\nimport * as Location from 'expo-location';\nimport * as Haptics from 'expo-haptics';\nimport * as Localization from 'expo-localization';\nimport * as Sentry from 'sentry-expo';\nimport {FontAwesome} from '@expo/vector-icons';\nimport Constants from 'expo-constants';\nimport {useTranslation} from 'react-i18next';\n\nimport EventSource from '../../utils/SourceEvent';\nimport {UserContext} from '../../contexts/UserContext';\nimport {SettingsContext} from '../../contexts/SettingsContext';\nimport {ThemeContext, WEB_MAX_WIDTH} from '../../contexts/ThemeContext';\nimport {throttle} from '../../utils/throttle';\nimport useSpeaker from '../../hooks/useSpeaker';\nimport Api from '../../constants/Api';\nimport StyledInput from '../../components/StyledInput';\n\nimport Chat from './Chat/Chat';\nimport Recorder from './Recorder';\nimport FooterButton, {BUTTON_MARGIN_LEFT, BUTTON_SIZE} from './FooterButton';\nimport StyledButton from '../../components/StyledButton';\n\nconst RTL_LANGUAGES = ['ar', 'he', 'iw', 'fa', 'dv', 'ha', 'ks', 'ku', 'ps', 'ur', 'yi'];\n\nconst DEFAULT_INPUT_HEIGHT = 19;\nconst FOOTER_PADDING_HORIZONTAL = 12;\nconst FOOTER_PADDING_VERTICAL = 8;\n\nlet shouldScroll = false;\nlet eventSource = null;\n\nlet backgroundTimestamp = null;\n\nlet pollingTimeout = null;\nlet preventPolling = false;\n\nconst ChatView = ({\n shouldntRefreshQueries,\n discussionId,\n updateDiscussionId,\n fetchDiscussions,\n querying,\n setQuerying,\n}) => {\n const {t} = useTranslation();\n const {userToken, deviceToken, user, setUser} = useContext(UserContext);\n const {settings: {language}} = useContext(SettingsContext);\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n const speaker = useSpeaker();\n\n const scrollViewRef = useRef(null);\n\n const [canStopGenerating, setCanStopGenerating] = useState(false);\n const [loading, setLoading] = useState(false);\n const [queries, setQueries] = useState([]);\n const [queryText, setQueryText] = useState('');\n\n const [examplesLoading, setExamplesLoading] = useState(false);\n const [exampleSections, setExampleSections] = useState([]);\n\n const [inputHeight, setInputHeight] = useState(DEFAULT_INPUT_HEIGHT);\n\n useEffect(() => {\n const loadExamples = async () => {\n try {\n setExamplesLoading(true);\n\n const response = await fetch(\n `${Api.apiBaseUrl}/${language.slice(0, 2)}/examples${Api.roomSlug ? `?room=${Api.roomSlug}` : ''}`,\n {\n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n }\n );\n\n const responseJson = await response.json();\n\n setExampleSections(responseJson);\n } catch (error) {\n // TODO: Handle errors\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n } finally {\n setExamplesLoading(false);\n }\n };\n\n loadExamples();\n }, [language]);\n\n const fetchQueries = useCallback(async () => {\n if (discussionId) {\n setLoading(true);\n\n const response = await fetch(`${Api.apiBaseUrl}/discussions/${discussionId}.json?user_token=${userToken}`, {method: 'GET'});\n const data = await response.json();\n\n setLoading(false);\n\n if (data.error) {\n // TODO: handle error\n console.log(data.error);\n return;\n }\n\n setQueries(data.queries);\n\n if ((data.queries || []).length) {\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: false}), 0);\n }\n } else {\n setQueries([]);\n }\n }, [userToken, setLoading, setQueries, discussionId]);\n\n useEffect(() => {\n return () => {\n if (pollingTimeout) {\n clearTimeout(pollingTimeout);\n }\n };\n }, []);\n\n useEffect(() => {\n if (!shouldntRefreshQueries) {\n if (pollingTimeout) {\n clearTimeout(pollingTimeout);\n }\n\n fetchQueries();\n }\n }, [userToken, shouldntRefreshQueries, discussionId, fetchQueries]);\n\n const appState = useRef(AppState.currentState);\n\n useEffect(() => {\n const subscription = AppState.addEventListener('change', nextAppState => {\n if (appState.current.match(/inactive|background/) && nextAppState === 'active') {\n // Note: App has come to the foreground\n\n if (backgroundTimestamp && Date.now() - backgroundTimestamp > 3 * 60 * 60 * 1000) {\n updateDiscussionId(null);\n } else {\n if (Platform.OS === 'ios') {\n fetchQueries();\n }\n }\n } else if (appState.current === 'active' && nextAppState.match(/inactive|background/)) {\n // Note: App leaves the foreground\n\n backgroundTimestamp = Date.now();\n }\n\n appState.current = nextAppState;\n });\n\n return () => {\n subscription.remove();\n };\n }, [fetchQueries, updateDiscussionId]);\n\n const pushCurrentQuery = useCallback(query => {\n preventPolling = true;\n\n const updatedQueries = [...(queries || []), query];\n\n setQueries(updatedQueries);\n\n return updatedQueries;\n }, [queries, setQueries]);\n\n const updateCurrentQuery = useCallback((queries, currentQuery) => {\n const currentQueryIndex = queries.findIndex(query => query.ref === currentQuery.ref);\n\n if (currentQueryIndex !== -1) {\n const updatedQueries = [...queries];\n updatedQueries[currentQueryIndex] = currentQuery;\n\n setQueries(updatedQueries);\n }\n }, []);\n\n const createQuery = useCallback(async (data, localQueries, localQuery) => {\n const fakeResponseId = uuid.v4();\n\n try {\n setQuerying(true);\n\n if (shouldScroll) {\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }\n\n const requestObject = {\n user_token: userToken,\n device_token: deviceToken,\n room: Api.roomSlug,\n stream: true,\n query: {\n discussion_id: discussionId || undefined,\n channel: data.channel,\n ref: data.ref,\n text: data.text,\n recording_id: data.recordingId || undefined,\n\n country_code: Localization.getLocales()[0].regionCode,\n timezone: Localization.timezone,\n\n language: language, // fr-FR\n device_brand: Device.brand, // Android: \"google\", \"xiaomi\"; iOS: \"Apple\"; web: null\n device_name: Device.deviceName, // \"Vivian's iPhone XS\"\n device_manufacturer: Device.manufacturer, // Android: \"Google\", \"xiaomi\"; iOS: \"Apple\"; web: \"Google\", null\n device_os_name: Platform.OS,\n device_os_version: Device.osVersion,\n device_model_name: Device.modelName,\n expo_session_id: Constants.sessionId,\n expo_installation_id: Constants.installationId,\n\n screen: 'chat',\n },\n };\n\n const deviceTypeIndex = await Device.getDeviceTypeAsync();\n\n if(deviceTypeIndex === 0) {\n requestObject.query.device_type = 'unknown';\n } else if(deviceTypeIndex === 1) {\n requestObject.query.device_type = 'phone';\n } else if(deviceTypeIndex === 2) {\n requestObject.query.device_type = 'tablet';\n } else if(deviceTypeIndex === 3) {\n requestObject.query.device_type = 'desktop';\n } else if(deviceTypeIndex === 4) {\n requestObject.query.device_type = 'tv';\n } else {\n requestObject.query.device_type = 'UNKNOWN';\n }\n\n let locationPerm = {};\n\n if (Platform.OS === 'web') {\n const {state} = await navigator.permissions.query({name: 'geolocation'});\n\n locationPerm = {granted: state === 'granted'};\n } else {\n locationPerm = await Location.getForegroundPermissionsAsync();\n }\n\n if (locationPerm.granted) {\n let location = await Location.getLastKnownPositionAsync({\n requiredAccuracy: 300, // Note: Return null if ~300 meter away from last known location\n maxAge: 600000, // Note: Return null if last known location is more than 10m old\n });\n\n if (!location) {\n location = await Location.getCurrentPositionAsync({\n accuracy: Location.Accuracy.High,\n mayShowUserSettingsDialog: false,\n });\n }\n\n requestObject.query.latitude = location.coords.latitude;\n requestObject.query.longitude = location.coords.longitude;\n }\n\n let appStateListener = null;\n\n const createRequest = new Promise((resolve, reject) => {\n eventSource = new EventSource(\n `${Api.apiBaseUrl}/queries.json`,\n {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestObject),\n // debug: true,\n }\n );\n\n eventSource.addEventListener('open', () => {\n console.log('Event \"open\" called');\n shouldScroll = true;\n\n speaker.initSpeakQueryState();\n });\n\n const throttledUpdateCurrentQuery = throttle(currentQuery => {\n updateCurrentQuery(localQueries, currentQuery);\n\n if (shouldScroll) {\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }\n }, 300, false, false);\n\n let message = '';\n\n eventSource.addEventListener('initial', async event => {\n setCanStopGenerating(true);\n\n const currentQuery = JSON.parse(event.data);\n console.log('[Event \"initial\"] currentQuery: ', currentQuery);\n\n // Note: If we weren't on a discussion (eg: first use, after a clear discussion) we fetchDiscussions and\n // update the discussionId and fetchDiscussions to update the drawer content\n if (!discussionId) {\n const fetchAndUpdateDiscussion = async () => {\n await fetchDiscussions();\n\n updateDiscussionId(currentQuery.discussion_id, true);\n };\n\n fetchAndUpdateDiscussion();\n }\n\n eventSource.addEventListener('message', event => {\n message += event.data;\n\n throttledUpdateCurrentQuery({\n ...currentQuery,\n responses: [{\n id: fakeResponseId,\n message,\n created_at: Date.now(),\n channel: currentQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n\n speaker.speakToken(event.data, currentQuery.language);\n });\n\n eventSource.addEventListener('loading_message', event => {\n // TODO:\n throttledUpdateCurrentQuery({\n ...currentQuery,\n responses: [{\n id: fakeResponseId,\n message: event.data,\n created_at: Date.now(),\n channel: currentQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n });\n\n if (Platform.OS === 'ios') {\n appStateListener = AppState.addEventListener('change', nextAppState => {\n appStateListener.remove();\n\n if (nextAppState === 'inactive' || nextAppState === 'background') {\n if (eventSource) {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n }\n\n preventPolling = false;\n setQuerying(false);\n setCanStopGenerating(false);\n }\n });\n }\n });\n\n eventSource.addEventListener('final', async event => {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n preventPolling = false;\n setQuerying(false);\n setCanStopGenerating(false);\n\n const query = JSON.parse(event.data);\n console.log('Event \"final\": ', query);\n\n if (query.responses && query.responses.length && query.responses[0].message) {\n speaker.emptyResponseBuffer(query.responses[0].message, query.responses[0].language);\n }\n\n const newQueries = [...queries, query];\n\n setQueries(newQueries);\n\n if (query.status === 'pending') {\n const pollPendingQueries = async () => {\n pollingTimeout = null;\n\n if (preventPolling) {\n // Note: If preventPolling is true we delay the next request\n pollingTimeout = setTimeout(pollPendingQueries, 5000);\n } else {\n const response = await fetch(`${Api.apiBaseUrl}/discussions/${query.discussion_id}.json?user_token=${userToken}`, {method: 'GET'});\n const updatedDiscussion = await response.json();\n\n const updatedQueries = (updatedDiscussion || {}).queries || [];\n\n if (updatedQueries.some(fetchedQuery => fetchedQuery.status === 'pending')) {\n pollingTimeout = setTimeout(pollPendingQueries, 5000);\n }\n\n setQueries(updatedQueries);\n }\n };\n\n pollingTimeout = setTimeout(pollPendingQueries, 5000);\n }\n\n const updatedUser = {...user, daily_queries: query.daily_queries};\n\n setUser(updatedUser);\n\n resolve(query);\n });\n\n eventSource.addEventListener('error', (event) => {\n // TODO: Handle errors\n if (event.type === 'error') {\n console.log('Connection error:', event.message);\n } else if (event.type === 'exception') {\n console.log('Error:', event.message, event.error);\n }\n\n updateCurrentQuery(localQueries, {\n ...localQuery,\n responses: [{\n id: fakeResponseId,\n message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n status: 'error',\n created_at: Date.now(),\n channel: localQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n preventPolling = false;\n setQuerying(false);\n setCanStopGenerating(false);\n reject({message: event.message});\n });\n\n eventSource.addEventListener('close', () => {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n preventPolling = false;\n setQuerying(false);\n setCanStopGenerating(false);\n });\n });\n\n const result = await createRequest;\n\n if (appStateListener) {\n appStateListener.remove();\n }\n\n console.log('createRequest() result', result);\n\n return result;\n } catch (error) {\n // TODO: Handle errors\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n\n if (eventSource) {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n }\n\n preventPolling = false;\n setQuerying(false);\n setCanStopGenerating(false);\n\n updateCurrentQuery(localQueries, {\n ...localQuery,\n responses: [{\n id: fakeResponseId,\n message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n status: 'error',\n created_at: Date.now(),\n channel: localQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n }\n }, [setQuerying, userToken, deviceToken, discussionId, language, speaker, updateCurrentQuery, fetchDiscussions, updateDiscussionId, queries, user, setUser, t]);\n\n const handleSendQuery = useCallback(async text => {\n if (\n !text.length\n || !text.trim().length\n // Note: THIS IS NOT A SIMPLE SPACE, DO NOT DELETE !\n // This is a weird character that is added when on iPhone someone use the voice control\n // but doesn't speak.\n || text === ''\n ) {\n if (Platform.OS !== 'web') {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n }\n\n return;\n }\n\n setQueryText('');\n setInputHeight(DEFAULT_INPUT_HEIGHT);\n Keyboard.dismiss();\n\n const ref = uuid.v4();\n\n const localQuery = {\n ref,\n status: 'processing',\n text: text.trim(),\n channel: 'keyboard',\n created_at: new Date(),\n updated_at: new Date(),\n responses: [],\n };\n\n // Note: Optimistically add a fake query until the server return the result\n const localQueries = pushCurrentQuery(localQuery);\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n\n await createQuery({ref, channel: 'keyboard', text: text.trim()}, localQueries, localQuery);\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }, [pushCurrentQuery, createQuery]);\n\n const handleReprocessQuery = useCallback(async (query, update) => {\n preventPolling = true;\n\n const fakeResponseId = uuid.v4();\n\n const optimisticallyUpdatedQuery = {\n ...query,\n responses: [\n ...query.responses,\n {\n id: fakeResponseId,\n created_at: Date.now(),\n channel: query.channel,\n source: undefined,\n cards: [],\n },\n ],\n };\n\n const queryIndex = queries.findIndex(query => query.ref === optimisticallyUpdatedQuery.ref);\n\n if (queryIndex !== -1) {\n const updatedQueries = [...queries];\n updatedQueries[queryIndex] = optimisticallyUpdatedQuery;\n\n setQueries(updatedQueries);\n }\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n\n const result = await fetch(`${Api.apiBaseUrl}/queries/${query.id}/reprocess`, {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n user_token: userToken,\n query: update,\n }),\n });\n\n const updatedQuery = await result.json();\n\n if (queryIndex !== -1) {\n const updatedQueries = [...queries];\n updatedQueries[queryIndex] = updatedQuery;\n\n setQueries(updatedQueries);\n }\n\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n\n preventPolling = false;\n }, [queries, userToken]);\n\n const onChangeText = useCallback(value => setQueryText(value), [setQueryText]);\n const onSubmitEditing = useCallback(event => handleSendQuery(event.nativeEvent.text), [handleSendQuery]);\n\n // Note: onContentSize doesn't work properly on web, check: https://github.com/necolas/react-native-web/issues/2160\n const onChangeWeb = useCallback(event => {\n if (Platform.OS !== 'web') return;\n\n window.requestAnimationFrame(() => {\n event.nativeEvent.target.style.height = '0px';\n\n const height = event.nativeEvent.target.scrollHeight < DEFAULT_INPUT_HEIGHT ? DEFAULT_INPUT_HEIGHT : Math.min(event.nativeEvent.target.scrollHeight, 120);\n\n event.nativeEvent.target.style.height = `${height}px`;\n\n setInputHeight(height < DEFAULT_INPUT_HEIGHT ? DEFAULT_INPUT_HEIGHT : Math.min(height, 120));\n });\n }, []);\n\n const onKeyPress = useCallback(event => {\n if (Platform.OS === 'web') {\n const {nativeEvent: {key, shiftKey}} = event;\n\n if (key === 'Enter' && !shiftKey) {\n handleSendQuery(queryText);\n }\n }\n }, [handleSendQuery, queryText]);\n\n const onContentSizeChange = useCallback(event => {\n const {nativeEvent: {contentSize: {height}}} = event;\n\n setInputHeight(height < DEFAULT_INPUT_HEIGHT ? DEFAULT_INPUT_HEIGHT : Math.min(height, 120));\n }, [setInputHeight]);\n\n return (\n \n \n {loading || examplesLoading\n ? (\n \n \n \n )\n : (\n {\n shouldScroll = false;\n }}\n queries={queries}\n setQueryText={setQueryText}\n handleReprocessQuery={handleReprocessQuery}\n exampleSections={exampleSections}\n canStopGenerating={canStopGenerating}\n />\n )}\n\n {canStopGenerating && (\n \n {\n if (eventSource) {\n eventSource.close();\n eventSource = null;\n }\n\n const updatedQueries = [...queries];\n const currentQuery = queries[queries.length - 1];\n\n updatedQueries[updatedQueries.length - 1] = {\n ...currentQuery,\n responses: currentQuery.responses.length\n ? currentQuery.responses.map(response => ({\n ...response,\n // TODO: Translate\n message: `${response.message} [canceled]`,\n }))\n : [{\n id: uuid.v4(),\n // TODO: Translate\n message: '[canceled]',\n created_at: Date.now(),\n channel: currentQuery.channel,\n source: undefined,\n cards: [],\n }],\n };\n\n setQueries(updatedQueries);\n preventPolling = false;\n setQuerying(false);\n }}\n />\n \n )}\n\n \n \n \n\n {queryText.length || (Platform.OS === 'web' && !window.MediaRecorder.isTypeSupported('audio/webm'))\n ? (\n handleSendQuery(queryText)}\n iconBackgroundColor={theme.colorPrimary}\n icon={}\n />\n )\n : (\n \n )\n }\n \n \n \n \n );\n};\n\nconst getStyles = theme => StyleSheet.create({\n footer: {\n ...(Platform.OS === 'web'\n ? {\n position: 'fixed',\n bottom: 0,\n }\n : {}),\n backgroundColor: theme.footerBackground,\n alignItems: 'center',\n width: '100%',\n borderTopColor: theme.footerBorderColor,\n borderTopWidth: 1,\n borderTopStyle: 'solid',\n },\n innerFooter: {\n flexDirection: 'row',\n alignItems: 'center',\n maxWidth: WEB_MAX_WIDTH,\n width: '100%',\n height: '100%',\n justifyContent: 'flex-start',\n paddingLeft: FOOTER_PADDING_HORIZONTAL,\n },\n});\n\nexport default ChatView;\n","import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';\nimport {\n Animated,\n Image,\n Platform,\n StyleSheet,\n TouchableOpacity,\n View,\n Easing,\n Text,\n ScrollView,\n AppState,\n Dimensions,\n} from 'react-native';\nimport {useHeaderHeight} from '@react-navigation/elements';\nimport {useTranslation} from 'react-i18next';\nimport Constants from 'expo-constants';\nimport uuid from 'react-native-uuid';\nimport * as Haptics from 'expo-haptics';\nimport * as Localization from 'expo-localization';\nimport * as Device from 'expo-device';\nimport * as Location from 'expo-location';\nimport * as Sentry from 'sentry-expo';\n\nimport {UserContext} from '../../contexts/UserContext';\nimport {SettingsContext} from '../../contexts/SettingsContext';\nimport {ThemeContext} from '../../contexts/ThemeContext';\nimport Api from '../../constants/Api';\nimport useRecorder from '../../hooks/useRecorder';\nimport useSpeaker from '../../hooks/useSpeaker';\nimport EventSource from '../../utils/SourceEvent';\nimport {throttle} from '../../utils/throttle';\n\nimport Card from './Chat/Card';\nimport PlacesMap from './Chat/PlacesMap';\n\nlet microphonePermissionsRequested = false;\n\nlet shouldScroll = false;\nlet eventSource = null;\n\nconst BUTTON_SIZE = 200;\nconst BORDER_SIZE = 10;\n\nconst CustomProgressBar = ({loading, rotationDegree}) => {\n const theme = useContext(ThemeContext);\n\n const COLOR = theme.light ? 'rgba(0, 0, 0, 0.3)' : '#fff';\n\n return (\n \n \n\n {loading && (\n \n )}\n \n );\n};\n\nconst MicrophoneView = ({discussionId, updateDiscussionId, fetchDiscussions}) => {\n const {t} = useTranslation();\n const {userToken, user, deviceToken, setUser} = useContext(UserContext);\n const {settings: {language}} = useContext(SettingsContext);\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n const headerHeight = useHeaderHeight();\n\n const recorder = useRecorder();\n const speaker = useSpeaker();\n\n const scrollViewRef = useRef(null);\n\n const topTextInitialValue = useMemo(() => ({\n text: t('MainScreen.MicrophoneView.Recorder.instruction', 'Push to talk'),\n style: 'system',\n }), [t]);\n\n const [loading, setLoading] = useState(false);\n const [topText, setTopText] = useState(topTextInitialValue);\n const [lastResponse, setLastResponse] = useState(null);\n\n useEffect(() => {\n if (discussionId == null) {\n setTopText(topTextInitialValue);\n setLastResponse(null);\n }\n }, [discussionId, topTextInitialValue]);\n\n // TODO: refactor createQuery with the one in ChatView\n const createQuery = useCallback(async (data, localQueries, localQuery) => {\n const fakeResponseId = uuid.v4();\n\n try {\n // setQuerying(true);\n\n if (shouldScroll) {\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }\n\n const requestObject = {\n user_token: userToken,\n device_token: deviceToken,\n room: Api.roomSlug,\n stream: true,\n query: {\n discussion_id: discussionId || undefined,\n channel: data.channel,\n ref: data.ref,\n text: data.text,\n recording_id: data.recordingId || undefined,\n\n country_code: Localization.getLocales()[0].regionCode,\n timezone: Localization.timezone,\n\n language: language, // fr-FR\n device_brand: Device.brand, // Android: \"google\", \"xiaomi\"; iOS: \"Apple\"; web: null\n device_name: Device.deviceName, // \"Vivian's iPhone XS\"\n device_manufacturer: Device.manufacturer, // Android: \"Google\", \"xiaomi\"; iOS: \"Apple\"; web: \"Google\", null\n device_os_name: Platform.OS,\n device_os_version: Device.osVersion,\n device_model_name: Device.modelName,\n expo_session_id: Constants.sessionId,\n expo_installation_id: Constants.installationId,\n\n screen: 'microphone',\n },\n };\n\n const deviceTypeIndex = await Device.getDeviceTypeAsync();\n\n if(deviceTypeIndex === 0) {\n requestObject.query.device_type = 'unknown';\n } else if(deviceTypeIndex === 1) {\n requestObject.query.device_type = 'phone';\n } else if(deviceTypeIndex === 2) {\n requestObject.query.device_type = 'tablet';\n } else if(deviceTypeIndex === 3) {\n requestObject.query.device_type = 'desktop';\n } else if(deviceTypeIndex === 4) {\n requestObject.query.device_type = 'tv';\n } else {\n requestObject.query.device_type = 'UNKNOWN';\n }\n\n let locationPerm = {};\n\n if (Platform.OS === 'web') {\n const {state} = await navigator.permissions.query({name: 'geolocation'});\n\n locationPerm = {granted: state === 'granted'};\n } else {\n locationPerm = await Location.getForegroundPermissionsAsync();\n }\n\n if (locationPerm.granted) {\n let location = await Location.getLastKnownPositionAsync({\n requiredAccuracy: 300, // Note: Return null if ~300 meter away from last known location\n maxAge: 600000, // Note: Return null if last known location is more than 10m old\n });\n\n if (!location) {\n location = await Location.getCurrentPositionAsync({\n accuracy: Location.Accuracy.High,\n mayShowUserSettingsDialog: false,\n });\n }\n\n requestObject.query.latitude = location.coords.latitude;\n requestObject.query.longitude = location.coords.longitude;\n }\n\n let appStateListener = null;\n\n const createRequest = new Promise((resolve, reject) => {\n eventSource = new EventSource(\n `${Api.apiBaseUrl}/queries.json`,\n {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestObject),\n // debug: true,\n }\n );\n\n eventSource.addEventListener('open', () => {\n console.log('Event \"open\" called');\n shouldScroll = true;\n\n speaker.initSpeakQueryState();\n });\n\n const throttledUpdateCurrentQuery = throttle(currentQuery => {\n // updateCurrentQuery(localQueries, currentQuery);\n setLastResponse(currentQuery.responses[0]);\n\n if (shouldScroll) {\n setTimeout(() => scrollViewRef && scrollViewRef.current && scrollViewRef.current.scrollToEnd({animated: true}), 0);\n }\n }, 300, false, false);\n\n let message = '';\n\n eventSource.addEventListener('initial', async event => {\n // setCanStopGenerating(true);\n\n const currentQuery = JSON.parse(event.data);\n console.log('[Event \"initial\"] currentQuery: ', currentQuery);\n\n // Note: If we weren't on a discussion (eg: first use, after a clear discussion) we fetchDiscussions and\n // update the discussionId and fetchDiscussions to update the drawer content\n if (!discussionId) {\n const fetchAndUpdateDiscussion = async () => {\n await fetchDiscussions();\n\n console.log('updateDiscussionId()', currentQuery, currentQuery.discussion_id);\n updateDiscussionId(currentQuery.discussion_id);\n };\n\n fetchAndUpdateDiscussion();\n }\n\n eventSource.addEventListener('message', event => {\n message += event.data;\n\n throttledUpdateCurrentQuery({\n ...currentQuery,\n responses: [{\n id: fakeResponseId,\n message,\n created_at: Date.now(),\n channel: currentQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n\n speaker.speakToken(event.data, currentQuery.language);\n });\n\n eventSource.addEventListener('loading_message', event => {\n // TODO:\n throttledUpdateCurrentQuery({\n ...currentQuery,\n responses: [{\n id: fakeResponseId,\n message: event.data,\n created_at: Date.now(),\n channel: currentQuery.channel,\n source: undefined,\n cards: [],\n }],\n });\n });\n\n if (Platform.OS === 'ios') {\n appStateListener = AppState.addEventListener('change', nextAppState => {\n appStateListener.remove();\n\n if (nextAppState === 'inactive' || nextAppState === 'background') {\n if (eventSource) {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n }\n\n // setQuerying(false);\n // setCanStopGenerating(false);\n }\n });\n }\n });\n\n eventSource.addEventListener('final', async event => {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n // setQuerying(false);\n // setCanStopGenerating(false);\n\n const query = JSON.parse(event.data);\n console.log('Event \"final\": ', query);\n\n if (query.responses && query.responses.length && query.responses[0].message) {\n speaker.emptyResponseBuffer(query.responses[0].message, query.responses[0].language);\n }\n\n const updatedUser = {...user, daily_queries: query.daily_queries};\n\n setUser(updatedUser);\n\n resolve(query);\n });\n\n eventSource.addEventListener('error', (event) => {\n // TODO: Handle errors\n if (event.type === 'error') {\n console.log('Connection error:', event.message);\n } else if (event.type === 'exception') {\n console.log('Error:', event.message, event.error);\n }\n\n // TODO:\n setLastResponse({\n message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n });\n\n // updateCurrentQuery(localQueries, {\n // ...localQuery,\n // responses: [{\n // id: fakeResponseId,\n // message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n // status: 'error',\n // created_at: Date.now(),\n // channel: localQuery.channel,\n // source: undefined,\n // cards: [],\n // }],\n // });\n\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n // setQuerying(false);\n // setCanStopGenerating(false);\n reject({message: event.message});\n });\n\n eventSource.addEventListener('close', () => {\n console.log('Close SSE connection.');\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n // setQuerying(false);\n // setCanStopGenerating(false);\n });\n });\n\n const result = await createRequest;\n\n if (appStateListener) {\n appStateListener.remove();\n }\n\n console.log('createRequest() result', result);\n\n return result;\n } catch (error) {\n // TODO: Handle errors\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n\n if (eventSource) {\n eventSource.removeAllEventListeners();\n eventSource.close();\n eventSource = null;\n // setQuerying(false);\n // setCanStopGenerating(false);\n }\n\n // TODO:\n setLastResponse({\n message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n });\n\n // updateCurrentQuery(localQueries, {\n // ...localQuery,\n // responses: [{\n // id: fakeResponseId,\n // message: t('MainScreen.ChatView.Recorder.errors.recordingFailed', 'Sorry, something went wrong, try again later.'),\n // status: 'error',\n // created_at: Date.now(),\n // channel: localQuery.channel,\n // source: undefined,\n // cards: [],\n // }],\n // });\n }\n }, [userToken, deviceToken, discussionId, language, speaker, user, setUser, fetchDiscussions, updateDiscussionId, t]);\n\n\n // TODO: refactor handleUploadRecording with the one in ChatView\n const handleUploadRecording = useCallback(async (uri, ref) => {\n try {\n const formData = new FormData();\n\n formData.append('user_token', userToken);\n formData.append('recording[ref]', ref);\n\n if(Platform.OS === 'web') {\n const blob = await (await fetch(uri)).blob();\n\n formData.append('recording[file]', blob, 'recording_' + Constants.sessionId + '_' + Date.now() + '.wav');\n } else {\n formData.append('recording[file]', {\n uri,\n type: 'audio/x-wav',\n name: 'recording_' + Constants.sessionId + '_' + Date.now() + '.wav',\n });\n }\n\n formData.append('recording[language]', language); // fr-FR\n formData.append('recording[device_brand]', Device.brand); // Android: \"google\", \"xiaomi\"; iOS: \"Apple\"; web: null\n formData.append('recording[device_name]', Device.deviceName); // \"Vivian's iPhone XS\"\n formData.append('recording[device_manufacturer]', Device.manufacturer); // Android: \"Google\", \"xiaomi\"; iOS: \"Apple\"; web: \"Google\", null\n formData.append('recording[device_os_name]', Platform.OS);\n formData.append('recording[device_os_version]', Device.osVersion);\n formData.append('recording[device_model_name]', Device.modelName);\n formData.append('recording[expo_session_id]', Constants.sessionId);\n formData.append('recording[expo_installation_id]', Constants.installationId);\n\n if(Platform.OS === 'web') {\n formData.append('recording[encoding]', 'WEBM_OPUS');\n formData.append('recording[sample_rate]', '48000');\n formData.append('recording[bit_rate]', '128000');\n } else if(Platform.OS === 'ios') {\n formData.append('recording[encoding]', 'LINEAR16');\n formData.append('recording[sample_rate]', '48000');\n formData.append('recording[bit_rate]', '128000');\n } else {\n formData.append('recording[encoding]', 'AMR_WB');\n formData.append('recording[sample_rate]', '16000');\n formData.append('recording[bit_rate]', '16000');\n }\n\n const deviceType = await Device.getDeviceTypeAsync();\n if(deviceType === 0) { formData.append('recording[device_type]', 'unknown'); }\n else if(deviceType === 1) { formData.append('recording[device_type]', 'phone'); }\n else if(deviceType === 2) { formData.append('recording[device_type]', 'tablet'); }\n else if(deviceType === 3) { formData.append('recording[device_type]', 'desktop'); }\n else if(deviceType === 4) { formData.append('recording[device_type]', 'tv'); }\n else { formData.append('recording[device_type]', 'UNKNOWN'); }\n\n let locationPerm = {};\n\n if (Platform.OS === 'web') {\n const {state} = await navigator.permissions.query({name: 'geolocation'});\n\n locationPerm = {granted: state === 'granted'};\n } else {\n locationPerm = await Location.getForegroundPermissionsAsync();\n }\n\n if (locationPerm.granted) {\n let location = await Location.getLastKnownPositionAsync({\n requiredAccuracy: 300, // Note: Return null if ~300 meter away from last known location\n maxAge: 600000, // Note: Return null if last known location is more than 10m old\n });\n\n if (!location) {\n location = await Location.getCurrentPositionAsync({\n accuracy: Location.Accuracy.High,\n mayShowUserSettingsDialog: false,\n });\n }\n\n formData.append('recording[latitude]', location.coords.latitude);\n formData.append('recording[longitude]', location.coords.longitude);\n }\n\n const response = await fetch(`${Api.apiBaseUrl}/recordings.json`, {\n method: 'POST',\n body: formData,\n });\n\n return await response.json();\n } catch (error) {\n // TODO: Handle errors\n console.log('There was an error during upload.', error);\n\n setLoading(false);\n loadingAnimation.stop();\n loadingAnimation.reset();\n\n Animated.sequence([\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: -10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: -10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 0, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n ]).start(() => {\n breathingAnimation.start();\n });\n\n throw error;\n }\n }, [userToken, language, loadingAnimation, shakeAnim, breathingAnimation]);\n\n const shakeAnim = useRef(new Animated.Value(0)).current;\n const rotationDegree = useRef(new Animated.Value(0)).current;\n const scaleAnim = React.useRef(new Animated.Value(1)).current;\n\n const breathingAnimation = Animated.loop(\n Animated.sequence([\n Animated.timing(scaleAnim, {toValue: 1.0, duration: 0, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(scaleAnim, {toValue: 1.2, duration: 1000, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(scaleAnim, {toValue: 1.0, duration: 1500, useNativeDriver: Platform.OS !== 'web'}),\n ])\n );\n\n useEffect(() => {\n breathingAnimation.start();\n }, []);\n\n const loadingAnimation = useMemo(() => (\n Animated.loop(Animated.timing(\n rotationDegree,\n {\n toValue: 360,\n duration: 1000,\n easing: Easing.linear,\n useNativeDriver: Platform.OS !== 'web',\n }\n ))\n ), [rotationDegree]);\n\n const handleLongPress = useCallback(async () => {\n // Note: If this press triggered a permission request we don't want to do anything else\n if (microphonePermissionsRequested) {\n microphonePermissionsRequested = false;\n return;\n }\n\n speaker.stop();\n\n if (Platform.OS !== 'web') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle.High);\n }\n\n await recorder.start();\n }, [recorder, speaker]);\n\n const handlePress = useCallback(async () => {\n // Note: If this press triggered a permission request we don't want to do anything else\n if (microphonePermissionsRequested) {\n microphonePermissionsRequested = false;\n return;\n }\n\n if (Platform.OS !== 'web') {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n }\n\n setLastResponse(null);\n setTopText({\n text: t('MainScreen.ChatView.Recorder.errors.holdButton', 'Hold the button while speaking.'),\n style: 'system',\n });\n\n Animated.sequence([\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: -10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: -10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 10, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n Animated.timing(shakeAnim, {toValue: 0, duration: 100, useNativeDriver: Platform.OS !== 'web'}),\n ]).start(() => {\n breathingAnimation.start();\n });\n }, []);\n\n const handlePressIn = useCallback(async () => {\n await recorder.requestPermissions(() => {\n microphonePermissionsRequested = true;\n });\n\n breathingAnimation.stop();\n breathingAnimation.reset();\n });\n\n const handlePressOut = useCallback(async () => {\n if (recorder.state.status === 'starting') {\n recorder.stop();\n } else if (recorder.state.status === 'recording') {\n try {\n setLoading(true);\n loadingAnimation.start();\n\n if (Platform.OS !== 'web') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle.High);\n }\n\n const uri = await recorder.stop();\n\n const ref = uuid.v4();\n\n const recording = await handleUploadRecording(uri, ref);\n\n if (recording.status === 'failed') {\n setLastResponse(null);\n setTopText({\n text: t('MainScreen.ChatView.Recorder.errors.noAudio', 'No audio detected'),\n style: 'system',\n });\n } else {\n setLastResponse(null);\n setTopText({\n text: recording.transcription,\n style: 'transcript',\n });\n\n const query = await createQuery({\n ref,\n channel: 'recording',\n text: recording.transcription,\n recordingId: recording.id,\n });\n\n if (Platform.OS !== 'web') {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);\n }\n\n if (query) {\n if (query.responses && query.responses.length && query.responses[0].message) {\n setLastResponse(query.responses[0]);\n }\n }\n }\n\n setLoading(false);\n loadingAnimation.stop();\n loadingAnimation.reset();\n } catch (error) {\n console.log('There was an error during upload.', error);\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n\n breathingAnimation.start();\n }\n }, [breathingAnimation, createQuery, handleUploadRecording, loadingAnimation, recorder, t]);\n\n return (\n \n \n {(topText || {}).text && (\n \n \n {topText.text}\n \n \n )}\n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n {lastResponse && (\n \n {\n shouldScroll = false;\n }}\n >\n \n {lastResponse.message}\n \n\n {(lastResponse.cards || []).length > 0 && lastResponse.cards.find(({card_type}) => card_type === 'place') && (\n \n )}\n\n {(lastResponse.cards || []).length > 0 && (\n \n {lastResponse.cards.map((card, i) => (\n \n ))}\n \n )}\n \n \n )}\n \n \n );\n};\n\n\nconst getStyles = theme => StyleSheet.create({\n container: {\n height: '100%',\n width: '100%',\n maxWidth: Math.min(Dimensions.get('window').width, 800),\n justifyContent: 'center',\n },\n systemTopTextContainer: {},\n systemTopTextText: {\n fontSize: 20,\n },\n transcriptTopTextContainer: {\n backgroundColor: theme.light ? '#ffffff' : '#383838',\n },\n transcriptTopTextText: {\n fontSize: 16,\n fontWeight: 'bold',\n },\n});\n\nexport default MicrophoneView;\n","import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';\nimport {\n Platform,\n StyleSheet,\n View,\n KeyboardAvoidingView,\n} from 'react-native';\nimport {useHeaderHeight} from '@react-navigation/elements';\nimport {useTranslation} from 'react-i18next';\n\nimport {UserContext} from '../../contexts/UserContext';\nimport {SettingsContext} from '../../contexts/SettingsContext';\nimport {ThemeContext} from '../../contexts/ThemeContext';\nimport useSpeaker from '../../hooks/useSpeaker';\nimport Api from '../../constants/Api';\n\nimport Drawer from './Drawer';\nimport ChatView from './ChatView';\nimport MicrophoneView from './MicrophoneView';\n\nconst MainScreen = ({\n chatMode,\n setChatMode,\n drawerOpen,\n closeDrawer,\n discussionId,\n setDiscussionId,\n discussions,\n setDiscussions,\n}) => {\n const {t} = useTranslation();\n const {user, userToken} = useContext(UserContext);\n const {settings: {language}} = useContext(SettingsContext);\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n const height = useHeaderHeight();\n const speaker = useSpeaker();\n\n const [discussionsLoading, setDiscussionsLoading] = useState(false);\n const [querying, setQuerying] = useState(false);\n\n const shouldntRefreshQueries = useRef(false);\n\n useEffect(() => {\n speaker.speak(t(\n 'MainScreen.greeting',\n 'Hello {{firstName}}, how can I help you ?',\n {firstName: user.first_name}\n ), language);\n }, []);\n\n const updateDiscussionId = useCallback((value, shouldntRefresh = false) => {\n shouldntRefreshQueries.current = shouldntRefresh;\n setDiscussionId(value);\n }, []);\n\n const fetchDiscussions = useCallback(async () => {\n setDiscussionsLoading(true);\n\n const response = await fetch(`${Api.apiBaseUrl}/discussions.json?user_token=${userToken}${Api.roomSlug ? `&room=${Api.roomSlug}` : ''}`, {method: 'GET'});\n const data = await response.json();\n\n setDiscussionsLoading(false);\n\n if (data.error) {\n // TODO: handle error\n console.log(data.error);\n return;\n }\n\n setDiscussions(data);\n return data;\n }, [userToken, setDiscussionsLoading, setDiscussions]);\n\n return (\n \n {\n setChatMode(value);\n shouldntRefreshQueries.current = false;\n }}\n chatMode={chatMode}\n discussions={discussions}\n setDiscussions={setDiscussions}\n discussionsLoading={discussionsLoading}\n discussionId={discussionId}\n updateDiscussionId={updateDiscussionId}\n fetchDiscussions={fetchDiscussions}\n setDiscussionsLoading={setDiscussionsLoading}\n />\n\n \n {chatMode\n ? (\n \n )\n : (\n \n )}\n \n \n );\n};\n\nconst getStyles = theme => StyleSheet.create({\n mainContainer: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: theme.backgroundPrimary,\n },\n});\n\nexport default MainScreen;\n","import React, {useContext, useMemo, useCallback, useState, useEffect} from 'react';\nimport {Alert, Image, Platform, StyleSheet, Text, View} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport {useNavigation} from '@react-navigation/native';\nimport * as Sentry from 'sentry-expo';\nimport Constants from 'expo-constants';\n\nimport {UserContext} from '../contexts/UserContext';\nimport {ThemeContext} from '../contexts/ThemeContext';\nimport StyledButton from '../components/StyledButton';\nimport Api from '../constants/Api';\nimport openLink from '../utils/openLink';\n\nconst PURCHASES_STUB = {\n\trestorePurchases: () => {},\n};\n\nlet Purchases = PURCHASES_STUB;\n\nif (Constants.appOwnership !== 'expo' && Platform.OS !== 'web') {\n\tPurchases = require('react-native-purchases').default;\n}\n\nconst AccountScreen = () => {\n\tconst {t} = useTranslation();\n\tconst {userId, userToken, user, signOut, subscribed} = useContext(UserContext);\n\tconst theme = useContext(ThemeContext);\n\tconst styles = useMemo(() => getStyles(theme), [theme]);\n\tconst {navigate} = useNavigation();\n\n\tconst [bankAccounts, setBankAccounts] = useState([]);\n\tconst [restoreLoading, setRestoreLoading] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst fetchBankAccounts = async () => {\n\t\t\ttry {\n\t\t\t\tconst result = await fetch(`${Api.apiBaseUrl}/bank_accounts.json?user_token=${userToken}`, {\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Accept': 'application/json',\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tconst bankAccountList = await result.json();\n\n\t\t\t\tif (bankAccountList.error) {\n\t\t\t\t\tthrow bankAccountList.error;\n\t\t\t\t}\n\n\t\t\t\tsetBankAccounts(bankAccountList);\n\t\t\t} catch (error) {\n\t\t\t\t// TODO: handle error\n\t\t\t\tconsole.log(error);\n\n\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t\t} else {\n\t\t\t\t\tSentry.Native.captureException(error);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfetchBankAccounts();\n\t}, [userToken]);\n\n\tconst logOut = useCallback(() => {\n\t\tif (Platform.OS === 'web') {\n\t\t\tif (window.confirm(t('AccountScreen.alerts.logout.message', 'Are you sure you want to log out ?'))) {\n\t\t\t\tsignOut();\n\t\t\t}\n\t\t} else {\n\t\t\tAlert.alert(\n\t\t\t\tt('AccountScreen.alerts.logout.title', 'Log out'),\n\t\t\t\tt('AccountScreen.alerts.logout.message', 'Are you sure you want to log out ?'),\n\t\t\t\t[\n\t\t\t\t\t{text: t('AccountScreen.alerts.logout.buttons.cancel', 'Cancel'), style: 'cancel'},\n\t\t\t\t\t{\n\t\t\t\t\t\ttext: t('AccountScreen.alerts.logout.buttons.confirm', 'Log out'),\n\t\t\t\t\t\tonPress: signOut,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t);\n\t\t}\n\t}, [signOut, t]);\n\n\tconst restore = useCallback(async () => {\n\t\ttry {\n\t\t\tif ((user || {}).status === 'guest') {\n\t\t\t\tAlert.alert(\n\t\t\t\t\tt('AccountScreen.alerts.restoreAsGuest.title', 'Restore purchases'),\n\t\t\t\t\tt('AccountScreen.alerts.restoreAsGuest.message', 'You must create an account before being able to restore purchases.'),\n\t\t\t\t\t[\n\t\t\t\t\t\t{text: t('AccountScreen.alerts.restoreAsGuest.buttons.cancel', 'Cancel'), style: 'cancel'},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttext: t('AccountScreen.alerts.restoreAsGuest.buttons.confirm', 'Create my free account'),\n\t\t\t\t\t\t\tonPress: () => navigate('Signup'),\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetRestoreLoading(true);\n\n\t\t\tawait Purchases.restorePurchases();\n\t\t} catch (e) {\n\t\t\t// TODO: Handle error\n\t\t} finally {\n\t\t\tsetRestoreLoading(false);\n\t\t}\n\t}, [navigate, t, user]);\n\n\tconst deleteAccount = useCallback(async () => {\n\t\tif (Platform.OS === 'web') {\n\t\t\tif (window.confirm(t('AccountScreen.alerts.delete.message', 'Are you sure to PERMANENTLY delete your account? This action is irreversible.'))) {\n\t\t\t\ttry {\n\t\t\t\t\tawait fetch(`${Api.apiBaseUrl}/users/${userId}`, {\n\t\t\t\t\t\tmethod: 'PATCH',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Accept': 'application/json',\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tuser_token: userToken,\n\t\t\t\t\t\t\tstatus: 'deleted',\n\t\t\t\t\t\t}),\n\t\t\t\t\t});\n\n\t\t\t\t\tsignOut();\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// TODO: Handle error\n\t\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tSentry.Native.captureException(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tAlert.alert(\n\t\t\t\tt('AccountScreen.alerts.delete.title', 'Delete account'),\n\t\t\t\tt('AccountScreen.alerts.delete.message', 'Are you sure to PERMANENTLY delete your account? This action is irreversible.'),\n\t\t\t\t[\n\t\t\t\t\t{text: t('AccountScreen.alerts.delete.buttons.cancel', 'Cancel'), style: 'cancel'},\n\t\t\t\t\t{\n\t\t\t\t\t\ttext: t('AccountScreen.alerts.delete.buttons.confirm', 'Delete account'),\n\t\t\t\t\t\tonPress: async () => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait fetch(`${Api.apiBaseUrl}/users/${userId}`, {\n\t\t\t\t\t\t\t\t\tmethod: 'PATCH',\n\t\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t\t'Accept': 'application/json',\n\t\t\t\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\t\t\t\tuser_token: userToken,\n\t\t\t\t\t\t\t\t\t\tstatus: 'deleted',\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tsignOut();\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\t// TODO: Handle error\n\t\t\t\t\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\t\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tSentry.Native.captureException(error);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t);\n\t\t}\n\t}, [signOut, t, userId, userToken]);\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\t\t{user && user.status !== 'guest' ? `${user.first_name || ''} ${user.last_name || ''}` : t('AccountScreen.guest', 'Guest user')}\n\t\t\t\t\n\n\t\t\t\t{(user && user.status !== 'guest')\n\t\t\t\t\t? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{user.email || ''}\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t\t: (\n\t\t\t\t\t\t navigate('Signup')}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\n\t\t\t\t{subscribed && (\n\t\t\t\t\t\n\t\t\t\t\t\t{t('AccountScreen.subscriptionActive', '★ Subscription active ★')}\n\t\t\t\t\t\n\t\t\t\t)}\n\n\t\t\t\t {\n\t\t\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\t\t\topenLink(`${Api.apiBaseUrl}/banks?user_token=${userToken}&persist=true`);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnavigate('Link bank');\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t/>\n\n\t\t\t\t{(bankAccounts || []).length\n\t\t\t\t\t? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{t('AccountScreen.bankAccountList.title', 'Connected bank accounts:')}\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t\t: null\n\t\t\t\t}\n\n\t\t\t\t{(bankAccounts || []).map(bankAccount => (\n\t\t\t\t\t\n\t\t\t\t\t\t{`• ${bankAccount.bank_name} - ${bankAccount.owner_name} - ${bankAccount.name}`}\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t\n\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\t\tv{Constants.expoConfig.version}\n\t\t\t\t\n\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t\n\t);\n};\n\nconst getStyles = theme => StyleSheet.create({\n\tmainContainer: {\n\t\tflex: 1,\n\t\tpaddingHorizontal: 12,\n\t\tpaddingTop: 40,\n\t\tpaddingBottom: 20,\n\t\talignItems: 'center',\n\t\tjustifyContent: 'space-between',\n\t\tbackgroundColor: theme.backgroundPrimary,\n\t},\n\ttopContainer: {\n\t\talignItems: 'center',\n\t},\n\timage: {\n\t\twidth: 100,\n\t\theight: 100,\n\t\tborderRadius: 50,\n\t\tmarginBottom: 20,\n\t},\n\tname: {\n\t\tfontSize: 20,\n\t\tcolor: theme.textPrimary,\n\t\tmarginBottom: 10,\n\t},\n});\n\nexport default AccountScreen;\n","import React, {useContext, useMemo, useCallback, useState, useEffect} from 'react';\nimport {\n\tPlatform,\n\tScrollView,\n\tStyleSheet,\n\tText,\n\tView,\n\tSafeAreaView,\n\tTouchableOpacity,\n\tImage,\n\tActivityIndicator,\n\tAlert,\n} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport Constants from 'expo-constants';\nimport * as Sentry from 'sentry-expo';\nimport {useNavigation} from '@react-navigation/native';\n\nimport {UserContext} from '../contexts/UserContext';\nimport {ThemeContext, WEB_MAX_WIDTH} from '../contexts/ThemeContext';\nimport StyledButton from '../components/StyledButton';\nimport Api from '../constants/Api';\n\nconst PURCHASES_STUB = {\n\tLOG_LEVEL: {},\n\tsetLogLevel: () => {},\n\tconfigure: () => {},\n\tgetOfferings: () => ({current : {availablePackages: [{identifier: 'FAKE', product: {priceString: 'FAKE', subscriptionPeriod: 'P1M'}}]}}),\n\tpurchasePackage: () => {},\n\tgetCustomerInfo: () => {},\n\taddCustomerInfoUpdateListener: () => {},\n};\n\nlet Purchases = PURCHASES_STUB;\n\nif (Constants.appOwnership !== 'expo' && Platform.OS !== 'web') {\n\tPurchases = require('react-native-purchases').default;\n}\n\nconst PurchasePackage = ({purchasePackage, selected, setSelectedPackage}) => {\n\tconst {t} = useTranslation();\n\tconst theme = useContext(ThemeContext);\n\tconst {user} = useContext(UserContext);\n\n\tconst {product: {priceString, subscriptionPeriod}} = purchasePackage;\n\n\tconst SUBSCRIPTION_PERIOD_TO_TITLE = useMemo(() => ({\n\t\tFREE: t(\n\t\t\t'UpgradeScreen.titles.free',\n\t\t\t'Limited to {{count}} queries per day.',\n\t\t\t{count: user.free_queries},\n\t\t),\n\t\tP1M: t('UpgradeScreen.titles.monthly', 'Premium Subscription - Monthly'),\n\t\tP1Y: t('UpgradeScreen.titles.yearly', 'Premium Subscription - Yearly'),\n\t}), [t, user.free_queries]);\n\n\tconst SUBSCRIPTION_PERIOD_TO_MODEL = useMemo(() => ({\n\t\tFREE: 'GPT-3.5',\n\t\tP1M: 'GPT-4',\n\t\tP1Y: 'GPT-4',\n\t}), []);\n\n\treturn (\n\t\t setSelectedPackage(purchasePackage)}\n\t\t>\n\t\t\t{priceString && (\n\t\t\t\t\n\t\t\t\t\t{priceString}\n\t\t\t\t\n\t\t\t)}\n\n\t\t\t\n\t\t\t\t{SUBSCRIPTION_PERIOD_TO_TITLE[subscriptionPeriod]}\n\t\t\t\n\n\t\t\t\n\t\t\t\t{SUBSCRIPTION_PERIOD_TO_MODEL[subscriptionPeriod]}\n\t\t\t\n\t\t\n\t);\n};\n\nconst UpgradeScreen = ({onboarding}) => {\n\tconst {t} = useTranslation();\n\tconst {userToken, userId, user, setUser, subscribed, setSubscribed} = useContext(UserContext);\n\tconst theme = useContext(ThemeContext);\n\tconst styles = useMemo(() => getStyles(theme), [theme]);\n\tconst {navigate} = useNavigation();\n\n\tconst [loading, setLoading] = useState(true);\n\tconst [confirmLoading, setConfirmLoading] = useState(false);\n\tconst [errorMessage, setErrorMessage] = useState(null);\n\tconst [packages, setPackages] = useState([]);\n\tconst [restoreLoading, setRestoreLoading] = useState(false);\n\n\tconst FREE_PACKAGE = useMemo(() => ({\n\t\tidentifier: 'free',\n\t\tproduct: {\n\t\t\tpriceString: t('UpgradeScreen.freePackage.priceString', 'Free'),\n\t\t\tsubscriptionPeriod: 'FREE',\n\t\t},\n\t}), [t]);\n\n\tconst [selectedPackage, setSelectedPackage] = useState(!onboarding ? FREE_PACKAGE : null);\n\n\tuseEffect(() => {\n\t\tconst getOfferings = async () => {\n\t\t\ttry {\n\t\t\t\tconst offerings = await Purchases.getOfferings();\n\n\t\t\t\tif (offerings.current !== null && offerings.current.availablePackages.length !== 0) {\n\t\t\t\t\tconst packages = [FREE_PACKAGE, ...offerings.current.availablePackages];\n\n\t\t\t\t\tsetPackages(packages);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// TODO: Handle errors\n\t\t\t\tsetErrorMessage(t('ContactsPermissionScreen.errors.request', 'Something went wrong, try again later.'));\n\n\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t\t} else {\n\t\t\t\t\tSentry.Native.captureException(error);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\t// TODO: Handle error\n\t\t\t\tsetLoading(false);\n\t\t\t}\n\t\t};\n\n\t\tgetOfferings();\n\t}, [FREE_PACKAGE, t, user.free_queries]);\n\n\tconst restore = useCallback(async () => {\n\t\ttry {\n\t\t\tif ((user || {}).status === 'guest') {\n\t\t\t\tAlert.alert(\n\t\t\t\t\tt('AccountScreen.alerts.restoreAsGuest.title', 'Restore purchases'),\n\t\t\t\t\tt('AccountScreen.alerts.restoreAsGuest.message', 'You must create an account before being able to restore purchases.'),\n\t\t\t\t\t[\n\t\t\t\t\t\t{text: t('AccountScreen.alerts.restoreAsGuest.buttons.cancel', 'Cancel'), style: 'cancel'},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttext: t('AccountScreen.alerts.restoreAsGuest.buttons.confirm', 'Create my free account'),\n\t\t\t\t\t\t\tonPress: () => navigate('Signup'),\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetRestoreLoading(true);\n\n\t\t\tawait Purchases.restorePurchases();\n\t\t} catch (e) {\n\t\t\t// TODO: Handle error\n\t\t} finally {\n\t\t\tsetRestoreLoading(false);\n\t\t}\n\t}, [navigate, t, user]);\n\n\tconst updateUserHasCompletedOnboarding = useCallback(async () => {\n\t\ttry {\n\t\t\tsetConfirmLoading(true);\n\t\t\tsetErrorMessage(null);\n\n\t\t\tconst result = await fetch(`${Api.apiBaseUrl}/users/${userId}`, {\n\t\t\t\tmethod: 'PATCH',\n\t\t\t\theaders: {\n\t\t\t\t\t'Accept': 'application/json',\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tuser_token: userToken,\n\t\t\t\t\thas_completed_onboarding: true,\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tconst updatedUser = await result.json();\n\n\t\t\tsetUser(updatedUser);\n\t\t\tsetConfirmLoading(false);\n\t\t} catch (error) {\n\t\t\tsetErrorMessage(t('ContactsPermissionScreen.errors.request', 'Something went wrong, try again later.'));\n\n\t\t\tif (Platform.OS === 'web') {\n\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t} else {\n\t\t\t\tSentry.Native.captureException(error);\n\t\t\t}\n\t\t} finally {\n\t\t\tsetConfirmLoading(false);\n\t\t}\n\t}, [userId, userToken, setUser, t]);\n\n\tconst buy = useCallback(async purchasePackage => {\n\t\ttry {\n\t\t\tsetConfirmLoading(true);\n\t\t\tconst purchaseMade = await Purchases.purchasePackage(purchasePackage);\n\n\t\t\tif (typeof purchaseMade.customerInfo.entitlements.active['premium'] !== 'undefined') {\n\t\t\t\tsetSubscribed(true);\n\n\t\t\t\tif (onboarding) {\n\t\t\t\t\tawait updateUserHasCompletedOnboarding();\n\t\t\t\t} else {\n\t\t\t\t\tnavigate('MainStack');\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!error.userCancelled) {\n\t\t\t\t// TODO: Handle error\n\t\t\t\tsetErrorMessage(t('ContactsPermissionScreen.errors.request', 'Something went wrong, try again later.'));\n\n\t\t\t\tif (Platform.OS === 'web') {\n\t\t\t\t\tSentry.Browser.captureException(error);\n\t\t\t\t} else {\n\t\t\t\t\tSentry.Native.captureException(error);\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tsetConfirmLoading(false);\n\t\t}\n\t}, [navigate, onboarding, setSubscribed, t, updateUserHasCompletedOnboarding]);\n\n\treturn (\n\t\t\n\t\t\t{loading\n\t\t\t\t? (\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)\n\t\t\t\t: (\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{t('UpgradeScreen.title', 'Premium access')}\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{t('UpgradeScreen.subtitle', 'Get access to an even more powerful assistant and unlimited queries.')}\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{packages.map(purchasePackage => (\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{t('UpgradeScreen.cancel', 'Cancel anytime')}\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t{/**/}\n\t\t\t\t\t\t\t\t{/*\t*/}\n\t\t\t\t\t\t\t\t{/*\t\tBy unlocking premium access you get access to:*/}\n\t\t\t\t\t\t\t\t{/*\t*/}\n\n\t\t\t\t\t\t\t\t{/*\t*/}\n\t\t\t\t\t\t\t\t{/*\t\t• Unlimited queries, have as many request as you'd like, anytime !*/}\n\t\t\t\t\t\t\t\t{/*\t*/}\n\n\t\t\t\t\t\t\t\t{/*\t*/}\n\t\t\t\t\t\t\t\t{/*\t\t• GPT-4 AI model, an even more powerful model that takes your personnal assistant to the next level.*/}\n\t\t\t\t\t\t\t\t{/*\t*/}\n\t\t\t\t\t\t\t\t{/**/}\n\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{subscribed && (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{t('AccountScreen.subscriptionActive', '★ Subscription active ★')}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t{errorMessage && (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{errorMessage}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t buy(selectedPackage)\n\t\t\t\t\t\t\t\t\t\t: () => {}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdisabled={!selectedPackage || (!onboarding && selectedPackage.identifier === 'free')}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)\n\t\t\t}\n\t\t\n\t);\n};\n\nconst getStyles = () => StyleSheet.create({\n\tmainContainer: {\n\t\talignSelf: 'center',\n\t\tmaxWidth: WEB_MAX_WIDTH,\n\t\twidth: '100%',\n\t\theight: '100%',\n\t\tflexDirection: 'column',\n\t\tpaddingHorizontal: 12,\n\t\tpaddingBottom: 8,\n\t},\n\n\tlogo: {\n\t\twidth: 80,\n\t\theight: 80,\n\t\tborderRadius: 40,\n\t\tmarginTop: 20,\n\t\tmarginBottom: 10,\n\t\tresizeMode: 'contain',\n\t},\n});\n\nexport default UpgradeScreen;\n","import React, {useContext} from 'react';\nimport {useNavigation} from '@react-navigation/native';\nimport {WebView} from 'react-native-webview';\n\nimport {UserContext} from '../contexts/UserContext';\nimport Api from '../constants/Api';\n\nconst LinkBankScreen = () => {\n\tconst {userToken} = useContext(UserContext);\n\tconst {navigate} = useNavigation();\n\n\treturn (\n\t\t navigate('Account')}\n\t\t/>\n\t);\n};\n\nexport default LinkBankScreen;\n","const capitalize = string => {\n\treturn `${string[0].toUpperCase()}${string.slice(1, string.length)}`;\n};\n\nexport default capitalize;\n","import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';\nimport {\n\tActivityIndicator,\n\tSafeAreaView,\n\tStyleSheet,\n\tView,\n\tText,\n\tImage,\n\tScrollView,\n} from 'react-native';\nimport {useNavigation, useRoute} from '@react-navigation/native';\n\nimport {UserContext} from '../contexts/UserContext';\nimport {ThemeContext, WEB_MAX_WIDTH} from '../contexts/ThemeContext';\nimport Api from '../constants/Api';\nimport Chat from './MainScreen/Chat/Chat';\nimport capitalize from '../utils/capitalize';\nimport {useTranslation} from 'react-i18next';\n\nconst QueryScreen = () => {\n\tconst {userToken} = useContext(UserContext);\n\tconst theme = useContext(ThemeContext);\n\tconst styles = useMemo(() => getStyles(theme), [theme]);\n\tconst {t} = useTranslation();\n\tconst {params} = useRoute();\n\tconst {setOptions} = useNavigation();\n\n\tconst [loading, setLoading] = useState(true);\n\tconst [query, setQuery] = useState(null);\n\n\tconst fetchQuery = useCallback(async () => {\n\t\tif (params.id) {\n\t\t\tsetLoading(true);\n\n\t\t\tconst response = await fetch(`${Api.apiBaseUrl}/queries/${params.id}.json?user_token=${userToken}`, {method: 'GET'});\n\t\t\tconst data = await response.json();\n\n\t\t\tif (data.error) {\n\t\t\t\t// TODO: handle error\n\t\t\t\tconsole.log(data.error);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetOptions({title: data.discussion.name});\n\n\t\t\tdata.steps.unshift({\n\t\t\t\tid: -1,\n\t\t\t\tname: 'Secrétaire AI',\n\t\t\t\ticon: 'icon-white.png',\n\t\t\t});\n\n\t\t\tif (data.device && data.device.device_type) {\n\t\t\t\tlet icon = 'icon-white.png';\n\n\t\t\t\tswitch(data.device.device_type) {\n\t\t\t\t\tcase 'desktop':\n\t\t\t\t\t\ticon = 'desktop.png';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'phone':\n\t\t\t\t\tcase 'mobile':\n\t\t\t\t\t\ticon = 'mobile.png';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'tablet':\n\t\t\t\t\t\ticon = 'tablet.png';\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tdata.steps.unshift({\n\t\t\t\t\tid: -2,\n\t\t\t\t\tname: capitalize(data.device.device_type),\n\t\t\t\t\ticon,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tsetQuery(data);\n\t\t\tsetLoading(false);\n\t\t}\n\t}, [params.id, userToken, setOptions]);\n\n\tuseEffect(() => {\n\t\tfetchQuery();\n\t}, [userToken, fetchQuery]);\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{loading\n\t\t\t\t\t? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t\t: (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{t('QueryScreen.stepTitle', 'Steps')}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{(query.steps || []).map((step, i) => (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{i !== 0 && (\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t↓\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{step.name}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\n\t\t\n\t);\n};\n\nconst getStyles = theme => StyleSheet.create({\n\tsafeArea: {\n\t\tflex:1,\n\t\twidth: '100%',\n\t\tbackgroundColor: theme.headerBackground,\n\t},\n\tcontainer: {\n\t\tflex: 1,\n\t\twidth: '100%',\n\t\tbackgroundColor: theme.backgroundPrimary,\n\t},\n\tloaderContainer: {\n\t\tflex: 1,\n\t\tjustifyContent: 'center',\n\t},\n\tscrollView: {paddingTop: 6},\n\ttitleContainer: {alignItems: 'center', paddingTop: 20},\n\ttitle: {fontSize: 20, color: theme.textSecondary},\n\tstepsContainer: {\n\t\tflexDirection: 'column',\n\t\talignItems: 'center',\n\t\tgap: 12,\n\t\tpaddingVertical: 20,\n\t},\n\tarrow: {fontSize: 36, color: theme.colorSecondary, marginBottom: 12},\n\tstep: {\n\t\twidth: 200,\n\t\theight: 200,\n\t\tborderRadius: 100,\n\t\tbackgroundColor: '#212529',\n\t\tjustifyContent: 'center',\n\t\talignItems: 'center',\n\t\tborderWidth: 1,\n\t\tborderColor: theme.inputBorderColor,\n\t},\n\tstepIcon: {\n\t\twidth: 70,\n\t\theight: 70,\n\t\tmarginBottom: 8,\n\t},\n\tstepName: {color: '#fff', fontWeight: 'bold', maxWidth: 120},\n});\n\nexport default QueryScreen;\n","import React, {useContext, useRef, useState} from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport {useNavigation} from '@react-navigation/native';\n\nimport {ThemeContext} from './contexts/ThemeContext';\nimport {UserContext} from './contexts/UserContext';\nimport Menu from './components/Menu';\nimport StyledButton from './components/StyledButton';\n\nconst getQueryNumberColor = (theme, remaining) => {\n switch (remaining) {\n case 0:\n return theme.error;\n case 1:\n return theme.warning;\n default:\n return theme.success;\n }\n};\n\nconst UpgradeMenu = () => {\n const {t} = useTranslation();\n const {user} = useContext(UserContext);\n const theme = useContext(ThemeContext);\n const {navigate} = useNavigation();\n const queryNumberButtonRef = useRef(null);\n\n const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);\n\n const remainingQueries = Math.max((user || {}).free_queries - (user || {}).daily_queries, 0);\n\n return (\n <>\n setUpgradeModalOpen(!upgradeModalOpen)}\n style={{\n marginRight: 5,\n backgroundColor: getQueryNumberColor(theme, remainingQueries),\n height: 28,\n width: 28,\n borderRadius: 14,\n justifyContent: 'center',\n alignItems: 'center',\n }}\n >\n \n {remainingQueries}\n \n \n\n \n >\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n height: '100%',\n },\n});\n\nexport default UpgradeMenu;\n","import React, {useCallback, useContext, useMemo, useState} from 'react';\nimport {StyleSheet, TouchableOpacity, View, Text, Dimensions, Platform} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport {createStackNavigator} from '@react-navigation/stack';\nimport {FontAwesome, Ionicons} from '@expo/vector-icons';\n\nimport MainScreen from './screens/MainScreen/MainScreen';\nimport SignupScreen from './screens/onboarding/SignupScreen/SignupScreen';\n\nimport {SettingsContext} from './contexts/SettingsContext';\nimport {SpeakerContext} from './contexts/SpeakerContext';\nimport {ThemeContext} from './contexts/ThemeContext';\nimport {RoomContext} from './contexts/RoomContext';\nimport {UserContext} from './contexts/UserContext';\nimport useSpeaker from './hooks/useSpeaker';\nimport AccountScreen from './screens/AccountScreen';\nimport UpgradeScreen from './screens/UpgradeScreen';\nimport LinkBankScreen from './screens/LinkBankScreen';\nimport QueryScreen from './screens/QueryScreen';\nimport UpgradeMenu from './UpgradeMenu';\n\nconst MainStack = createStackNavigator();\n\nconst MainStackNavigator = ({drawerOpen, toggleDrawer, closeDrawer}) => {\n const {t} = useTranslation();\n const {user, subscribed} = useContext(UserContext);\n const theme = useContext(ThemeContext);\n const {settings, updateSettings} = useContext(SettingsContext);\n const {room} = useContext(RoomContext);\n const {speaking} = useContext(SpeakerContext);\n const speaker = useSpeaker();\n\n const [discussionId, setDiscussionId] = useState(null);\n const [discussions, setDiscussions] = useState((user || {}).discussions || []);\n\n const toggleMuted = useCallback(() => updateSettings({...settings, muted: !settings.muted}), [settings, updateSettings]);\n\n const setChatMode = useCallback(value => updateSettings({...settings, chatMode: value}), [settings, updateSettings]);\n\n const currentDiscussion = useMemo(() => {\n return discussions.find(discussion => discussion.id === discussionId);\n }, [discussionId, discussions]);\n\n return (\n \n (\n \n {discussionId && currentDiscussion && currentDiscussion.name\n ? currentDiscussion.name\n : room && room.name ? room.name : 'Secrétaire AI'}\n \n ),\n headerLeft: () => (\n \n \n \n \n \n ),\n headerRight: () => (\n \n {/* setChatMode(!settings.chatMode)}*/}\n {/* style={{padding: 10, marginRight: 5}}*/}\n {/*>*/}\n {/* */}\n {/**/}\n\n {/*{user*/}\n {/* && (user.daily_queries !== undefined && user.free_queries !== undefined)*/}\n {/* && (user.status === 'guest' || subscribed === false) && (*/}\n {/* */}\n {/*)}*/}\n\n \n \n \n \n ),\n }}\n >\n {() => (\n \n )}\n \n\n \n\n \n\n \n\n \n\n \n \n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n height: '100%',\n },\n});\n\nexport default MainStackNavigator;\n","import React, {useState, useContext, useMemo, useCallback, useEffect} from 'react';\nimport {\n Keyboard,\n Platform,\n StyleSheet,\n Text,\n TouchableOpacity,\n View,\n} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport {FontAwesome} from '@expo/vector-icons';\nimport * as Sentry from 'sentry-expo';\n\nimport {SettingsContext} from '../../contexts/SettingsContext';\nimport {ThemeContext} from '../../contexts/ThemeContext';\nimport {UserContext} from '../../contexts/UserContext';\nimport Api from '../../constants/Api';\nimport OnboardingLayout from './components/OnboardingLayout';\nimport StyledButton from '../../components/StyledButton';\nimport countries from '../../assets/countries';\nimport languageList from '../../assets/languages-list';\nimport SearchModal from '../../components/SearchModal';\n\nconst DEFAULT_LANGUAGE = languageList.find(language => language.code === 'en-US');\nconst DEFAULT_COUNTRY = countries.find(({code}) => code === 'US');\n\nconst getLanguageObject = code => languageList.find(language => code.length === 2\n ? code === language.code.slice(0, 2)\n : code === language.code\n) || DEFAULT_LANGUAGE;\n\nconst LanguageScreen = ({navigation}) => {\n const {t} = useTranslation();\n const {settings, updateSettings} = useContext(SettingsContext);\n const {setUser, userId, userToken} = useContext(UserContext);\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n\n const [loading, setLoading] = useState(false);\n\n const [languageModalOpen, setLanguageModalOpen] = useState(false);\n\n const [country, setCountry] = useState(DEFAULT_COUNTRY);\n const [countryModalOpen, setCountryModalOpen] = useState(false);\n\n useEffect(() => {\n const initCountry = async () => {\n let countryCode;\n let locale = settings.language;\n\n if (Platform.OS === 'web') {\n // eslint-disable-next-line no-undef\n if (Intl && Intl.DateTimeFormat) {\n // eslint-disable-next-line no-undef\n locale = Intl.DateTimeFormat().resolvedOptions().locale;\n }\n }\n\n const splitted = locale.split('-');\n\n countryCode = splitted.length > 1 ? splitted[1] : locale;\n\n if (countryCode) {\n setCountry(countries.find(({code}) => code === countryCode.toUpperCase()) || DEFAULT_COUNTRY);\n }\n };\n\n initCountry();\n }, []);\n\n const openLanguageModal = useCallback(() => {\n setLanguageModalOpen(true);\n }, [setLanguageModalOpen]);\n\n const closeLanguageModal = useCallback(() => {\n setLanguageModalOpen(false);\n }, [setLanguageModalOpen]);\n\n const handleChangeLanguage = useCallback(newLanguage => {\n updateSettings({...settings, language: newLanguage.code});\n }, [settings, updateSettings]);\n\n const openCountryModal = useCallback(() => {\n setCountryModalOpen(true);\n }, [setCountryModalOpen]);\n\n const closeCountryModal = useCallback(() => {\n setCountryModalOpen(false);\n }, [setCountryModalOpen]);\n\n const handlePress = async () => {\n try {\n setLoading(true);\n\n const response = await fetch(`${Api.apiBaseUrl}/users/${userId}`, {\n method: 'PATCH',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n user_token: userToken,\n language: settings.language,\n country_code: country.code,\n }),\n });\n\n const responseJson = await response.json();\n\n setLoading(false);\n\n if (responseJson) {\n setUser(responseJson);\n navigation.navigate('Name', {onboarding: true});\n }\n\n return responseJson;\n } catch (error) {\n // TODO: Error handling\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n };\n\n const language = useMemo(() => getLanguageObject(settings.language), [settings.language]);\n\n return (\n \n }\n >\n \n {t('LanguageScreen.fields.language.label', 'Language')}\n \n\n \n {\n if (Platform.OS !== 'web') {\n Keyboard.dismiss();\n }\n openLanguageModal();\n }}\n accessible={false}\n >\n \n \n {language.flag}\n \n\n \n {language.name}\n \n\n \n \n \n\n \n \n\n \n {t('LanguageScreen.fields.country.label', 'Country')}\n \n\n \n {\n if (Platform.OS !== 'web') {\n Keyboard.dismiss();\n }\n openCountryModal();\n }}\n accessible={false}\n >\n \n \n {country.flag}\n \n\n \n {country.name}\n \n\n \n \n \n\n {\n setCountry(item);\n }}\n getText={item => item.name}\n />\n \n \n );\n};\n\nconst getStyles = theme => StyleSheet.create({\n inputContainer: {\n flexDirection: 'row',\n width: 320,\n marginTop: 15,\n marginBottom: 25,\n padding: 10,\n borderRadius: 50,\n backgroundColor: theme.inputBackgroundColor,\n borderColor: theme.inputBorderColor,\n },\n flag: {\n fontSize: 28,\n marginLeft: 5,\n marginRight: 5,\n },\n iconStyle: {\n color: theme.colorSecondary,\n marginTop: 5,\n marginBottom: 0,\n },\n});\n\nexport default LanguageScreen;\n","import React, {useContext} from 'react';\nimport {Text, TouchableOpacity, View} from 'react-native';\nimport {ThemeContext} from '../contexts/ThemeContext';\n\nconst RadioButton = ({\n text,\n selected,\n disabled,\n onPress,\n style,\n ...props\n}) => {\n const theme = useContext(ThemeContext);\n\n return (\n \n \n {selected && (\n \n )}\n \n\n \n {text}\n \n \n );\n};\n\nexport default RadioButton;\n","import React, {useState, useContext} from 'react';\nimport {Platform, Text, View} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport * as Sentry from 'sentry-expo';\n\nimport {UserContext} from '../../contexts/UserContext';\nimport {ThemeContext} from '../../contexts/ThemeContext';\nimport Api from '../../constants/Api';\nimport StyledButton from '../../components/StyledButton';\nimport RadioButton from '../../components/RadioButton';\n\nimport OnboardingLayout from './components/OnboardingLayout';\n\nconst GenderScreen = ({navigation}) => {\n const {t} = useTranslation();\n const {setUser, user, userId, userToken} = useContext(UserContext);\n const theme = useContext(ThemeContext);\n\n const [loading, setLoading] = useState(false);\n const [gender, setGender] = useState((user || {}).gender || '');\n const [errorMessage, setErrorMessage] = useState(null);\n\n const handlePress = async () => {\n console.log('gender', gender, gender.length);\n if (!gender.length) {\n setErrorMessage(t('GenderScreen.errors.missing', 'Please choose an option to continue.'));\n return;\n }\n\n try {\n setErrorMessage(null);\n setLoading(true);\n\n const response = await fetch(`${Api.apiBaseUrl}/users/${userId}`, {\n method: 'PATCH',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n user_token: userToken,\n gender: gender,\n }),\n });\n\n const responseJson = await response.json();\n\n console.log('#######responseJson');\n console.log(responseJson);\n\n setLoading(false);\n\n if(responseJson && responseJson.gender === gender) {\n setUser(responseJson);\n navigation.navigate('Username', {onboarding: true});\n } else {\n setErrorMessage(responseJson.gender);\n }\n\n return responseJson;\n } catch (error) {\n console.error('error');\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n };\n\n return (\n }>\n \n \n {t('GenderScreen.label', 'What is your gender ?')}\n \n\n {errorMessage\n ? (\n \n {errorMessage}\n \n ) : null\n }\n\n \n setGender('male')}\n />\n\n setGender('female')}\n />\n\n setGender('na')}\n />\n \n \n \n );\n};\n\nexport default GenderScreen;\n","import React, {useContext, useMemo} from 'react';\n\nimport {\n Image,\n View,\n Text,\n StyleSheet,\n SafeAreaView,\n KeyboardAvoidingView,\n Platform,\n TouchableOpacity,\n} from 'react-native';\nimport {useTranslation} from 'react-i18next';\n\nimport {ThemeContext} from '../contexts/ThemeContext';\nimport openLink from '../utils/openLink';\nimport Api from '../constants/Api';\n\nconst RoomNotFoundScreen = () => {\n const {t} = useTranslation();\n const theme = useContext(ThemeContext);\n const styles = useMemo(() => getStyles(theme), [theme]);\n\n return (\n \n \n \n \n \n openLink(Api.webBaseUrl)}>\n \n \n \n\n \n {t('RoomNotFoundScreen.message', 'Invalid room')}\n \n \n \n \n \n );\n};\n\nconst getStyles = theme => StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: theme.backgroundPrimary,\n },\n textContainer: {\n alignItems: 'center',\n marginTop: 10,\n marginBottom: 10,\n },\n logoContainer: {\n marginTop: 50,\n marginBottom: 20,\n padding: 10,\n borderRadius: 60,\n },\n logo: {\n width: 80,\n height: 80,\n resizeMode: 'contain',\n borderRadius: 20,\n },\n title: {\n fontSize: 22,\n fontWeight: 'bold',\n color: theme.textPrimary,\n },\n});\n\nexport default RoomNotFoundScreen;\n","import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';\nimport {Platform, StyleSheet, View, Dimensions} from 'react-native';\nimport {useTranslation} from 'react-i18next';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport {NavigationContainer} from '@react-navigation/native';\nimport {createStackNavigator} from '@react-navigation/stack';\nimport * as Notifications from 'expo-notifications';\nimport * as SplashScreen from 'expo-splash-screen';\nimport * as Linking from 'expo-linking';\nimport * as Font from 'expo-font';\nimport * as NavigationBar from 'expo-navigation-bar';\nimport * as SecureStore from 'expo-secure-store';\nimport * as Sentry from 'sentry-expo';\nimport uuid from 'react-native-uuid';\nimport {Ionicons} from '@expo/vector-icons';\nimport Constants from 'expo-constants';\n\nimport SignupScreen from './screens/onboarding/SignupScreen/SignupScreen';\nimport NameScreen from './screens/onboarding/NameScreen';\nimport UsernameScreen from './screens/onboarding/UsernameScreen';\nimport GeolocationPermissionScreen from './screens/onboarding/GeolocationPermissionScreen';\nimport PushNotificationPermissionScreen from './screens/onboarding/PushNotificationPermissionScreen';\nimport ContactsPermissionScreen from './screens/onboarding/ContactsPermissionScreen';\n\nimport Api from './constants/Api';\nimport {DEFAULT_SETTINGS, SettingsContext} from './contexts/SettingsContext';\nimport {SpeakerContext} from './contexts/SpeakerContext';\nimport {ThemeContext, themes} from './contexts/ThemeContext';\nimport {UserContext} from './contexts/UserContext';\nimport {RoomContext} from './contexts/RoomContext';\nimport {NotificationsContext} from './contexts/NotificationsContext';\nimport OnboardingHeader from './components/OnboardingHeader';\nimport StyledStatusBar from './components/StyledStatusBar';\nimport MainStackNavigator from './MainStackNavigator';\nimport UpgradeScreen from './screens/UpgradeScreen';\nimport LanguageScreen from './screens/onboarding/LanguageScreen';\nimport GenderScreen from './screens/onboarding/GenderScreen';\nimport RoomNotFoundScreen from './screens/RoomNotFoundScreen';\n\n// import * as Sentry from 'sentry-expo';\n//\n// Sentry.init({\n// dsn: 'https://d995b8a71e40433aa619adbc440345d1@o103811.ingest.sentry.io/5757724',\n// enableInExpoDevelopment: true,\n// debug: true, // Sentry will try to print out useful debugging information if something goes wrong with sending an event. Set this to `false` in production.\n// });\n\n// Access any @sentry/react-native exports via:\n// Sentry.Native.*\n\n// Access any @sentry/browser exports via:\n// Sentry.Browser.*\n\n// Test Sentry Error\n// throw new Error(\"My first Sentry error!\");\n\nconst PURCHASES_STUB = {\n LOG_LEVEL: {},\n setLogLevel: () => {},\n configure: () => {},\n getOfferings: () => {},\n purchasePackage: () => {},\n getCustomerInfo: () => {},\n addCustomerInfoUpdateListener: () => {},\n};\n\nlet Purchases = PURCHASES_STUB;\n\nif (Constants.appOwnership !== 'expo' && Platform.OS !== 'web') {\n Purchases = require('react-native-purchases').default;\n}\n\nconst Stack = createStackNavigator();\n\n// Prepare the notification channel\nNotifications.setNotificationChannelAsync('new-match', {\n name: 'Match notifications',\n importance: Notifications.AndroidImportance.HIGH,\n sound: 'email-sound.wav', // <- for Android 8.0+, see channelId property below\n});\n\nNotifications.setNotificationChannelAsync('new-message', {\n name: 'Message notifications',\n importance: Notifications.AndroidImportance.HIGH,\n sound: 'email-sound.wav', // <- for Android 8.0+, see channelId property below\n});\n\n// When app is in foreground\nNotifications.setNotificationHandler({\n handleNotification: async () => ({\n shouldShowAlert: true,\n shouldPlaySound: true,\n shouldSetBadge: false,\n }),\n});\n\nSplashScreen.preventAutoHideAsync();\n\nconst Splash = () => {\n const theme = useContext(ThemeContext);\n\n return (\n \n );\n};\n\nconst App = () => {\n const containerRef = useRef();\n\n const [isLoadingComplete, setLoadingComplete] = useState(false);\n const [isLoggedIn, setIsLoggedIn] = useState(false);\n const [userId, setUserId] = useState(null);\n const [userToken, setUserToken] = useState(null);\n const [deviceToken, setDeviceToken] = useState(null);\n const [user, setUser] = useState(null);\n\n const [pushNotification, setPushNotification] = useState(true);\n const [notificationsCount, setNotificationsCount] = useState(null);\n\n const [room, setRoom] = useState(null);\n const [settings, setSettings] = useState(DEFAULT_SETTINGS);\n const [speaking, setSpeaking] = useState(false);\n\n const [subscribed, setSubscribed] = useState(null);\n\n const {t, i18n} = useTranslation();\n\n useEffect(() => {\n i18n.changeLanguage(settings.language);\n }, [i18n, settings]);\n\n const [drawerOpen, setDrawerOpen] = useState(Dimensions.get('window').width >= 1500);\n\n const toggleDrawer = useCallback(() => setDrawerOpen(!drawerOpen), [setDrawerOpen, drawerOpen]);\n const closeDrawer = useCallback( () => {\n if (Dimensions.get('window').width >= 1500) {\n // NOTE: We ignore close() call on web/tablets since the drawer can stay open on those platforms\n return ;\n }\n\n setDrawerOpen(false);\n }, [setDrawerOpen]);\n\n // Deep linking\n const linking = {\n prefixes: [Linking.createURL('/'), 'secretaire://', 'https://www.secretaire.ai'],\n config: {\n screens: {\n Root: {\n screens: {\n MainStack: 'app',\n Query: {\n path: 'queries/:id',\n parse: {\n id: Number,\n },\n },\n },\n },\n },\n },\n };\n\n const checkSubscriptionStatus = useCallback(async () => {\n try {\n const customerInfo = await Purchases.getCustomerInfo();\n\n if (typeof customerInfo.entitlements.active['premium'] !== 'undefined') {\n setSubscribed(true);\n } else {\n setSubscribed(false);\n }\n } catch (error) {\n // TODO: Handle error\n }\n }, [setSubscribed]);\n\n useEffect(() => {\n if (user) {\n if (__DEV__) {\n Purchases.setLogLevel(Purchases.LOG_LEVEL.VERBOSE);\n }\n\n if (Platform.OS === 'ios') {\n Purchases.configure({\n apiKey: 'appl_KusxmSTLIKMeunVcvUvMOraiSxw',\n appUserID: userId,\n // TODO: Delete\n // HACK: Ids of user that subscribed so we can't change their appUserID\n // appUserID: ['5310', '5019', '2262'].includes(userId) ? userToken : userId,\n });\n } else if (Platform.OS === 'android') {\n Purchases.configure({\n apiKey: 'goog_aHyALiJxFOavJngXoCadVptudaW',\n appUserID: userId,\n // TODO: Delete\n // HACK: Ids of user that subscribed so we can't change their appUserID\n // appUserID: ['5310', '5019', '2262'].includes(userId) ? userToken : userId,\n });\n }\n\n checkSubscriptionStatus();\n\n Purchases.addCustomerInfoUpdateListener(customerInfo => {\n if (typeof customerInfo.entitlements.active['premium'] !== 'undefined') {\n setSubscribed(true);\n } else {\n setSubscribed(false);\n }\n });\n }\n }, [checkSubscriptionStatus, setSubscribed, user, userId, userToken]);\n\n useEffect(() => {\n if (Platform.OS === 'android') {\n NavigationBar.setBackgroundColorAsync(themes[settings.theme].footerBackground);\n NavigationBar.setButtonStyleAsync(settings.theme);\n }\n }, [settings.theme]);\n\n useEffect(() => {\n const subscription = Notifications.addNotificationReceivedListener(notification => {\n console.log('@@@ notification @@@');\n console.log(notification);\n });\n\n return () => subscription.remove();\n }, []);\n\n const theme = useMemo(() => themes[settings.theme], [settings.theme]);\n\n const retrieveDeviceToken = useCallback(async () => {\n try {\n let deviceTokenFromStorage;\n\n if (Platform.OS === 'web') {\n deviceTokenFromStorage = await AsyncStorage.getItem('deviceToken');\n } else {\n deviceTokenFromStorage = await SecureStore.getItemAsync('deviceToken');\n }\n\n if (deviceTokenFromStorage) {\n setDeviceToken(deviceTokenFromStorage);\n } else {\n const deviceToken = uuid.v4();\n\n if (Platform.OS === 'web') {\n await AsyncStorage.setItem('deviceToken', deviceToken);\n } else {\n await SecureStore.setItemAsync('deviceToken', deviceToken);\n }\n\n setDeviceToken(deviceToken);\n }\n } catch (error) {\n // TODO: Handle error\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n }, [setDeviceToken]);\n\n const signOut = useCallback(async () => {\n await AsyncStorage.removeItem('userId');\n await AsyncStorage.removeItem('userToken');\n\n setUser(null);\n setUserId(null);\n setUserToken(null);\n setIsLoggedIn(false);\n }, []);\n\n const fetchUser = useCallback(async (userId, userToken) => {\n console.log('*** fetchUser ***');\n\n const response = await fetch(\n `${Api.apiBaseUrl}/users/${userId}.json?user_token=${userToken}${Api.roomSlug ? `&room=${Api.roomSlug}` : ''}`,\n {\n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n }\n );\n\n const responseJson = await response.json();\n\n if(responseJson && responseJson.id) {\n setUser(responseJson);\n } else {\n // signOut();\n }\n }, [signOut]);\n\n const retrieveToken = useCallback(async () => {\n\n // Check whether user is already authenticated server-side\n try {\n console.log(USER_ID, USER_TOKEN);\n if(USER_ID && USER_TOKEN) {\n setUserId(USER_ID);\n setUserToken(USER_TOKEN);\n console.log('USER_ID & USER_TOKEN set from server-side authentication');\n await fetchUser(USER_ID, USER_TOKEN);\n } else {\n console.log('USER_ID & USER_TOKEN do not exist in server-side authentication');\n }\n } catch (error) {\n console.log('USER_ID & USER_TOKEN do not exist in server-side authentication.');\n }\n\n try {\n if (userId && userToken) {\n setIsLoggedIn(true);\n\n await fetchUser(userId, userToken);\n } else {\n const userIdFromStorage = await AsyncStorage.getItem('userId');\n const userTokenFromStorage = await AsyncStorage.getItem('userToken');\n\n if (userIdFromStorage && userTokenFromStorage) {\n setUserId(userIdFromStorage);\n setUserToken(userTokenFromStorage);\n setIsLoggedIn(true);\n\n await fetchUser(userIdFromStorage, userTokenFromStorage);\n } else {\n setIsLoggedIn(false);\n }\n }\n } catch (error) {\n // Error retrieving data\n console.error('***** Error retrieving data *****');\n console.error(error);\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n\n setIsLoggedIn(false);\n }\n }, [fetchUser, userId, userToken]);\n\n const retrieveSettings = useCallback(async () => {\n try {\n const settingsFromStorage = await AsyncStorage.getItem('settings');\n\n if (settingsFromStorage) {\n setSettings(JSON.parse(settingsFromStorage));\n }\n } catch (error) {\n // TODO: Handle error, for now settings will be set to DEFAULT_SETTINGS which is probably fine\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n }, [setSettings]);\n\n const updateSettings = useCallback(async value => {\n setSettings(value);\n\n try {\n await AsyncStorage.setItem('settings', JSON.stringify(value));\n } catch (error) {\n // TODO: Handle error\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n }\n }, [setSettings]);\n\n const fetchRoom = useCallback(async () => {\n \n if (Api.roomSlug) {\n const response = await fetch(\n `${Api.apiBaseUrl}/room/${Api.roomSlug}`,\n {\n method: 'GET',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n }\n );\n\n const responseJson = await response.json();\n\n console.log('room', responseJson);\n\n if(responseJson) {\n setRoom(responseJson);\n }\n }\n }, []);\n\n // Only for Web, with User authenticated by the backend\n const getUserFromBackend = useCallback(async () => {\n try {\n console.log(USER_I, USER_TOKEN);\n console.log('USER_ID and USER_TOKEN exist.');\n return [USER_ID, USER_TOKEN];\n } catch (error) {\n console.log('USER_ID and USER_TOKEN do not exist');\n }\n }, []);\n\n\n // Load any resources or data that we need prior to rendering the app\n useEffect(() => {\n const loadResourcesAndDataAsync = async () => {\n try {\n await Promise.all([\n retrieveDeviceToken(),\n retrieveToken(),\n retrieveSettings(),\n fetchRoom(),\n // Load fonts\n // Font.loadAsync({\n // ...Ionicons.font,\n // 'space-mono': require('./assets/fonts/SpaceMono-Regular.ttf'),\n // }),\n ]);\n } catch (error) {\n console.warn(error);\n\n if (Platform.OS === 'web') {\n Sentry.Browser.captureException(error);\n } else {\n Sentry.Native.captureException(error);\n }\n } finally {\n // TODO: We probably need an error screen and not use finally to set loading complete\n setLoadingComplete(true);\n\n if (Platform.OS !== 'web') {\n const initialURL = await Linking.getInitialURL();\n\n if (initialURL) {\n Linking.openURL(initialURL);\n }\n }\n\n await SplashScreen.hideAsync();\n }\n };\n\n loadResourcesAndDataAsync();\n }, []);\n\n let screens = null;\n\n if (!isLoadingComplete) {\n screens = ;\n } else if (Api.roomSlug && !room) {\n screens = ;\n } else if (!userToken || !user) {\n screens = ;\n } else if ((user.status === 'guest' && !user.has_completed_guest_onboarding) || (user.status !== 'guest' && !user.has_completed_onboarding)) {\n screens = (\n \n {user && user.status !== 'guest' && (\n \n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n \n )}\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n >\n {() => (\n \n )}\n \n \n );\n } else {\n screens = (\n \n {() => (\n \n )}\n \n );\n }\n\n return (\n \n \n \n \n \n \n \n \n\n \n \n {screens}\n \n \n \n \n \n \n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n height: '100%',\n },\n});\n\nexport default App;\n","import {registerRootComponent} from 'expo';\nimport * as Sentry from 'sentry-expo';\nimport 'intl-pluralrules';\n\nimport './i18n';\nimport App from './App';\n\nSentry.init({\n\tdsn: 'https://59488a0eb7d54984a03338b886218de8@o4505266437816320.ingest.sentry.io/4505267738116096',\n\tenableInExpoDevelopment: false,\n\tdebug: false,\n\tsampleRate: 0.2,\n});\n\nregisterRootComponent(App);\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","__webpack_require__.amdO = {};","var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);\nvar leafPrototypes;\n// create a fake namespace object\n// mode & 1: value is a module id, require it\n// mode & 2: merge all properties of value into the ns\n// mode & 4: return value when already ns object\n// mode & 16: return value when it's Promise-like\n// mode & 8|1: behave like require\n__webpack_require__.t = function(value, mode) {\n\tif(mode & 1) value = this(value);\n\tif(mode & 8) return value;\n\tif(typeof value === 'object' && value) {\n\t\tif((mode & 4) && value.__esModule) return value;\n\t\tif((mode & 16) && typeof value.then === 'function') return value;\n\t}\n\tvar ns = Object.create(null);\n\t__webpack_require__.r(ns);\n\tvar def = {};\n\tleafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];\n\tfor(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {\n\t\tObject.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));\n\t}\n\tdef['default'] = () => (value);\n\t__webpack_require__.d(ns, def);\n\treturn ns;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.hmd = (module) => {\n\tmodule = Object.create(module);\n\tif (!module.children) module.children = [];\n\tObject.defineProperty(module, 'exports', {\n\t\tenumerable: true,\n\t\tset: () => {\n\t\t\tthrow new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);\n\t\t}\n\t});\n\treturn module;\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","__webpack_require__.p = \"/\";","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t179: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkweb\"] = self[\"webpackChunkweb\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [343], () => (__webpack_require__(7170)))\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["i18n","initReactI18next","init","resources","en","translation","fr","es","zh","pt","ru","it","de","he","iw","ar","debug","lng","fallbackLng","supportedLngs","load","keySeparator","saveMissing","interpolation","escapeValue","console","log","apiBaseUrl","assetsBaseUrl","webBaseUrl","webSocketBaseUrl","roomSlug","Platform","API_BASE_URL","error","ROOM_SLUG","splitHostName","window","location","hostname","split","length","includes","appName","WEB_MAX_WIDTH","themes","light","backgroundPrimary","backgroundSecondary","headerBackground","footerBackground","headerBorderColor","footerBorderColor","textPrimary","textSecondary","colorPrimary","colorSecondary","success","warning","inputBackgroundColor","inputBorderColor","buttonText","link","dark","ThemeContext","createContext","getStyles","theme","StyleSheet","button","paddingVertical","paddingHorizontal","borderRadius","textAlign","alignItems","regular","backgroundColor","outlined","borderColor","borderWidth","outlinedSecondary","outlinedError","textButtonPrimary","textButtonSecondary","textButtonError","regularText","color","outlinedText","outlinedSecondaryText","outlinedErrorText","textButtonPrimaryText","textButtonSecondaryText","textButtonErrorText","_ref","text","disabled","loading","onPress","_ref$variant","variant","_ref$fontSize","fontSize","style","props","_objectWithoutProperties","_excluded","useContext","styles","useMemo","_jsx","TouchableOpacity","_objectSpread","opacity","concat","_toConsumableArray","children","ActivityIndicator","size","marginVertical","Text","lineHeight","RoomContext","React","room","DEVICE_TYPE_LABELS","UserContext","isLoggedIn","user","userId","userToken","deviceToken","setUserId","setUserToken","setDeviceToken","setUser","signOut","inputContainer","justifyContent","width","height","input","inputStyle","errors","textInputStyle","_jsxs","View","TextInput","placeholderTextColor","map","marginTop","container","flex","countryStyle","flexDirection","borderTopColor","borderTopWidth","closeContainer","padding","data","open","close","onClick","_ref$getText","getText","item","name","t","useTranslation","_useState","useState","_useState2","_slicedToArray","searchText","setSearchText","useEffect","onChangeText","useCallback","value","handleClick","filteredList","filter","_ref2","toLowerCase","Modal","animationType","transparent","visible","onRequestClose","onDismiss","SafeAreaView","KeyboardAvoidingView","behavior","Math","min","Dimensions","FontAwesome","FlatList","keyboardShouldPersistTaps","keyExtractor","code","renderItem","_ref3","flag","StyledInput","placeholder","url","Linking","openURL","marginBottom","subtitle","maxWidth","iconStyle","marginLeft","marginRight","fontWeight","legalText","hyperlinkStyle","country","setCountry","status","setStatus","phoneNumber","setPhoneNumber","email","setEmail","signupWithPhone","setSignupWithPhone","_useContext","setErrors","_useState3","_useState4","countryModalOpen","setCountryModalOpen","_useState5","_useState6","signingUpAsGuest","setSigningUpAsGuest","openCountryModal","closeCountryModal","handleEmailSignupPress","_asyncToGenerator","response","fetch","Api","method","headers","body","JSON","stringify","expo_installation_id","Constants","expo_session_id","responseJson","json","Sentry","apply","arguments","handlePhoneSignupPress","alert","Alert","dial_code","startsWith","match","phone_number","_storeToken","_ref4","AsyncStorage","_x","_x2","handleSignupAsGuestPress","_ref5","deviceType","Device","getDeviceTypeAsync","device_token","language","Localization","getLocales","languageTag","country_code","regionCode","timezone","device_type","device_model_name","modelName","brand","manufacturer","os_name","os_version","osVersion","deviceName","userResponse","user_id","user_token","userResponseJson","id","_Fragment","StyledButton","Trans","i18nKey","openLink","marginHorizontal","Keyboard","accessible","TouchableWithoutFeedback","keyboardType","returnKeyType","autoCapitalize","autoCorrect","secureTextEntry","val","phoneNumberRegex","RegExp","replace","SearchModal","countries","email_address","onKeyPress","event","nativeEvent","key","autoFocus","cancelButtonText","confirmationCode","setConfirmationCode","setLoading","handleConfirmPress","upgrade","confirmation_code","rounded","cancelButton","DEFAULT_COUNTRY","find","textContainer","logoContainer","logo","resizeMode","title","formContainer","_useState7","_useState8","_useState9","_useState10","initCountry","countryCode","locale","Intl","DateTimeFormat","resolvedOptions","splitted","toUpperCase","Image","source","image_url","uri","require","short_description","SignupForm","ConfirmForm","safeArea","content","stepImage","alignSelf","paddingTop","footer","borderStyle","footerInner","useHeaderHeight","keyboardVerticalOffset","ScrollView","formRow","rowTitle","navigation","first_name","firstName","setFirstName","last_name","lastName","setLastName","birth_date","birthDateDay","setBirthDateDay","_useState11","_useState12","birthDateMonth","setBirthDateMonth","_useState13","_useState14","birthDateYear","setBirthDateYear","_useState15","_useState16","handlePress","validationError","Object","keys","undefined","navigate","onboarding","OnboardingLayout","bold","username","setUsername","errorMessage","setErrorMessage","has_completed_guest_onboarding","has_completed_onboarding","paddingBottom","renderRules","strong","node","parent","selectable","em","s","code_inline","inheritedStyles","code_block","charAt","substring","fence","onLinkPress","customCallback","result","openUrl","attributes","href","textgroup","hardbreak","softbreak","inline","span","bubble","dateBubble","queryBubble","exampleBubble","errorQueryBubble","errorBubble","responseBubble","dateText","responseText","isMarkdown","search","matches","matchAll","index","_loop","start","end","fullLink","slice","push","Markdown","rules","paragraph","ios","fontFamily","android","NEXT_SCREEN","handleNotNowButtonClick","handleAllowButtonClick","Location","requestForegroundPermissionsAsync","updateUserLocation","getCurrentPositionAsync","enableHighAccuracy","latitude","coords","longitude","paddingRight","paddingLeft","Bubble","LOADING_INITIAL_STATE","buttonClicked","loadingState","setLoadingState","updateUserHasCompletedOnboarding","tokenInfos","updatedUser","Notifications","token","push_token","has_notifications_enabled","Contacts","requestPermissionsAsync","updateUserContacts","contacts","getContactsAsync","fields","Fields","Addresses","Birthday","Company","ContactType","Dates","Emails","ExtraNames","FirstName","ID","ImageAvailable","InstantMessageAddresses","JobTitle","LastName","MaidenName","MiddleName","Name","NamePrefix","NameSuffix","Nickname","NonGregorianBirthday","PhoneNumbers","PhoneticFirstName","PhoneticLastName","PhoneticMiddleName","RawImage","Relationships","SocialProfiles","UrlAddresses","contact","facebookProfile","socialProfiles","service","origin","ref","contact_type","contactType","middle_name","middleName","email_address_1","emails","email_address_1_label","label","email_address_2","email_address_2_label","email_address_3","email_address_3_label","phone_number_1","phoneNumbers","digits","number","phone_number_1_formatted","phone_number_1_label","phone_number_2","phone_number_2_formatted","phone_number_2_label","phone_number_3","phone_number_3_formatted","phone_number_3_label","phone_number_4","phone_number_4_formatted","phone_number_4_label","phone_number_5","phone_number_5_formatted","phone_number_5_label","facebook_username","facebook_url","DEFAULT_SETTINGS","muted","chatMode","SettingsContext","SpeakerContext","speaking","setSpeaking","NotificationsContext","notificationsCount","setNotificationsCount","unreadNotificationsCount","setUnreadNotificationsCount","unansweredNotificationsCount","setUnansweredNotificationsCount","undismissdNotificationsCount","setUndismissNotificationsCount","header","borderBottomWidth","borderBottomColor","backContainer","position","elevation","zIndex","back","step","HeaderBackButton","canGoBack","tintColor","goBack","StatusBar","barStyle","responseBuffer","tokenBuffer","shouldSpeakQuery","speakingFalseInterval","_useContext$settings","settings","stop","Speech","speak","languageToSpeak","Audio","setAudioModeAsync","playsInSilentModeIOS","allowsRecordingIOS","sound","playAsync","onStart","setInterval","speechSynthesis","clearInterval","onResume","onPause","onStopped","onDone","pending","initSpeakQueryState","speakToken","emptyResponseBuffer","message","_x3","TextButton","_ref$selected","selected","_ref$icon","icon","numberOfLines","EDIT_DISCUSSION_INITIAL_STATE","drawerContainer","top","left","drawer","borderRightWidth","borderRightColor","bottomContainer","closeArea","querying","drawerOpen","closeDrawer","setChatMode","discussions","setDiscussions","discussionsLoading","discussionId","updateDiscussionId","fetchDiscussions","setDiscussionsLoading","subscribed","_useContext2","updateSettings","useNavigation","clearDiscussionLoading","setClearDiscussionLoading","editDiscussionState","setEditDiscussionState","languageModalOpen","setLanguageModalOpen","createDiscussion","clearDiscussion","clearDiscussionId","updatedDiscussions","onEditDiscussionTitle","confirm","discussion","clearDiscussions","updatedDiscussion","toggleTheme","currentLanguage","languageList","openLanguageModal","closeLanguageModal","handleChangeLanguage","newLanguage","handleClearDiscussion","handleSettingsClick","handleContactSupport","Ionicons","daily_queries","free_queries","MaterialIcons","MediaRecorder","isTypeSupported","EventSource","options","_classCallCheck","ERROR","CONNECTING","OPEN","CLOSED","this","interval","pollingInterval","lastEventId","lastIndexProcessed","eventType","eventHandlers","timeout","timeOut","_xhr","_pollTimer","toString","SyntaxError","_pollAgain","_createClass","time","_this","setTimeout","_this2","XMLHttpRequest","entries","setRequestHeader","onreadystatechange","xhr","readyState","DONE","LOADING","dispatch","type","_handleEvent","xhrStatus","xhrState","UNSENT","onerror","send","e","parts","substr","lastIndexOf","retry","line","i","indexOf","parseInt","isNaN","join","listener","handler","availableTypes","Error","values","clearTimeout","abort","throttle","func","wait","leading","trailing","context","ctx","args","previous","later","Date","now","remaining","formatDate","date","today","getDate","getMonth","getFullYear","formatHoursAndMinutes","hours","minutes","formatTime","getHours","getMinutes","PlaceCard","card","isLast","distance_text","distance","round","image","rating","total_ratings","price_level","currencySymbol","repeat","link_1_title","link_1_url","WeatherCard","temperature","min_temperature","max_temperature","ImageCard","hasAdditionnalInfo","description","link_2_title","link_3_title","link_4_title","link_5_title","link_6_title","borderTopLeftRadius","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","link_2_url","link_3_url","link_4_url","link_5_url","link_6_url","card_type","ResponseLink","query","handleReprocessQuery","count","getForegroundPermissionsAsync","canAskAgain","results","gap","Maps","PlacesMap","cards","mapViewRef","useRef","showUserLocation","setShowUserLocation","checkLocationPermission","granted","overflow","default","provider","PROVIDER_GOOGLE","showsUserLocation","scrollEnabled","onMapReady","current","fitToSuppliedMarkers","animated","edgePadding","right","bottom","Marker","identifier","coordinate","_Maps$useLoadScript","useLoadScript","googleMapsApiKey","isLoaded","loadError","GoogleMap","mapContainerStyle","gestureHandling","zoom","onLoad","mapInstance","bounds","google","maps","LatLngBounds","forEach","extend","LatLng","fitBounds","lat","CHANNEL_ICONS","Recording","recording","Web","web","Whatsapp","whatsapp","Email","Slack","slack","Telegram","telegram","keyboard","Sms","sms","Message","examplesContainer","scrollViewRef","onScrollBeginDrag","queries","setQueryText","exampleSections","canStopGenerating","_ref$disableResponseL","disableResponseLinks","_ref$disableQueryLink","disableQueryLinks","currentDate","created_at","handleShare","immediate","callNow","debounce","cardsText","texts","reduce","Share","subject","action","activityType","scrollEventThrottle","contentContainerStyle","examples","horizontal","showsHorizontalScrollIndicator","flexGrow","example","shouldDisplayDate","queryDate","expert_mode","channel","responses","sort","a","b","ResponseLinks","Card","getTime","recordingOptions","isMeteringEnabled","extension","outputFormat","audioEncoder","sampleRate","numberOfChannels","bitRate","audioQuality","linearPCMBitDepth","linearPCMIsBigEndian","linearPCMIsFloat","mimeType","bitsPerSecond","INITIAL_STATE","recordingFile","startingPromise","state","setState","requestPermissions","beforeRequestingCallback","audioPerm","audioRecording","Promise","resolve","interruptionModeIOS","InterruptionModeIOS","shouldDuckAndroid","interruptionModeAndroid","InterruptionModeAndroid","playThroughEarpieceIOS","playThroughEarpieceAndroid","beforePrep","prepareToRecordAsync","beforeStart","startAsync","stopAndUnloadAsync","getURI","TRIANGLE_SIZE","modalView","modalText","triangle","borderLeftWidth","borderLeftColor","innerTriangle","outerTriangle","anchor","_ref$position","menuRef","x","menuMeasurement","setMenuMeasurement","anchorMeasurement","setAnchorMeasurement","measure","fx","fy","px","Pressable","_defineProperty","MODAL_HORIZONTAL_MARGIN","FooterButton","forwardRef","iconBackgroundColor","touch","BUTTON_SIZE","alertCloseTimeout","timerInterval","microphonePermissionsRequested","centeredView","pushLocalQuery","updateCurrentQuery","createQuery","recordButtonRef","recorder","useRecorder","speaker","useSpeaker","shortPressAlertVisible","setShortPressAlertVisible","displayTime","setDisplayTime","uploading","setUploading","handleUploadRecording","formData","FormData","append","blob","locationPerm","navigator","permissions","getLastKnownPositionAsync","requiredAccuracy","maxAge","accuracy","mayShowUserSettingsDialog","scaleAnim","Animated","translateAnim","recorderAnimation","toValue","duration","useNativeDriver","handleLongPress","Haptics","impactAsync","startTime","notificationAsync","handlePressIn","handlePressOut","uuid","localQuery","updated_at","localQueries","scrollToEnd","transcription","reset","recordingId","transform","translateX","scale","floor","onPressIn","onPressOut","onLongPress","delayLongPress","Menu","RTL_LANGUAGES","DEFAULT_INPUT_HEIGHT","shouldScroll","eventSource","backgroundTimestamp","pollingTimeout","preventPolling","borderTopStyle","innerFooter","shouldntRefreshQueries","setQuerying","setCanStopGenerating","setQueries","queryText","examplesLoading","setExamplesLoading","setExampleSections","inputHeight","setInputHeight","loadExamples","fetchQueries","appState","AppState","subscription","nextAppState","remove","pushCurrentQuery","updatedQueries","currentQuery","currentQueryIndex","findIndex","fakeResponseId","requestObject","stream","discussion_id","recording_id","device_brand","device_name","device_manufacturer","device_os_name","device_os_version","screen","deviceTypeIndex","appStateListener","createRequest","reject","addEventListener","throttledUpdateCurrentQuery","parse","fetchAndUpdateDiscussion","_ref6","removeAllEventListeners","_x4","_ref7","newQueries","pollPendingQueries","_ref8","some","fetchedQuery","_x5","handleSendQuery","_ref9","trim","_x6","_ref10","update","optimisticallyUpdatedQuery","queryIndex","updatedQuery","_x7","_x8","onSubmitEditing","onChangeWeb","requestAnimationFrame","target","scrollHeight","_event$nativeEvent","shiftKey","onContentSizeChange","contentSize","Chat","onChange","multiline","Recorder","CustomProgressBar","rotationDegree","COLOR","accessibilityRole","rotateZ","interpolate","inputRange","outputRange","systemTopTextContainer","systemTopTextText","transcriptTopTextContainer","transcriptTopTextText","headerHeight","topTextInitialValue","topText","setTopText","lastResponse","setLastResponse","loadingAnimation","shakeAnim","breathingAnimation","easing","Easing","maxHeight","_ref12","mainContainer","setDiscussionId","shouldntRefresh","Drawer","ChatView","MicrophoneView","Purchases","restorePurchases","topContainer","bankAccounts","setBankAccounts","restoreLoading","setRestoreLoading","fetchBankAccounts","bankAccountList","logOut","restore","deleteAccount","_onPress","bankAccount","bank_name","owner_name","LOG_LEVEL","setLogLevel","configure","getOfferings","availablePackages","product","priceString","subscriptionPeriod","purchasePackage","getCustomerInfo","addCustomerInfoUpdateListener","PurchasePackage","setSelectedPackage","_purchasePackage$prod","SUBSCRIPTION_PERIOD_TO_TITLE","FREE","P1M","P1Y","SUBSCRIPTION_PERIOD_TO_MODEL","setSubscribed","confirmLoading","setConfirmLoading","packages","setPackages","FREE_PACKAGE","selectedPackage","offerings","buy","customerInfo","entitlements","active","userCancelled","display","WebView","onMessage","string","loaderContainer","scrollView","titleContainer","stepsContainer","arrow","stepIcon","stepName","params","useRoute","setOptions","setQuery","fetchQuery","steps","unshift","device","capitalize","MainStack","createStackNavigator","toggleDrawer","toggleMuted","currentDiscussion","Navigator","screenOptions","headerTitleAlign","headerStyle","headerBackgroundContainerStyle","headerTitleStyle","headerTintColor","headerShadowVisible","cardStyle","Screen","headerTitle","headerLeft","headerRight","MainScreen","default_screen","component","SignupScreen","headerBackTitleVisible","AccountScreen","UpgradeScreen","LinkBankScreen","QueryScreen","DEFAULT_LANGUAGE","gender","setGender","minWidth","RadioButton","Stack","importance","handleNotification","_handleNotification","shouldShowAlert","shouldPlaySound","shouldSetBadge","SplashScreen","Splash","containerRef","isLoadingComplete","setLoadingComplete","setIsLoggedIn","pushNotification","setPushNotification","_useState17","_useState18","setRoom","_useState19","_useState20","setSettings","_useState21","_useState22","_useState23","_useState24","_useTranslation","changeLanguage","_useState25","_useState26","setDrawerOpen","linking","prefixes","createURL","config","screens","Root","Query","path","Number","checkSubscriptionStatus","apiKey","appUserID","NavigationBar","setBackgroundColorAsync","setButtonStyleAsync","notification","retrieveDeviceToken","deviceTokenFromStorage","SecureStore","getItemAsync","setItemAsync","fetchUser","retrieveToken","USER_ID","USER_TOKEN","userIdFromStorage","userTokenFromStorage","retrieveSettings","settingsFromStorage","fetchRoom","USER_I","loadResourcesAndDataAsync","all","warn","initialURL","getInitialURL","RoomNotFoundScreen","headerShown","Group","LanguageScreen","_ref11","OnboardingHeader","NameScreen","GenderScreen","_ref13","UsernameScreen","_ref14","GeolocationPermissionScreen","_ref15","PushNotificationPermissionScreen","_ref16","ContactsPermissionScreen","_ref17","_ref18","MainStackNavigator","Provider","StyledStatusBar","NavigationContainer","dsn","enableInExpoDevelopment","registerRootComponent","App","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","loaded","__webpack_modules__","call","m","amdO","deferred","O","chunkIds","fn","priority","notFulfilled","Infinity","fulfilled","j","every","splice","r","n","getter","__esModule","d","leafPrototypes","getProto","getPrototypeOf","obj","mode","then","ns","create","def","getOwnPropertyNames","definition","o","defineProperty","enumerable","get","g","globalThis","Function","hmd","set","prop","prototype","hasOwnProperty","Symbol","toStringTag","p","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","chunkLoadingGlobal","self","bind","__webpack_exports__"],"sourceRoot":""}