Skip to content

Commit

Permalink
Feat: redesigned ObsDetails screen in debug mode (#2580)
Browse files Browse the repository at this point in the history
* Create ObsDetailsDefaultMode and rearrange items on top of screen

* Move activity, details, and more into three different sections instead of tabs

* Styling cleanup; change Activity name to Community

* Fix scroll to activity item from Notifications

* Add ObsDetailsDefaultMode unit tests

* Show kebab menu on other users' observations
  • Loading branch information
albullington authored Dec 19, 2024
1 parent ce7e91e commit 1d340eb
Show file tree
Hide file tree
Showing 67 changed files with 6,510 additions and 199 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// @flow
import classnames from "classnames";
import ActivityHeaderKebabMenu from "components/ObsDetails/ActivityTab/ActivityHeaderKebabMenu";
import WithdrawIDSheet from "components/ObsDetails/Sheets/WithdrawIDSheet";
import {
ActivityIndicator,
Body4, DateDisplay,
INatIcon, InlineUser, TextInputSheet, WarningSheet
} from "components/SharedComponents";
import {
View
} from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React, { useCallback, useState } from "react";
import colors from "styles/tailwindColors";

type Props = {
classNameMargin?: string,
currentUser: boolean,
deleteComment: Function,
flagged: boolean,
idWithdrawn?: boolean,
isConnected?: boolean,
item: Object,
loading: boolean,
updateCommentBody: Function,
updateIdentification: Function,
geoprivacy: string,
taxonGeoprivacy: string,
belongsToCurrentUser: boolean
}

const ActivityHeader = ( {
classNameMargin,
currentUser,
deleteComment,
flagged,
idWithdrawn,
isConnected,
item,
loading,
updateCommentBody,
updateIdentification,
geoprivacy,
taxonGeoprivacy,
belongsToCurrentUser
}:Props ): Node => {
const [showEditCommentSheet, setShowEditCommentSheet] = useState( false );
const [showDeleteCommentSheet, setShowDeleteCommentSheet] = useState( false );
const [showWithdrawIDSheet, setShowWithdrawIDSheet] = useState( false );
const { category, user, vision } = item;

const itemType = item.category
? "Identification"
: "Comment";

const renderIcon = useCallback( () => {
if ( idWithdrawn ) {
return (
<View className="opacity-50">
<INatIcon name="ban" color={colors.primary} size={22} />
</View>
);
}

if ( vision ) return <INatIcon name="sparkly-label" size={22} />;
if ( flagged ) return <INatIcon name="flag" color={colors.warningYellow} size={22} />;
return null;
}, [
flagged,
idWithdrawn,
vision
] );

const renderStatus = useCallback( () => {
if ( flagged ) {
return (
<Body4 maxFontSizeMultiplier={1}>
{t( "Flagged" )}
</Body4>
);
}
if ( idWithdrawn ) {
return (
<Body4 maxFontSizeMultiplier={1}>
{ t( "ID-Withdrawn" )}
</Body4>
);
}
if ( category ) {
let categoryText;
switch ( category ) {
case "improving":
categoryText = t( "improving--identification" );
break;
case "maverick":
categoryText = t( "maverick--identification" );
break;
case "leading":
categoryText = t( "leading--identification" );
break;
default:
categoryText = t( "supporting--identification" );
}
return (
<Body4 maxFontSizeMultiplier={1}>
{ categoryText }
</Body4>
);
}
return (
<Body4 />
);
}, [
category,
flagged,
idWithdrawn
] );

return (
<View className={classnames( "flex-row justify-between", classNameMargin )}>
<InlineUser user={user} isConnected={isConnected} />
<View className="flex-row items-center space-x-[15px] -mr-[15px]">
{renderIcon()}
{renderStatus()}
{/*
Note that even though the date is conditionally rendered, we need to
wrap it in a View that's always there so the space-x-[] can be
calculated
*/}
<View>
{item.created_at && (
<DateDisplay
dateString={item.updated_at || item.created_at}
geoprivacy={geoprivacy}
taxonGeoprivacy={taxonGeoprivacy}
belongsToCurrentUser={belongsToCurrentUser}
maxFontSizeMultiplier={1}
/>
)}
</View>
{
loading
? (
<View className="mr-[20px]">
<ActivityIndicator size={10} />
</View>
)
: (
<ActivityHeaderKebabMenu
currentUser={currentUser}
itemType={itemType}
current={item.current}
setShowWithdrawIDSheet={setShowWithdrawIDSheet}
updateIdentification={updateIdentification}
setShowEditCommentSheet={setShowEditCommentSheet}
setShowDeleteCommentSheet={setShowDeleteCommentSheet}
/>
)
}
{( currentUser && showWithdrawIDSheet ) && (
<WithdrawIDSheet
onPressClose={() => setShowWithdrawIDSheet( false )}
taxon={item.taxon}
updateIdentification={updateIdentification}
/>
)}

{( currentUser && showEditCommentSheet ) && (
<TextInputSheet
onPressClose={() => setShowEditCommentSheet( false )}
headerText={t( "EDIT-COMMENT" )}
initialInput={item.body}
confirm={textInput => updateCommentBody( textInput )}
/>
)}
{( currentUser && showDeleteCommentSheet ) && (
<WarningSheet
onPressClose={( ) => setShowDeleteCommentSheet( false )}
headerText={t( "DELETE-COMMENT--question" )}
confirm={deleteComment}
buttonText={t( "DELETE" )}
handleSecondButtonPress={( ) => setShowDeleteCommentSheet( false )}
secondButtonText={t( "CANCEL" )}
/>
)}
</View>
</View>
);
};

export default ActivityHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// @flow

import { deleteComments, updateComment } from "api/comments";
import { updateIdentification as apiUpdateIdentification } from "api/identifications";
import { isCurrentUser } from "components/LoginSignUp/AuthenticationService.ts";
import type { Node } from "react";
import React, { useEffect, useState } from "react";
import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation";

import ActivityHeader from "./ActivityHeader";

type Props = {
classNameMargin?: string,
idWithdrawn?: boolean,
isConnected?: boolean,
item: Object,
refetchRemoteObservation?: Function,
geoprivacy: string,
taxonGeoprivacy: string,
belongsToCurrentUser: boolean
}

const ActivityHeaderContainer = ( {
classNameMargin,
idWithdrawn,
isConnected,
item,
refetchRemoteObservation,
geoprivacy,
taxonGeoprivacy,
belongsToCurrentUser
}:Props ): Node => {
const [currentUser, setCurrentUser] = useState( false );
const [loading, setLoading] = useState( false );
const { user } = item;

const flagged = item.flags?.some( flag => !flag.resolved );

useEffect( ( ) => {
const isActiveUserTheCurrentUser = async ( ) => {
const current = await isCurrentUser( user?.login );
setCurrentUser( current );
};
isActiveUserTheCurrentUser( );
}, [user] );

const deleteCommentMutation = useAuthenticatedMutation(
( uuid, optsWithAuth ) => deleteComments( uuid, optsWithAuth ),
{
onSuccess: ( ) => {
setLoading( false );
if ( refetchRemoteObservation ) {
refetchRemoteObservation( );
}
},
onError: () => {
setLoading( false );
}
}
);

const deleteUserComment = () => {
setLoading( true );
deleteCommentMutation.mutate( item.uuid );
};

const updateCommentMutation = useAuthenticatedMutation(
( uuid, optsWithAuth ) => updateComment( uuid, optsWithAuth ),
{
onSuccess: () => {
setLoading( false );
if ( refetchRemoteObservation ) {
refetchRemoteObservation( );
}
},
onError: () => {
setLoading( false );
}
}
);

const updateCommentBody = comment => {
const updateCommentParams = {
id: item.uuid,
comment: {
body: comment
}
};
setLoading( true );
updateCommentMutation.mutate( updateCommentParams );
};

const updateIdentificationMutation = useAuthenticatedMutation(
( uuid, optsWithAuth ) => apiUpdateIdentification( uuid, optsWithAuth ),
{
onSuccess: () => {
setLoading( false );
if ( refetchRemoteObservation ) {
refetchRemoteObservation( );
}
},
onError: () => {
setLoading( false );
}
}
);

const updateIdentification = identification => {
const updateIdentificationParams = {
id: item.uuid,
identification
};
setLoading( true );
updateIdentificationMutation.mutate( updateIdentificationParams );
};

return (
<ActivityHeader
loading={loading}
item={item}
currentUser={currentUser}
idWithdrawn={idWithdrawn}
classNameMargin={classNameMargin}
flagged={flagged}
updateCommentBody={updateCommentBody}
deleteComment={deleteUserComment}
updateIdentification={updateIdentification}
isConnected={isConnected}
geoprivacy={geoprivacy}
taxonGeoprivacy={taxonGeoprivacy}
belongsToCurrentUser={belongsToCurrentUser}
/>
);
};

export default ActivityHeaderContainer;
Loading

0 comments on commit 1d340eb

Please sign in to comment.