From 5bfa7940e1281ea01e4be3afdcf5a04e49e80e01 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:09:46 -0700 Subject: [PATCH] Refactor ObsDetails and show ID after agree button pressed (#738) --- src/components/ObsDetails/ActivityItem.js | 128 ---- src/components/ObsDetails/ActivityTab.js | 131 ----- .../{ => ActivityTab}/ActivityHeader.js | 116 ++-- .../ObsDetails/ActivityTab/ActivityItem.js | 82 +++ .../ObsDetails/ActivityTab/ActivityTab.js | 55 ++ .../ObsDetails/ActivityTab/FloatingButtons.js | 60 ++ src/components/ObsDetails/AddCommentModal.js | 165 ------ .../ObsDetails/DataQualityAssessment.js | 2 +- .../{ => DetailsTab}/Attribution.js | 0 .../{ => DetailsTab}/DQAVoteButtons.js | 0 .../ObsDetails/{ => DetailsTab}/DetailsTab.js | 2 +- src/components/ObsDetails/ObsDetails.js | 548 +++--------------- .../ObsDetails/ObsDetailsContainer.js | 338 +++++++++++ src/components/ObsDetails/PhotoDisplay.js | 140 +++++ .../ObsDetails/PhotoDisplayContainer.js | 90 +++ .../ObsDetails/Sheets/AgreeWithIDSheet.js | 66 +-- .../SharedComponents/DisplayTaxon.js | 4 +- src/components/UiLibrary.js | 2 +- src/navigation/BottomTabNavigator/index.js | 4 +- src/sharedHooks/useLocalObservation.js | 13 +- tests/integration/ObsDetails.test.js | 4 +- .../ObsDetails/ActivityItem.test.js | 5 +- .../components/ObsDetails/ActivityTab.test.js | 10 +- .../ObsDetails/DataQualityAssessment.test.js | 4 +- .../components/ObsDetails/DetailsTab.test.js | 15 +- .../unit/components/ObsDetails/Flags.test.js | 2 +- .../components/ObsDetails/ObsDetails.test.js | 19 +- 27 files changed, 958 insertions(+), 1047 deletions(-) delete mode 100644 src/components/ObsDetails/ActivityItem.js delete mode 100644 src/components/ObsDetails/ActivityTab.js rename src/components/ObsDetails/{ => ActivityTab}/ActivityHeader.js (67%) create mode 100644 src/components/ObsDetails/ActivityTab/ActivityItem.js create mode 100644 src/components/ObsDetails/ActivityTab/ActivityTab.js create mode 100644 src/components/ObsDetails/ActivityTab/FloatingButtons.js delete mode 100644 src/components/ObsDetails/AddCommentModal.js rename src/components/ObsDetails/{ => DetailsTab}/Attribution.js (100%) rename src/components/ObsDetails/{ => DetailsTab}/DQAVoteButtons.js (100%) rename src/components/ObsDetails/{ => DetailsTab}/DetailsTab.js (98%) create mode 100644 src/components/ObsDetails/ObsDetailsContainer.js create mode 100644 src/components/ObsDetails/PhotoDisplay.js create mode 100644 src/components/ObsDetails/PhotoDisplayContainer.js diff --git a/src/components/ObsDetails/ActivityItem.js b/src/components/ObsDetails/ActivityItem.js deleted file mode 100644 index 2c61fd3f6..000000000 --- a/src/components/ObsDetails/ActivityItem.js +++ /dev/null @@ -1,128 +0,0 @@ -// @flow - -import ActivityHeader from "components/ObsDetails/ActivityHeader"; -import AgreeWithIDSheet from "components/ObsDetails/Sheets/AgreeWithIDSheet"; -import { - Divider, INatIcon, UserText -} from "components/SharedComponents"; -import DisplayTaxon from "components/SharedComponents/DisplayTaxon"; -import { - Pressable, View -} from "components/styledComponents"; -import { t } from "i18next"; -import _ from "lodash"; -import type { Node } from "react"; -import React, { useState } from "react"; -import { textStyles } from "styles/obsDetails/obsDetails"; - -import AddCommentModal from "./AddCommentModal"; - -type Props = { - item: Object, - navToTaxonDetails: Function, - toggleRefetch: Function, - refetchRemoteObservation: Function, - onAgree: Function, - currentUserId?: Number, - observationUUID: string, - userAgreedId?: string -} - -const ActivityItem = ( { - item, navToTaxonDetails, toggleRefetch, refetchRemoteObservation, onAgree, currentUserId, - observationUUID, userAgreedId -}: Props ): Node => { - const { taxon, user } = item; - const userId = currentUserId; - const showAgreeButton = taxon && user && user.id !== userId && taxon.rank_level <= 10 - && userAgreedId !== taxon?.id; - const [showAgreeWithIdSheet, setShowAgreeWithIdSheet] = useState( false ); - const [showCommentBox, setShowCommentBox] = useState( false ); - const [comment, setComment] = useState( "" ); - - const isCurrent = item.current !== undefined - ? item.current - : undefined; - - const idWithdrawn = isCurrent !== undefined && !isCurrent; - - const onAgreePressed = () => { - const agreeParams = { - observation_id: observationUUID, - taxon_id: taxon?.id, - body: comment - }; - - onAgree( agreeParams ); - setShowAgreeWithIdSheet( false ); - }; - - const openCommentBox = () => setShowCommentBox( true ); - - const onCommentAdded = newComment => { - setComment( newComment ); - }; - - const agreeIdSheetDiscardChanges = () => { - setComment( "" ); - setShowAgreeWithIdSheet( false ); - }; - - const onIDAgreePressed = () => { - setShowAgreeWithIdSheet( true ); - }; - - return ( - - - {taxon && ( - - - { showAgreeButton && ( - - - - )} - - )} - { !_.isEmpty( item?.body ) && ( - - - - )} - - - - - ); -}; - -export default ActivityItem; diff --git a/src/components/ObsDetails/ActivityTab.js b/src/components/ObsDetails/ActivityTab.js deleted file mode 100644 index b9ee78cfb..000000000 --- a/src/components/ObsDetails/ActivityTab.js +++ /dev/null @@ -1,131 +0,0 @@ -// @flow -import createIdentification from "api/identifications"; -import { Text, View } from "components/styledComponents"; -import { formatISO } from "date-fns"; -import { t } from "i18next"; -import * as React from "react"; -import { useEffect, useState } from "react"; -import { Alert } from "react-native"; -import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation"; -import useCurrentUser from "sharedHooks/useCurrentUser"; - -import ActivityItem from "./ActivityItem"; - -type Props = { - observation:Object, - comments:Array, - navToTaxonDetails: Function, - toggleRefetch: Function, - refetchRemoteObservation: Function, - uuid: string -} - -const ActivityTab = ( { - observation, comments, navToTaxonDetails, - toggleRefetch, refetchRemoteObservation, uuid -}: Props ): React.Node => { - const currentUser = useCurrentUser( ); - const userId = currentUser?.id; - const [ids, setIds] = useState>( [] ); - - const onAgreeErrorAlert = ( ) => { - Alert.alert( - "Cannot agree with ID at this time" - ); - }; - - const onIDAdded = async identification => { - // Add temporary ID to observation.identifications ("ghosted" ID, while we're trying to add it) - const newId = { - body: identification.body, - taxon: identification.taxon, - user: { - id: userId, - login: currentUser?.login, - signedIn: true, - icon_url: currentUser?.icon_url - }, - created_at: formatISO( Date.now() ), - uuid: identification.uuid, - vision: false, - // This tells us to render is ghosted (since it's temporarily visible - // until getting a response from the server) - temporary: true - }; - setIds( [newId, ...ids] ); - }; - - const createIdentificationMutation = useAuthenticatedMutation( - ( params, optsWithAuth ) => createIdentification( params, optsWithAuth ), - { - onSuccess: data => { - // TODO - // reload activity list to update suggest id icon - toggleRefetch(); - onIDAdded( data[0] ); - if ( refetchRemoteObservation ) { - refetchRemoteObservation( ); - } - }, - onError: () => { - onAgreeErrorAlert(); - } - } - ); - - const onAgree = agreeParams => { - createIdentificationMutation.mutate( { identification: agreeParams } ); - }; - - // finds the user's most recent id - const findRecentUserAgreedToID = () => { - const currentIds = observation?.identifications; - const userAgree = currentIds.filter( id => id.user?.id === userId ); - return userAgree.length > 0 - ? userAgree[userAgree.length - 1].taxon.id - : undefined; - }; - - const userAgreedToId = findRecentUserAgreedToID(); - - useEffect( ( ) => { - // set initial ids for activity tab - const currentIds = observation?.identifications; - if ( currentIds - && ids.length === 0 - && currentIds.length !== ids.length ) { - setIds( currentIds ); - } - }, [observation, ids] ); - - if ( comments.length === 0 && ids.length === 0 ) { - return {t( "No-comments-or-ids-to-display" )}; - } - - const activityItems = ids.concat( [...comments] ) - .sort( ( a, b ) => ( new Date( a.created_at ) - new Date( b.created_at ) ) ); - - // this should all perform similarly to the activity tab on web - // https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/components/activity_item.jsx - const activitytemsList = activityItems.map( item => ( - - ) ); - - return ( - - {activitytemsList} - - ); -}; - -export default ActivityTab; diff --git a/src/components/ObsDetails/ActivityHeader.js b/src/components/ObsDetails/ActivityTab/ActivityHeader.js similarity index 67% rename from src/components/ObsDetails/ActivityHeader.js rename to src/components/ObsDetails/ActivityTab/ActivityHeader.js index 092a0b9f9..41f66927c 100644 --- a/src/components/ObsDetails/ActivityHeader.js +++ b/src/components/ObsDetails/ActivityTab/ActivityHeader.js @@ -6,7 +6,6 @@ import classnames from "classnames"; import { isCurrentUser } from "components/LoginSignUp/AuthenticationService"; import FlagItemModal from "components/ObsDetails/FlagItemModal"; import { Body4, INatIcon, InlineUser } from "components/SharedComponents"; -import DateDisplay from "components/SharedComponents/DateDisplay"; import KebabMenu from "components/SharedComponents/KebabMenu"; import { View @@ -26,12 +25,13 @@ const { useRealm } = RealmContext; type Props = { item: Object, refetchRemoteObservation?: Function, - toggleRefetch?: Function, - classNameMargin?: string + classNameMargin?: string, + idWithdrawn: boolean } const ActivityHeader = ( { - item, refetchRemoteObservation, toggleRefetch, classNameMargin + item, refetchRemoteObservation, classNameMargin, + idWithdrawn }:Props ): Node => { const [currentUser, setCurrentUser] = useState( null ); const [kebabMenuVisible, setKebabMenuVisible] = useState( false ); @@ -40,11 +40,6 @@ const ActivityHeader = ( { const realm = useRealm( ); const queryClient = useQueryClient( ); const { user } = item; - const isCurrent = item.current !== undefined - ? item.current - : undefined; - - const idWithdrawn = isCurrent !== undefined && !isCurrent; const itemType = item.category ? "Identification" @@ -121,59 +116,58 @@ const ActivityHeader = ( { ); }; - const ifCommentOrID = () => ( - - {renderIcon()} - { - renderStatus() - } - {item.created_at + return ( + + + + {renderIcon()} + { + renderStatus() + } + {item.created_at && ( {formatIdDate( item.updated_at || item.created_at, t )} )} - { - item.body && currentUser - ? ( - - { + { + item.body && currentUser + ? ( + + { // first delete locally - Comment.deleteComment( item.uuid, realm ); - // then delete remotely - deleteCommentMutation.mutate( item.uuid ); - if ( toggleRefetch ) { - toggleRefetch( ); - } - setKebabMenuVisible( false ); - }} - title={t( "Delete-comment" )} - /> - - ) - : ( - - {!currentUser - ? ( - setFlagModalVisible( true )} - title={t( "Flag" )} - testID="MenuItem.Flag" - /> - ) - : undefined} - - - ) - } - {!currentUser + Comment.deleteComment( item.uuid, realm ); + // then delete remotely + deleteCommentMutation.mutate( item.uuid ); + setKebabMenuVisible( false ); + }} + title={t( "Delete-comment" )} + /> + + ) + : ( + + {!currentUser + ? ( + setFlagModalVisible( true )} + title={t( "Flag" )} + testID="MenuItem.Flag" + /> + ) + : undefined} + + + ) + } + {!currentUser && ( )} - - ); - - return ( - - - {( item._created_at ) - ? - : ifCommentOrID()} + ); }; diff --git a/src/components/ObsDetails/ActivityTab/ActivityItem.js b/src/components/ObsDetails/ActivityTab/ActivityItem.js new file mode 100644 index 000000000..e659c7365 --- /dev/null +++ b/src/components/ObsDetails/ActivityTab/ActivityItem.js @@ -0,0 +1,82 @@ +// @flow + +import { useNavigation } from "@react-navigation/native"; +import { + Divider, INatIcon, UserText +} from "components/SharedComponents"; +import DisplayTaxon from "components/SharedComponents/DisplayTaxon"; +import { + Pressable, View +} from "components/styledComponents"; +import { t } from "i18next"; +import _ from "lodash"; +import type { Node } from "react"; +import React from "react"; +import { textStyles } from "styles/obsDetails/obsDetails"; + +import ActivityHeader from "./ActivityHeader"; + +type Props = { + item: Object, + refetchRemoteObservation: Function, + currentUserId?: Number, + userAgreedId?: string, + onIDAgreePressed: Function +} + +const ActivityItem = ( { + item, refetchRemoteObservation, currentUserId, + userAgreedId, onIDAgreePressed +}: Props ): Node => { + const navigation = useNavigation( ); + const { taxon, user } = item; + const userId = currentUserId; + const isCurrent = item.current !== undefined + ? item.current + : undefined; + + const idWithdrawn = isCurrent !== undefined && !isCurrent; + + const showAgreeButton = ( user?.id !== userId ) + && taxon?.rank_level <= 10 + && ( userAgreedId !== taxon?.id ); + + const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: taxon.id } ); + + return ( + + + {taxon && ( + + + { showAgreeButton && ( + + + + )} + + )} + { !_.isEmpty( item?.body ) && ( + + + + )} + + + ); +}; + +export default ActivityItem; diff --git a/src/components/ObsDetails/ActivityTab/ActivityTab.js b/src/components/ObsDetails/ActivityTab/ActivityTab.js new file mode 100644 index 000000000..4a9b5ac5b --- /dev/null +++ b/src/components/ObsDetails/ActivityTab/ActivityTab.js @@ -0,0 +1,55 @@ +// @flow +import { Body3 } from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import * as React from "react"; +import { useCurrentUser, useTranslation } from "sharedHooks"; + +import ActivityItem from "./ActivityItem"; + +type Props = { + observation:Object, + refetchRemoteObservation: Function, + activityItems: Array, + onIDAgreePressed: Function +} + +const ActivityTab = ( { + observation, + refetchRemoteObservation, + activityItems, + onIDAgreePressed +}: Props ): React.Node => { + const { t } = useTranslation( ); + const currentUser = useCurrentUser( ); + const userId = currentUser?.id; + + // finds the user's most recent id + const findRecentUserAgreedToID = ( ) => { + const currentIds = observation?.identifications; + const userAgree = currentIds.filter( id => id.user?.id === userId ); + return userAgree.length > 0 && userAgree[userAgree.length - 1].current + ? userAgree[userAgree.length - 1].taxon.id + : undefined; + }; + + const userAgreedToId = findRecentUserAgreedToID( ); + + return ( + + {activityItems.length === 0 + ? {t( "No-comments-or-ids-to-display" )} + : activityItems.map( item => ( + + ) )} + + ); +}; + +export default ActivityTab; diff --git a/src/components/ObsDetails/ActivityTab/FloatingButtons.js b/src/components/ObsDetails/ActivityTab/FloatingButtons.js new file mode 100644 index 000000000..c88f46f9c --- /dev/null +++ b/src/components/ObsDetails/ActivityTab/FloatingButtons.js @@ -0,0 +1,60 @@ +// @flow +import { + Button +} from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import type { Node } from "react"; +import React from "react"; +import { + useTranslation +} from "sharedHooks"; +import { getShadowStyle } from "styles/global"; +import colors from "styles/tailwindColors"; + +type Props = { + navToAddID: Function, + showCommentBox: Function, + openCommentBox: Function +} + +const FloatingButtons = ( { + navToAddID, + openCommentBox, + showCommentBox +}: Props ): Node => { + const { t } = useTranslation( ); + + return ( + +