Skip to content

Commit

Permalink
Merge pull request #35045 from callstack-internal/issues/30123
Browse files Browse the repository at this point in the history
Add offline search functionality for addresses
  • Loading branch information
neil-marcellini authored May 28, 2024
2 parents 611b86f + ae0ce2d commit 849a0d4
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 56 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const onboardingChoices = {
type OnboardingPurposeType = ValueOf<typeof onboardingChoices>;

const CONST = {
RECENT_WAYPOINTS_NUMBER: 20,
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],

// Note: Group and Self-DM excluded as these are not tied to a Workspace
Expand Down
61 changes: 48 additions & 13 deletions src/components/AddressSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,25 @@ import CONST from '@src/CONST';
import type {Address} from '@src/types/onyx/PrivatePersonalDetails';
import CurrentLocationButton from './CurrentLocationButton';
import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer';
import type {AddressSearchProps} from './types';
import listViewOverflow from './listViewOverflow';
import type {AddressSearchProps, PredefinedPlace} from './types';

/**
* Check if the place matches the search by the place name or description.
* @param search The search string for a place
* @param place The place to check for a match on the search
* @returns true if search is related to place, otherwise it returns false.
*/
function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean {
if (!search) {
return true;
}
if (!place) {
return false;
}
const fullSearchSentence = `${place.name ?? ''} ${place.description}`;
return search.split(' ').every((searchTerm) => !searchTerm || (searchTerm && fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())));
}

// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
Expand All @@ -41,6 +59,7 @@ function AddressSearch(
isLimitedToUSA = false,
label,
maxInputLength,
onFocus,
onBlur,
onInputChange,
onPress,
Expand Down Expand Up @@ -298,10 +317,16 @@ function AddressSearch(
};
}, []);

const filteredPredefinedPlaces = useMemo(() => {
if (!isOffline || !searchValue) {
return predefinedPlaces ?? [];
}
return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? [];
}, [isOffline, predefinedPlaces, searchValue]);

const listEmptyComponent = useCallback(
() =>
!!isOffline || !isTyping ? null : <Text style={[styles.textLabel, styles.colorMuted, styles.pv4, styles.ph3, styles.overflowAuto]}>{translate('common.noResultsFound')}</Text>,
[isOffline, isTyping, styles, translate],
() => (!isTyping ? null : <Text style={[styles.textLabel, styles.colorMuted, styles.pv4, styles.ph3, styles.overflowAuto]}>{translate('common.noResultsFound')}</Text>),
[isTyping, styles, translate],
);

const listLoader = useCallback(
Expand Down Expand Up @@ -338,11 +363,10 @@ function AddressSearch(
ref={containerRef}
>
<GooglePlacesAutocomplete
disableScroll
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
predefinedPlaces={predefinedPlaces ?? undefined}
predefinedPlaces={filteredPredefinedPlaces}
listEmptyComponent={listEmptyComponent}
listLoaderComponent={listLoader}
renderHeaderComponent={renderHeaderComponent}
Expand All @@ -351,7 +375,7 @@ function AddressSearch(
const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text;
return (
<View>
{!!title && <Text style={[styles.googleSearchText]}>{title}</Text>}
{!!title && <Text style={styles.googleSearchText}>{title}</Text>}
<Text style={[title ? styles.textLabelSupporting : styles.googleSearchText]}>{subtitle}</Text>
</View>
);
Expand Down Expand Up @@ -385,6 +409,7 @@ function AddressSearch(
shouldSaveDraft,
onFocus: () => {
setIsFocused(true);
onFocus?.();
},
onBlur: (event) => {
if (!isCurrentTargetInsideContainer(event, containerRef)) {
Expand Down Expand Up @@ -414,10 +439,18 @@ function AddressSearch(
}}
styles={{
textInputContainer: [styles.flexColumn],
listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}],
listView: [
StyleUtils.getGoogleListViewStyle(displayListViewBorder),
listViewOverflow,
styles.borderLeft,
styles.borderRight,
styles.flexGrow0,
!isFocused && styles.h0,
],
row: [styles.pv4, styles.ph3, styles.overflowAuto],
description: [styles.googleSearchText],
separator: [styles.googleSearchSeparator],
container: [styles.mh100],
}}
numberOfLines={2}
isRowScrollable={false}
Expand All @@ -441,11 +474,13 @@ function AddressSearch(
)
}
placeholder=""
/>
<LocationErrorMessage
onClose={() => setLocationErrorCode(null)}
locationErrorCode={locationErrorCode}
/>
listViewDisplayed
>
<LocationErrorMessage
onClose={() => setLocationErrorCode(null)}
locationErrorCode={locationErrorCode}
/>
</GooglePlacesAutocomplete>
</View>
</ScrollView>
{isFetchingCurrentLocation && <FullScreenLoadingIndicator />}
Expand Down
4 changes: 4 additions & 0 deletions src/components/AddressSearch/listViewOverflow/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import {defaultStyles} from '@styles/index';

export default defaultStyles.overflowHidden;
4 changes: 4 additions & 0 deletions src/components/AddressSearch/listViewOverflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import {defaultStyles} from '@styles/index';

export default defaultStyles.overflowAuto;
11 changes: 9 additions & 2 deletions src/components/AddressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ type StreetValue = {
street: string;
};

type PredefinedPlace = Place & {
name?: string;
};

type AddressSearchProps = {
/** The ID used to uniquely identify the input in a Form */
inputID?: string;

/** Saves a draft of the input value when used in a form */
shouldSaveDraft?: boolean;

/** Callback that is called when the text input is focused */
onFocus?: () => void;

/** Callback that is called when the text input is blurred */
onBlur?: () => void;

Expand Down Expand Up @@ -65,7 +72,7 @@ type AddressSearchProps = {
canUseCurrentLocation?: boolean;

/** A list of predefined places that can be shown when the user isn't searching for something */
predefinedPlaces?: Place[] | null;
predefinedPlaces?: PredefinedPlace[] | null;

/** A map of inputID key names */
renamedInputKeys?: Address;
Expand All @@ -85,4 +92,4 @@ type AddressSearchProps = {

type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent<TextInputFocusEventData>, containerRef: RefObject<View | HTMLElement>) => boolean;

export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue};
export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace};
3 changes: 3 additions & 0 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProvider

/** Whether to apply flex to the submit button */
submitFlexEnabled?: boolean;

/** Whether the form container should grow or adapt to the viewable available space */
shouldContainerGrow?: boolean;
};

function FormProvider(
Expand Down
14 changes: 9 additions & 5 deletions src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useCallback, useMemo, useRef} from 'react';
import type {RefObject} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native';
import {Keyboard} from 'react-native';
import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native';
import {Keyboard, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
Expand Down Expand Up @@ -32,6 +32,9 @@ type FormWrapperProps = ChildrenProps &
/** Whether to apply flex to the submit button */
submitFlexEnabled?: boolean;

/** Whether the form container should grow or adapt to the viewable available space */
shouldContainerGrow?: boolean;

/** Server side errors keyed by microtime */
errors: FormInputErrors;

Expand Down Expand Up @@ -60,6 +63,7 @@ function FormWrapper({
scrollContextEnabled = false,
shouldHideFixErrorsAlert = false,
disablePressOnEnter = true,
shouldContainerGrow = true,
}: FormWrapperProps) {
const styles = useThemeStyles();
const formRef = useRef<RNScrollView>(null);
Expand Down Expand Up @@ -104,7 +108,7 @@ function FormWrapper({
ref={formContentRef}
style={[style, safeAreaPaddingBottomStyle.paddingBottom ? safeAreaPaddingBottomStyle : styles.pb5]}
>
{children}
<View style={styles.flex1}>{children}</View>
{isSubmitButtonVisible && (
<FormAlertWithSubmitButton
buttonText={submitButtonText}
Expand Down Expand Up @@ -155,7 +159,7 @@ function FormWrapper({
scrollContextEnabled ? (
<ScrollViewWithContext
style={[styles.w100, styles.flex1]}
contentContainerStyle={styles.flexGrow1}
contentContainerStyle={shouldContainerGrow ? styles.flexGrow1 : styles.flex1}
keyboardShouldPersistTaps="handled"
ref={formRef}
>
Expand All @@ -164,7 +168,7 @@ function FormWrapper({
) : (
<ScrollView
style={[styles.w100, styles.flex1]}
contentContainerStyle={styles.flexGrow1}
contentContainerStyle={shouldContainerGrow ? styles.flexGrow1 : styles.flex1}
keyboardShouldPersistTaps="handled"
ref={formRef}
>
Expand Down
33 changes: 33 additions & 0 deletions src/hooks/useSubmitButtonVisibility.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useState} from 'react';
import useSafeAreaInsets from './useSafeAreaInsets';
import useThemeStyles from './useThemeStyles';

// Useful when there's a need to hide the submit button from FormProvider,
// to let form content fill the page when virtual keyboard is shown
function useSubmitButtonVisibility() {
const styles = useThemeStyles();
const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true);
const {bottom} = useSafeAreaInsets();

const showSubmitButton = () => {
setIsSubmitButtonVisible(true);
};

const hideSubmitButton = () => {
setIsSubmitButtonVisible(false);
};

// When the submit button is hidden there's a need to manually
// add its bottom style to the FormProvider style prop,
// otherwise the form content will touch the bottom of the page/screen
const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5;

return {
isSubmitButtonVisible,
showSubmitButton,
hideSubmitButton,
formStyle,
};
}

export default useSubmitButtonVisibility;
58 changes: 58 additions & 0 deletions src/hooks/useSubmitButtonVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {useEffect, useRef, useState} from 'react';
import {Dimensions} from 'react-native';
import useSafeAreaInsets from './useSafeAreaInsets';
import useThemeStyles from './useThemeStyles';
import useWindowDimensions from './useWindowDimensions';

// Useful when there's a need to hide the submit button from FormProvider,
// to let form content fill the page when virtual keyboard is shown
function useSubmitButtonVisibility() {
const styles = useThemeStyles();
const {windowHeight, isSmallScreenWidth} = useWindowDimensions();
const [isSubmitButtonVisible, setIsSubmitButtonVisible] = useState(true);
const initialWindowHeightRef = useRef(windowHeight);
const isSmallScreenWidthRef = useRef(isSmallScreenWidth);
const {bottom} = useSafeAreaInsets();

// Web: the submit button is shown when the height of the window is the same or greater,
// otherwise it's hidden
useEffect(() => {
const dimensionsListener = Dimensions.addEventListener('change', ({window}) => {
if (!isSmallScreenWidthRef.current) {
return;
}

if (window.height < initialWindowHeightRef.current) {
setIsSubmitButtonVisible(false);
return;
}

setIsSubmitButtonVisible(true);
});

return () => dimensionsListener.remove();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Web: the submit button is only shown when the window height is the same or greater,
// so executing this function won't do anything
const showSubmitButton = () => {};

// Web: the submit button is only hidden when the window height becomes smaller,
// so executing this function won't do anything
const hideSubmitButton = () => {};

// When the submit button is hidden there's a need to manually
// add its bottom style to the FormProvider style prop,
// otherwise the form content will touch the bottom of the page/screen
const formStyle = !isSubmitButtonVisible && bottom === 0 && styles.mb5;

return {
isSubmitButtonVisible,
showSubmitButton,
hideSubmitButton,
formStyle,
};
}

export default useSubmitButtonVisibility;
2 changes: 1 addition & 1 deletion src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp
if (!recentWaypointAlreadyExists && waypoint !== null) {
const clonedWaypoints = lodashClone(recentWaypoints);
clonedWaypoints.unshift(waypoint);
Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, 5));
Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER));
}
}

Expand Down
Loading

0 comments on commit 849a0d4

Please sign in to comment.