TaskOwnerBadge.tsx2.4 KBView on GitHub 'use client';
import { useViewerId, type TaskOwner } from '@/modules/userTasks/utils/task-ownership';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { cn } from '@/lib/utils';
export type { TaskOwner };
/**
* The task's owner when it is someone *other* than the viewer, else null.
*
* Fails closed: a task counts as a teammate's only once we positively know both identities and
* they differ. `useSession()` resolves asynchronously (and yields nothing at all on the server
* render), so comparing against an unresolved `session.user.id` made every task — your own
* included — look foreign, which is how the owner badge ended up on tasks that are yours. An
* owner without an `id` is likewise unattributable, so it gets no badge either.
*/
export function useForeignTaskOwner(owner: TaskOwner | null | undefined): TaskOwner | null {
const viewerId = useViewerId();
if (!viewerId) return null;
if (!owner?.id || owner.id === viewerId) return null;
return owner;
}
/**
* Whose task this is — an avatar + name pill, rendered **only when the task isn't the viewer's**.
*
* A conversation is shared across the org and so is its task list: every task on a conversation
* shows to everyone who can see that conversation. Without this, a teammate's task reads as your
* own, which is how a task you can't run ends up looking like one you forgot to.
*
* Self-hiding by design — callers pass the owner unconditionally and get nothing back for their own
* tasks, so no surface has to repeat the "is this mine" comparison.
*/
export function TaskOwnerBadge({
owner,
className,
}: {
owner: TaskOwner | null | undefined;
className?: string;
}) {
const foreignOwner = useForeignTaskOwner(owner);
if (!foreignOwner) return null;
const label = foreignOwner.name || foreignOwner.email || 'Teammate';
return (
<span
className={cn(
'bg-muted/60 flex shrink-0 items-center gap-1 rounded-full py-0.5 pl-0.5 pr-2',
className,
)}
title={`${label}'s task`}
>
<Avatar className="h-4 w-4 rounded-full">
<AvatarImage src={foreignOwner.image || undefined} />
<AvatarFallback className="bg-primary/10 text-primary text-[9px] font-semibold">
{label[0]?.toUpperCase() || '?'}
</AvatarFallback>
</Avatar>
<span className="text-xs text-muted-foreground">{label}</span>
</span>
);
}