Files
LlamaCasty/TASKS/task-43-native-alerts.md
T
gramps d4aa588da0 feat(alerts): TASK 43 — native events & alerts (six unique animations) + chat parity
Replace the external StreamElements feed with NATIVE YouTube events per the creator's
2026-09-23 question.

Chat parity half:
- YouTubeChatService decodes ALL SIX liveChat/messages event types into
  ChatMessage.Kind (superChat/superSticker/newSponsor/membershipGifting/
  giftMembershipReceived/memberMilestoneChat) — the four formerly-empty overlay
  rows are real now — and re-arms its one-shot poll on the server's
  pollingIntervalMillis (streamList connection semantics; clamp 1000-6000ms,
  maxResults=2000). ParsePage internal static seam + ChatPage record for
  deterministic tests; optional HttpClient ctor seam kept; InjectSimulatedMessage
  (TASK 41) preserved.

Alerts half (creator rulings: one celebration zone, six UNIQUE animations, no
menus/polls, sub mention = chat row only, NO viewer count):
- New SourceType.AlertBox 'Stream Alerts' (one per layout, CanAddAlerts gate
  mirroring chat; idle = transparent).
- AlertRenderer: six distinct branded animations (SuperChat slide-up/shine/
  count-up, SuperSticker scale-pop, NewMember drop-in/flash, MemberGift slide-left/
  chip-fan, GiftReceived confetti, MemberMilestone rise/growth-bar), every card
  drawing the 'made with LlamaCasty!' brand line.
- AlertOverlayLayer: true component (Commit-G pattern) — queue (cap 10, drop tail
  never stall), 33ms UI ticker, cache-first RenderFrame + UpdatePreview,
  Advance(double) as the deterministic test clock; ChatEventKind.None rows never
  enqueue.
- Wired: resolver RenderAlertBox, _alertLayer ctor + dispose, LoadLayout previews,
  Add menu item (Controls/LeftPanel.xaml), TestSessionViewModel sims tagged
  (member->NewMember, superchat->SuperChat).

Good Dog ONE integration test: AlertLayerTests (RealApp STA, real WPF raster) —
ParsePage classifies all six kinds + cadence fields; None rows enqueue nothing;
six events play pairwise-distinct moving frames then drain to null.

Gate: clean build 0 warnings; full suite 316/317, the one failure
(LayerReorderPersistenceTests.RealMouseDrag) repros on the clean tree — the
known environmental class (real-mouse-drag no-ops with a game/fullscreen window
focused).

References (OBS/overlay ecosystem):
- streamList semantics: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList
- OBS alert-box pattern (designated celebration zone, idle transparent): creator-chosen model
2026-09-24 08:27:00 -07:00

125 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK 43 — Native events & alerts (the "go" unit: chat parity + six animation alert box)
> Catalog: [`TASKS.md`](../TASKS.md). Status: ✅ **SHIPPED 2026-09-24** — creator said
> **"go"** on the combined work unit (native-chat parity + native alert box). See
> [`ai.md`](../ai.md) → "Native events & alerts" for the architecture.
## Provenance (recorded per the feature-provenance rule)
- **2026-09-23, the creator asked** — "Could you use native YouTube events instead of
StreamElements?" (the app was designed to feed an external StreamElements alert box).
Research (websearch + the liveChat/messages + streamList docs, URLs in the commit
message) established the API facts below; the creator then ruled on product shape.
- **Decisions the creator made in conversation (asked ONCE, then held):**
1. **No menus / no polls** — "I've asked you before to not use menus/polls." Decisions
are asked on the open floor once and then executed.
2. **Events = ONE animated alert box** (OBS alert-box pattern), a designated area on
the canvas where celebration animations play. NOT a list pane, NOT per-source.
3. **Six monetization events → SIX UNIQUE animations** — "not cookie-cutter": each of
Super Chat, Super Sticker, New Member (newSponsor), Member Gift, Gift Received,
Member Milestone gets its own distinct gesture, palette and layout. Each animation
is a free branded product-placement ad in the stream (the brand line is drawn into
every card) and a celebration of the supporter's investment.
4. **Free subscribers: NO alert** — "subscriber mention in chat is sufficient."
YouTube exposes no public event for free subs (API fact); a mention arrives as a
normal chat row and is deliberately `ChatEventKind.None` → the alert engine never
fires. The app keeps its opinion: none. (The third-party socket + delta-counter
folklore was explicitly rejected with it.)
5. **Viewer count: dropped entirely** — demotivating and weaponizable against the
creator; not even hidden.
6. **Native chat replaces the SE URI** — the chat box already exists in-app
(`SourceType.ChatBox`, fed by real `liveChat/messages` polling). The remaining
parity gaps closed here: the four previously-undecoded event types displayed as
**empty rows** in the overlay, and the poll cadence was a hardcoded 2s instead of
the server's `pollingIntervalMillis`.
## API facts (researched + cited in commit)
- Native event feed = the **same** `liveChat/messages` endpoint already polled.
`streamList` is a *connection semantics* description of that endpoint, not a separate
call: response carries `pollingIntervalMillis`; reconnect with `nextPageToken` on that
cadence, never faster (YouTube rejects rapid polling). The code now re-arms the
one-shot poll on the server's number (clamped 1000–6000ms), and `maxResults=2000`.
Cite: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList
- Six `snippet.type` event kinds exist: `superChatEvent`, `superStickerEvent`,
`newSponsorEvent`, `membershipGiftingEvent`, `giftMembershipReceivedEvent`,
`memberMilestoneChatEvent`. Free subscriptions emit **nothing** — no API event, no
delta, no count (the "newSponsorEvent counts" folklore is wrong for public subs).
- `videos.list → liveStreamingDetails.concurrentViewers` exists but was **vetoed** by
the creator (decision 4 above).
## Product model (as shipped)
- **`SourceType.AlertBox`** ("Stream Alerts") — a one-per-layout celebration zone,
mirroring the chat layer's gate (`CanAddAlerts` = no AlertBox exists in any scene;
`AddSource` refuses a second). Defaults 620×430 → 680×200. Draggable, previews like a
source, idle = fully transparent (nothing on stream until an event actually fires).
- **Playback:** events from the chat feed enqueue one-at-a-time on a 33ms UI
dispatcher tick, play their own animation (4–5s each), then drain; back-to-back floods
cap the queue at 10 and drop the tail, never stalling playback. The clock is exposed
as `Advance(double)` so tests drive playback deterministically with no timer.
- **The six animations** (`Services/AlertRenderer.cs`), phases enter[0–0.16] /
hold[0.16–0.8] / exit[0.8–1]:
| Kind | Gesture | Palette |
|---|---|---|
| SuperChat | slide-up + gold amount count-up + shine sweep | green `#0d3b2b→#14532d` |
| SuperSticker | scale-pop overshoot, tilted sticker chip, bob | purple `#4a044e→#7e22ce` |
| NewMember | drop-in + white flash band + gold level chip | violet `#2e1065→#5b21b6` |
| MemberGift | slide-left + gift-chip fan `×N` | amber `#422006→#a16207` |
| GiftReceived | confetti burst scale-in | green→purple `#14532d→#4c1d95` |
| MemberMilestone | rise-up + tenure growth bar | orange `#431407→#c2410c` |
- Every card draws **"made with LlamaCasty!"** bottom-right — each broadcast of an event
is in-video product placement (creator decision: "it's a free ad each time").
## Chat parity (half 1 of the unit)
- `YouTubeChatService` rewritten around the real cadence + all six event parses:
`ParsePage(string)` (internal static seam) → `ChatPage(Messages, NextPageToken,
PollIntervalMs)`; fields landed on `ChatMessage`: `Kind` (`ChatEventKind`),
`SuperChatDisplayString`, `SuperStickerDescription`, `GiftNewMembersCount`,
`GifterName`, `MilestoneMonths` (best-effort `(\d+) months` regex from the milestone
chat text). The four formerly-empty event types now parse as real rows AND drive
animations. `InjectSimulatedMessage` (TASK 41) preserved.
## Test (Good Dog — ONE integration test per change)
`ytLive.Tests/AlertLayerTests.cs` (RealApp STA host, real WPF raster) — `AlertLayerTests`:
1. **Parse:** a mocked 7-item feed → all six kinds classified, `None` for plain chat,
`pollingIntervalMillis`/`nextPageToken` honored, fields verified (incl. the
formerly-empty `3 memberships` / `gifted you a membership` rows).
2. **Playback:** subscriber-mention (`None`) enqueues nothing; the six events play
through the queue with non-null, non-blank, *moving* frames; every event's mid-hold
frame is **pairwise distinct from every other** (six unique animations, pinned);
playback drains to transparent/null.
## Files (declared scope)
- `Models/YouTube.cs` (ChatEventKind + fields), `Models/Source.cs` (SourceType.AlertBox
+ IsDraggable/Kind/DisplaySource)
- `Services/YouTubeChatService.cs` (cadence + six-event ParsePage seam)
- `Services/TestSessionViewModel.cs` (sims tagged: member→NewMember, superchat→SuperChat)
- **NEW** `Services/AlertRenderer.cs`, `Services/AlertOverlayLayer.cs`
- `ViewModels/MainViewModel.cs`, `ViewModels/MainViewModel.Chat.cs`,
`ViewModels/MainViewModel.Sources.cs` (wiring: `_alertLayer`, render resolver,
`CanAddAlerts`, menu name, shakedown `LoadLayout`, dispose)
- `Controls/LeftPanel.xaml` (Add → Stream Alerts menu item)
- **NEW** `ytLive.Tests/AlertLayerTests.cs`
- Docs: `ai.md`, `TASKS.md`, this file, `Services/index.md`, `ViewModels/index.md`,
`Models/index.md`, `TASKS/task-03-capture-pipeline.md`, `HANDOFF.md`
## Gate
Clean build 0 warnings (Windows dotnet host); full suite **316/317** — the one failure
`LayerReorderPersistenceTests.RealMouseDrag…` repros on the CLEAN tree (proved via
`git stash`; it is a real-mouse-drag test that no-ops when a game/fullscreen window
steals the mouse — HANDOFF-documented environmental class). Scope check green.
## Open follow-ups (NOT this unit)
- TASK 3 item 17 (Alerts) is DONE via native events; **item 20 stays open** — the
RewardEvent SQLite persistence + session-report rollup + `superChatEvents.list`
backfill were scoped as the capture foundation and remain queued (the shipped alert
path parses events directly into `ChatMessage`, which is what the report reads).
- TASK 3 item 16 (Text source) still open.
- Creator report still open: "I still cannot post a chat message in the TEST tab" —
not yet diagnosed; the TEST drawer's Mock Chat Input posts via a real insert.