{"version":3,"file":"activity-presentation-OSfAUOoY.cjs","names":["ShoppingCart","MessageCircle","Play","UserPlus","Eye","Calendar","Star","SmilePlus","CheckSquare","Bell","Trophy","User","AudioWaveform"],"sources":["../../core/src/activity-slug.ts","../../core/src/activity-presentation.ts"],"sourcesContent":["import type { ActivitySlug } from \"./widgets-api-types\";\n\n// Typing as `Record<ActivitySlug, true>` makes TS reject this object when a new\n// value is added to `ActivitySlug` and not registered here, so the guard stays\n// exhaustive without a separate test.\nconst KNOWN_ACTIVITY_SLUGS: Record<ActivitySlug, true> = {\n  abandoned_cart: true,\n  announcements: true,\n  cart_item_removed: true,\n  cart_items_added: true,\n  comment_like: true,\n  comment_reply: true,\n  direct_message: true,\n  emoji_reaction: true,\n  enrollment: true,\n  external_order: true,\n  fantasy_point: true,\n  imported: true,\n  media_views: true,\n  new_lead: true,\n  order_placed: true,\n  page_views: true,\n  page_views_contact: true,\n  tasks: true,\n  upcoming_event: true,\n  video: true,\n  video_complete: true,\n  video_complete_contact: true,\n  video_contact: true,\n  video_started: true,\n  message_received: true,\n  message_sent: true,\n  new_cart_items_added: true,\n  smart_link_clicked: true,\n  review_left: true,\n};\n\n/**\n * Narrows a server slug to one this client knows how to present.\n *\n * This is a presentation lookup, never a filter: the server's slug set grows\n * independently, and an unrecognized slug still renders with generic treatment.\n */\nexport function isActivitySlug(slug: string): slug is ActivitySlug {\n  return Object.hasOwn(KNOWN_ACTIVITY_SLUGS, slug);\n}\n","import {\n  AudioWaveform,\n  Bell,\n  Calendar,\n  CheckSquare,\n  Eye,\n  MessageCircle,\n  Play,\n  ShoppingCart,\n  SmilePlus,\n  Star,\n  Trophy,\n  User,\n  UserPlus,\n  type LucideIcon,\n} from \"lucide-react\";\nimport type { TranslationApi } from \"@fluid-app/i18n/translation-api\";\nimport { isActivitySlug } from \"./activity-slug\";\nimport type { ShellDict } from \"./shell-translation-dictionary\";\nimport type { ActivityActor, ActivitySlug } from \"./widgets-api-types\";\n\n/**\n * The shell translator, passed in rather than read from context, so the pieces\n * below a screen stay context-free and the widget can share them.\n */\nexport type ShellT = TranslationApi<ShellDict>[\"t\"];\n\n/**\n * Presentation shared by the Recent Activity widget and the Activity screen.\n * Both surfaces must agree — the widget's count is a promise about what the\n * screen will show — so the icon map and date bucketing live here rather than\n * being duplicated per renderer.\n */\n\nconst ACTIVITY_ICON_MAP: Record<ActivitySlug, LucideIcon> = {\n  order_placed: ShoppingCart,\n  external_order: ShoppingCart,\n  abandoned_cart: ShoppingCart,\n  cart_items_added: ShoppingCart,\n  cart_item_removed: ShoppingCart,\n  new_cart_items_added: ShoppingCart,\n  direct_message: MessageCircle,\n  comment_reply: MessageCircle,\n  comment_like: MessageCircle,\n  message_received: MessageCircle,\n  message_sent: MessageCircle,\n  video: Play,\n  video_started: Play,\n  video_complete: Play,\n  video_contact: Play,\n  video_complete_contact: Play,\n  media_views: Play,\n  new_lead: UserPlus,\n  enrollment: UserPlus,\n  imported: UserPlus,\n  page_views_contact: UserPlus,\n  smart_link_clicked: UserPlus,\n  page_views: Eye,\n  upcoming_event: Calendar,\n  review_left: Star,\n  emoji_reaction: SmilePlus,\n  tasks: CheckSquare,\n  announcements: Bell,\n  fantasy_point: Trophy,\n};\n\n/**\n * Slugs grouped into the categories a rep filters by. Reps think in \"orders\"\n * and \"videos\", not in the ~30 slugs the platform writes, and each category\n * expands to a server-side slug filter so the results stay honest across\n * pagination.\n *\n * A slug missing from every category is still shown; it just cannot be\n * filtered to on its own.\n */\nexport const ACTIVITY_CATEGORIES = {\n  orders: [\"order_placed\", \"external_order\"],\n  carts: [\n    \"abandoned_cart\",\n    \"cart_items_added\",\n    \"new_cart_items_added\",\n    \"cart_item_removed\",\n  ],\n  messages: [\n    \"direct_message\",\n    \"message_received\",\n    \"message_sent\",\n    \"comment_reply\",\n    \"comment_like\",\n  ],\n  videos: [\n    \"video\",\n    \"video_started\",\n    \"video_complete\",\n    \"video_contact\",\n    \"video_complete_contact\",\n    \"media_views\",\n  ],\n  pageViews: [\"page_views\", \"page_views_contact\", \"smart_link_clicked\"],\n  leads: [\"new_lead\", \"enrollment\", \"imported\"],\n  reviews: [\"review_left\"],\n  reactions: [\"emoji_reaction\"],\n  // Every category has to be able to return a row, since a filter is just another\n  // view of the same types the feed renders cards for. Two are absent for that\n  // reason: tasks, upcoming_event and announcements are written to notifications\n  // rather than activities, and fantasy_point has no writer at all. Offering\n  // either would have been a filter that always came back empty.\n} as const satisfies Record<string, readonly ActivitySlug[]>;\n\nexport type ActivityCategory = keyof typeof ACTIVITY_CATEGORIES;\n\nexport const ACTIVITY_CATEGORY_KEYS = Object.keys(\n  ACTIVITY_CATEGORIES,\n) as ActivityCategory[];\n\n/** Flattens selected categories into the slug list the API filter takes. */\nexport function slugsForCategories(\n  categories: readonly ActivityCategory[],\n): string[] {\n  return categories.flatMap((category) => [...ACTIVITY_CATEGORIES[category]]);\n}\n\n/** Stand-in for a slug this client has no specific treatment for. */\nexport const GenericActivityIcon: LucideIcon = User;\n\n/** Icon for the empty feed, exported so widget and screen match. */\nexport const EmptyActivityIcon: LucideIcon = AudioWaveform;\n\n/**\n * Icon for a raw server slug. Unrecognized slugs get the generic icon rather\n * than being hidden — the server's slug set grows independently of this client.\n */\nexport function getActivityIcon(slug: string): LucideIcon {\n  return isActivitySlug(slug) ? ACTIVITY_ICON_MAP[slug] : GenericActivityIcon;\n}\n\n/** Local-time YYYY-MM-DD, the key activities are grouped into days by. */\nexport function getActivityDateKey(timestamp: string): string {\n  const date = new Date(timestamp);\n  if (!Number.isFinite(date.getTime())) return timestamp;\n\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\n  const day = String(date.getDate()).padStart(2, \"0\");\n  return `${year}-${month}-${day}`;\n}\n\n/** Labels only the caller can supply, because only it has a translator. */\n/**\n * How to label and style whoever an activity is about.\n *\n * Composed here rather than read off `userName`, which the server writes in\n * English. `muted` is what makes an anonymous visitor read differently from an\n * identified contact, the distinction mobile draws with a different avatar.\n */\nexport function describeActor(\n  actor: ActivityActor,\n  t: ShellT,\n): { label: string; muted: boolean } {\n  switch (actor.type) {\n    case \"contact\":\n      return {\n        label: actor.name?.trim() || t(\"activity_actor_contact\"),\n        muted: false,\n      };\n    case \"visitor\": {\n      // City and state go in as separate values rather than a joined string. The\n      // separator and their order are part of the sentence, and both vary: ja and\n      // zh use a full-width comma with no space, ar and he need RTL ordering, and\n      // several locales put the region first. Joining in TS gives a translator no\n      // handle on any of it.\n      const city = actor.city?.trim();\n      const state = actor.state?.trim();\n\n      if (city && state) {\n        return {\n          label: t(\"activity_actor_visitor_from_city_state\", { city, state }),\n          muted: true,\n        };\n      }\n\n      const single = city || state;\n\n      return {\n        label: single\n          ? t(\"activity_actor_visitor_from_city\", { city: single })\n          : t(\"activity_actor_visitor\"),\n        muted: true,\n      };\n    }\n    case \"unknown\":\n      return { label: t(\"activity_actor_unknown\"), muted: true };\n    default:\n      // ActivityActor is closed and local, so a variant added without a case here\n      // should fail the typecheck rather than quietly read as unknown.\n      return describeUnhandledActor(actor, t);\n  }\n}\n\n/**\n * First and last initial of whoever a row is about, empty when there is no person\n * to show. First and last rather than the first two, matching the contacts list\n * and the profile menu, so one contact does not get two different monograms.\n *\n * Reads the name off the actor rather than a rendered label: a contact the server\n * sent no name labels as the translated word \"Contact\", and its first letter\n * would read as a real person's initial in every one of the 34 locales.\n */\nexport function initialsForActor(actor: ActivityActor): string {\n  if (actor.type !== \"contact\") return \"\";\n\n  const name = actor.name?.trim() ?? \"\";\n\n  // A contact with no name falls back to their email or phone, and neither makes a\n  // monogram. An address is the case worth naming: \"jane@example.com\" starts with a\n  // letter, so it would otherwise pass for a first name.\n  if (name === \"\" || name.includes(\"@\")) return \"\";\n\n  // Spread rather than charAt, so a name starting outside the BMP keeps its first\n  // character whole instead of half a surrogate pair. Letters only, so a phone\n  // number does not put a plus sign in the circle where a face goes.\n  const letters = name\n    .split(/\\s+/)\n    .map((word) => [...word][0] ?? \"\")\n    .filter((char) => /\\p{L}/u.test(char));\n\n  const first = letters[0];\n  if (first === undefined) return \"\";\n\n  const last = letters.length > 1 ? letters[letters.length - 1] : \"\";\n\n  return (first + last).toUpperCase();\n}\n\nfunction describeUnhandledActor(\n  actor: never,\n  t: ShellT,\n): { label: string; muted: boolean } {\n  void actor;\n\n  return { label: t(\"activity_actor_unknown\"), muted: true };\n}\n\nexport type ActivityDayLabels = {\n  today: string;\n  /**\n   * Omit to head yesterday with its date instead.\n   *\n   * The activity screen does, because it reserves Yesterday for the timeframe\n   * filter: seeing it as a group heading as well made one word mean two things.\n   * The widget still passes it.\n   */\n  yesterday?: string | undefined;\n};\n\n/**\n * Heading for a day group: the caller's label for today, the same for yesterday\n * when it supplies one, and otherwise a localized month and day.\n */\nexport function formatActivityDateHeader(\n  timestamp: string,\n  nowMs: number,\n  labels: ActivityDayLabels,\n  locale = \"en-US\",\n): string {\n  const date = new Date(timestamp);\n  if (!Number.isFinite(date.getTime())) return timestamp;\n\n  const now = new Date(nowMs);\n  const yesterday = new Date(now);\n  yesterday.setDate(now.getDate() - 1);\n\n  const dateKey = getActivityDateKey(timestamp);\n  if (dateKey === getActivityDateKey(now.toISOString())) return labels.today;\n  if (\n    labels.yesterday &&\n    dateKey === getActivityDateKey(yesterday.toISOString())\n  ) {\n    return labels.yesterday;\n  }\n\n  return date.toLocaleDateString(locale, { month: \"long\", day: \"numeric\" });\n}\n\n/** Clock time for an activity row, e.g. \"4:12 PM\". */\nexport function formatActivityTime(\n  timestamp: string,\n  locale = \"en-US\",\n): string {\n  return new Date(timestamp).toLocaleTimeString(locale, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    hour12: true,\n  });\n}\n\n/**\n * Compact age, e.g. \"now\", \"5m\", \"3h\", \"2d\", then a date past a week.\n *\n * The units are unlocalized single letters; only the widget uses this, and its\n * copy is not translated yet.\n */\nexport function formatActivityRelativeTime(\n  timestamp: string,\n  now: number,\n  locale = \"en-US\",\n): string {\n  const then = new Date(timestamp).getTime();\n  const diffSec = Math.max(0, Math.round((now - then) / 1000));\n\n  if (diffSec < 60) return \"now\";\n  const diffMin = Math.floor(diffSec / 60);\n  if (diffMin < 60) return `${diffMin}m`;\n  const diffHr = Math.floor(diffMin / 60);\n  if (diffHr < 24) return `${diffHr}h`;\n  const diffDay = Math.floor(diffHr / 24);\n  if (diffDay < 7) return `${diffDay}d`;\n\n  return new Date(timestamp).toLocaleDateString(locale, {\n    month: \"short\",\n    day: \"numeric\",\n  });\n}\n"],"mappings":"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"}