d4aa588da0
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
7.9 KiB
7.9 KiB
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). Seeai.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):
- 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.
- 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.
- 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.
- 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.) - Viewer count: dropped entirely — demotivating and weaponizable against the creator; not even hidden.
- Native chat replaces the SE URI — the chat box already exists in-app
(
SourceType.ChatBox, fed by realliveChat/messagespolling). 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'spollingIntervalMillis.
API facts (researched + cited in commit)
- Native event feed = the same
liveChat/messagesendpoint already polled.streamListis a connection semantics description of that endpoint, not a separate call: response carriespollingIntervalMillis; reconnect withnextPageTokenon 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), andmaxResults=2000. Cite: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList - Six
snippet.typeevent 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.concurrentViewersexists 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;AddSourcerefuses 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→#14532dSuperSticker scale-pop overshoot, tilted sticker chip, bob purple #4a044e→#7e22ceNewMember drop-in + white flash band + gold level chip violet #2e1065→#5b21b6MemberGift slide-left + gift-chip fan ×Namber #422006→#a16207GiftReceived confetti burst scale-in green→purple #14532d→#4c1d95MemberMilestone 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)
YouTubeChatServicerewritten around the real cadence + all six event parses:ParsePage(string)(internal static seam) →ChatPage(Messages, NextPageToken, PollIntervalMs); fields landed onChatMessage:Kind(ChatEventKind),SuperChatDisplayString,SuperStickerDescription,GiftNewMembersCount,GifterName,MilestoneMonths(best-effort(\d+) monthsregex 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:
- Parse: a mocked 7-item feed → all six kinds classified,
Nonefor plain chat,pollingIntervalMillis/nextPageTokenhonored, fields verified (incl. the formerly-empty3 memberships/gifted you a membershiprows). - 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, shakedownLoadLayout, 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.listbackfill were scoped as the capture foundation and remain queued (the shipped alert path parses events directly intoChatMessage, 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.