diff --git a/src/components/ObsDetails/ObsDetailsContainer.js b/src/components/ObsDetails/ObsDetailsContainer.js index e5b9d9e83..84c607718 100644 --- a/src/components/ObsDetails/ObsDetailsContainer.js +++ b/src/components/ObsDetails/ObsDetailsContainer.js @@ -2,41 +2,29 @@ import { useNetInfo, } from "@react-native-community/netinfo"; -import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native"; -import { useQueryClient } from "@tanstack/react-query"; -import { fetchSubscriptions } from "api/observations"; +import { useRoute } from "@react-navigation/native"; import IdentificationSheets from "components/ObsDetailsDefaultMode/IdentificationSheets"; import useMarkViewedMutation from "components/ObsDetailsSharedComponents/hooks/useMarkViewedMutation"; -import { RealmContext } from "providers/contexts"; +import useObsDetailsSharedLogic + from "components/ObsDetailsSharedComponents/hooks/useObsDetailsSharedLogic"; import type { Node } from "react"; import React, { - useCallback, - useEffect, - useReducer, useState, } from "react"; import { LogBox } from "react-native"; import Observation from "realmModels/Observation"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; import { - useAuthenticatedQuery, useCurrentUser, useLayoutPrefs, useLocalObservation, - useObservationsUpdates, useTranslation, } from "sharedHooks"; -import useRemoteObservation, { - fetchRemoteObservationKey, -} from "sharedHooks/useRemoteObservation"; +import useRemoteObservation from "sharedHooks/useRemoteObservation"; import { OBS_DETAILS_TAB } from "stores/createLayoutSlice"; -import useStore from "stores/useStore"; import ObsDetails from "./ObsDetails"; -const { useRealm } = RealmContext; - // this is getting triggered by passing dates, like _created_at, through // react navigation via the observation object. it doesn't seem to // actually be breaking anything, for the moment (May 2, 2022) @@ -44,77 +32,7 @@ LogBox.ignoreLogs( [ "Non-serializable values were found in the navigation state", ] ); -const sortItems = ( ids, comments ) => ids.concat( [...comments] ).sort( - ( a, b ) => ( new Date( a.created_at ) - new Date( b.created_at ) ), -); - -const initialState = { - activityItems: [], - addingActivityItem: false, - agreeIdentification: null, - observationShown: null, - showAddCommentSheet: false, - showAgreeWithIdSheet: false, -}; - -const SHOW_AGREE_SHEET = "SHOW_AGREE_SHEET"; -const HIDE_AGREE_SHEET = "HIDE_AGREE_SHEET"; -const SET_ADD_COMMENT_SHEET = "SET_ADD_COMMENT_SHEET"; -const SET_INITIAL_OBSERVATION = "SET_INITIAL_OBSERVATION"; -const ADD_ACTIVITY_ITEM = "ADD_ACTIVITY_ITEM"; -const LOADING_ACTIVITY_ITEM = "LOADING_ACTIVITY_ITEM"; - -const reducer = ( state, action ) => { - switch ( action.type ) { - case SET_INITIAL_OBSERVATION: - return { - ...state, - observationShown: action.observationShown, - activityItems: sortItems( - action.observationShown?.identifications || [], - action.observationShown?.comments || [], - ), - }; - case ADD_ACTIVITY_ITEM: - return { - ...state, - observationShown: action.observationShown, - addingActivityItem: false, - activityItems: sortItems( - action.observationShown?.identifications || [], - action.observationShown?.comments || [], - ), - }; - case LOADING_ACTIVITY_ITEM: - return { - ...state, - addingActivityItem: true, - }; - - case SHOW_AGREE_SHEET: - return { - ...state, - showAgreeWithIdSheet: true, - agreeIdentification: action.agreeIdentification, - }; - case HIDE_AGREE_SHEET: - return { - ...state, - showAgreeWithIdSheet: false, - agreeIdentification: null, - }; - case SET_ADD_COMMENT_SHEET: - return { - ...state, - showAddCommentSheet: action.showAddCommentSheet, - }; - default: - throw new Error( ); - } -}; - const ObsDetailsContainer = ( ): Node => { - const setObservations = useStore( state => state.setObservations ); const { obsDetailsTab, setObsDetailsTab, @@ -125,23 +43,10 @@ const ObsDetailsContainer = ( ): Node => { targetActivityItemID, uuid, } = params; - const navigation = useNavigation( ); - const realm = useRealm( ); const { t } = useTranslation( ); const { isConnected } = useNetInfo( ); - const [state, dispatch] = useReducer( reducer, initialState ); const [remoteObsWasDeleted, setRemoteObsWasDeleted] = useState( false ); - const { - activityItems, - addingActivityItem, - agreeIdentification, - observationShown, - showAddCommentSheet, - showAgreeWithIdSheet, - } = state; - const queryClient = useQueryClient( ); - const { localObservation, markDeletedLocally, @@ -163,23 +68,6 @@ const ObsDetailsContainer = ( ): Node => { useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation ); - // If we tried to get a remote observation but it no longer exists, the user - // can't do anything so we need to send them back and remove the local - // copy of this observation - useEffect( ( ) => { - setRemoteObsWasDeleted( fetchRemoteObservationError?.status === 404 ); - }, [fetchRemoteObservationError?.status] ); - const confirmRemoteObsWasDeleted = useCallback( ( ) => { - if ( localObservation ) { - markDeletedLocally( ); - } - if ( navigation.canGoBack( ) ) navigation.goBack( ); - }, [ - localObservation, - markDeletedLocally, - navigation, - ] ); - const observation = localObservation || Observation.mapApiToRealm( remoteObservation ); // In theory the only situation in which an observation would not have a @@ -191,51 +79,39 @@ const ObsDetailsContainer = ( ): Node => { || ( !observation?.user && !observation?.id ) ); - const { data: subscriptions, refetch: refetchSubscriptions } = useAuthenticatedQuery( - [ - "fetchSubscriptions", - ], - optsWithAuth => fetchSubscriptions( { uuid, fields: "user_id" }, optsWithAuth ), - { - enabled: !!( currentUser ) && !belongsToCurrentUser, - }, - ); - - const invalidateRemoteObservationFetch = useCallback( ( ) => { - if ( observation?.uuid ) { - queryClient.invalidateQueries( { - queryKey: [fetchRemoteObservationKey, observation.uuid], - } ); - } - }, [queryClient, observation?.uuid] ); - - useFocusEffect( - // this ensures activity items load after a user taps suggest id - // and adds a remote id on the Suggestions screen - useCallback( ( ) => { - invalidateRemoteObservationFetch( ); - }, [invalidateRemoteObservationFetch] ), - ); - - useEffect( ( ) => { - if ( !observationShown ) { - dispatch( { - type: SET_INITIAL_OBSERVATION, - observationShown: observation, - } ); - } - }, [observation, observationShown] ); - - useEffect( ( ) => { - // if observation does not belong to current user, show - // new activity items after a refetch - if ( remoteObservation && !isRefetching ) { - dispatch( { - type: ADD_ACTIVITY_ITEM, - observationShown: Observation.mapApiToRealm( remoteObservation ), - } ); - } - }, [remoteObservation, isRefetching] ); + const { + activityItems, + addingActivityItem, + agreeIdentification, + observationShown, + showAddCommentSheet, + showAgreeWithIdSheet, + subscriptionResults, + openAddCommentSheet, + hideAddCommentSheet, + openAgreeWithIdSheet, + closeAgreeWithIdSheet, + navToSuggestions, + invalidateQueryAndRefetch, + handleIdentificationMutationSuccess, + handleCommentMutationSuccess, + confirmRemoteObsWasDeleted, + loadActivityItem, + refetchSubscriptions, + } = useObsDetailsSharedLogic( { + observation, + uuid, + localObservation, + remoteObservation, + markViewedLocally, + markDeletedLocally, + setRemoteObsWasDeleted, + fetchRemoteObservationError, + currentUser, + belongsToCurrentUser, + isRefetching, + refetchRemoteObservation, + } ); const tabs = [ { @@ -252,116 +128,8 @@ const ObsDetailsContainer = ( ): Node => { }, ]; - const hasPhotos = observation?.observationPhotos?.length > 0; - - const { refetch: refetchObservationUpdates } = useObservationsUpdates( - !!currentUser && !!observation, - ); - - const openAddCommentSheet = useCallback( ( ) => { - dispatch( { - type: SET_ADD_COMMENT_SHEET, - showAddCommentSheet: true, - } ); - }, [] ); - - const hideAddCommentSheet = useCallback( ( ) => dispatch( { - type: SET_ADD_COMMENT_SHEET, - showAddCommentSheet: false, - } ), [] ); - - const openAgreeWithIdSheet = useCallback( taxon => { - dispatch( { - type: SHOW_AGREE_SHEET, - agreeIdentification: { taxon }, - } ); - }, [] ); - - const navToSuggestions = useCallback( ( ) => { - setObservations( [observation] ); - if ( hasPhotos ) { - navigation.push( "Suggestions", { - entryScreen: "ObsDetails", - lastScreen: "ObsDetails", - hideSkip: true, - } ); - } else { - // Go directly to taxon search in case there are no photos - navigation.navigate( "SuggestionsTaxonSearch", { lastScreen: "ObsDetails" } ); - } - }, [hasPhotos, navigation, observation, setObservations] ); - const showActivityTab = obsDetailsTab === OBS_DETAILS_TAB.ACTIVITY; - const invalidateQueryAndRefetch = useCallback( ( ) => { - invalidateRemoteObservationFetch( ); - refetchRemoteObservation( ); - refetchObservationUpdates( ); - }, [invalidateRemoteObservationFetch, refetchObservationUpdates, refetchRemoteObservation] ); - - const subscriptionResults = !belongsToCurrentUser - ? subscriptions?.results - : []; - - const handleIdentificationMutationSuccess = useCallback( data => { - refetchRemoteObservation( ); - if ( belongsToCurrentUser ) { - const createdIdent = data[0]; - // Try to find an existing taxon b/c otherwise realm will try to - // create the taxon when updating the observation and error out - let taxon; - if ( createdIdent.taxon?.id ) { - taxon = realm?.objectForPrimaryKey( "Taxon", createdIdent.taxon.id ); - } - taxon = taxon || createdIdent.taxon; - safeRealmWrite( realm, ( ) => { - createdIdent.user = currentUser; - if ( taxon ) createdIdent.taxon = taxon; - localObservation?.identifications?.push( createdIdent ); - }, "setting local identification in ObsDetailsContainer" ); - if ( uuid ) { - const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); - dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); - } - } - }, [ - belongsToCurrentUser, - currentUser, - localObservation?.identifications, - realm, - refetchRemoteObservation, - uuid, - ] ); - - const handleCommentMutationSuccess = useCallback( data => { - refetchRemoteObservation( ); - if ( belongsToCurrentUser ) { - safeRealmWrite( realm, ( ) => { - const localComments = localObservation?.comments; - const newComment = data[0]; - newComment.user = currentUser; - localComments?.push( newComment ); - }, "setting local comment in ObsDetailsContainer" ); - const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); - dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); - } - }, [ - belongsToCurrentUser, - currentUser, - localObservation?.comments, - realm, - refetchRemoteObservation, - uuid, - ] ); - - const closeAgreeWithIdSheet = useCallback( ( ) => { - dispatch( { type: HIDE_AGREE_SHEET } ); - }, [] ); - - const loadActivityItem = useCallback( ( ) => { - dispatch( { type: LOADING_ACTIVITY_ITEM } ); - }, [] ); - return observationShown && ( <> ids.concat( [...comments] ).sort( - ( a, b ) => ( new Date( a.created_at ) - new Date( b.created_at ) ), -); - -const SHOW_AGREE_SHEET = "SHOW_AGREE_SHEET"; -const HIDE_AGREE_SHEET = "HIDE_AGREE_SHEET"; -const SET_ADD_COMMENT_SHEET = "SET_ADD_COMMENT_SHEET"; -const SET_INITIAL_OBSERVATION = "SET_INITIAL_OBSERVATION"; -const ADD_ACTIVITY_ITEM = "ADD_ACTIVITY_ITEM"; -const LOADING_ACTIVITY_ITEM = "LOADING_ACTIVITY_ITEM"; - -const initialState = { - activityItems: [], - addingActivityItem: false, - observationShown: null, - showAddCommentSheet: false, -}; - -const reducer = ( state, action ) => { - switch ( action.type ) { - case SET_INITIAL_OBSERVATION: - return { - ...state, - observationShown: action.observationShown, - activityItems: sortItems( - action.observationShown?.identifications || [], - action.observationShown?.comments || [], - ), - }; - case ADD_ACTIVITY_ITEM: - return { - ...state, - observationShown: action.observationShown, - addingActivityItem: false, - activityItems: sortItems( - action.observationShown?.identifications || [], - action.observationShown?.comments || [], - ), - }; - case LOADING_ACTIVITY_ITEM: - return { - ...state, - addingActivityItem: true, - }; - case SHOW_AGREE_SHEET: - return { - ...state, - showAgreeWithIdSheet: true, - agreeIdentification: action.agreeIdentification, - }; - case HIDE_AGREE_SHEET: - return { - ...state, - showAgreeWithIdSheet: false, - agreeIdentification: null, - }; - case SET_ADD_COMMENT_SHEET: - return { - ...state, - commentIsOptional: action.commentIsOptional, - showAddCommentSheet: action.showAddCommentSheet, - }; - default: - throw new Error( ); - } -}; - type Props = { belongsToCurrentUser: boolean, currentUser: ?Object, @@ -121,11 +36,6 @@ type Props = { } const ObsDetailsDefaultModeContainer = ( props: Props ): Node => { - const setObservations = useStore( state => state.setObservations ); - const navigation = useNavigation( ); - const realm = useRealm( ); - const [state, dispatch] = useReducer( reducer, initialState ); - const { observation, targetActivityItemID, @@ -144,6 +54,8 @@ const ObsDetailsDefaultModeContainer = ( props: Props ): Node => { remoteObsWasDeleted, } = props; + useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation ); + const { activityItems, addingActivityItem, @@ -151,186 +63,33 @@ const ObsDetailsDefaultModeContainer = ( props: Props ): Node => { observationShown, showAddCommentSheet, showAgreeWithIdSheet, - } = state; - const queryClient = useQueryClient( ); - - useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation ); - - // If we tried to get a remote observation but it no longer exists, the user - // can't do anything so we need to send them back and remove the local - // copy of this observation - useEffect( ( ) => { - setRemoteObsWasDeleted( fetchRemoteObservationError?.status === 404 ); - }, [fetchRemoteObservationError?.status, setRemoteObsWasDeleted] ); - - const confirmRemoteObsWasDeleted = useCallback( ( ) => { - if ( localObservation ) { - markDeletedLocally( ); - } - if ( navigation.canGoBack( ) ) navigation.goBack( ); - }, [ + subscriptionResults, + wasSynced, + openAddCommentSheet, + hideAddCommentSheet, + openAgreeWithIdSheet, + closeAgreeWithIdSheet, + navToSuggestions, + invalidateQueryAndRefetch, + handleIdentificationMutationSuccess, + handleCommentMutationSuccess, + confirmRemoteObsWasDeleted, + loadActivityItem, + refetchSubscriptions, + } = useObsDetailsSharedLogic( { + observation, + uuid, localObservation, + remoteObservation, + markViewedLocally, markDeletedLocally, - navigation, - ] ); - - const wasSynced = !!( localObservation && localObservation?.wasSynced() ); - - const hasPhotos = observation?.observationPhotos?.length > 0; - - const { data: subscriptions, refetch: refetchSubscriptions } = useAuthenticatedQuery( - [ - "fetchSubscriptions", - ], - optsWithAuth => fetchSubscriptions( { uuid, fields: "user_id" }, optsWithAuth ), - { - enabled: !!( currentUser ) && !belongsToCurrentUser, - }, - ); - - const invalidateRemoteObservationFetch = useCallback( ( ) => { - if ( observation?.uuid ) { - queryClient.invalidateQueries( { - queryKey: [fetchRemoteObservationKey, observation.uuid], - } ); - } - }, [queryClient, observation?.uuid] ); - - useFocusEffect( - // this ensures activity items load after a user taps suggest id - // and adds a remote id on the Suggestions screen - useCallback( ( ) => { - invalidateRemoteObservationFetch( ); - }, [invalidateRemoteObservationFetch] ), - ); - - useEffect( ( ) => { - if ( !observationShown ) { - dispatch( { - type: SET_INITIAL_OBSERVATION, - observationShown: observation, - } ); - } - }, [observation, observationShown] ); - - useEffect( ( ) => { - // if observation does not belong to current user, show - // new activity items after a refetch - if ( remoteObservation && !isRefetching ) { - dispatch( { - type: ADD_ACTIVITY_ITEM, - observationShown: Observation.mapApiToRealm( remoteObservation ), - } ); - } - }, [remoteObservation, isRefetching] ); - - const { refetch: refetchObservationUpdates } = useObservationsUpdates( - !!currentUser && !!observation, - ); - - const openAddCommentSheet = useCallback( ( { isOptional = false } ) => { - dispatch( { - type: SET_ADD_COMMENT_SHEET, - showAddCommentSheet: true, - commentIsOptional: isOptional || false, - } ); - }, [] ); - - const hideAddCommentSheet = useCallback( ( ) => dispatch( { - type: SET_ADD_COMMENT_SHEET, - showAddCommentSheet: false, - comment: null, - } ), [] ); - - const openAgreeWithIdSheet = useCallback( taxon => { - dispatch( { - type: SHOW_AGREE_SHEET, - agreeIdentification: { taxon }, - } ); - }, [] ); - - const navToSuggestions = useCallback( ( ) => { - setObservations( [observation] ); - if ( hasPhotos ) { - navigation.push( "Suggestions", { - entryScreen: "ObsDetails", - lastScreen: "ObsDetails", - hideSkip: true, - } ); - } else { - // Go directly to taxon search in case there are no photos - navigation.navigate( "SuggestionsTaxonSearch", { lastScreen: "ObsDetails" } ); - } - }, [hasPhotos, navigation, observation, setObservations] ); - - const invalidateQueryAndRefetch = useCallback( ( ) => { - invalidateRemoteObservationFetch( ); - refetchRemoteObservation( ); - refetchObservationUpdates( ); - }, [invalidateRemoteObservationFetch, refetchObservationUpdates, refetchRemoteObservation] ); - - const subscriptionResults = !belongsToCurrentUser - ? subscriptions?.results - : []; - - const handleIdentificationMutationSuccess = useCallback( data => { - refetchRemoteObservation( ); - if ( belongsToCurrentUser ) { - const createdIdent = data[0]; - // Try to find an existing taxon b/c otherwise realm will try to - // create the taxon when updating the observation and error out - let taxon; - if ( createdIdent.taxon?.id ) { - taxon = realm?.objectForPrimaryKey( "Taxon", createdIdent.taxon.id ); - } - taxon = taxon || createdIdent.taxon; - safeRealmWrite( realm, ( ) => { - createdIdent.user = currentUser; - if ( taxon ) createdIdent.taxon = taxon; - localObservation?.identifications?.push( createdIdent ); - }, "setting local identification in ObsDetailsContainer" ); - if ( uuid ) { - const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); - dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); - } - } - }, [ - belongsToCurrentUser, + setRemoteObsWasDeleted, + fetchRemoteObservationError, currentUser, - localObservation?.identifications, - realm, - refetchRemoteObservation, - uuid, - ] ); - - const handleCommentMutationSuccess = useCallback( data => { - refetchRemoteObservation( ); - if ( belongsToCurrentUser ) { - safeRealmWrite( realm, ( ) => { - const localComments = localObservation?.comments; - const newComment = data[0]; - newComment.user = currentUser; - localComments?.push( newComment ); - }, "setting local comment in ObsDetailsContainer" ); - const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); - dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); - } - }, [ belongsToCurrentUser, - currentUser, - localObservation?.comments, - realm, + isRefetching, refetchRemoteObservation, - uuid, - ] ); - - const closeAgreeWithIdSheet = useCallback( ( ) => { - dispatch( { type: HIDE_AGREE_SHEET } ); - }, [] ); - - const loadActivityItem = useCallback( ( ) => { - dispatch( { type: LOADING_ACTIVITY_ITEM } ); - }, [] ); + } ); return observationShown && ( <> diff --git a/src/components/ObsDetailsSharedComponents/hooks/useObsDetailsSharedLogic.ts b/src/components/ObsDetailsSharedComponents/hooks/useObsDetailsSharedLogic.ts new file mode 100644 index 000000000..5edb139fe --- /dev/null +++ b/src/components/ObsDetailsSharedComponents/hooks/useObsDetailsSharedLogic.ts @@ -0,0 +1,398 @@ +import type { NavigationProp, ParamListBase } from "@react-navigation/native"; +import { useFocusEffect, useNavigation } from "@react-navigation/native"; +import { useQueryClient } from "@tanstack/react-query"; +import { fetchSubscriptions } from "api/observations"; +import type { ApiComment, ApiIdentification, ApiObservation } from "api/types"; +import { RealmContext } from "providers/contexts"; +import { + useCallback, + useEffect, + useReducer, +} from "react"; +import Observation from "realmModels/Observation"; +import type { RealmObservation, RealmTaxon, RealmUser } from "realmModels/types"; +import { log } from "sharedHelpers/logger"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import { + useAuthenticatedQuery, + useObservationsUpdates, +} from "sharedHooks"; +import { + fetchRemoteObservationKey, +} from "sharedHooks/useRemoteObservation"; +import useStore from "stores/useStore"; + +const { useRealm } = RealmContext; + +interface ActivityItem { + created_at: string; +} + +const sortItems = ( + ids: ActivityItem[], + comments: ActivityItem[], +): ActivityItem[] => ids.concat( [...comments] ).sort( + ( a, b ) => ( new Date( a.created_at ) - new Date( b.created_at ) ), +); + +const SHOW_AGREE_SHEET = "SHOW_AGREE_SHEET"; +const HIDE_AGREE_SHEET = "HIDE_AGREE_SHEET"; +const SET_ADD_COMMENT_SHEET = "SET_ADD_COMMENT_SHEET"; +const SET_INITIAL_OBSERVATION = "SET_INITIAL_OBSERVATION"; +const ADD_ACTIVITY_ITEM = "ADD_ACTIVITY_ITEM"; +const LOADING_ACTIVITY_ITEM = "LOADING_ACTIVITY_ITEM"; + +interface AgreeIdentification { + taxon: RealmTaxon; +} + +interface State { + activityItems: ActivityItem[]; + addingActivityItem: boolean; + agreeIdentification: AgreeIdentification | null; + observationShown: ApiObservation | null; + showAddCommentSheet: boolean; + showAgreeWithIdSheet: boolean; +} + +type Action = + | { type: typeof SET_INITIAL_OBSERVATION; observationShown: ApiObservation } + | { type: typeof ADD_ACTIVITY_ITEM; observationShown: ApiObservation } + | { type: typeof LOADING_ACTIVITY_ITEM } + | { type: typeof SHOW_AGREE_SHEET; agreeIdentification: AgreeIdentification } + | { type: typeof HIDE_AGREE_SHEET } + | { type: typeof SET_ADD_COMMENT_SHEET; showAddCommentSheet: boolean }; + +const initialState: State = { + activityItems: [], + addingActivityItem: false, + agreeIdentification: null, + observationShown: null, + showAddCommentSheet: false, + showAgreeWithIdSheet: false, +}; + +const logger = log.extend( "useObsDetailsSharedLogic" ); + +const reducer = ( state: State, action: Action ): State => { + switch ( action.type ) { + case SET_INITIAL_OBSERVATION: + return { + ...state, + observationShown: action.observationShown, + activityItems: sortItems( + action.observationShown?.identifications || [], + action.observationShown?.comments || [], + ), + }; + case ADD_ACTIVITY_ITEM: + return { + ...state, + observationShown: action.observationShown, + addingActivityItem: false, + activityItems: sortItems( + action.observationShown?.identifications || [], + action.observationShown?.comments || [], + ), + }; + case LOADING_ACTIVITY_ITEM: + return { + ...state, + addingActivityItem: true, + }; + case SHOW_AGREE_SHEET: + return { + ...state, + showAgreeWithIdSheet: true, + agreeIdentification: action.agreeIdentification, + }; + case HIDE_AGREE_SHEET: + return { + ...state, + showAgreeWithIdSheet: false, + agreeIdentification: null, + }; + case SET_ADD_COMMENT_SHEET: + return { + ...state, + showAddCommentSheet: action.showAddCommentSheet, + }; + default: + logger.error( "Unknown action in useObsDetailsSharedLogic reducer: ", action ); + return state; + } +}; + +interface UseObsDetailsSharedLogicParams { + observation: RealmObservation; + uuid: string; + localObservation: RealmObservation | null; + remoteObservation: RealmObservation | null; + markViewedLocally: ( ) => void; + markDeletedLocally: ( ) => void; + setRemoteObsWasDeleted: ( deleted: boolean ) => void; + fetchRemoteObservationError: { status?: number } | null; + currentUser: RealmUser | null; + belongsToCurrentUser: boolean; + isRefetching: boolean; + refetchRemoteObservation: ( ) => void; +} + +interface UseObsDetailsSharedLogicReturn { + // State + activityItems: ActivityItem[]; + addingActivityItem: boolean; + agreeIdentification: AgreeIdentification | null; + observationShown: RealmObservation | null; + showAddCommentSheet: boolean; + showAgreeWithIdSheet: boolean; + + // Computed + subscriptionResults: unknown[]; + wasSynced: boolean; + + // Callbacks + openAddCommentSheet: () => void; + hideAddCommentSheet: () => void; + openAgreeWithIdSheet: ( taxon: RealmTaxon ) => void; + closeAgreeWithIdSheet: () => void; + navToSuggestions: () => void; + invalidateQueryAndRefetch: () => void; + handleIdentificationMutationSuccess: ( data: ApiIdentification[] ) => void; + handleCommentMutationSuccess: ( data: ApiComment[] ) => void; + confirmRemoteObsWasDeleted: () => void; + loadActivityItem: () => void; + refetchSubscriptions: () => void; +} + +const useObsDetailsSharedLogic = ( { + observation, + uuid, + localObservation, + markDeletedLocally, + remoteObservation, + setRemoteObsWasDeleted, + fetchRemoteObservationError, + currentUser, + belongsToCurrentUser, + isRefetching, + refetchRemoteObservation, +}: UseObsDetailsSharedLogicParams ): UseObsDetailsSharedLogicReturn => { + const setObservations = useStore( state => state.setObservations ); + const navigation = useNavigation>( ); + const realm = useRealm( ); + const [state, dispatch] = useReducer( reducer, initialState ); + const queryClient = useQueryClient( ); + + const { + activityItems, + addingActivityItem, + agreeIdentification, + observationShown, + showAddCommentSheet, + showAgreeWithIdSheet, + } = state; + + // If we tried to get a remote observation but it no longer exists, the user + // can't do anything so we need to send them back and remove the local + // copy of this observation + useEffect( ( ) => { + setRemoteObsWasDeleted( fetchRemoteObservationError?.status === 404 ); + }, [fetchRemoteObservationError?.status, setRemoteObsWasDeleted] ); + + const confirmRemoteObsWasDeleted = useCallback( ( ) => { + if ( localObservation ) { + markDeletedLocally( ); + } + if ( navigation.canGoBack( ) ) navigation.goBack( ); + }, [ + localObservation, + markDeletedLocally, + navigation, + ] ); + + const wasSynced = !!( localObservation && localObservation?.wasSynced() ); + + const hasPhotos = observation?.observationPhotos?.length > 0; + + const { data: subscriptions, refetch: refetchSubscriptions } = useAuthenticatedQuery( + [ + "fetchSubscriptions", + ], + optsWithAuth => fetchSubscriptions( { uuid, fields: "user_id" }, optsWithAuth ), + { + enabled: !!( currentUser ) && !belongsToCurrentUser, + }, + ); + + const invalidateRemoteObservationFetch = useCallback( ( ) => { + if ( observation?.uuid ) { + queryClient.invalidateQueries( { + queryKey: [fetchRemoteObservationKey, observation.uuid], + } ); + } + }, [queryClient, observation?.uuid] ); + + useFocusEffect( + // this ensures activity items load after a user taps suggest id + // and adds a remote id on the Suggestions screen + useCallback( ( ) => { + invalidateRemoteObservationFetch( ); + }, [invalidateRemoteObservationFetch] ), + ); + + useEffect( ( ) => { + if ( !observationShown ) { + dispatch( { + type: SET_INITIAL_OBSERVATION, + observationShown: observation, + } ); + } + }, [observation, observationShown] ); + + useEffect( ( ) => { + // if observation does not belong to current user, show + // new activity items after a refetch + if ( remoteObservation && !isRefetching ) { + dispatch( { + type: ADD_ACTIVITY_ITEM, + observationShown: Observation.mapApiToRealm( remoteObservation ), + } ); + } + }, [remoteObservation, isRefetching] ); + + const { refetch: refetchObservationUpdates } = useObservationsUpdates( + !!currentUser && !!observation, + ); + + const openAddCommentSheet = useCallback( ( ) => { + dispatch( { + type: SET_ADD_COMMENT_SHEET, + showAddCommentSheet: true, + } ); + }, [] ); + + const hideAddCommentSheet = useCallback( ( ) => dispatch( { + type: SET_ADD_COMMENT_SHEET, + showAddCommentSheet: false, + } ), [] ); + + const openAgreeWithIdSheet = useCallback( ( taxon: RealmTaxon ) => { + dispatch( { + type: SHOW_AGREE_SHEET, + agreeIdentification: { taxon }, + } ); + }, [] ); + + const navToSuggestions = useCallback( ( ) => { + setObservations( [observation] ); + if ( hasPhotos ) { + navigation.push( "Suggestions", { + entryScreen: "ObsDetails", + lastScreen: "ObsDetails", + hideSkip: true, + } ); + } else { + // Go directly to taxon search in case there are no photos + navigation.navigate( "SuggestionsTaxonSearch", { lastScreen: "ObsDetails" } ); + } + }, [hasPhotos, navigation, observation, setObservations] ); + + const invalidateQueryAndRefetch = useCallback( ( ) => { + invalidateRemoteObservationFetch( ); + refetchRemoteObservation( ); + refetchObservationUpdates( ); + }, [invalidateRemoteObservationFetch, refetchObservationUpdates, refetchRemoteObservation] ); + + const subscriptionResults = !belongsToCurrentUser + ? subscriptions?.results + : []; + + const handleIdentificationMutationSuccess = useCallback( ( data: ApiIdentification[] ) => { + refetchRemoteObservation( ); + if ( belongsToCurrentUser ) { + const createdIdent = data[0]; + // Try to find an existing taxon b/c otherwise realm will try to + // create the taxon when updating the observation and error out + let taxon; + if ( createdIdent.taxon?.id ) { + taxon = realm?.objectForPrimaryKey( "Taxon", createdIdent.taxon.id ); + } + taxon = taxon || createdIdent.taxon; + safeRealmWrite( realm, ( ) => { + createdIdent.user = currentUser; + if ( taxon ) createdIdent.taxon = taxon; + localObservation?.identifications?.push( createdIdent ); + }, "setting local identification in ObsDetailsContainer" ); + if ( uuid ) { + const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); + dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); + } + } + }, [ + belongsToCurrentUser, + currentUser, + localObservation?.identifications, + realm, + refetchRemoteObservation, + uuid, + ] ); + + const handleCommentMutationSuccess = useCallback( ( data: ApiComment[] ) => { + refetchRemoteObservation( ); + if ( belongsToCurrentUser ) { + safeRealmWrite( realm, ( ) => { + const localComments = localObservation?.comments; + const newComment = data[0]; + newComment.user = currentUser; + localComments?.push( newComment ); + }, "setting local comment in ObsDetailsContainer" ); + const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid ); + dispatch( { type: ADD_ACTIVITY_ITEM, observationShown: updatedLocalObservation } ); + } + }, [ + belongsToCurrentUser, + currentUser, + localObservation?.comments, + realm, + refetchRemoteObservation, + uuid, + ] ); + + const closeAgreeWithIdSheet = useCallback( ( ) => { + dispatch( { type: HIDE_AGREE_SHEET } ); + }, [] ); + + const loadActivityItem = useCallback( ( ) => { + dispatch( { type: LOADING_ACTIVITY_ITEM } ); + }, [] ); + + return { + // State + activityItems, + addingActivityItem, + agreeIdentification, + observationShown, + showAddCommentSheet, + showAgreeWithIdSheet, + + // Computed + subscriptionResults, + wasSynced, + + // Callbacks + openAddCommentSheet, + hideAddCommentSheet, + openAgreeWithIdSheet, + closeAgreeWithIdSheet, + navToSuggestions, + invalidateQueryAndRefetch, + handleIdentificationMutationSuccess, + handleCommentMutationSuccess, + confirmRemoteObsWasDeleted, + loadActivityItem, + refetchSubscriptions, + }; +}; + +export default useObsDetailsSharedLogic; diff --git a/src/components/SharedComponents/Sheets/BottomSheet.tsx b/src/components/SharedComponents/Sheets/BottomSheet.tsx index 64ca41872..b3f3b9696 100644 --- a/src/components/SharedComponents/Sheets/BottomSheet.tsx +++ b/src/components/SharedComponents/Sheets/BottomSheet.tsx @@ -99,6 +99,7 @@ const StandardBottomSheet = ( { handleSnapPress( ); }, [hidden, handleSnapPress] ); + // To me, this implies this is a good candidate for splitting into 2 components const BottomSheetComponent = insideModal ? BottomSheet : BottomSheetModal; diff --git a/src/components/SharedComponents/Sheets/BottomSheetV2.tsx b/src/components/SharedComponents/Sheets/BottomSheetV2.tsx index 3d5099c20..02d4dc3b5 100644 --- a/src/components/SharedComponents/Sheets/BottomSheetV2.tsx +++ b/src/components/SharedComponents/Sheets/BottomSheetV2.tsx @@ -11,6 +11,13 @@ import { Dimensions, StyleSheet } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "sharedHooks"; +// This component is an iteration on BottomSheet. +// The main motivation was to avoid messing with the inconsistent use of the +// onPressClose prop, whis gets passed to onDismiss and called differently depending +// on whether the X button or backdrop is pressed. + +// To start, it contains just what's necessary for the usage in SuggestIDSheet. + const { width } = Dimensions.get( "window" ); const styles = StyleSheet.create( {