diff --git a/app.json b/app.json index 7cbdfe579..3150bfe3e 100644 --- a/app.json +++ b/app.json @@ -46,7 +46,7 @@ [ "expo-font", { - "fonts": ["./public/Rubik-Regular.ttf", "./public/Rubik-Medium.ttf"] + "fonts": ["node_modules/@expo-google-fonts/rubik/Rubik_400Regular.ttf", "node_modules/@expo-google-fonts/rubik/Rubik_500Medium.ttf"] } ], [ diff --git a/messages/en.json b/messages/en.json index df2f35e19..43491fbbd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -824,6 +824,9 @@ "description": "Button to share an observation", "message": "Share" }, + "screens.Observation.TrackList.track": { + "message": "Track" + }, "screens.Observation.cancel": { "description": "Button to cancel delete of observation", "message": "Cancel" @@ -1451,6 +1454,9 @@ "screens.Sync.ProjectSyncDisplay.waitingForDevices": { "message": "Waiting for devices" }, + "screens.Track.ObservationList.observation": { + "message": "Observation" + }, "screens.Track.ObservationList.observations": { "message": "Observations" }, diff --git a/package-lock.json b/package-lock.json index 5302d0233..6a4305df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,23 @@ { "name": "comapeo-mobile", - "version": "1.1.0-pre", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "comapeo-mobile", - "version": "1.1.0-pre", + "version": "1.1.0", "hasInstallScript": true, "dependencies": { "@bam.tech/react-native-image-resizer": "^3.0.7", "@comapeo/ipc": "2.0.2", "@dev-plugins/react-navigation": "^0.0.6", + "@expo-google-fonts/rubik": "^0.2.3", "@formatjs/intl-getcanonicallocales": "^2.3.0", "@formatjs/intl-locale": "^3.3.2", "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", - "@gorhom/bottom-sheet": "^4.5.1", + "@gorhom/bottom-sheet": "^5.0.5", "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", @@ -69,12 +70,12 @@ "react-native-android-open-settings": "^1.3.0", "react-native-confirmation-code-field": "^7.3.1", "react-native-device-info": "^10.14.0", - "react-native-gesture-handler": "~2.14.0", + "react-native-gesture-handler": "^2.20.2", "react-native-indicators": "^0.17.0", "react-native-linear-gradient": "^2.8.3", "react-native-mmkv": "^2.12.1", "react-native-progress": "^5.0.1", - "react-native-reanimated": "~3.6.2", + "react-native-reanimated": "^3.16.1", "react-native-restart": "^0.0.27", "react-native-safe-area-context": "4.8.2", "react-native-scale-bar": "^1.0.6", @@ -1650,19 +1651,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-assign": { - "version": "7.22.5", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.22.11", "license": "MIT", @@ -2774,6 +2762,11 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@expo-google-fonts/rubik": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@expo-google-fonts/rubik/-/rubik-0.2.3.tgz", + "integrity": "sha512-6qqK3qgBDtjqjfDfgzh7mcduRLp5AuKCkOfafde+VaA+q0S/6levqCLg27st5vNWGGcsRLgT4xWtm2+Fv+agJQ==" + }, "node_modules/@expo/bunyan": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", @@ -4818,8 +4811,9 @@ "license": "MIT" }, "node_modules/@gorhom/bottom-sheet": { - "version": "4.5.1", - "license": "MIT", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.0.5.tgz", + "integrity": "sha512-OPMbwrU/sx/o8AOHe4Bmthp0oR/a1jsTYmdjeGlnWmpnwAVa13QEALsgCm8WaDKA39qeoD7rT38e4/GYeL4myA==", "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" @@ -4829,8 +4823,8 @@ "@types/react-native": "*", "react": "*", "react-native": "*", - "react-native-gesture-handler": ">=1.10.1", - "react-native-reanimated": ">=2.2.0" + "react-native-gesture-handler": ">=2.16.1", + "react-native-reanimated": ">=3.10.1" }, "peerDependenciesMeta": { "@types/react": { @@ -23072,15 +23066,13 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.14.1.tgz", - "integrity": "sha512-YiM1BApV4aKeuwsM6O4C2ufwewYEKk6VMXOt0YqEZFMwABBFWhXLySFZYjBSNRU2USGppJbfHP1q1DfFQpKhdA==", - "license": "MIT", + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz", + "integrity": "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==", "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4", - "lodash": "^4.17.21", "prop-types": "^15.7.2" }, "peerDependencies": { @@ -23132,23 +23124,24 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.3.tgz", - "integrity": "sha512-2KkkPozoIvDbJcHuf8qeyoLROXQxizSi+2CTCkuNVkVZOxxY4B0Omvgq61aOQhSZUh/649x1YHoAaTyGMGDJUw==", - "license": "MIT", + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.1.tgz", + "integrity": "sha512-Wnbo7toHZ6kPLAD8JWKoKCTfNoqYOMW5vUEP76Rr4RBmJCrdXj6oauYP0aZnZq8NCbiP5bwwu7+RECcWtoetnQ==", "dependencies": { - "@babel/plugin-transform-object-assign": "^7.16.7", + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0-0", - "@babel/plugin-proposal-optional-chaining": "^7.0.0-0", - "@babel/plugin-transform-arrow-functions": "^7.0.0-0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", - "@babel/plugin-transform-template-literals": "^7.0.0-0", "react": "*", "react-native": "*" } @@ -26505,9 +26498,10 @@ } }, "node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index b6e1f67e5..24187b3dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "comapeo-mobile", - "version": "1.1.0-pre", + "version": "1.1.0", "private": true, "main": "index.js", "scripts": { @@ -30,11 +30,12 @@ "@bam.tech/react-native-image-resizer": "^3.0.7", "@comapeo/ipc": "2.0.2", "@dev-plugins/react-navigation": "^0.0.6", + "@expo-google-fonts/rubik": "^0.2.3", "@formatjs/intl-getcanonicallocales": "^2.3.0", "@formatjs/intl-locale": "^3.3.2", "@formatjs/intl-pluralrules": "^5.2.4", "@formatjs/intl-relativetimeformat": "^11.2.4", - "@gorhom/bottom-sheet": "^4.5.1", + "@gorhom/bottom-sheet": "^5.0.5", "@osm_borders/maritime_10000m": "^1.1.0", "@react-native-community/hooks": "^2.8.0", "@react-native-community/netinfo": "11.1.0", @@ -87,12 +88,12 @@ "react-native-android-open-settings": "^1.3.0", "react-native-confirmation-code-field": "^7.3.1", "react-native-device-info": "^10.14.0", - "react-native-gesture-handler": "~2.14.0", + "react-native-gesture-handler": "^2.20.2", "react-native-indicators": "^0.17.0", "react-native-linear-gradient": "^2.8.3", "react-native-mmkv": "^2.12.1", "react-native-progress": "^5.0.1", - "react-native-reanimated": "~3.6.2", + "react-native-reanimated": "^3.16.1", "react-native-restart": "^0.0.27", "react-native-safe-area-context": "4.8.2", "react-native-scale-bar": "^1.0.6", diff --git a/src/frontend/Navigation/Drawer.tsx b/src/frontend/Navigation/Drawer.tsx index 2d2f0ac7f..e1eddd9f1 100644 --- a/src/frontend/Navigation/Drawer.tsx +++ b/src/frontend/Navigation/Drawer.tsx @@ -8,8 +8,7 @@ import {defineMessages, useIntl} from 'react-intl'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import EntypoIcon from 'react-native-vector-icons/Entypo'; import {NavigatorScreenParams} from '@react-navigation/native'; -import {View} from 'react-native'; -import {Text} from '../sharedComponents/Text'; +import {View, Text} from 'react-native'; import {VERY_LIGHT_GREY, WHITE} from '../lib/styles'; import {useProjectSettings} from '../hooks/server/projects'; import {AppStackParamsList} from '../sharedTypes/navigation'; @@ -121,6 +120,7 @@ const DrawerContent = ({navigation}: DrawerContentComponentProps) => { style={{alignSelf: 'flex-end', marginRight: 20}} onPress={navigation.closeDrawer} /> + {/* This text component is one of the exceptions that does not use the shared text components as requested by Sabella */} { textAlign: 'center', paddingHorizontal: 40, fontSize: 18, + fontFamily: 'Rubik_400Regular', }}> {data?.name ? formatMessage(m.projName, {projectName: data.name}) diff --git a/src/frontend/Navigation/Stack/index.tsx b/src/frontend/Navigation/Stack/index.tsx index fe5576134..90157d3ea 100644 --- a/src/frontend/Navigation/Stack/index.tsx +++ b/src/frontend/Navigation/Stack/index.tsx @@ -103,6 +103,7 @@ export const NavigatorScreenOptions: NativeStackNavigationOptions = { presentation: 'card', contentStyle: {backgroundColor: WHITE}, headerStyle: {backgroundColor: WHITE}, + headerTitleStyle: {fontFamily: 'Rubik_500Medium'}, headerLeft: props => , // This only hides the DEFAULT back button. We render a custom one in headerLeft, so the default one should always be hidden. // This **might** cause a problem for IOS diff --git a/src/frontend/hooks/server/invites.ts b/src/frontend/hooks/server/invites.ts index d5b382489..066c4122d 100644 --- a/src/frontend/hooks/server/invites.ts +++ b/src/frontend/hooks/server/invites.ts @@ -58,24 +58,6 @@ export function useRejectInvite() { }); } -export function useClearAllPendingInvites() { - const queryClient = useQueryClient(); - const mapeoApi = useApi(); - - return useMutation({ - mutationFn: ({inviteIds}: {inviteIds: Array}) => { - return Promise.all( - inviteIds.map(id => mapeoApi.invite.reject({inviteId: id})), - ); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [INVITE_KEY], - }); - }, - }); -} - export function useSendInvite() { const queryClient = useQueryClient(); const {projectApi} = useActiveProject(); diff --git a/src/frontend/hooks/server/maps.ts b/src/frontend/hooks/server/maps.ts index a9efb8fd8..040c9bedb 100644 --- a/src/frontend/hooks/server/maps.ts +++ b/src/frontend/hooks/server/maps.ts @@ -47,7 +47,7 @@ export function useImportCustomMapFile() { return useMutation({ mutationFn: async (opts: {uri: string}) => { - await FileSystem.moveAsync({ + await FileSystem.copyAsync({ from: opts.uri, to: DEFAULT_CUSTOM_MAP_FILE_PATH, }); diff --git a/src/frontend/hooks/useFormattedTimeSince.ts b/src/frontend/hooks/useFormattedTimeSince.ts index 363baf2ff..8c394dc07 100644 --- a/src/frontend/hooks/useFormattedTimeSince.ts +++ b/src/frontend/hooks/useFormattedTimeSince.ts @@ -3,7 +3,7 @@ import {Duration} from 'luxon'; export const useFormattedTimeSince = (start: Date | null, interval: number) => { const [currentTime, setCurrentTime] = useState(new Date()); - let startDate = start ? start : new Date(); + let startDate = start ? new Date(start) : new Date(); useEffect(() => { setCurrentTime(new Date()); diff --git a/src/frontend/images/checkbox/CheckboxError.svg b/src/frontend/images/checkbox/CheckboxError.svg new file mode 100644 index 000000000..08eb882a2 --- /dev/null +++ b/src/frontend/images/checkbox/CheckboxError.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/images/checkbox/CheckboxSelected.svg b/src/frontend/images/checkbox/CheckboxSelected.svg new file mode 100644 index 000000000..f050fd508 --- /dev/null +++ b/src/frontend/images/checkbox/CheckboxSelected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/images/checkbox/CheckboxUnselected.svg b/src/frontend/images/checkbox/CheckboxUnselected.svg new file mode 100644 index 000000000..970cf7c52 --- /dev/null +++ b/src/frontend/images/checkbox/CheckboxUnselected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/lib/file-system.ts b/src/frontend/lib/file-system.ts index d23307e9e..a319aa1a0 100644 --- a/src/frontend/lib/file-system.ts +++ b/src/frontend/lib/file-system.ts @@ -9,14 +9,16 @@ export function convertFileUriToPosixPath(fileUri: string) { } // TODO: Some overlap with selectFile() from lib/utils but fixes some usage limitations. Ideally use this for everything -export async function selectFile(opts: { - copyToCache?: boolean; +export async function selectFile({ + mimeFilters, + extensionFilters, +}: { mimeFilters?: Array; extensionFilters?: Array; -}) { +} = {}) { const documentResult = await DocumentPicker.getDocumentAsync({ - type: opts.mimeFilters, - copyToCacheDirectory: opts.copyToCache, + type: mimeFilters, + copyToCacheDirectory: false, multiple: false, }); @@ -28,17 +30,14 @@ export async function selectFile(opts: { throw new Error(); } - const hasValidExtension = opts.extensionFilters - ? opts.extensionFilters.some(extension => - asset.uri.endsWith(`.${extension}`), - ) - : true; - - if (!hasValidExtension) { - FileSystem.deleteAsync(asset.uri).catch(err => { - console.log(err); + if (extensionFilters) { + const hasValidExtension = extensionFilters.some(extension => { + return asset.name.endsWith(`.${extension}`); }); - throw new Error('Invalid extension'); + + if (!hasValidExtension) { + throw new Error('Invalid extension'); + } } return asset; diff --git a/src/frontend/lib/styles.ts b/src/frontend/lib/styles.ts index bca8a9a31..ee9ec2ba2 100644 --- a/src/frontend/lib/styles.ts +++ b/src/frontend/lib/styles.ts @@ -24,3 +24,4 @@ export const DARK_ORANGE = '#E86826'; export const SYNC_BACKGROUND = '#2348B2'; export const GPS_MODAL_TEXT = 'rgb(40,40,40)'; export const DARK_GREEN = '#59A553'; +export const WARNING_RED = '#D92222'; diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index eb2919fb2..d66aad3c5 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -1,10 +1,9 @@ -import React, {FC, useState} from 'react'; -import {Linking, View} from 'react-native'; +import React, {FC, useEffect, useRef} from 'react'; +import {Linking, View, AppState, AppStateStatus} from 'react-native'; import {defineMessages, useIntl} from 'react-intl'; import AudioPermission from '../../images/observationEdit/AudioPermission.svg'; import {BottomSheetModalContent} from '../../sharedComponents/BottomSheetModal'; import {Audio} from 'expo-av'; -import {PermissionResponse} from 'expo-modules-core'; const m = defineMessages({ title: { @@ -39,14 +38,42 @@ const m = defineMessages({ interface PermissionAudioBottomSheetContentProps { closeSheet: () => void; setShouldNavigateToAudioTrue: () => void; + permissionStatus: Audio.PermissionStatus | null; + isOpen: boolean; } export const PermissionAudioBottomSheetContent: FC< PermissionAudioBottomSheetContentProps -> = ({closeSheet, setShouldNavigateToAudioTrue}) => { +> = ({closeSheet, setShouldNavigateToAudioTrue, permissionStatus, isOpen}) => { const {formatMessage: t} = useIntl(); - const [permissionResponse, setPermissionResponse] = - useState(null); + const appState = useRef(AppState.currentState); + + useEffect(() => { + if (!isOpen) return; + + const handleAppStateChange = async (nextAppState: AppStateStatus) => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + const {status} = await Audio.getPermissionsAsync(); + if (status === 'granted') { + closeSheet(); + setShouldNavigateToAudioTrue(); + } + } + appState.current = nextAppState; + }; + + const subscription = AppState.addEventListener( + 'change', + handleAppStateChange, + ); + + return () => { + subscription.remove(); + }; + }, [isOpen, closeSheet, setShouldNavigateToAudioTrue]); const handleOpenSettings = () => { Linking.openSettings(); @@ -55,7 +82,6 @@ export const PermissionAudioBottomSheetContent: FC< const handleRequestPermission = async () => { const response = await Audio.requestPermissionsAsync(); closeSheet(); - setPermissionResponse(response); if (response.status === 'granted') { setShouldNavigateToAudioTrue(); } else if (response.status === 'denied' && !response.canAskAgain) { @@ -63,16 +89,18 @@ export const PermissionAudioBottomSheetContent: FC< } }; - const onPressActionButton = !permissionResponse - ? handleRequestPermission - : permissionResponse.status === 'denied' - ? handleOpenSettings - : handleRequestPermission; - const actionButtonText = !permissionResponse - ? t(m.allowButtonText) - : permissionResponse.status === 'denied' - ? t(m.goToSettingsButtonText) - : t(m.allowButtonText); + const onPressActionButton = + !permissionStatus || permissionStatus === 'undetermined' + ? handleRequestPermission + : permissionStatus === 'denied' + ? handleOpenSettings + : handleRequestPermission; + const actionButtonText = + !permissionStatus || permissionStatus === 'undetermined' + ? t(m.allowButtonText) + : permissionStatus === 'denied' + ? t(m.goToSettingsButtonText) + : t(m.allowButtonText); return ( diff --git a/src/frontend/screens/Audio/index.tsx b/src/frontend/screens/Audio/index.tsx index bc3a74fca..b9e8ec87b 100644 --- a/src/frontend/screens/Audio/index.tsx +++ b/src/frontend/screens/Audio/index.tsx @@ -13,7 +13,6 @@ export function Audio({route}: NativeRootNavigationProps<'Audio'>) { const {deleteAudio} = useDraftObservation(); const {isEditing, uri, isSavedUri = false} = route.params; - console.log({isEditing}); return ( <> {uri ? ( diff --git a/src/frontend/screens/MapScreen/ObservationMapLayer.tsx b/src/frontend/screens/MapScreen/ObservationMapLayer.tsx index 6f64a48c4..44074f511 100644 --- a/src/frontend/screens/MapScreen/ObservationMapLayer.tsx +++ b/src/frontend/screens/MapScreen/ObservationMapLayer.tsx @@ -1,7 +1,6 @@ import React from 'react'; import MapboxGL from '@rnmapbox/maps'; -import {usePersistedTrack} from '../../hooks/persistedState/usePersistedTrack'; import {useObservations} from '../../hooks/server/observations'; import {usePresetsQuery} from '../../hooks/server/presets'; import {useNavigationFromHomeTabs} from '../../hooks/useNavigationWithTypes'; @@ -13,7 +12,6 @@ import { export const ObservationMapLayer = () => { const {data: observations} = useObservations(); const {navigate} = useNavigationFromHomeTabs(); - const isTracking = usePersistedTrack(state => state.isTracking); const {data: presets} = usePresetsQuery(); @@ -38,11 +36,7 @@ export const ObservationMapLayer = () => { }} id="observations-source" shape={displayedFeatures}> - + ); }; diff --git a/src/frontend/screens/MapScreen/index.tsx b/src/frontend/screens/MapScreen/index.tsx index 56016b979..aefe75497 100644 --- a/src/frontend/screens/MapScreen/index.tsx +++ b/src/frontend/screens/MapScreen/index.tsx @@ -112,9 +112,9 @@ export const MapScreen = () => { )} - {isFinishedLoading && } {isFinishedLoading && ( <> + diff --git a/src/frontend/screens/Observation/Buttons.tsx b/src/frontend/screens/Observation/Buttons.tsx index 248783d74..0fc97d115 100644 --- a/src/frontend/screens/Observation/Buttons.tsx +++ b/src/frontend/screens/Observation/Buttons.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import {Alert, StyleSheet, TouchableOpacity, View} from 'react-native'; import {Field, Observation} from '@comapeo/schema'; import {DARK_GREY} from '../../lib/styles'; @@ -6,18 +6,18 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import {defineMessages, useIntl} from 'react-intl'; import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes'; import {useDeleteObservation} from '../../hooks/server/observations'; -import {Text} from '../../sharedComponents/Text'; import Share from 'react-native-share'; -import {useAttachmentUrlQueries} from '../../hooks/server/media.ts'; import {useObservationWithPreset} from '../../hooks/useObservationWithPreset.ts'; import {formatCoords} from '../../lib/utils.ts'; import {UIActivityIndicator} from 'react-native-indicators'; import {convertUrlToBase64} from '../../utils/base64.ts'; -import {useState} from 'react'; import {usePersistedSettings} from '../../hooks/persistedState/usePersistedSettings.ts'; import * as Sentry from '@sentry/react-native'; import {CoordinateFormat} from '../../sharedTypes/index.ts'; import {getValueLabel} from '../../sharedComponents/FormattedData.tsx'; +import {useActiveProject} from '../../contexts/ActiveProjectContext'; +import {BodyText} from '../../sharedComponents/Text/BodyText.tsx'; +import {isSavedPhoto} from '../../lib/attachmentTypeChecks.ts'; const m = defineMessages({ delete: { @@ -95,11 +95,8 @@ export const ButtonFields = ({ const deleteObservationMutation = useDeleteObservation(); const {observation, preset} = useObservationWithPreset(observationId); const format = usePersistedSettings(store => store.coordinateFormat); - const attachmentUrlQueries = useAttachmentUrlQueries( - observation.attachments, - 'original', - ); const [isShareButtonLoading, setShareButtonLoading] = useState(false); + const {projectApi} = useActiveProject(); function handlePressDelete() { Alert.alert(t(m.deleteTitle), undefined, [ @@ -117,36 +114,40 @@ export const ButtonFields = ({ ]); } - async function handlePressShare() { + async function fetchFreshUrls() { const {attachments} = observation; - setShareButtonLoading(true); - const getValidUrls = (queries: typeof attachmentUrlQueries) => { - const urls = queries - .map(query => query.data?.url) - .filter((url): url is string => url !== undefined && url !== null); - - return urls; - }; - let urls: string[] = []; + if (!attachments || attachments.length === 0) { + return []; + } + const photoAttachments = attachments.filter(isSavedPhoto); + if (photoAttachments.length === 0) { + return []; + } - if (attachments.length > 0) { - urls = getValidUrls(attachmentUrlQueries); + return await Promise.all( + photoAttachments.map(async attachment => { + return projectApi.$blobs.getUrl({ + driveId: attachment.driveDiscoveryId, + name: attachment.name, + type: 'photo', + variant: 'original', + }); + }), + ); + } - if (urls.length === 0) { - setShareButtonLoading(false); - Alert.alert('Error', 'Unable to share this observation.'); - return; - } - } + async function handlePressShare() { + setShareButtonLoading(true); try { - const base64Urls = await Promise.all( - urls.map(url => convertUrlToBase64(url)), - ); + const urls = await fetchFreshUrls(); + const base64Urls = + urls.length > 0 + ? await Promise.all(urls.map(url => convertUrlToBase64(url))) + : []; const completedFields: Array<{label: string; value: string}> = []; - for (const field of fields) { const value = observation.tags[field.tagKey]; @@ -155,9 +156,7 @@ export const ButtonFields = ({ } const displayedValue = (Array.isArray(value) ? value : [value]) - .map(v => { - return getValueLabel(v, field).trim(); - }) + .map(v => getValueLabel(v, field).trim()) .join(', '); completedFields.push({label: field.label, value: displayedValue}); @@ -177,6 +176,7 @@ export const ButtonFields = ({ timestamp: formatDate(observation.createdAt, {format: 'long'}), titleText: t(m.shareMessageTitle), }), + failOnCancel: false, }); } catch (err) { Sentry.captureException(err); @@ -224,7 +224,9 @@ const Button = ({onPress, isLoading, iconName, title}: ButtonProps) => ( style={styles.buttonIcon} /> )} - {title} + + {title} + ); diff --git a/src/frontend/screens/Observation/FieldDetails.tsx b/src/frontend/screens/Observation/FieldDetails.tsx index 706e5462d..548cf9344 100644 --- a/src/frontend/screens/Observation/FieldDetails.tsx +++ b/src/frontend/screens/Observation/FieldDetails.tsx @@ -1,37 +1,37 @@ import * as React from 'react'; -import {View, Text, StyleSheet} from 'react-native'; -import {MEDIUM_GREY, DARK_GREY, BLACK, LIGHT_GREY} from '../../lib/styles'; +import {View, StyleSheet} from 'react-native'; +import {MEDIUM_GREY} from '../../lib/styles'; import { FormattedFieldProp, FormattedFieldValue, } from '../../sharedComponents/FormattedData'; import {Field, Observation} from '@comapeo/schema'; +import {ViewStyleProp} from '../../sharedTypes'; +import {HeaderText} from '../../sharedComponents/Text/HeaderText'; +import {BodyText} from '../../sharedComponents/Text/BodyText'; export const FieldDetails = ({ fields, observation, + style, }: { fields: Field[]; observation: Observation; + style?: ViewStyleProp; }) => { return ( {fields.map(field => { const value = observation.tags[field.tagKey]; return ( - - + + - - + + - + ); })} @@ -40,23 +40,11 @@ export const FieldDetails = ({ }; const styles = StyleSheet.create({ - fieldAnswer: { - fontSize: 20, - fontWeight: '100', - }, fieldTitle: { - color: BLACK, - fontSize: 14, - fontWeight: '700', marginBottom: 10, }, section: { flex: 1, - marginHorizontal: 15, paddingVertical: 15, }, - optionalSection: { - borderTopColor: LIGHT_GREY, - borderTopWidth: 1, - }, }); diff --git a/src/frontend/screens/Observation/PresetHeader.tsx b/src/frontend/screens/Observation/PresetHeader.tsx index edd8840a0..e2f544bd1 100644 --- a/src/frontend/screens/Observation/PresetHeader.tsx +++ b/src/frontend/screens/Observation/PresetHeader.tsx @@ -1,21 +1,31 @@ import React from 'react'; -import {View, Text, StyleSheet} from 'react-native'; -import {BLACK} from '../../lib/styles'; +import {View, StyleSheet} from 'react-native'; import {FormattedPresetName} from '../../sharedComponents/FormattedData'; import {PresetCircleIcon} from '../../sharedComponents/icons/PresetIcon'; import {Preset} from '@comapeo/schema'; +import {ViewStyleProp} from '../../sharedTypes'; +import {HeaderText} from '../../sharedComponents/Text/HeaderText'; -export const PresetHeader = ({preset}: {preset?: Preset}) => { +export const PresetHeader = ({ + preset, + style, +}: { + preset?: Preset; + style?: ViewStyleProp; +}) => { return ( - + - + - + ); }; @@ -26,9 +36,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, categoryLabel: { - color: BLACK, - fontWeight: 'bold', - fontSize: 20, marginLeft: 10, }, }); diff --git a/src/frontend/screens/Observation/TrackAccordian.tsx b/src/frontend/screens/Observation/TrackAccordian.tsx new file mode 100644 index 000000000..ee11faf68 --- /dev/null +++ b/src/frontend/screens/Observation/TrackAccordian.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import ChainIcon from '../../images/Chain.svg'; +import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes.ts'; +import {defineMessages, useIntl} from 'react-intl'; +import {Accordian} from '../../sharedComponents/Accordian.tsx'; +import {HeaderText} from '../../sharedComponents/Text/HeaderText.tsx'; +import {TrackListItem} from '../ObservationsList/TrackListItem.tsx'; +import {useTracks} from '../../hooks/server/track.ts'; +import {View} from 'react-native'; +import {LIGHT_GREY} from '../../lib/styles.ts'; +import {findAssociatedTrack} from './findAssociatedTrack.ts'; + +const m = defineMessages({ + track: { + id: 'screens.Observation.TrackList.track', + defaultMessage: 'Track', + }, +}); + +export function TrackAccordian({observationId}: {observationId: string}) { + const navigation = useNavigationFromRoot(); + const {data: allTracks} = useTracks(); + const track = + allTracks === undefined + ? undefined + : findAssociatedTrack({tracks: allTracks, observationId}); + const {formatMessage} = useIntl(); + + if (!track) return null; + + return ( + + + {1} + + {formatMessage(m.track)} + + } + innerAccordianDetails={ + { + navigation.push('Track', {trackId: track.docId}); + }} + testID={`trackListItem:${track.docId}`} + /> + } + /> + + ); +} diff --git a/src/frontend/screens/Observation/findAssociatedTrack.ts b/src/frontend/screens/Observation/findAssociatedTrack.ts new file mode 100644 index 000000000..259d3b834 --- /dev/null +++ b/src/frontend/screens/Observation/findAssociatedTrack.ts @@ -0,0 +1,13 @@ +import {type Track} from '@comapeo/schema'; + +export function findAssociatedTrack({ + tracks, + observationId, +}: { + tracks: Track[]; + observationId: string; +}) { + return tracks.find(trackData => + trackData.observationRefs.some(ref => ref.docId === observationId), + ); +} diff --git a/src/frontend/screens/Observation/index.tsx b/src/frontend/screens/Observation/index.tsx index 2dba46db3..bf7b8cb71 100644 --- a/src/frontend/screens/Observation/index.tsx +++ b/src/frontend/screens/Observation/index.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import {Text, View, ScrollView, StyleSheet} from 'react-native'; +import {View, ScrollView, StyleSheet} from 'react-native'; import {defineMessages} from 'react-intl'; -import {BLACK, WHITE, DARK_GREY, LIGHT_GREY} from '../../lib/styles'; +import {WHITE, DARK_GREY, LIGHT_GREY, BLUE_GREY} from '../../lib/styles'; import {UIActivityIndicator} from 'react-native-indicators'; import {FormattedObservationDate} from '../../sharedComponents/FormattedData'; @@ -22,6 +22,10 @@ import {SavedPhoto} from '../../contexts/PhotoPromiseContext/types.ts'; import {ButtonFields} from './Buttons.tsx'; import {AudioAttachment} from '../../sharedTypes/audio.ts'; import {isSavedPhoto, isAudioAttachment} from '../../lib/attachmentTypeChecks'; +import {TrackAccordian} from './TrackAccordian.tsx'; +import {Divider} from '../../sharedComponents/Divider.tsx'; +import {BodyText} from '../../sharedComponents/Text/BodyText.tsx'; +import {HeaderText} from '../../sharedComponents/Text/HeaderText.tsx'; const m = defineMessages({ deleteTitle: { @@ -89,20 +93,23 @@ export const ObservationScreen: NativeNavigationComponent<'Observation'> = ({ {/* check lat and lon are not null or undefined */} {lat != null && lon != null && } - + - + - - + + + }> + + {typeof observation.tags.notes === 'string' ? ( - - {observation.tags.notes} - + + {observation.tags.notes} + ) : null} {attachments.length > 0 && ( = ({ )} {fields.length > 0 && ( - + <> + + + )} {isDeviceInfoPending || isDeviceIdPending ? ( @@ -134,29 +148,24 @@ ObservationScreen.navTitle = m.title; const styles = StyleSheet.create({ root: { backgroundColor: WHITE, - flex: 1, flexDirection: 'column', }, scrollContent: {minHeight: '100%'}, divider: { - backgroundColor: LIGHT_GREY, + backgroundColor: BLUE_GREY, paddingVertical: 15, }, section: { flex: 1, - marginHorizontal: 15, paddingVertical: 15, }, textNotes: { - fontSize: 22, color: DARK_GREY, fontWeight: '100', - marginLeft: 10, + padding: 20, }, time: { - color: BLACK, backgroundColor: LIGHT_GREY, - fontSize: 14, paddingVertical: 10, textAlign: 'center', }, diff --git a/src/frontend/screens/ObservationFields/Number.tsx b/src/frontend/screens/ObservationFields/Number.tsx new file mode 100644 index 000000000..522ca7f80 --- /dev/null +++ b/src/frontend/screens/ObservationFields/Number.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import {StyleSheet, TextInput} from 'react-native'; +import {QuestionLabel} from './QuestionLabel'; +import {Field} from '@comapeo/schema'; +import {usePersistedDraftObservation} from '../../hooks/persistedState/usePersistedDraftObservation'; +import {useDraftObservation} from '../../hooks/useDraftObservation'; + +export const Number = React.memo<{field: Field}>(({field}) => { + const tags = usePersistedDraftObservation(store => store.value?.tags); + const {updateTags} = useDraftObservation(); + const value = tags ? tags[field.tagKey] : ''; + return ( + + + + updateTags( + field.tagKey, + newVal + .replace(/[^0-9.-]/g, '') // Allow digits, decimal, and negative sign + .replace(/(?!^)-/g, '') // Remove any minus sign that is not at the start + .replace(/(\..*?)\./g, '$1'), // Remove additional decimal points + ) + } + keyboardType="numeric" + style={styles.textInput} + underlineColorAndroid="transparent" + multiline + scrollEnabled={false} + textContentType="none" + autoFocus + /> + + ); +}); + +const styles = StyleSheet.create({ + textInput: { + flex: 1, + minHeight: 150, + fontSize: 20, + padding: 20, + marginBottom: 20, + color: 'black', + alignItems: 'flex-start', + justifyContent: 'flex-start', + textAlignVertical: 'top', + }, +}); diff --git a/src/frontend/screens/ObservationFields/Question.tsx b/src/frontend/screens/ObservationFields/Question.tsx index 1085e9bf7..fa81bf5ab 100644 --- a/src/frontend/screens/ObservationFields/Question.tsx +++ b/src/frontend/screens/ObservationFields/Question.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {SelectOne} from './SelectOne'; import {SelectMultiple} from './SelectMultiple'; import {TextArea} from './TextArea'; +import {Number} from './Number'; import {Field} from '@comapeo/schema'; import { SelectMultipleField, @@ -22,5 +23,9 @@ export const Question = ({field}: QuestionProps) => { return ; } + if (field.type === 'number') { + return ; + } + return