task-ownership.ts2.5 KBView on GitHub
import { useViewerUserId } from '@/modules/auth/hooks/use-viewer-user-id';
import { useMemo } from 'react';

export interface TaskOwner {
  id: string;
  name?: string | null;
  email?: string | null;
  image?: string | null;
}

/** Anything with an owner — every task shape the conversation surfaces render. */
interface OwnedTask {
  owner?: TaskOwner | null;
}

/**
 * The viewer's user id, or null while it isn't positively known.
 *
 * The session resolves asynchronously and yields nothing on the server render, so callers
 * must treat null as "can't tell whose task this is yet" and fall back to the neutral answer
 * rather than guessing — see the fail-closed note on `useForeignTaskOwner`.
 *
 * A thin alias over `useViewerUserId` so this module keeps its own vocabulary while the rule
 * for WHO the viewer is stays in one place: under Cedar-staff admin view it is the
 * impersonated user, so that person's own follow-ups do not all read as a teammate's.
 */
export function useViewerId(): string | null {
  return useViewerUserId();
}

/**
 * Split a task list into the viewer's own tasks and their teammates'.
 *
 * A conversation is shared across the org and so is its task list, so a deal that two people
 * work shows both people's follow-ups in one column. Interleaved, that column stops answering
 * the question it exists for — "what is on me?" — because a teammate's task looks exactly like
 * one of yours that you've forgotten, and you can't act on it anyway (every task mutation is
 * owner-scoped server side).
 *
 * Fails closed like the owner badge: with no viewer id, or a task whose owner is unattributable,
 * the task counts as the viewer's. A wrong guess demotes your own work to the bottom of the
 * page under someone else's name, which is worse than not separating at all.
 */
export function partitionTasksByOwner<T extends OwnedTask>(
  tasks: T[],
  viewerId: string | null | undefined,
): { mine: T[]; theirs: T[] } {
  if (!viewerId) return { mine: tasks, theirs: [] };
  const mine: T[] = [];
  const theirs: T[] = [];
  for (const task of tasks) {
    if (task.owner?.id && task.owner.id !== viewerId) theirs.push(task);
    else mine.push(task);
  }
  return { mine, theirs };
}

/** `partitionTasksByOwner` bound to the signed-in viewer. */
export function useTasksByOwnership<T extends OwnedTask>(tasks: T[]): { mine: T[]; theirs: T[] } {
  const viewerId = useViewerId();
  return useMemo(() => partitionTasksByOwner(tasks, viewerId), [tasks, viewerId]);
}