Skip to content

Commit

Permalink
feat: add interface mode preference to Settings (#2583)
Browse files Browse the repository at this point in the history
* feat: add interface mode preference to Settings

* Refactor existing layout preferences so they are all persisted
* Made a new namespace within the layout slice so that all values there will
  get persisted without having to add stuff to the persistence code AND we
  avoid namespace collisions
* Wrapped layout slice getters and setters in a hook to provide descriptive
  names without modifying the underlying values

* Remove duplicate testID that was causing e2e to fail

---------

Co-authored-by: Amanda Bullington <albullington@gmail.com>
  • Loading branch information
kueda and albullington authored Dec 26, 2024
1 parent 768bea7 commit 31ce899
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 44 deletions.
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,7 @@ SPEC CHECKSUMS:
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5
RCTTypeSafety: ede1e2576424d89471ef553b2aed09fbbcc038e3
React: 2ddb437e599df2f1bffa9b248de2de4cfa0227f0
Expand Down
19 changes: 11 additions & 8 deletions src/components/AddObsModal/AddObsButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { t } from "i18next";
import { getCurrentRoute } from "navigation/navigationUtils.ts";
import * as React from "react";
import { log } from "sharedHelpers/logger";
import { useLayoutPrefs } from "sharedHooks";
import useStore from "stores/useStore";

const logger = log.extend( "AddObsButton" );
Expand All @@ -19,12 +20,14 @@ const AddObsButton = (): React.Node => {
const closeModal = React.useCallback( () => setModal( false ), [] );

const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const isAdvancedUser = useStore( state => state.isAdvancedUser );
const { isAllAddObsOptionsMode } = useLayoutPrefs( );
const navigation = useNavigation( );
React.useEffect( ( ) => {
// don't remove this logger.info statement: it's used for internal metrics
logger.info( `isAdvancedUser: ${isAdvancedUser}` );
}, [isAdvancedUser] );
// don't remove this logger.info statement: it's used for internal
// metrics. isAdvancedUser name is vestigial, changing it will make it
// impossible to compare with older log data
logger.info( `isAdvancedUser: ${isAllAddObsOptionsMode}` );
}, [isAllAddObsOptionsMode] );

const navAndCloseModal = ( screen, params ) => {
const currentRoute = getCurrentRoute();
Expand Down Expand Up @@ -74,15 +77,15 @@ const AddObsButton = (): React.Node => {
/>
<GradientButton
sizeClassName="w-[69px] h-[69px]"
onPress={isAdvancedUser
onPress={isAllAddObsOptionsMode
? openModal
: navToARCamera}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={isAdvancedUser
accessibilityHint={isAllAddObsOptionsMode
? t( "Shows-observation-creation-options" )
: t( "Opens-AI-camera" )}
iconName={isAdvancedUser && "plus"}
iconSize={isAdvancedUser && 31}
iconName={isAllAddObsOptionsMode && "plus"}
iconSize={isAllAddObsOptionsMode && 31}
/>
</>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/Notifications/NotificationsListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import classnames from "classnames";
import ObsNotification from "components/Notifications/ObsNotification.tsx";
import { Pressable, View } from "components/styledComponents";
import React from "react";
import { useLayoutPrefs } from "sharedHooks";
import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";
import { ACTIVITY_TAB } from "stores/createLayoutSlice";
import useStore from "stores/useStore";

type Props = {
notification: Notification
};

const NotificationsListItem = ( { notification }: Props ) => {
const setObsDetailsTab = useStore( state => state.setObsDetailsTab );
const { setObsDetailsTab } = useLayoutPrefs( );
const navigation = useNavigation( );
const viewedStatus = notification.viewed;

Expand Down
7 changes: 5 additions & 2 deletions src/components/ObsDetails/ObsDetailsContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { fetchTaxonAndSave } from "sharedHelpers/taxon";
import {
useAuthenticatedMutation,
useCurrentUser,
useLayoutPrefs,
useLocalObservation,
useObservationsUpdates,
useTranslation
Expand Down Expand Up @@ -174,8 +175,10 @@ const reducer = ( state, action ) => {

const ObsDetailsContainer = ( ): Node => {
const setObservations = useStore( state => state.setObservations );
const obsDetailsTab = useStore( state => state.obsDetailsTab );
const setObsDetailsTab = useStore( state => state.setObsDetailsTab );
const {
obsDetailsTab,
setObsDetailsTab
} = useLayoutPrefs( );
const currentUser = useCurrentUser( );
const { params } = useRoute();
const {
Expand Down
75 changes: 47 additions & 28 deletions src/components/Settings/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import {
useAuthenticatedMutation,
useCurrentUser,
useLayoutPrefs,
useTranslation,
useUserMe
} from "sharedHooks";
import useStore from "stores/useStore";

import LanguageSetting from "./LanguageSetting";

Expand All @@ -54,8 +54,12 @@ const Settings = ( ) => {
const {
remoteUser, isLoading, refetchUserMe
} = useUserMe();
const isAdvancedUser = useStore( state => state.isAdvancedUser );
const setIsAdvancedUser = useStore( state => state.setIsAdvancedUser );
const {
isDefaultMode,
isAllAddObsOptionsMode,
setIsDefaultMode,
setIsAllAddObsOptionsMode
} = useLayoutPrefs( );
const [settings, setSettings] = useState( {} );
const [isSaving, setIsSaving] = useState( false );
const [showingWebViewSettings, setShowingWebViewSettings] = useState( false );
Expand Down Expand Up @@ -141,30 +145,6 @@ const Settings = ( ) => {
updateUserMutation.mutate( payload );
}, [settings?.id, updateUserMutation] );

const renderLoggedOut = ( ) => (
<>
<Heading4>{t( "OBSERVATION-BUTTON" )}</Heading4>
<Body2 className="mt-3">{t( "When-tapping-the-green-observation-button" )}</Body2>
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={!isAdvancedUser}
onPress={() => setIsAdvancedUser( false )}
label={t( "iNaturalist-AI-Camera" )}
/>
</View>
<View className="mt-4 pr-5">
<RadioButtonRow
testID="all-observation-option"
smallLabel
checked={isAdvancedUser}
onPress={() => setIsAdvancedUser( true )}
label={t( "All-observation-option" )}
/>
</View>
</>
);

const renderLoggedIn = ( ) => (
<View>
{( isSaving || isLoading ) && (
Expand Down Expand Up @@ -261,7 +241,46 @@ const Settings = ( ) => {
<ScrollViewWrapper>
<StatusBar barStyle="dark-content" />
<View className="p-5">
{renderLoggedOut( )}
<View className="mb-5">
<Heading4>{t( "INATURALIST-INTERFACE-MODE" )}</Heading4>
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={isDefaultMode}
onPress={( ) => setIsDefaultMode( true )}
label={t( "Default--interface-mode" )}
/>
</View>
<View className="mt-4 pr-5">
<RadioButtonRow
smallLabel
checked={!isDefaultMode}
onPress={( ) => setIsDefaultMode( false )}
label={t( "Advanced--interface-mode" )}
/>
</View>
</View>
<View className="mb-5">
<Heading4>{t( "OBSERVATION-BUTTON" )}</Heading4>
<Body2 className="mt-3">{t( "When-tapping-the-green-observation-button" )}</Body2>
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={!isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( false )}
label={t( "iNaturalist-AI-Camera" )}
/>
</View>
<View className="mt-4 pr-5">
<RadioButtonRow
testID="all-observation-option"
smallLabel
checked={isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( true )}
label={t( "All-observation-option" )}
/>
</View>
</View>
{currentUser && renderLoggedIn( )}
</View>
</ScrollViewWrapper>
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/l10n/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Add-optional-notes = Add optional notes
Adds-your-vote-of-agreement = Adds your vote of agreement
# Hint for a button that adds a vote of disagreement
Adds-your-vote-of-disagreement = Adds your vote of disagreement
Advanced--interface-mode = Advanced
Affiliation = Affiliation: { $site }
# Label for button that adds an identification of the same taxon as another identification
Agree = Agree
Expand Down Expand Up @@ -346,6 +347,7 @@ datetime-format-long = Pp
datetime-format-short = M/d/yy h:mm a
# Month of December
December = December
Default--interface-mode = Default
DELETE = DELETE
Delete-all-observations = Delete all observations
Delete-comment = Delete comment
Expand Down Expand Up @@ -579,6 +581,7 @@ iNaturalist-has-no-ID-suggestions-for-this-photo = iNaturalist has no ID suggest
INATURALIST-HELP-PAGE = INATURALIST HELP PAGE
iNaturalist-helps-you-identify = iNaturalist helps you identify the plants and animals around you while generating data for science and conservation. Get connected with a community of millions scientists and naturalists who can help you learn more about nature!
iNaturalist-identification-suggestions-are-based-on = iNaturalist's identification suggestions are based on observations and identifications made by the iNaturalist community, including { $user1 }, { $user2 }, { $user3 }, and many others.
INATURALIST-INTERFACE-MODE = INATURALIST INTERFACE MODE
iNaturalist-is-a-501 = iNaturalist is a 501(c)(3) non-profit in the United States of America (Tax ID/EIN 92-1296468).
iNaturalist-is-a-community-of-naturalists = iNaturalist is a community of naturalists that works together to create and identify wild biodiversity observations.
iNaturalist-is-loading-ID-suggestions = iNaturalist is loading ID suggestions...
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/l10n/en.ftl.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"Add-optional-notes": "Add optional notes",
"Adds-your-vote-of-agreement": "Adds your vote of agreement",
"Adds-your-vote-of-disagreement": "Adds your vote of disagreement",
"Advanced--interface-mode": "Advanced",
"Affiliation": "Affiliation: { $site }",
"Agree": "Agree",
"AGREE": "AGREE",
Expand Down Expand Up @@ -188,6 +189,7 @@
"datetime-format-long": "Pp",
"datetime-format-short": "M/d/yy h:mm a",
"December": "December",
"Default--interface-mode": "Default",
"DELETE": "DELETE",
"Delete-all-observations": "Delete all observations",
"Delete-comment": "Delete comment",
Expand Down Expand Up @@ -335,6 +337,7 @@
"INATURALIST-HELP-PAGE": "INATURALIST HELP PAGE",
"iNaturalist-helps-you-identify": "iNaturalist helps you identify the plants and animals around you while generating data for science and conservation. Get connected with a community of millions scientists and naturalists who can help you learn more about nature!",
"iNaturalist-identification-suggestions-are-based-on": "iNaturalist's identification suggestions are based on observations and identifications made by the iNaturalist community, including { $user1 }, { $user2 }, { $user3 }, and many others.",
"INATURALIST-INTERFACE-MODE": "INATURALIST INTERFACE MODE",
"iNaturalist-is-a-501": "iNaturalist is a 501(c)(3) non-profit in the United States of America (Tax ID/EIN 92-1296468).",
"iNaturalist-is-a-community-of-naturalists": "iNaturalist is a community of naturalists that works together to create and identify wild biodiversity observations.",
"iNaturalist-is-loading-ID-suggestions": "iNaturalist is loading ID suggestions...",
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/strings.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Add-optional-notes = Add optional notes
Adds-your-vote-of-agreement = Adds your vote of agreement
# Hint for a button that adds a vote of disagreement
Adds-your-vote-of-disagreement = Adds your vote of disagreement
Advanced--interface-mode = Advanced
Affiliation = Affiliation: { $site }
# Label for button that adds an identification of the same taxon as another identification
Agree = Agree
Expand Down Expand Up @@ -346,6 +347,7 @@ datetime-format-long = Pp
datetime-format-short = M/d/yy h:mm a
# Month of December
December = December
Default--interface-mode = Default
DELETE = DELETE
Delete-all-observations = Delete all observations
Delete-comment = Delete comment
Expand Down Expand Up @@ -579,6 +581,7 @@ iNaturalist-has-no-ID-suggestions-for-this-photo = iNaturalist has no ID suggest
INATURALIST-HELP-PAGE = INATURALIST HELP PAGE
iNaturalist-helps-you-identify = iNaturalist helps you identify the plants and animals around you while generating data for science and conservation. Get connected with a community of millions scientists and naturalists who can help you learn more about nature!
iNaturalist-identification-suggestions-are-based-on = iNaturalist's identification suggestions are based on observations and identifications made by the iNaturalist community, including { $user1 }, { $user2 }, { $user3 }, and many others.
INATURALIST-INTERFACE-MODE = INATURALIST INTERFACE MODE
iNaturalist-is-a-501 = iNaturalist is a 501(c)(3) non-profit in the United States of America (Tax ID/EIN 92-1296468).
iNaturalist-is-a-community-of-naturalists = iNaturalist is a community of naturalists that works together to create and identify wild biodiversity observations.
iNaturalist-is-loading-ID-suggestions = iNaturalist is loading ID suggestions...
Expand Down
1 change: 1 addition & 0 deletions src/sharedHooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as useInfiniteUserScroll } from "./useInfiniteUserScroll";
export { default as useInterval } from "./useInterval";
export { default as useKeyboardInfo } from "./useKeyboardInfo";
export { default as useLastScreen } from "./useLastScreen";
export { default as useLayoutPrefs } from "./useLayoutPrefs";
export { default as useLocalObservation } from "./useLocalObservation";
export { default as useLocalObservations } from "./useLocalObservations";
export { default as useLocationPermission } from "./useLocationPermission";
Expand Down
15 changes: 15 additions & 0 deletions src/sharedHooks/useLayoutPrefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import useStore from "stores/useStore";

// Wraps values from the layout slice with descriptive names
const useLayoutPrefs = ( ) => useStore( state => ( {
// Vestigial stuff
obsDetailsTab: state.obsDetailsTab,
setObsDetailsTab: state.setObsDetailsTab,
isAllAddObsOptionsMode: state.isAdvancedUser,
setIsAllAddObsOptionsMode: state.setIsAdvancedUser,

// newer stuff
...state.layout
} ) );

export default useLayoutPrefs;
16 changes: 15 additions & 1 deletion src/stores/createLayoutSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ export const ACTIVITY_TAB = "ACTIVITY";
export const DETAILS_TAB = "DETAILS";

const createLayoutSlice = set => ( {
// Vestigial un-namespaced values
isAdvancedUser: false,
setIsAdvancedUser: newValue => set( { isAdvancedUser: newValue } ),

obsDetailsTab: ACTIVITY_TAB,
setObsDetailsTab: newValue => set( { obsDetailsTab: newValue } )
setObsDetailsTab: newValue => set( { obsDetailsTab: newValue } ),

// Please put new stuff in this namespace so they will be saved to disk
layout: {
// Controls all all layouts related to default mode
isDefaultMode: true,
setIsDefaultMode: newValue => set( state => ( {
layout: {
...state.layout,
isDefaultMode: newValue
}
} ) )
}
} );

export default createLayoutSlice;
18 changes: 16 additions & 2 deletions src/stores/useStore.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from "lodash";
import { MMKV } from "react-native-mmkv";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
Expand Down Expand Up @@ -73,9 +74,22 @@ const useStore = create( persist(
{
name: "persisted-zustand",
partialize: state => ( {
isAdvancedUser: state.isAdvancedUser
// Vestigial un-namespaced values in the layout slice
isAdvancedUser: state.isAdvancedUser,
obsDetailsTab: state.obsDetailsTab,

// Dynamically select all values in the layout slice's namespace
layout: ( Object.keys( state.layout ).reduce( ( memo, key ) => {
if ( typeof ( state.layout[key] ) !== "function" ) {
memo[key] = state.layout[key];
}
return memo;
}, {} ) )
} ),
storage: createJSONStorage( () => zustandStorage )
storage: createJSONStorage( () => zustandStorage ),
// We need to deep merge to persist nested objects, like layout
// https://zustand.docs.pmnd.rs/middlewares/persist#persisting-a-state-with-nested-objects
merge: ( persisted, current ) => _.merge( current, persisted )
}
) );

Expand Down

0 comments on commit 31ce899

Please sign in to comment.