{"version":3,"file":"chat-groups.cjs","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** What a bubble can carry besides text. */\nexport type ChatAttachmentKind = \"image\" | \"video\" | \"audio\" | \"voice\" | \"file\";\n\n/** One attachment on a message. */\nexport interface ChatAttachment {\n    /** Decides how it renders: a picture, a player, a voice note, a download row. */\n    kind: ChatAttachmentKind;\n    /** Where the bytes are. A blob URL works for a message still uploading. */\n    url: string;\n    /** File name, shown for `\"file\"` and used as the download name. */\n    name?: string;\n    /** MIME type, when the app knows it. */\n    mimeType?: string;\n    /** Size in bytes, shown next to a file. */\n    sizeBytes?: number;\n    /** Duration in ms, for audio, voice and video. */\n    durationMs?: number;\n    /** Poster for a video, or a smaller copy of an image. */\n    thumbnailUrl?: string;\n    /**\n     * Normalised peaks, `0`–`1`, for a voice note.\n     *\n     * A voice note without one is a grey rectangle: the waveform is what tells\n     * somebody whether this is a word or a two-minute monologue.\n     */\n    waveform?: readonly number[];\n    /** Alternative text for an image. */\n    alt?: string;\n}\n\n/**\n * The stub of the message being replied to, shown above the body.\n *\n * `revoked` is not decoration: deleting the quoted message has to blank the\n * quoted text, or it leaks through everyone who replied to it.\n */\nexport interface ChatQuote {\n    /** Id of the quoted message, so the app can scroll to it. */\n    messageId: string;\n    /** Who wrote it. */\n    senderName?: string;\n    /** A short piece of what it said. The component never truncates for you. */\n    excerpt?: string;\n    /** What the quoted message was, when it was not text. */\n    kind?: \"text\" | ChatAttachmentKind;\n    /** The quoted message was deleted — the excerpt is not shown. */\n    revoked?: boolean;\n}\n\n/** One emoji on a message, with its tally. */\nexport interface ChatReaction {\n    /** The emoji itself. */\n    emoji: string;\n    /** How many people reacted with it. */\n    count: number;\n    /** Whether the current user is one of them — the chip reads as pressed. */\n    reacted?: boolean;\n    /** Who reacted, for the chip's tooltip. */\n    names?: readonly string[];\n}\n\n/**\n * Per-recipient delivery, which is what a group needs.\n *\n * `status` is a boolean for the sender: it cannot say \"delivered to everyone\"\n * versus \"read by everyone\", and only the second one turns the ticks blue.\n */\nexport interface ChatReceipt {\n    /** How many recipients have received it. */\n    deliveredTo: number;\n    /** How many have read it. */\n    readBy: number;\n    /** How many recipients there are. */\n    totalRecipients: number;\n}\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n    /** Stable identity. Used as the React key and by `onRetry`. */\n    id: string;\n    /**\n     * What was said. A node, so an app can render a link, an image or a quote.\n     *\n     * Optional, because a message can be all attachment, or be a tombstone: a\n     * deleted message has no body to show, and requiring one made every app\n     * invent a placeholder string for the state the component now owns.\n     */\n    body?: ReactNode;\n    /** Who said it. Compared against `currentUserId` to decide sides. */\n    authorId: string;\n    /** Display name. Falls back to `authorId` in the header of a run. */\n    authorName?: string;\n    /** Epoch milliseconds. */\n    sentAt: number;\n    /**\n     * Delivery state of an outgoing message.\n     *\n     * `\"failed\"` is the one that matters: without it an app has to invent its own\n     * way to say \"this never left\", and the user re-types a message that is\n     * sitting right there.\n     */\n    status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n    /**\n     * Per-recipient delivery. Takes precedence over `status` for the ticks.\n     *\n     * Use it in a group, where \"delivered to all\" and \"read by all\" are different\n     * states and `status` can only say one thing.\n     */\n    receipt?: ChatReceipt;\n    /** Media, voice notes and documents carried by this message. */\n    attachments?: readonly ChatAttachment[];\n    /** The message this one replies to. */\n    quote?: ChatQuote;\n    /** Emoji reactions, already tallied by the app. */\n    reactions?: readonly ChatReaction[];\n    /**\n     * The message was deleted — it renders as a tombstone.\n     *\n     * A state, not a `body` the app swaps for a string: as a state the quote of\n     * it can be blanked too, and every app stops writing its own wording.\n     */\n    deleted?: boolean;\n    /** The message was edited after it was sent. */\n    edited?: boolean;\n    /** Anything the app wants to carry through to its own renderers. */\n    data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n    kind: \"run\";\n    /** `${authorId}-${first message id}` — stable across re-renders. */\n    key: string;\n    authorId: string;\n    authorName?: string;\n    /** Whether this run belongs to the current user. */\n    own: boolean;\n    messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n    kind: \"day\";\n    key: string;\n    /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n    date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n    const date = new Date(timestamp);\n    date.setHours(0, 0, 0, 0);\n    return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n    messages,\n    currentUserId,\n    windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n    messages: readonly ChatMessage[];\n    currentUserId?: string;\n    windowMs?: number;\n}): ChatSection[] {\n    const sections: ChatSection[] = [];\n    let day: number | null = null;\n    let run: ChatRun | null = null;\n\n    for (const message of messages) {\n        const messageDay = startOfDay(message.sentAt);\n        if (messageDay !== day) {\n            day = messageDay;\n            run = null;\n            sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n        }\n\n        const previous = run?.messages[run.messages.length - 1];\n        const continues =\n            run !== null &&\n            previous !== undefined &&\n            run.authorId === message.authorId &&\n            message.sentAt - previous.sentAt <= windowMs;\n\n        if (continues && run) {\n            run.messages.push(message);\n            continue;\n        }\n\n        run = {\n            kind: \"run\",\n            key: `${message.authorId}-${message.id}`,\n            authorId: message.authorId,\n            authorName: message.authorName,\n            own: currentUserId !== undefined && message.authorId === currentUserId,\n            messages: [message],\n        };\n        sections.push(run);\n    }\n\n    return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n    thread: string;\n    today: string;\n    yesterday: string;\n    you: string;\n    typingOne: (name: string) => string;\n    typingTwo: (a: string, b: string) => string;\n    typingMany: (n: number) => string;\n    sending: string;\n    sent: string;\n    read: string;\n    failed: string;\n    retry: string;\n    empty: string;\n    placeholder: string;\n    send: string;\n    deleted: string;\n    edited: string;\n    replyingTo: (name: string) => string;\n    quoteRevoked: string;\n    quoteKind: Record<ChatAttachmentKind, string>;\n    reactions: string;\n    react: (emoji: string, count: number) => string;\n    deliveredAll: string;\n    deliveredSome: (delivered: number, total: number) => string;\n    readAll: string;\n    readSome: (read: number, total: number) => string;\n    messageActions: string;\n    voiceNote: string;\n    download: string;\n}\n\nconst PT_BR: ChatStrings = {\n    thread: \"Conversa\",\n    today: \"Hoje\",\n    yesterday: \"Ontem\",\n    you: \"Você\",\n    typingOne: (name) => `${name} está digitando…`,\n    typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n    typingMany: (n) => `${n} pessoas estão digitando…`,\n    sending: \"Enviando\",\n    sent: \"Enviada\",\n    read: \"Lida\",\n    failed: \"Falhou ao enviar\",\n    retry: \"Tentar de novo\",\n    empty: \"Nenhuma mensagem ainda\",\n    placeholder: \"Escreva uma mensagem\",\n    send: \"Enviar\",\n    deleted: \"Esta mensagem foi apagada\",\n    edited: \"editada\",\n    replyingTo: (name) => `Em resposta a ${name}`,\n    quoteRevoked: \"Mensagem apagada\",\n    quoteKind: {\n        image: \"Foto\",\n        video: \"Vídeo\",\n        audio: \"Áudio\",\n        voice: \"Mensagem de voz\",\n        file: \"Documento\",\n    },\n    reactions: \"Reações\",\n    react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"pessoa\" : \"pessoas\"}`,\n    deliveredAll: \"Entregue a todos\",\n    deliveredSome: (delivered, total) => `Entregue a ${delivered} de ${total}`,\n    readAll: \"Lida por todos\",\n    readSome: (read, total) => `Lida por ${read} de ${total}`,\n    messageActions: \"Ações da mensagem\",\n    voiceNote: \"Mensagem de voz\",\n    download: \"Baixar\",\n};\n\nconst EN: ChatStrings = {\n    thread: \"Conversation\",\n    today: \"Today\",\n    yesterday: \"Yesterday\",\n    you: \"You\",\n    typingOne: (name) => `${name} is typing…`,\n    typingTwo: (a, b) => `${a} and ${b} are typing…`,\n    typingMany: (n) => `${n} people are typing…`,\n    sending: \"Sending\",\n    sent: \"Sent\",\n    read: \"Read\",\n    failed: \"Failed to send\",\n    retry: \"Try again\",\n    empty: \"No messages yet\",\n    placeholder: \"Write a message\",\n    send: \"Send\",\n    deleted: \"This message was deleted\",\n    edited: \"edited\",\n    replyingTo: (name) => `Replying to ${name}`,\n    quoteRevoked: \"Message deleted\",\n    quoteKind: {\n        image: \"Photo\",\n        video: \"Video\",\n        audio: \"Audio\",\n        voice: \"Voice message\",\n        file: \"Document\",\n    },\n    reactions: \"Reactions\",\n    react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"person\" : \"people\"}`,\n    deliveredAll: \"Delivered to everyone\",\n    deliveredSome: (delivered, total) => `Delivered to ${delivered} of ${total}`,\n    readAll: \"Read by everyone\",\n    readSome: (read, total) => `Read by ${read} of ${total}`,\n    messageActions: \"Message actions\",\n    voiceNote: \"Voice message\",\n    download: \"Download\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n    return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n    date: number,\n    { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n    const strings = chatStrings(locale);\n    const today = startOfDay(now ?? Date.now());\n    const dayMs = 24 * 60 * 60 * 1000;\n    if (date === today) return strings.today;\n    if (date === today - dayMs) return strings.yesterday;\n    return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n        day: \"2-digit\",\n        month: \"short\",\n        year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n    });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n    return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n    });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n    names: readonly string[],\n    locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n    const strings = chatStrings(locale);\n    if (names.length === 0) return null;\n    if (names.length === 1) return strings.typingOne(names[0]);\n    if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n    return strings.typingMany(names.length);\n}\n"],"mappings":"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"}