diff --git a/src/components/HotspotConfigurationPicker.tsx b/src/components/HotspotConfigurationPicker.tsx index 12ae8ea1..3623570c 100644 --- a/src/components/HotspotConfigurationPicker.tsx +++ b/src/components/HotspotConfigurationPicker.tsx @@ -17,12 +17,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, @@ -30,6 +59,8 @@ const HotspotConfigurationPicker = ({ onGainUpdated, onElevationUpdated, outline, + gain, + elevation, }: Props) => { const { t } = useTranslation() const colors = useColors() @@ -37,13 +68,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( @@ -62,12 +97,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 = () => @@ -91,69 +121,43 @@ const HotspotConfigurationPicker = ({ elevationInputRef.current?.focus() } - const parseGainFloat = (floatString?: string) => - floatString - ? parseFloat( - floatString - .replace(groupSeparator, '') - .replace(decimalSeparator, '.'), - ) - : 0 - const onChangeGain = (text: string) => { - let gainFloat = parseGainFloat(text) - if (!gainFloat || gainFloat <= 1) { - gainFloat = 1 - } else if (gainFloat >= 15) { - gainFloat = 15 - } - setGain(text) - onGainUpdated(gainFloat) + if (!isEditingGain) setIsEditingGain(true) + setTmpGain(text) } - const onDoneEditingGain = () => { - const gainFloat = parseGainFloat(gain) - 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) Keyboard.dismiss() } - 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) + Keyboard.dismiss() } 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 00000000..b2a52a18 --- /dev/null +++ b/src/features/hotspots/root/HotspotAntennaUpdateScreen.tsx @@ -0,0 +1,83 @@ +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.data, + ) + 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 d10b6a92..3f3a9100 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 a0ea3c29..cf0ff093 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 70dc0d78..221a6202 100644 --- a/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx +++ b/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx @@ -47,21 +47,37 @@ 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 dispatch = useAppDispatch() 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) @@ -79,6 +95,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, @@ -97,14 +131,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]) @@ -418,6 +445,8 @@ const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { onGainUpdated={setGain} onElevationUpdated={setElevation} selectedAntenna={antenna} + gain={gain} + elevation={elevation} />