import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { formatDistanceToNow } from "date-fns"; import { de, enGB, es, fr, it, nl } from "date-fns/locale"; import { HiOutlineArrowLeft, HiOutlineArrowRight, HiOutlineCheckCircle, HiOutlinePencil, HiOutlinePlus, HiOutlineTag, HiOutlineTrash, HiOutlineUserMinus, HiOutlineUserPlus, } from "react-icons/hi2"; import type { GetCardByIdOutput } from "@kan/api/types"; import { authClient } from "@kan/auth/client"; import Avatar from "~/components/Avatar"; import { useLocalisation } from "~/hooks/useLocalisation"; import Comment from "./Comment"; type ActivityType = NonNullable["activities"][number]["type"]; const dateLocaleMap = { en: enGB, fr: fr, de: de, es: es, it: it, nl: nl, } as const; const truncate = (value: string | null, maxLength = 50) => { if (!value) return value; return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value; }; const getActivityText = ({ type, toTitle, fromList, toList, memberName, isSelf, label, fromTitle, }: { type: ActivityType; toTitle: string | null; fromList: string | null; toList: string | null; memberName: string | null; isSelf: boolean; label: string | null; fromTitle?: string | null; }) => { const ACTIVITY_TYPE_MAP = { "card.created": t`created the card`, "card.updated.title": t`updated the title`, "card.updated.description": t`updated the description`, "card.updated.list": t`moved the card to another list`, "card.updated.label.added": t`added a label to the card`, "card.updated.label.removed": t`removed a label from the card`, "card.updated.member.added": t`added a member to the card`, "card.updated.member.removed": t`removed a member from the card`, "card.updated.checklist.added": t`added a checklist`, "card.updated.checklist.renamed": t`renamed a checklist`, "card.updated.checklist.deleted": t`deleted a checklist`, "card.updated.checklist.item.added": t`added a checklist item`, "card.updated.checklist.item.updated": t`updated a checklist item`, "card.updated.checklist.item.completed": t`completed a checklist item`, "card.updated.checklist.item.uncompleted": t`marked a checklist item as incomplete`, "card.updated.checklist.item.deleted": t`deleted a checklist item`, } as const; if (!(type in ACTIVITY_TYPE_MAP)) return null; const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP]; const TextHighlight = ({ children }: { children: React.ReactNode }) => ( {children} ); if (type === "card.updated.title" && toTitle) { return ( updated the title to {truncate(toTitle)} ); } if (type === "card.updated.list" && fromList && toList) { return ( moved the card from {truncate(fromList)}{" "} to {truncate(toList)} ); } if (type === "card.updated.member.added" && memberName) { if (isSelf) return self-assigned the card; return ( assigned {truncate(memberName)} to the card ); } if (type === "card.updated.member.removed" && memberName) { if (isSelf) return unassigned themselves from the card; return ( unassigned {truncate(memberName)} from the card ); } if (type === "card.updated.label.added" && label) { return ( added label {truncate(label)} ); } if (type === "card.updated.label.removed" && label) { return ( removed label {truncate(label)} ); } if (type === "card.updated.checklist.added" && toTitle) { return ( added checklist {truncate(toTitle)} ); } if (type === "card.updated.checklist.renamed" && toTitle) { return ( renamed checklist {truncate(toTitle)} ); } if (type === "card.updated.checklist.deleted" && fromTitle) { return ( deleted checklist {truncate(fromTitle)} ); } if (type === "card.updated.checklist.item.added" && toTitle) { return ( added checklist item {truncate(toTitle)} ); } if (type === "card.updated.checklist.item.updated" && toTitle) { return ( renamed checklist item to{" "} {truncate(toTitle)} ); } if (type === "card.updated.checklist.item.completed" && toTitle) { return ( completed checklist item{" "} {truncate(toTitle)} ); } if (type === "card.updated.checklist.item.uncompleted" && toTitle) { return ( marked checklist item {truncate(toTitle)}{" "} as incomplete ); } if (type === "card.updated.checklist.item.deleted" && fromTitle) { return ( deleted checklist item{" "} {truncate(fromTitle)} ); } return baseText; }; const ACTIVITY_ICON_MAP: Partial> = { "card.created": , "card.updated.title": , "card.updated.description": , "card.updated.label.added": , "card.updated.label.removed": , "card.updated.member.added": , "card.updated.member.removed": , "card.updated.checklist.added": , "card.updated.checklist.renamed": , "card.updated.checklist.deleted": , "card.updated.checklist.item.added": , "card.updated.checklist.item.updated": , "card.updated.checklist.item.completed": , "card.updated.checklist.item.uncompleted": , "card.updated.checklist.item.deleted": , } as const; const getActivityIcon = ( type: ActivityType, fromIndex?: number | null, toIndex?: number | null, ): React.ReactNode | null => { if (type === "card.updated.list" && fromIndex != null && toIndex != null) { return fromIndex > toIndex ? ( ) : ( ); } return ACTIVITY_ICON_MAP[type] ?? null; }; const ActivityList = ({ activities, cardPublicId, isLoading, isAdmin, isViewOnly, }: { activities: NonNullable["activities"]; cardPublicId: string; isLoading: boolean; isAdmin?: boolean; isViewOnly?: boolean; }) => { const { data } = authClient.useSession(); const { locale } = useLocalisation(); const currentDateLocale = dateLocaleMap[locale] || enGB; return (
{activities.map((activity, index) => { const activityText = getActivityText({ type: activity.type, toTitle: activity.toTitle, fromList: activity.fromList?.name ?? null, toList: activity.toList?.name ?? null, memberName: activity.member?.user?.name ?? null, isSelf: activity.member?.user?.id === data?.user.id, label: activity.label?.name ?? null, fromTitle: activity.fromTitle ?? null, }); if (activity.type === "card.updated.comment.added") return ( ); if (!activityText) return null; return (
{index !== activities.length - 1 && activities[index + 1]?.type !== "card.updated.comment.added" && (
)}

{`${activity.user?.name} `} {activityText} · {formatDistanceToNow(new Date(activity.createdAt), { addSuffix: true, locale: currentDateLocale, })}

); })}
); }; export default ActivityList;