Skip to content
Merged
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
2 changes: 1 addition & 1 deletion components/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function IconButton<const NAME>(props: Props<NAME>) {
borderRadius: '50%',
outlineStyle: 'dashed',
outlineOffset: 3,
outlineColor: active ? '#000000' : 'transparent',
outlineColor: active ? '#ffffff' : 'transparent',
outlineWidth: active ? 4 : undefined,
}}
>
Expand Down
1 change: 0 additions & 1 deletion components/TileGridMappingSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Expand Down
2 changes: 2 additions & 0 deletions components/ValidateImageMappingSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const createStyles = (_: AppTheme) => (
},
buttons: {
flexGrow: 0,
flexShrink: 0,
padding: 20,
},
})
);
Expand Down
172 changes: 114 additions & 58 deletions components/ValidateMappingSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import {
SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import {
ActivityIndicator,
FlatList,
StyleSheet,
View,
} from 'react-native';
import {
isDefined,
Expand All @@ -17,9 +20,9 @@ import { decode } from 'base-64';
import { inflate } from 'pako';

import BlockListView from '@/components/BlockListView';
import Button from '@/components/Button';
import InlineListView from '@/components/InlineListView';
import MapTile from '@/components/MapTile';
import { SCREEN_WIDTH } from '@/constants/dimensions';
import useFirebaseDatabase from '@/hooks/useFirebaseDatabase';
import { firebaseRef } from '@/utils/firebase';
import {
Expand All @@ -32,7 +35,30 @@ import {
import { type IconName } from './Icon';
import IconButton from './IconButton';

type RefType = FlatList<ValidateTask> | null;
const viewabilityConfig = {
viewAreaCoveragePercentThreshold: 50,
};

const styles = StyleSheet.create({
view: {
flex: 1,
flexDirection: 'column',
},
tasks: {
flex: 2,
flexGrow: 1,
width: SCREEN_WIDTH,
},
task: {
width: SCREEN_WIDTH,
padding: 20,
},
buttons: {
flexGrow: 0,
flexShrink: 0,
padding: 20,
},
loadingContainer: {
alignItems: 'center',
},
Expand Down Expand Up @@ -64,7 +90,13 @@ function ValidateMappingSession(props: Props) {
});

const taskList = useMemo(() => {
if (isNotDefined(compressedTasks) || typeof compressedTasks !== 'string') {
if (isNotDefined(compressedTasks)) {
return [] as ValidateTask[];
}
if (Array.isArray(compressedTasks)) {
return compressedTasks as ValidateTask[];
}
if (typeof compressedTasks !== 'string') {
return [];
}

Expand All @@ -74,31 +106,14 @@ function ValidateMappingSession(props: Props) {
const decompressedTasks = inflate(binaryCompressedTasks, { to: 'string' });

// FIXME: add schema validation
return JSON.parse(decompressedTasks) as unknown[];
return JSON.parse(decompressedTasks) as ValidateTask[];
}, [compressedTasks]);

const currentTask = taskList[currentTaskIndex] as ValidateTask | undefined;
const options = projectDetails.customOptions;
const maxTasks = taskList.length;

const handleNextPress = useCallback(() => {
setCurrentTaskIndex(
(prevTaskIndex) => Math.min(
prevTaskIndex + 1,
maxTasks,
),
);
}, [maxTasks]);

const handlePrevPress = useCallback(() => {
setCurrentTaskIndex(
(prevTaskIndex) => Math.max(
prevTaskIndex - 1,
0,
),
);
}, []);

const options = projectDetails.customOptions;
const flatListRef = useRef<RefType>(null);

const handleAnswerSelect = useCallback((newValue: number) => {
if (isDefined(currentTask)) {
Expand All @@ -107,54 +122,95 @@ function ValidateMappingSession(props: Props) {
[currentTask.taskId]: newValue,
}));
}
}, [currentTask, onResultsChange]);

const nextIndex = currentTaskIndex + 1;
if (nextIndex < maxTasks) {
setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: nextIndex,
animated: true,
});
}, 0);
}
}, [currentTask, onResultsChange, maxTasks, currentTaskIndex]);

const selectedValue = isDefined(currentTask)
? results[currentTask.taskId]
: undefined;

const onViewableItemsChanged = useCallback(({
viewableItems,
}: { viewableItems: { index: number | null | undefined }[] }) => {
if (viewableItems.length > 0) {
const { index } = viewableItems[0];
if (index !== undefined && index !== null) {
setCurrentTaskIndex(index);
}
}
}, []);

let totalSwipedTasks = 0;
if (results) {
totalSwipedTasks = Object.keys(results).length;
if ('startTime' in results) {
totalSwipedTasks -= 1;
}
}

const limitedTasks = [...(taskList ?? [])].slice(0, totalSwipedTasks + 1);

const disableOptions = currentTaskIndex === undefined || currentTaskIndex === -1;

return (
<BlockListView withPadding>
<BlockListView style={styles.view}>
{isNotDefined(currentTask?.geojson) && (
<BlockListView style={styles.loadingContainer}>
<ActivityIndicator size="large" />
</BlockListView>
)}
{isDefined(currentTask?.geojson) && (
<>
<MapTile
geoJson={currentTask.geojson as FeatureGeoJson}
tileServer={projectDetails.tileServer}
/>
<InlineListView withCenteredContent>
{options?.map((option) => (
<IconButton
name={option.value}
key={option.value}
title={option.title}
onPress={handleAnswerSelect}
// FIXME: No casting
iconName={option.icon as IconName}
tintColor={option.iconColor}
active={selectedValue === option.value}
/>
))}
</InlineListView>
<InlineListView withSpaceBetweenContents>
<Button
name="prev"
title="Prev"
onPress={handlePrevPress}
<FlatList
style={styles.tasks}
ref={flatListRef}
data={limitedTasks}
keyExtractor={(_, index) => index.toString()}
renderItem={({ item }) => (
<View style={styles.task}>
<MapTile
geoJson={item.geojson as FeatureGeoJson}
tileServer={projectDetails.tileServer}
/>
<Button
name="next"
title="Next"
onPress={handleNextPress}
disabled={isNotDefined(selectedValue)}
/>
</InlineListView>
</>
)}
</View>
)}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
horizontal
getItemLayout={(_, index) => ({
length: SCREEN_WIDTH,
offset: SCREEN_WIDTH * index,
index,
})}
pagingEnabled
showsHorizontalScrollIndicator={false}
/>
<InlineListView
withCenteredContent
style={styles.buttons}
>
{options?.map((option) => (
<IconButton
name={option.value}
key={option.value}
title={option.title}
onPress={handleAnswerSelect}
// FIXME: No casting
iconName={option.icon as IconName}
tintColor={option.iconColor}
active={selectedValue === option.value}
disabled={disableOptions}
textColorVariant="brand"
/>
))}
</InlineListView>
</BlockListView>
);
}
Expand Down
Loading