# `MishkaCmsCoreWeb.Live.Admin.Components.ActivityFeed`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core_web/live/admin/components/activity_feed.ex#L5)

Real-time activity feed driven by a pluggable list of sources, rendered as a
collapsed "Logs" toggle over a System Logs panel.

Source adapters implement `MishkaCmsCore.Activity.Behaviours.Source`. Each adapter
knows its own topics, broadcasts, entry shape, and initial fetch — this
component just composes them.

## Usage

Wire a page with `setup/2` and render it with `feed/1`; see `setup/2` for the
options and the ordering rule. A page with a collapsible panel of its own pairs
the two with `close_panel/2`, so only one of them is open at a time.

## Behaviour

  * The panel is collapsed on load. The toggle shows/hides it client-side with
    `Phoenix.LiveView.JS` (a fade/slide transition), and pushes to the server so
    the first open can fetch.
  * History is fetched on the FIRST open, not on mount — a page carrying a feed
    pays nothing for it until asked. Skeleton rows stand in while the fetch runs.
  * The panel does not page. It shows one window of `:limit` rows and links out
    to the full activity page, which pages properly — asking a reader to manage
    a growing preview and a full list at once was the worse of the two.
  * Live broadcasts arriving before the first open are buffered and merged into
    that first fetch — sources with no history (`RuntimeCompile`) only ever
    appear live, so dropping them on the opening fetch would lose them.

## Parent responsibilities

The component runs in the parent LiveView's process, so PubSub messages
arrive at the parent's mailbox. `setup/2` handles this; a parent driving the
component directly must:

  1. On `mount`, subscribe to `topics_for/2,3`.
  2. Forward incoming `%Phoenix.Socket.Broadcast{}` messages to this
     component via `send_update/3`, under `:broadcast`. Use
     `handles?/2` to filter.

See also: `MishkaCmsCore.Activity.Sources.PaperTrail`,
`MishkaCmsCore.Activity.Sources.Oban`.

# `close_panel`

JS that closes the feed's panel: the client-side hide, plus the push that tells the feed its panel
is shut so its toggle stops reading open.

A page with a panel of its own chains this onto that panel's toggle, and passes the reverse to
`feed/1`'s `:on_toggle`, so opening either panel closes the other. Both halves are unconditional:
hiding an already-hidden panel does nothing, so neither toggle has to know which panel is open.

    <ActivityFeed.feed setup={@activity_feed} on_toggle={close_issues()} />

    defp toggle_issues(setup) do
      %JS{}
      |> ActivityFeed.close_panel(setup)
      |> JS.toggle(to: "#issues-panel")
      |> JS.push("toggle_issues")
    end

Takes the `@activity_feed` map from `setup/2`, whose `:id` names both the panel and the feed's own
root element — the push is aimed at that root, so it reaches the component rather than the page.

# `feed`

Renders the feed from the `@activity_feed` map produced by `setup/2`. The
scope (`resources`/`sources`) is fixed by `setup/2` so the subscription and
the display always agree; only `:limit` may be overridden per call.

    <ActivityFeed.feed setup={@activity_feed} />
    <ActivityFeed.feed setup={@activity_feed} limit={10} />

## Attributes

* `setup` (`:map`) (required)
* `limit` (`:integer`) - Defaults to `nil`.
* `on_toggle` (`Phoenix.LiveView.JS`) - JS to run when the feed's toggle is clicked, ahead of the panel's own transition. A page with a
  panel of its own passes the JS closing that panel, so only one is ever open; `close_panel/2` is
  the mirror image, for that panel's toggle to close the feed.

  Defaults to `%Phoenix.LiveView.JS{ops: []}`.
## Slots

* `toggles` - Buttons that sit beside the feed's own toggle, on its row. A page with its own panel puts its
  toggle here rather than beside the feed: the feed is full width so its panel is, and wrapping it
  in a column to make room would narrow the panel too.

# `handle_async`

A failed fetch still opens the feed to whatever arrives live.

The history could not be read, but broadcasts keep coming, and they are the only entries this feed
will ever have. Leaving `loaded?` false would buffer every one of them into `:pending` while the
header went on counting them — a list that says "3 entries" and shows none.

# `handle_event`

Closes the panel without regard to what it was doing, so a page that closed it client-side to make
room for its own panel can say so. Unlike `"toggle_logs"` this is not a flip: a page fires it on
every one of its own opens, whether the feed was open or not.

# `handles?`

True when at least one source claims ownership of the broadcast. Use in
the parent's `handle_info/2` to decide whether to forward.

# `setup`

One-call wiring for a parent LiveView. Subscribes to the activity topics for
`opts[:resources]` and attaches a `handle_info` hook that forwards matching
broadcasts to this component — the page's own `handle_info` clauses are left
untouched (resource notifications pass through so the table still updates;
Oban broadcasts are consumed here so they never reach an unmatched clause).

Because that hook halts the Oban and runtime-compile broadcasts, any hook of
the page's own that needs them (`RuntimeStatusLive.setup/2`,
`PreviewHelpers.setup/1`, a site-health hook) must be attached BEFORE this one.

Call it in `mount/3` after `current_user` is assigned, then render the feed
with `feed/1`, passing the single `@activity_feed` assign it sets:

    socket =
      ActivityFeed.setup(socket,
        resources: [MishkaCmsCore.Runtime.Component],
        feed_id: "components-activity"
      )

    # in the template:
    <ActivityFeed.feed setup={@activity_feed} />

## Options

  * `:resources` — modules to scope to; omit for a site-wide feed.
  * `:feed_id` — this component's id (default `"activity-feed"`).
  * `:sources` — source adapters (default: all built-in — PaperTrail, EventLog,
    Oban, RuntimeCompile, RuntimeDraft).
  * `:limit` — how many rows the panel shows (default `5`). A
    template may override it per call via
    `<ActivityFeed.feed setup={...} limit={10} />`.

# `topics_for`

PubSub topics the parent should subscribe to so the feed receives live
updates. Pass the same `current_user`, `sources`, and `resources` you
pass to the component itself.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
