Files
LlamaCasty/TASKS/task-43-native-alerts.md
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

7.9 KiB
Raw Permalink Blame History

TASK 43 — Native events & alerts (the "go" unit: chat parity + six animation alert box)

Catalog: TASKS.md. Status: ✅ SHIPPED 2026-09-24 — creator said "go" on the combined work unit (native-chat parity + native alert box). See 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.