/**
 * Shared message store — the messages currently rendered in the list.
 *
 * Selection state used to live here too, with a broadcast-subscribe model
 * that let the viewer subscribe to "selected changed" events independently
 * of the list. That made drift between the highlighted row and the preview
 * pane possible: two subscribers, two render paths, two opportunities for
 * a missed update or stale read. Selection is now owned by message-list
 * (the only thing that has DOM rows in the first place); the viewer is a
 * passive `show(msg)` / `clear()` API the list calls directly. With one
 * code path, drift is structurally impossible.
 *
 * What remains here: the array of currently-loaded messages, plus the
 * removeMessages helper that filters the array and lets the caller decide
 * which row to focus next (if any) — that policy is the list's, not the
 * store's.
 */
let messages = [];
const listeners = [];
export function subscribe(fn) {
    listeners.push(fn);
    return () => {
        const i = listeners.indexOf(fn);
        if (i >= 0)
            listeners.splice(i, 1);
    };
}
function notify() {
    for (const fn of listeners) {
        try {
            fn();
        }
        catch { /* don't let one subscriber break others */ }
    }
}
/** Replace the entire message list (folder load, search, unified inbox). */
export function setMessages(msgs) {
    messages = msgs;
    notify();
}
export function getMessages() {
    return messages;
}
/**
 * Remove messages from the list (after move or delete).
 *
 * Returns a RemovalOutcome the caller uses to drive focus handoff. We
 * pre-capture the next-survivor *by identity* (not by index) before the
 * filter mutates the array, so subsequent index shifts don't matter — if
 * every deleted row had a survivor below it, the chosen survivor is the
 * same regardless of how many got deleted. Falls back to walking
 * backward when the deletion is at the bottom.
 */
export function removeMessages(uids, currentlyFocused) {
    // Key on (account, FOLDER, uid) when a folderId is supplied. IMAP UIDs are
    // per-folder, so in a mixed view (e.g. a search spanning INBOX + Trash) a
    // Trash row can share a uid with a SURVIVING INBOX message — keying on
    // (account, uid) alone then kept the deleted Trash row visible (Bob
    // 2026-06-12: "the trash ones did not disappear"). Legacy callers that
    // don't pass folderId fall back to the loose (account, uid) match — those
    // are single-folder contexts with no collision risk.
    const fullSet = new Set();
    const looseSet = new Set();
    for (const u of uids) {
        if (u.folderId != null)
            fullSet.add(`${u.accountId}:${u.folderId}:${u.uid}`);
        else
            looseSet.add(`${u.accountId}:${u.uid}`);
    }
    const isRemoved = (m) => (m.folderId != null && fullSet.has(`${m.accountId}:${m.folderId}:${m.uid}`)) ||
        looseSet.has(`${m.accountId}:${m.uid}`);
    const focusedWasRemoved = currentlyFocused !== null && isRemoved(currentlyFocused);
    let nextSurvivor = null;
    if (focusedWasRemoved) {
        let lastRemovedIdx = -1;
        for (let i = 0; i < messages.length; i++) {
            if (isRemoved(messages[i]))
                lastRemovedIdx = i;
        }
        for (let i = lastRemovedIdx + 1; i < messages.length; i++) {
            if (!isRemoved(messages[i])) {
                nextSurvivor = messages[i];
                break;
            }
        }
        if (!nextSurvivor) {
            for (let i = lastRemovedIdx - 1; i >= 0; i--) {
                if (!isRemoved(messages[i])) {
                    nextSurvivor = messages[i];
                    break;
                }
            }
        }
    }
    // dupeCount fix-up: if we removed one half of a unified-inbox duplicate
    // pair, the surviving row's ⇆ marker should drop now rather than wait
    // for the next server fetch. Decrement once per remaining row whose
    // messageId matches a removed one.
    const removedIds = new Set();
    for (const m of messages) {
        if (isRemoved(m) && m.messageId) {
            removedIds.add(m.messageId);
        }
    }
    messages = messages.filter(m => !isRemoved(m));
    if (removedIds.size > 0) {
        for (const m of messages) {
            if (m.messageId && removedIds.has(m.messageId) && typeof m.dupeCount === "number") {
                m.dupeCount = Math.max(0, m.dupeCount - 1);
            }
        }
    }
    notify();
    return {
        focusedWasRemoved,
        nextSurvivor: nextSurvivor
            ? { accountId: nextSurvivor.accountId, uid: nextSurvivor.uid, folderId: nextSurvivor.folderId }
            : null,
    };
}
/** Update flags on a message in the list. List re-renders the row's
 *  flag/unread classes inline; no broadcast. */
export function updateMessageFlags(accountId, uid, flags) {
    const msg = messages.find(m => m.uid === uid && m.accountId === accountId);
    if (msg)
        msg.flags = flags;
}
import { subscribeStore } from "./api-client.js";
/** Subscribe to Store-bus flag changes so any source — UI click, server-side
 *  sync, another window — updates this in-memory list. The IPC click→render
 *  path was already optimistic (caller patches `msg.flags` before awaiting),
 *  so this listener is the redundancy that catches the cases where the local
 *  patch and the canonical Store row diverge (server pushed a different flag
 *  set during the round trip, sync rebound the row, etc.). */
subscribeStore("*", (event) => {
    if (event.kind !== "flagsChanged")
        return;
    const accountId = event.accountId;
    const uid = event.uid;
    const flags = event.flags;
    if (!accountId || typeof uid !== "number" || !Array.isArray(flags))
        return;
    const msg = messages.find(m => m.uid === uid && m.accountId === accountId);
    if (!msg)
        return;
    const before = JSON.stringify(msg.flags);
    msg.flags = flags;
    if (before !== JSON.stringify(flags))
        notify();
});
//# sourceMappingURL=message-state.js.map