import { Link, router, usePage } from '@inertiajs/react'; import { CornerDownRight } from 'lucide-react'; import type { MouseEvent, PointerEvent, ReactNode } from 'react'; import { useCallback, useContext, useRef, useState } from 'react'; import CommitController from '@/actions/App/Http/Controllers/CommitController'; import CommitLinkPreviewController from '@/actions/App/Http/Controllers/CommitLinkPreviewController'; import { CommitAuthorFollowButton } from '@/components/commit-author-follow-button'; import { FoldedCommitBody } from '@/components/commit-body'; import { CommitImages } from '@/components/commit-images'; import { CommitPastelButton } from '@/components/commit-pastel-button'; import { CommitPoll } from '@/components/commit-poll'; import { CommitQuoteButton } from '@/components/commit-quote-button'; import { CommitSecondaryActions } from '@/components/commit-secondary-actions'; import { CommitStarButton } from '@/components/commit-star-button'; import { CommitStatusBadge } from '@/components/commit-status-badge'; import { ShowThreadLink, ThreadActions, ThreadReplies, ThreadReplyComposer, useThread, } from '@/components/commit-thread'; import { useCommitTranslation } from '@/components/commit-translation'; import { GraphDimmed, GraphScope, useGraphNode, } from '@/components/graph-canvas'; import { ChainedEntrance, GraphRail, RailAvatar, RailDot, RailSegment, railDotRing, } from '@/components/graph-rail'; import { HiddenLinkPreviewContext } from '@/components/hidden-link-preview-context'; import { LinkPreview } from '@/components/link-preview'; import { LiveStreamTrigger } from '@/components/live-stream-card'; import { QuotedCommit } from '@/components/quoted-commit'; import { ThreadPositionBadge, type ThreadPosition, } from '@/components/thread-position-badge'; import { TimestampTooltip } from '@/components/timestamp-tooltip'; import { Button } from '@/components/ui/button'; import { UserPreview } from '@/components/user-preview'; import { useCommitNotFound, useIsCommitDeleted, } from '@/hooks/use-deleted-resources'; import { useNow } from '@/hooks/use-now'; import { useOwnsCommit } from '@/hooks/use-owns-commit'; import { clickedCommit } from '@/lib/commit-click'; import { commitMutationOptions } from '@/lib/commit-mutation-options'; import { measureCommitSize } from '@/lib/commit-size'; import { authorLaneColor, cn, formatFullDateTime, formatRelativeTime, isDeleted, isWithinSeconds, shortHash, } from '@/lib/utils'; import { show } from '@/routes/user'; import type { Commit, CursorPaginated, ThreadCommit } from '@/types'; export function CommitCard({ commit, previousLane, isDimmed = false, isPreviousDimmed = false, isHighlighted = false, isGroupedWithPrevious = false, onAuthorHover, onHover, actions, isFocused = false, canDelete = true, isFolded = true, initialReplies, threadActions, showImages = true, loadsReplies = false, endsWithDot = false, railNodePosition = 'top', hasRailTail = true, exceptReplyId, threadPosition, graphAfter, }: { commit: Commit; previousLane?: string; /** In a graph canvas, the node the commit follows on the rail. */ graphAfter?: string; isDimmed?: boolean; isPreviousDimmed?: boolean; isHighlighted?: boolean; isGroupedWithPrevious?: boolean; onAuthorHover?: (isHovering: boolean) => void; onHover?: (isHovering: boolean, event: PointerEvent) => void; actions?: ReactNode; isFocused?: boolean; canDelete?: boolean; isFolded?: boolean; initialReplies?: CursorPaginated; threadActions?: ReactNode; showImages?: boolean; loadsReplies?: boolean; endsWithDot?: boolean; railNodePosition?: 'top' | 'center' | 'bottom' | 'author'; hasRailTail?: boolean; exceptReplyId?: string; threadPosition?: ThreadPosition; }) { const { props: { auth }, component, } = usePage(); const now = useNow(); const hiddenLinkPreviewUrl = useContext(HiddenLinkPreviewContext); const { bodyRef, ...translation } = useCommitTranslation(commit); const nodeRef = useRef(null); const graphNodeRef = useGraphNode({ id: commit.id, lane: authorLaneColor(commit.author), after: graphAfter, dimmed: isDimmed, ring: isGroupedWithPrevious ? railDotRing : 0, }); // A stable ref, so the node stays registered across renders, like hover dimming its lane. const nodeLinkRef = useCallback( (element: HTMLAnchorElement | null) => { nodeRef.current = element; graphNodeRef(element); }, [graphNodeRef], ); const deleteButtonRef = useRef(null); const thread = useThread( commit, initialReplies, loadsReplies, exceptReplyId, ); const wasDeleted = useIsCommitDeleted(commit.id); const commitNotFound = useCommitNotFound(); const [deletion, setDeletion] = useState< 'idle' | 'confirming' | 'deleting' >('idle'); const hasVisibleReplies = thread.isOpen && !!thread.replies?.length; const endsOnDot = hasRailTail && railNodePosition !== 'bottom' && endsWithDot; const graphEndId = `${commit.id}:end`; const isOwn = useOwnsCommit()(commit); const isFresh = isOwn && isWithinSeconds(commit.created_at, 15); const isDeletable = isOwn && canDelete; const hash = shortHash(commit.id); const permalink = CommitController.show({ profile: commit.author, commit }); const openClickedCommit = (event: MouseEvent) => { const click = clickedCommit( event, window.getSelection()?.toString() ?? '', window.location.pathname, ); if (click?.newTab) { window.open(click.url, '_blank', 'noopener'); } else if (click) { router.visit(click.url); } }; const deleteFrom = isDeletable ? (trigger: HTMLButtonElement | null) => { deleteButtonRef.current = trigger; setDeletion('confirming'); } : undefined; const prefetchClickedCommit = (event: MouseEvent) => { const click = clickedCommit(event, '', window.location.pathname); if (click && !click.newTab) { router.prefetch(click.url, { method: 'get' }, {}); } }; const cancelDeletion = () => { setDeletion('idle'); deleteButtonRef.current?.focus(); }; const deleteCommit = () => { setDeletion('deleting'); const prefersReducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)', ).matches; window.setTimeout( () => router .optimistic( (props: { commits?: CursorPaginated; user?: { commits_count?: number }; pinnedCommit?: Commit | null; }) => ({ ...(props.commits?.data ? { commits: { ...props.commits, data: props.commits.data.filter( (c: Commit) => c.id !== commit.id, ), }, } : {}), ...(props.user?.commits_count !== undefined ? { user: { ...props.user, commits_count: Math.max( 0, props.user.commits_count - 1, ), }, } : {}), ...(props.pinnedCommit?.id === commit.id ? { pinnedCommit: null } : {}), }), ) .visit(CommitController.destroy(commit), { ...commitMutationOptions(component), onError: () => setDeletion('idle'), onHttpException: (response) => { if (commitNotFound(commit.id, response)) { setDeletion('idle'); return false; } }, }), prefersReducedMotion ? 0 : 700, ); }; return (
event.pointerType === 'mouse' && onHover(true, event)) } onPointerLeave={onHover && ((event) => onHover(false, event))} > {commit.author.type === 'user' && ( )}
{isGroupedWithPrevious ? ( ) : ( )} } />
{commit.author.name} @{commit.author.handle} · {hash}
{commit.parent && isDeleted(commit.parent) && (

replying to [deleted]

)} {commit.parent && !isDeleted(commit.parent) && ( replying to @{commit.parent.author.handle} )}
{translation.notice} {showImages && } {commit.poll && ( )} {commit.quoted_commit_id !== null && ( )} {commit.link_preview && commit.link_preview.url !== hiddenLinkPreviewUrl && ( )} {isFocused && (

· commit {hash}

)} {!isFocused && } {commit.replies_count === undefined ? (
{translation.button}
) : ( {translation.button} {threadActions} } /> )}
{/* What opens below the actions row gets a row of its own, so the rail's row, and its end dot, end on the actions. */}
{actions} {deletion === 'deleting' && (

rm {hash} → /dev/null

)} {deletion === 'confirming' && (
{ if (event.key === 'Escape') { cancelDeletion(); } }} className="mt-3 flex min-h-12 flex-wrap items-center gap-x-3 gap-y-2 rounded-md bg-destructive/8 px-3 py-2 text-sm" > Delete this commit? You can undo for 30 seconds.
)}
{commit.replies_count !== undefined && ( <>
)}
); } export function CommitTombstone({ id, previousLane, endsWithDot = false, graphAfter, }: { id?: string; previousLane?: string; endsWithDot?: boolean; graphAfter?: string; }) { return (
} />

[deleted] This commit was deleted.

); }