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>
  );
}