Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 76 additions & 125 deletions components/Feed/FeedItemActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import { useHideFromFeed } from '@/hooks/useHideFromFeed';
import { ConfirmationModal } from '@/components/ui/form/ConfirmationModal';
import type { FeedEntry } from '@/types/feed';
import { getFlagTarget } from './lib/flagTarget';

// Basic media query hook (can be moved to a utility file later)
const useMediaQuery = (query: string): boolean => {
Expand Down Expand Up @@ -209,6 +210,8 @@
variant?: 'default' | 'inline' | 'card';
/** Leave the ⋯ menu out, for cards that put their own elsewhere. */
hideMoreMenu?: boolean;
/** Leave the copy-link button out, for rows that have no page of their own to link to. */
hideShareButton?: boolean;
}

// Define interface for avatar items used in local state
Expand Down Expand Up @@ -250,6 +253,7 @@
className,
variant = 'default',
hideMoreMenu = false,
hideShareButton = false,
}) => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { user } = useUser();
Expand Down Expand Up @@ -400,32 +404,13 @@
// Close the dropdown menu before opening the modal
setIsMenuOpen(false);

// Map feedContentType to ContentType
let contentType: ContentType;
let commentId: string | undefined;

if (feedContentType === 'PAPER') {
contentType = 'paper';
} else if (feedContentType === 'POST' || feedContentType === 'PREREGISTRATION') {
contentType = 'post';
} else if (feedContentType === 'BOUNTY' && relatedDocumentContentType) {
contentType = relatedDocumentContentType;
commentId = votableEntityId.toString(); // Use votableEntityId as commentId for bounties
} else if (feedContentType === 'COMMENT') {
contentType = relatedDocumentContentType || 'post';
commentId = votableEntityId.toString();
} else {
contentType = 'post'; // Default fallback
}

openFlagModal(
// For comments and bounties, use relatedDocumentId as the documentId
(feedContentType === 'COMMENT' || feedContentType === 'BOUNTY') && relatedDocumentId
? relatedDocumentId.toString()
: votableEntityId.toString(),
contentType,
commentId
);
const { documentId, contentType, commentId } = getFlagTarget({
feedContentType,
votableEntityId,
relatedDocumentId,
relatedDocumentContentType,
});
openFlagModal(documentId, contentType, commentId);
});
};

Expand Down Expand Up @@ -481,7 +466,7 @@
feedContentType !== 'APPLICATION' &&
showPeerReviews;

const showShare = variant !== 'inline';
const showShare = variant !== 'inline' && !hideShareButton;
const showMoreMenu =
!hideMoreMenu &&
(!!(listDetailContext && relatedDocumentUnifiedDocumentId) ||
Expand Down Expand Up @@ -620,6 +605,65 @@
)
) : null;

// Tips and awards: one control in every variant. With something received it reads
// as the amount, with a breakdown on hover; without, it offers "Tip". Read-only
// where tipping isn't offered, as on the author's own comment.
const showTipTooltip = showTooltips && totalAwarded > 0;
const tipIconClass = showTipTooltip || isCard ? 'h-[18px] w-[18px]' : 'h-4 w-4';
const tipContent = (
<>
<Icon name="tipRSC" size={showTipTooltip || isCard ? 18 : 16} className={tipIconClass} />
{totalAwarded > 0 ? (
<span>
{formatCurrency({ amount: totalAwarded, showUSD, exchangeRate, shorten: true })}
</span>
) : (
<span>Tip</span>
)}
</>
);
const tipBaseClass = isCard
? 'flex h-7 items-center gap-1 rounded-full px-2 text-[13px] font-medium text-gray-500'
: 'flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-gray-700';
const tipControl = onTip ? (
<Button
variant="ghost"
size="sm"
className={cn(
isCard ? `${tipBaseClass} transition-colors` : `${tipBaseClass} transition-all`,
isCard ? cardIconButtonClass : 'hover:bg-white hover:text-gray-900 hover:shadow-sm'
)}
tooltip={showTooltips && !showTipTooltip ? 'Tip' : undefined}
onClick={handleTip}
>
{tipContent}
</Button>
) : (
<div className={cn(tipBaseClass)}>{tipContent}</div>
);
const tipAction =
onTip || totalAwarded > 0 ? (
showTipTooltip ? (
<Tooltip
content={
<TipTooltip
tips={tips}
awardedBountyAmount={awardedBountyAmount}
totalAwarded={totalAwarded}
showUSD={showUSD}
exchangeRate={exchangeRate}
/>
}
position="top"
width="w-[320px]"
>
{tipControl}
</Tooltip>
) : (
tipControl
)

Check warning on line 664 in components/Feed/FeedItemActions.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaFoX3viUdnA8_P_hUcu&open=AaFoX3viUdnA8_P_hUcu&pullRequest=1237
) : null;

const moreMenu = showMoreMenu ? (
<BaseMenu
trigger={moreMenuTrigger}
Expand Down Expand Up @@ -671,7 +715,10 @@
<div className={cn('flex items-center justify-between', isFlat ? 'gap-3' : 'gap-2')}>
<div
className={cn(
'flex items-center flex-nowrap overflow-visible',
'flex items-center',
// A card's actions wrap rather than run past a narrow column.
isCard ? 'min-w-0 flex-wrap' : 'flex-nowrap',
'overflow-visible',
isFlat ? 'gap-4' : undefined,
className
)}
Expand Down Expand Up @@ -760,103 +807,7 @@
testId="feed-item-comment-action"
/>
)}
{(onTip || totalAwarded > 0) &&
(showTooltips && totalAwarded > 0 ? (
<Tooltip
content={
<TipTooltip
tips={tips}
awardedBountyAmount={awardedBountyAmount}
totalAwarded={totalAwarded}
showUSD={showUSD}
exchangeRate={exchangeRate}
/>
}
position="top"
width="w-[320px]"
>
{onTip ? (
<Button
variant="ghost"
size="sm"
className={cn(
'flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-gray-700 transition-all',
'hover:bg-white hover:text-gray-900 hover:shadow-sm'
)}
onClick={handleTip}
>
<Icon name="tipRSC" size={18} className="h-[18px] w-[18px]" />
{totalAwarded > 0 ? (
<span>
{formatCurrency({
amount: totalAwarded,
showUSD,
exchangeRate,
shorten: true,
})}
</span>
) : (
<span>Tip</span>
)}
</Button>
) : (
<div
className={cn(
'flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-gray-700'
)}
>
<Icon name="tipRSC" size={18} className="h-[18px] w-[18px]" />
{totalAwarded > 0 ? (
<span>
{formatCurrency({
amount: totalAwarded,
showUSD,
exchangeRate,
shorten: true,
})}
</span>
) : (
<span>Tip</span>
)}
</div>
)}
</Tooltip>
) : onTip ? (
<Button
variant="ghost"
size="sm"
className={cn(
'flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-gray-700 transition-all',
'hover:bg-white hover:text-gray-900 hover:shadow-sm'
)}
tooltip={showTooltips ? 'Tip' : undefined}
onClick={handleTip}
>
<Icon name="tipRSC" size={16} className="h-4 w-4" />
{totalAwarded > 0 ? (
<span>
{formatCurrency({ amount: totalAwarded, showUSD, exchangeRate, shorten: true })}
</span>
) : (
<span>Tip</span>
)}
</Button>
) : (
<div
className={cn(
'flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium text-gray-700'
)}
>
<Icon name="tipRSC" size={16} className="h-4 w-4" />
{totalAwarded > 0 ? (
<span>
{formatCurrency({ amount: totalAwarded, showUSD, exchangeRate, shorten: true })}
</span>
) : (
<span>Tip</span>
)}
</div>
))}
{tipAction}
{onExpand && (
<ActionButton
icon={Maximize2}
Expand Down
54 changes: 54 additions & 0 deletions components/Feed/lib/flagTarget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { FeedContentType } from '@/types/feed';
import type { ContentType } from '@/types/work';

/** What a flag is filed against: the document, and the comment when it is one. */
export interface FlagTarget {
documentId: string;
contentType: ContentType;
commentId?: string;
}

interface FlagTargetInput {
feedContentType: FeedContentType;
/** The item's own id: the document's, or the comment's for comments and bounties. */
votableEntityId: number;
relatedDocumentId?: string;
relatedDocumentContentType?: ContentType;
}

/**
* Comments and bounties are flagged as a comment on the document they belong to;
* papers as papers; everything else as a post.
*/
export function getFlagTarget({
feedContentType,
votableEntityId,
relatedDocumentId,
relatedDocumentContentType,
}: FlagTargetInput): FlagTarget {
let contentType: ContentType;
let commentId: string | undefined;

if (feedContentType === 'PAPER') {
contentType = 'paper';
} else if (feedContentType === 'POST' || feedContentType === 'PREREGISTRATION') {
contentType = 'post';
} else if (feedContentType === 'BOUNTY' && relatedDocumentContentType) {
contentType = relatedDocumentContentType;
commentId = votableEntityId.toString();
} else if (feedContentType === 'COMMENT') {
contentType = relatedDocumentContentType || 'post';
commentId = votableEntityId.toString();
} else {
contentType = 'post';
}

const isOnDocument = feedContentType === 'COMMENT' || feedContentType === 'BOUNTY';

return {
documentId:
isOnDocument && relatedDocumentId ? relatedDocumentId.toString() : votableEntityId.toString(),
contentType,
commentId,
};
}
Loading