diff --git a/src/components/HotspotConfigurationPicker.tsx b/src/components/HotspotConfigurationPicker.tsx index 5807ed4a0..3c81bd78a 100644 --- a/src/components/HotspotConfigurationPicker.tsx +++ b/src/components/HotspotConfigurationPicker.tsx @@ -16,12 +16,41 @@ import { useColors } from '../theme/themeHooks' import { AntennaModelKeys, AntennaModels } from '../makers' import { MakerAntenna } from '../makers/antennaMakerTypes' +function gainFloatToString(gainFloat?: number): string { + return gainFloat != null + ? gainFloat.toLocaleString(locale, { + maximumFractionDigits: 1, + minimumFractionDigits: 1, + }) + : '' +} +function gainStringToFloat(gainStr?: string): number | undefined { + return gainStr + ? parseFloat( + gainStr.replace(groupSeparator, '').replace(decimalSeparator, '.'), + ) + : undefined +} +function elevationStringToInt(elevationStr?: string): number | undefined { + return elevationStr + ? parseInt( + elevationStr.replace(groupSeparator, '').replace(decimalSeparator, '.'), + 10, + ) + : undefined +} +function elevationIntToString(elevationInt?: number): string { + return elevationInt != null ? elevationInt.toLocaleString(locale) : '' +} + type Props = { onAntennaUpdated: (antenna: MakerAntenna) => void - onGainUpdated: (gain: number) => void - onElevationUpdated: (elevation: number) => void + onGainUpdated: (gain: number | undefined) => void + onElevationUpdated: (elevation: number | undefined) => void selectedAntenna?: MakerAntenna outline?: boolean + gain?: number + elevation?: number } const HotspotConfigurationPicker = ({ selectedAntenna, @@ -29,6 +58,8 @@ const HotspotConfigurationPicker = ({ onGainUpdated, onElevationUpdated, outline, + gain, + elevation, }: Props) => { const { t } = useTranslation() const colors = useColors() @@ -36,13 +67,17 @@ const HotspotConfigurationPicker = ({ const gainInputRef = useRef(null) const elevationInputRef = useRef(null) - const [gain, setGain] = useState( - selectedAntenna - ? selectedAntenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }) - : undefined, + // Use state to track temporary raw edits for gain and elevation so that we can delay actual + // updates (delegated to parent component) until the user has finished editing. This prevents + // the need to reformat input as the user is actively typing, while ensuring the parent component + // only receives updates when the user has finished. + const [isEditingGain, setIsEditingGain] = useState(false) + const [isEditingElevation, setIsEditingElevation] = useState(false) + const [tmpGain, setTmpGain] = useState( + gain != null ? gainFloatToString(gain) : undefined, + ) + const [tmpElevation, setTmpElevation] = useState( + elevation != null ? elevationIntToString(elevation) : undefined, ) const antennas = useMemo( @@ -59,12 +94,7 @@ const HotspotConfigurationPicker = ({ const antenna = antennas[index] onAntennaUpdated(antenna) onGainUpdated(antenna.gain) - setGain( - antenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }), - ) + setTmpGain(gainFloatToString(antenna.gain)) } const showElevationInfo = () => @@ -82,53 +112,41 @@ const HotspotConfigurationPicker = ({ elevationInputRef.current?.focus() } - const onChangeGain = (text: string) => setGain(text) + const onChangeGain = (text: string) => { + if (!isEditingGain) setIsEditingGain(true) + setTmpGain(text) + } const onDoneEditingGain = () => { - const gainFloat = gain - ? parseFloat( - gain.replace(groupSeparator, '').replace(decimalSeparator, '.'), - ) - : 0 - let gainString - if (!gainFloat || gainFloat <= 1) { - gainString = '1' - } else if (gainFloat >= 15) { - gainString = '15' - } else { - gainString = gainFloat.toLocaleString(locale, { - maximumFractionDigits: 1, - }) + setIsEditingGain(false) + const gainStrRaw = tmpGain + let gainFloat = gainStringToFloat(gainStrRaw) + if (gainFloat) { + if (gainFloat < 1) gainFloat = 1 + if (gainFloat >= 15) gainFloat = 15 } - setGain(gainString) + const gainStr = gainFloatToString(gainFloat) + setTmpGain(gainStr) onGainUpdated(gainFloat) } - const onChangeElevation = (text: string) => { - const elevationInteger = text - ? parseInt( - text.replace(groupSeparator, '').replace(decimalSeparator, '.'), - 10, - ) - : 0 - let stringElevation - if (!elevationInteger) { - stringElevation = '0' - } else { - stringElevation = elevationInteger.toString() - } - onElevationUpdated(parseInt(stringElevation, 10)) + if (!isEditingElevation) setIsEditingElevation(true) + setTmpElevation(text) + } + const onDoneEditingElevation = () => { + setIsEditingElevation(false) + const elevationStrRaw = tmpElevation + const elevationInt = elevationStringToInt(elevationStrRaw) + const elevationStr = elevationIntToString(elevationInt) + setTmpElevation(elevationStr) + onElevationUpdated(elevationInt) } useEffect(() => { if (selectedAntenna) { - setGain( - selectedAntenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }), - ) + onGainUpdated(selectedAntenna.gain) + setTmpGain(gainFloatToString(selectedAntenna.gain)) } - }, [selectedAntenna]) + }, [selectedAntenna, onGainUpdated]) return ( diff --git a/src/features/hotspots/root/HotspotAntennaUpdateScreen.tsx b/src/features/hotspots/root/HotspotAntennaUpdateScreen.tsx new file mode 100644 index 000000000..2b9dca283 --- /dev/null +++ b/src/features/hotspots/root/HotspotAntennaUpdateScreen.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { KeyboardAvoidingView, Modal, Platform } from 'react-native' +import { RouteProp, useNavigation } from '@react-navigation/native' +import { useSelector } from 'react-redux' + +import BlurBox from '../../../components/BlurBox' +import Box from '../../../components/Box' +import Card from '../../../components/Card' +import SafeAreaBox from '../../../components/SafeAreaBox' + +import { HotspotStackParamList } from './hotspotTypes' +import { RootState } from '../../../store/rootReducer' +import UpdateHotspotConfig from '../settings/updateHotspot/UpdateHotspotConfig' + +type Route = RouteProp + +type Props = { + route: Route +} + +/** + * HotspotAntennaUpdateScreen allows users to update the antenna of one of their hotspots within + * a single view. It simply renders the "UpdateHotspotConfig" component in "antenna" state with + * prefilled values for gain and elevation (as provided in the route parameters). + */ +function HotspotAntennaUpdateScreen({ route }: Props) { + const { hotspotAddress, gain, elevation } = route.params + const hotspots = useSelector((state: RootState) => state.hotspots.hotspots) + const hotspot = hotspots.find((h) => h.address === hotspotAddress) + + const navigation = useNavigation() + const onClose = () => navigation.goBack() + + return ( + + + + + + + {!!hotspot && ( + + )} + + + + + + ) +} + +export default HotspotAntennaUpdateScreen diff --git a/src/features/hotspots/root/HotspotsNavigator.tsx b/src/features/hotspots/root/HotspotsNavigator.tsx index d10b6a92e..3f3a91002 100644 --- a/src/features/hotspots/root/HotspotsNavigator.tsx +++ b/src/features/hotspots/root/HotspotsNavigator.tsx @@ -3,6 +3,7 @@ import { createStackNavigator } from '@react-navigation/stack' import defaultScreenOptions from '../../../navigation/defaultScreenOptions' import HotspotsScreen from './HotspotsScreen' import HotspotLocationUpdateScreen from './HotspotLocationUpdateScreen' +import HotspotAntennaUpdateScreen from './HotspotAntennaUpdateScreen' import { HotspotStackParamList } from './hotspotTypes' const HotspotsStack = createStackNavigator() @@ -18,6 +19,10 @@ const Hotspots = () => { name="HotspotLocationUpdateScreen" component={HotspotLocationUpdateScreen} /> + ) } diff --git a/src/features/hotspots/root/hotspotTypes.tsx b/src/features/hotspots/root/hotspotTypes.tsx index c44f95550..7739378ca 100644 --- a/src/features/hotspots/root/hotspotTypes.tsx +++ b/src/features/hotspots/root/hotspotTypes.tsx @@ -8,6 +8,11 @@ export type HotspotStackParamList = { hotspotAddress: string location: { longitude: number; latitude: number } } + HotspotAntennaUpdateScreen: { + hotspotAddress: string + gain?: number + elevation?: number + } } export type HotspotNavigationProp = StackNavigationProp diff --git a/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx b/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx index 4c98a17cd..094418b8f 100644 --- a/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx +++ b/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx @@ -39,20 +39,36 @@ type Props = { onClose: () => void onCloseSettings: () => void hotspot: Hotspot | Witness + antennaGain?: number + antennaElevation?: number + initState?: State } type State = 'antenna' | 'location' | 'confirm' -const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { +const UpdateHotspotConfig = ({ + onClose, + onCloseSettings, + hotspot, + antennaGain, + antennaElevation, + initState, +}: Props) => { const { t } = useTranslation() const submitTxn = useSubmitTxn() const navigation = useNavigation() const [state, setState] = useState( - isDataOnly(hotspot) ? 'location' : 'antenna', + initState ?? (isDataOnly(hotspot) ? 'location' : 'antenna'), + ) + const [antenna, setAntenna] = useState( + antennaGain != null + ? ({ name: 'Custom Antenna', gain: antennaGain } as MakerAntenna) + : undefined, + ) + const [gain, setGain] = useState(antennaGain) + const [elevation, setElevation] = useState( + antennaElevation, ) - const [antenna, setAntenna] = useState() - const [gain, setGain] = useState() - const [elevation, setElevation] = useState(0) const [location, setLocation] = useState() const [locationName, setLocationName] = useState() const [fullScreen, setFullScreen] = useState(false) @@ -70,6 +86,24 @@ const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { enableBack(onClose) }, [enableBack, onClose]) + useEffect(() => { + if (!!(antennaGain || antennaElevation) && initState === 'confirm') { + updateLocationFeeForUpdatingAntenna() + setIsLocationChange(false) + } + }, [antennaGain, antennaElevation, initState]) + + const updateLocationFeeForUpdatingAntenna = () => { + const feeData = calculateAssertLocFee(undefined, undefined, undefined) + const feeDc = new Balance(feeData.fee, CurrencyType.dataCredit) + setLocationFee( + feeDc.toString(0, { + groupSeparator, + decimalSeparator, + }), + ) + } + const toggleUpdateAntenna = () => { animateTransition('UpdateHotspotConfig.ToggleUpdateAntenna', { enabledOnAndroid: false, @@ -88,14 +122,7 @@ const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { animateTransition('UpdateHotspotConfig.OnConfirm', { enabledOnAndroid: false, }) - const feeData = calculateAssertLocFee(undefined, undefined, undefined) - const feeDc = new Balance(feeData.fee, CurrencyType.dataCredit) - setLocationFee( - feeDc.toString(0, { - groupSeparator, - decimalSeparator, - }), - ) + updateLocationFeeForUpdatingAntenna() setState('confirm') } const updatingAntenna = useMemo(() => state === 'antenna', [state]) @@ -340,6 +367,8 @@ const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { onGainUpdated={setGain} onElevationUpdated={setElevation} selectedAntenna={antenna} + gain={gain} + elevation={elevation} />