AT-based link agregator. Mirror of https://github.com/likeandscribe/frontpage

Prevent showing report button if not logged in (#320)

* Prevent report button showing on user hover card and profile page if there is no user logged in

* Improve composition of user hover card

* Use getSession instead of getUser for efficiency

* Prevent report button from showing for logged-in users viewing their own profile

* Restrict report button visibility to logged-in users not viewing their own profile

---------

Co-authored-by: Tom Sherman <tom@sherman.is>

authored by

Timothy
Tom Sherman
and committed by
GitHub
e109fa6c d1972ef1

+81 -77
+10 -5
packages/frontpage/app/(app)/profile/[user]/page.tsx
··· 23 23 import { reportUserAction } from "@/lib/components/user-hover-card"; 24 24 import { type Metadata } from "next"; 25 25 import { LinkAlternateAtUri } from "@/lib/components/link-alternate-at"; 26 + import { getSession } from "@/lib/auth"; 26 27 27 28 export async function generateMetadata( 28 29 props: PageProps<"/profile/[user]">, ··· 74 75 return b.createdAt.getTime() - a.createdAt.getTime(); 75 76 }); 76 77 78 + const session = await getSession(); 79 + 77 80 return ( 78 81 <> 79 82 <LinkAlternateAtUri authority={did} /> ··· 83 86 <h1 className="md:text-2xl font-bold"> 84 87 {userHandle ?? "handle.invalid"} 85 88 </h1> 86 - <EllipsisDropdown aria-label="User actions"> 87 - <ReportDialogDropdownButton 88 - reportAction={reportUserAction.bind(null, { did })} 89 - /> 90 - </EllipsisDropdown> 89 + {session !== null && session.did !== did ? ( 90 + <EllipsisDropdown aria-label="User actions"> 91 + <ReportDialogDropdownButton 92 + reportAction={reportUserAction.bind(null, { did })} 93 + /> 94 + </EllipsisDropdown> 95 + ) : null} 91 96 </div> 92 97 </div> 93 98 <Tabs defaultValue="overview">
+18 -62
packages/frontpage/lib/components/user-hover-card-client.tsx
··· 2 2 3 3 import useSWR, { preload } from "swr"; 4 4 import { type DID } from "../data/atproto/did"; 5 - import { HoverCardTrigger, HoverCardContent } from "./ui/hover-card"; 6 - import { type ReactNode, Suspense } from "react"; 7 - import { Skeleton } from "./ui/skeleton"; 5 + import { HoverCardTrigger } from "./ui/hover-card"; 6 + import { type ReactNode } from "react"; 8 7 import { ChatBubbleIcon, Link1Icon } from "@radix-ui/react-icons"; 9 8 import { type ApiRouteResponse } from "../api-route"; 10 9 import type { GET as GetHoverCardContent } from "@/app/api/hover-card-content/route"; 11 10 import Link from "next/link"; 12 - import { ReportDialogIcon } from "@/app/(app)/_components/report-dialog"; 13 - import { Separator } from "./ui/separator"; 14 11 15 - type Props = { 16 - did: DID; 17 - children: ReactNode; 18 - asChild?: boolean; 19 - avatar: ReactNode; 20 - handle: string | null; 21 - reportAction: (formData: FormData) => Promise<void>; 22 - }; 23 - 24 - export function UserHoverCardClient({ 25 - did, 12 + export function UserHoverCardTrigger({ 26 13 children, 27 14 asChild, 28 - avatar, 29 - handle, 30 - reportAction, 31 - }: Props) { 15 + did, 16 + }: { 17 + children: ReactNode; 18 + asChild?: boolean; 19 + did: DID; 20 + }) { 32 21 return ( 33 - <> 34 - <HoverCardTrigger 35 - asChild={asChild} 36 - onMouseEnter={() => { 37 - void preload(did, getHoverCardData); 38 - }} 39 - > 40 - {children} 41 - </HoverCardTrigger> 42 - <HoverCardContent className="w-80"> 43 - <div className="flex gap-4"> 44 - <Link href={`/profile/${handle ?? did}`} className="shrink-0"> 45 - {avatar} 46 - </Link> 47 - <div className="flex flex-col gap-1 basis-full"> 48 - <Suspense fallback={<Fallback handle={handle} did={did} />}> 49 - <Content did={did} /> 50 - </Suspense> 51 - </div> 52 - </div> 53 - <Separator className="my-2" /> 54 - <div> 55 - <ReportDialogIcon reportAction={reportAction} /> 56 - </div> 57 - </HoverCardContent> 58 - </> 22 + <HoverCardTrigger 23 + asChild={asChild} 24 + onMouseEnter={() => { 25 + void preload(did, getHoverCardData); 26 + }} 27 + > 28 + {children} 29 + </HoverCardTrigger> 59 30 ); 60 31 } 61 32 62 - function Content({ did }: { did: DID }) { 33 + export function UserHoverCardContent({ did }: { did: DID }) { 63 34 const { data } = useSWR(did, getHoverCardData, { 64 35 suspense: true, 65 36 revalidateOnMount: false, ··· 85 56 > 86 57 <Link1Icon /> {data.postCount} 87 58 </p> 88 - </> 89 - ); 90 - } 91 - 92 - function Fallback({ handle, did }: { handle: string | null; did: DID }) { 93 - return ( 94 - <> 95 - <Link 96 - href={`/profile/${handle ?? did}`} 97 - className="text-sm font-semibold" 98 - > 99 - @{handle ?? "handle.invalid"} 100 - </Link> 101 - <Skeleton className="h-5 w-12" /> 102 - <Skeleton className="h-5 w-12" /> 103 59 </> 104 60 ); 105 61 }
+53 -10
packages/frontpage/lib/components/user-hover-card.tsx
··· 1 1 import { UserAvatar } from "./user-avatar"; 2 - import { HoverCard } from "@/lib/components/ui/hover-card"; 2 + import { HoverCard, HoverCardContent } from "@/lib/components/ui/hover-card"; 3 3 import { type DID } from "../data/atproto/did"; 4 4 import { getVerifiedHandle } from "../data/atproto/identity"; 5 - import { UserHoverCardClient } from "./user-hover-card-client"; 5 + import { 6 + UserHoverCardContent, 7 + UserHoverCardTrigger, 8 + } from "./user-hover-card-client"; 6 9 import { ensureUser } from "../data/user"; 7 10 import { parseReportForm } from "../data/db/report-shared"; 8 11 import { createReport } from "../data/db/report"; 12 + import Link from "next/link"; 13 + import { Skeleton } from "./ui/skeleton"; 14 + import { Suspense } from "react"; 15 + import { Separator } from "./ui/separator"; 16 + import { ReportDialogIcon } from "@/app/(app)/_components/report-dialog"; 17 + import { getSession } from "../auth"; 9 18 10 19 type Props = { 11 20 did: DID; ··· 16 25 export async function UserHoverCard({ did, children, asChild }: Props) { 17 26 // Fetch this early on the server because it's almost certainly already cached this request 18 27 const handle = await getVerifiedHandle(did); 28 + const session = await getSession(); 29 + 19 30 return ( 20 31 <HoverCard> 21 - <UserHoverCardClient 22 - avatar={<UserAvatar did={did} size="medium" />} 23 - did={did} 24 - asChild={asChild} 25 - handle={handle} 26 - reportAction={reportUserAction.bind(null, { did })} 27 - > 32 + <UserHoverCardTrigger did={did} asChild={asChild}> 28 33 {children} 29 - </UserHoverCardClient> 34 + </UserHoverCardTrigger> 35 + <HoverCardContent className="w-80"> 36 + <div className="flex gap-4"> 37 + <Link href={`/profile/${handle ?? did}`} className="shrink-0"> 38 + <UserAvatar did={did} size="medium" /> 39 + </Link> 40 + <div className="flex flex-col gap-1 basis-full"> 41 + <Suspense fallback={<ContentFallback handle={handle} did={did} />}> 42 + <UserHoverCardContent did={did} /> 43 + </Suspense> 44 + </div> 45 + </div> 46 + {session !== null && session.did !== did ? ( 47 + <> 48 + <Separator className="my-2" /> 49 + <div> 50 + <ReportDialogIcon 51 + reportAction={reportUserAction.bind(null, { did })} 52 + /> 53 + </div> 54 + </> 55 + ) : null} 56 + </HoverCardContent> 30 57 </HoverCard> 31 58 ); 32 59 } 33 60 61 + function ContentFallback({ handle, did }: { handle: string | null; did: DID }) { 62 + return ( 63 + <> 64 + <Link 65 + href={`/profile/${handle ?? did}`} 66 + className="text-sm font-semibold" 67 + > 68 + @{handle ?? "handle.invalid"} 69 + </Link> 70 + <Skeleton className="h-5 w-12" /> 71 + <Skeleton className="h-5 w-12" /> 72 + </> 73 + ); 74 + } 75 + 76 + // TODO: Find a better place for this action, it doesn't belong as part of the hover card component as it's used elsewhere. Or maybe just duplicate it? 34 77 export async function reportUserAction( 35 78 input: { 36 79 did: DID;