Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QR scan support for asserting antenna gain/elevation and hotspot transfer #1344

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 76 additions & 67 deletions src/components/HotspotConfigurationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,68 @@ 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,
onAntennaUpdated,
onGainUpdated,
onElevationUpdated,
outline,
gain,
elevation,
}: Props) => {
const { t } = useTranslation()
const colors = useColors()

const gainInputRef = useRef<TextInput | null>(null)
const elevationInputRef = useRef<TextInput | null>(null)

const [gain, setGain] = useState<string | undefined>(
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<string | undefined>(
gain != null ? gainFloatToString(gain) : undefined,
)
const [tmpElevation, setTmpElevation] = useState<string | undefined>(
elevation != null ? elevationIntToString(elevation) : undefined,
)

const antennas = useMemo(
Expand All @@ -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 = () =>
Expand All @@ -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 (
<Box
Expand Down Expand Up @@ -205,7 +209,7 @@ const HotspotConfigurationPicker = ({
style={styles.textInput}
ref={gainInputRef}
keyboardType="numeric"
value={gain}
value={isEditingGain ? tmpGain : gainFloatToString(gain)}
returnKeyType="done"
maxFontSizeMultiplier={1.2}
onChangeText={onChangeGain}
Expand Down Expand Up @@ -245,7 +249,12 @@ const HotspotConfigurationPicker = ({
returnKeyType="done"
maxFontSizeMultiplier={1.2}
onChangeText={onChangeElevation}
onEndEditing={Keyboard.dismiss}
onEndEditing={onDoneEditingElevation}
value={
isEditingElevation
? tmpElevation
: elevationIntToString(elevation)
}
/>
</Box>
</TouchableWithoutFeedback>
Expand Down
83 changes: 83 additions & 0 deletions src/features/hotspots/root/HotspotAntennaUpdateScreen.tsx
Original file line number Diff line number Diff line change
@@ -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<HotspotStackParamList, 'HotspotAntennaUpdateScreen'>

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 (
<Modal
presentationStyle="overFullScreen"
transparent
visible
animationType="fade"
statusBarTranslucent
>
<BlurBox
top={0}
left={0}
bottom={0}
right={0}
blurAmount={70}
blurType="dark"
position="absolute"
/>
<SafeAreaBox flex={1} flexDirection="column" marginBottom="m">
<Box
marginTop="none"
marginBottom="ms"
justifyContent="flex-end"
flex={1}
marginHorizontal="none"
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<Card variant="modal" backgroundColor="white" overflow="hidden">
{!!hotspot && (
<UpdateHotspotConfig
antennaElevation={elevation}
antennaGain={gain}
hotspot={hotspot}
initState="confirm"
onClose={onClose}
onCloseSettings={onClose}
/>
)}
</Card>
</KeyboardAvoidingView>
</Box>
</SafeAreaBox>
</Modal>
)
}

export default HotspotAntennaUpdateScreen
5 changes: 5 additions & 0 deletions src/features/hotspots/root/HotspotsNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HotspotStackParamList>()
Expand All @@ -18,6 +19,10 @@ const Hotspots = () => {
name="HotspotLocationUpdateScreen"
component={HotspotLocationUpdateScreen}
/>
<HotspotsStack.Screen
name="HotspotAntennaUpdateScreen"
component={HotspotAntennaUpdateScreen}
/>
</HotspotsStack.Navigator>
)
}
Expand Down
5 changes: 5 additions & 0 deletions src/features/hotspots/root/hotspotTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HotspotStackParamList>
Expand Down
Loading