Conversation
…om tooltip
IVR badge API returns title field (e.g. "Moderator", "Broadcaster", "Verified",
sub tenure strings) — store {url, title} in _twitchBadges instead of raw URL,
render as data-tooltip so platform-badge custom tooltip picks them up. Extend
selector from .pi[data-tooltip] to also match .bdg-img[data-tooltip].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…edeem events USERNOTICE msg-ids: sub, resub, subgift, submysterygift — each emits a structured message with isSubEvent/isSubGift/isGiftBundle flag. Dedicated renderers (_renderSubEvent, _renderGiftEvent, _renderRedeemEvent) produce platform-less layouts: purple pill for gift bundles with ×N counter, per-gift lines with gift icon + "Gifted a Tier N Sub to Recipient", resub lines with months + streak, redeem lines with reward name placeholder + cost. PRIVMSG with custom-reward-id → isRedeem (reward name N/A anonymously, only UUID exposed); msg-id=highlighted-message → isHighlight accent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…k bg Twitch's vanilla chat silently lifts dark user colors (e.g. DarkRed #8B0000) so they read on dark backgrounds — otherwise "named" CSS colors like DarkRed render nearly invisible. readableColor() clamps HSL L to [0.5, 0.88] (Memoized). Applied at every .un style.color assignment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e GQL _flushColorLookups now queries open Twitch tabs for rendered .seventv-chat-user (or .chat-author__display-name) inline colors first — free, matches exactly what vanilla chat shows (user-set hex included). Only usernames still missing after the DOM phase fall through to the GQL batch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
IRC carries only redeems that require a message body — community-goal contributions, "unlock an emote", and other text-less PubSub events never reach an anonymous justinfan connection. content/twitch.js now runs a MutationObserver over the live Twitch chat DOM and relays any line whose text or className matches a redeem/highlight/community-goal heuristic (username + "redeemed " + reward + optional cost). Sidepanel._handleDomRedeem dedups (5s bucket + username+reward+msg key) and funnels into the existing isRedeem render path. Channel-scoped so redeems from other open Twitch tabs don't leak in. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…OM alive - Reply bodies strip "@username " prefix; emote tag positions are still computed from the ORIGINAL message. Previously we nulled the emotes tag entirely, so subscriber/native Twitch emotes like always43Booty inside replies rendered as plain text. Now we track the stripped prefix length (twitchEmotesOffset) and shift positions in _splitTwitchEmotes. - UC button no longer collapses vanilla Twitch chat (which unmounts .chat-shell and breaks DOM mirroring for redeems, credits, colors). Instead injects a scoped <style> that hides the right-column via width:0 + visibility:hidden. Reversed when UC closes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Optimistic messages seed badgesRaw from a last-known cache entry that can be stale (previous channel, pre-sub session, etc). _upgradeOptimistic previously only added badges when the DOM had none, so stale partial sets (just the sub badge) would stick even when the real IRC echo carried the full set (mod + sub + founder…). Now we always remove and re-render badges on upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… chat Twitch reserves horizontal space for the chat column via BEM modifier classes on .channel-root__player and .channel-root__info (--with-chat suffix). Hiding the right-column alone left that gap in place. Now when UC hides chat we remove the modifier (marking elements with a data-attr for later restore) and run a MutationObserver to re-strip on SPA/channel re-renders. Closing UC restores the modifier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e Sbalit - content/twitch.js: capture-phase click on [data-a-target= right-column__toggle-collapse-btn] short-circuits Twitch's collapse (which unmounts .chat-shell and kills DOM mirroring) and toggles our hideTwitchChatVisually soft-hide instead. Chat stays live. - Community highlights MutationObserver snapshots .community-highlight cards (hype train, gift-sub leaderboard, pinned message) — classifies kind heuristically, relays text to sidepanel via TW_HIGHLIGHTS. Diff hashing collapses spurious re-emits. - Sidepanel renders a compact #highlights-banner above #chat with a per-card pill (locomotive icon for hype train, gift for leaderboard). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…isting IRC entry Previously IRC (custom-reward-id PRIVMSG → "Channel Points Reward" placeholder + user message) and the DOM mirror (real reward name + cost, no message) both emitted separate messages for the same redeem — and the DOM extractor sometimes scraped a multi-chat-line container, concatenating "reward2000mojma98: Veru Veru..." into the reward field. Fixes: - extractRedeem rejects container nodes with >1 chat-line descendant, inline message bodies, or a ":" after "redeemed" in the snapshot text. - _handleDomRedeem looks for a recent (<10s) IRC redeem from the same user and upgrades its reward name/cost in place instead of adding a duplicate message. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_processMentions only consulted _chatUsers, which stays empty for users who were merely @mentioned without having sent a message. Those mentions got no color at all (fell back to default), while mentions of the same user AFTER they spoke looked correct. - Tag every .mention span with data-mention-user so retints find them. - For Twitch mentions missing a _chatUsers entry, enqueue _enqueueTwitchColorLookup — same pipeline (DOM scrape → GQL batch) we already use for chat-line usernames. - _flushColorLookups retints .mention[data-mention-user=…] spans in both the DOM-resolved and GQL-resolved branches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When UC hides the vanilla chat we inject a speech-bubble icon into .top-nav__actions (just left of the user avatar/bell). Clicking it reverses hideTwitchChatVisually → chat reappears and the button removes itself. MutationObserver re-injects on SPA navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous .top-nav__actions selectors didn't match Twitch's current DOM — button never appeared. Now: try a few semantic data-a-target selectors, then walk up from the user avatar / login button to the first short flex row holding multiple siblings. Insert the button immediately left of the avatar wrapper instead of at row start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs in _scrapeExistingChat could silently drop messages from the gap between cache and live IRC: 1. 120s cache-recent skip: any cached message younger than 2 minutes would short-circuit the entire scrape, so messages that arrived while UC was closed for 30–120s never made it in. Removed — dedup downstream (_seenContentKeys / _seenMsgIds) handles overlap. 2. Boundary detection picked the LATEST suffix match in scraped usernames, so when the same user-pair repeats later in chat we'd align the cache to that later position and skip the messages between the two occurrences. Now iterates start→end and picks the earliest match — possible dupes get deduplicated by content key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking the 💾 button now bundles a snapshot of common debugging state into the log so a single download answers most "why is X colored Y" questions without follow-up: - version, channel/config, provider connection state - cache + dedup-set sizes, badge/paint definition counts - per-rendered-Twitch-user color/paint/userId/_fromGQL/_paintChecked - chrome.storage.local.uc_user_colors slim summary - live Twitch DOM color snapshot per open Twitch tab (ground truth) - last 30 cached messages with relevant flags Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three reasons the dump was practically empty: 1. MV3 service worker sleeps after ~30s idle and loses module-scope _logs[] on wake. Now persisted to chrome.storage.session and hydrated before any read/write so logs survive worker restarts. 2. UC_LOG handler expected msg.args but the new diagnostics dispatch sent msg.text — payload was silently dropped. Handler now accepts either field and the relay returns ack. 3. Sidepanel triggered DUMP_LOGS without awaiting the UC_LOG round-trip, so the file write often raced ahead of the diag append. Now awaits. Empty-log dump now writes a one-line note explaining the worker just woke up so users know to reproduce + redump. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + WCAG-aware readability boost
Diagnosed from a debug-log dump: zakk_lh's entry kept showing _fromGQL:false
and entryColor:"#0000FF" no matter how many lookups fired, because every
new IRC PRIVMSG from that user was overwriting the _chatUsers entry with
a fresh {name, platform, color, badgesRaw} object — wiping _fromGQL,
_paint, _paintChecked, userId. Lookup queue refired forever; the rendered
color stayed stuck on raw IRC #0000FF (boosted only to #8B58FF instead of
Twitch's #9999FF).
- Preserve prior entry fields when overwriting on each IRC message.
- Don't downgrade a GQL/DOM-resolved color back to raw IRC color (the
resolved value already includes Twitch's readability adjustment).
- readableColor: use WCAG relative luminance to set the L floor — pure
blue (luminance ~0.07) now lifts to L=0.78 to match Twitch (~#9999FF),
while pure red stays unboosted (luminance ~0.21).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Twitch's redeem line textContent concatenates "<reward>" + "<cost>" with no separator, so "Contribute to Cult's Totem500" arrived as a single string. The previous regex first failed to split on the icon then the fallback grabbed trailing digits as cost — but didn't strip them from the rewardName. We rendered the reward as "Contribute to Cult's Totem500" alongside the correct ⊙ 500 cost pill. Now: extract everything after "redeemed", then peel the trailing digit run (with optional preceding bullet/icon) off as the cost. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_processMentions previously only handled @username mentions. Twitch's vanilla chat also colors bare usernames inside message bodies (StreamElements "sullivan_cz odchází…", quoted handles, etc.). Pass 2 walks remaining text nodes (skipping anything already inside a .mention span), tokenizes on word boundaries (3–25 alphanumeric/underscore), and wraps any token that exists in _chatUsers as a chatter. Restricting to known chatters keeps generic words from getting falsely colored. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
snapshotHighlights queried four overlapping selectors and the same DOM node matched several of them — wrapping container + inner card got emitted as separate cards, so "Pořiďte si předplatné a získejte Drops!" showed three times in our top banner. Now: collect all candidates first, drop any element that's an ancestor of another candidate (keep innermost matches), then text-level dedup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The original Twitch global face emotes ship at a much lower native resolution than channel/subscriber emotes. Scaling them up to our standard chat-emote size made them blurry and pushed them visually out of proportion with vanilla Twitch chat. Detected via the small-integer ID (≤100) inside the jtvnw v2 URL — this only matches Twitch's emoticon CDN so BTTV/FFZ/7TV emotes that happen to share a name (e.g. someone's BTTV ":D") are unaffected (they live on different CDNs). Tagged with .emote-tiny class; CSS shrinks height to 1.25em (1.4em medium / 1.55em large). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous detection used a small-integer ID regex on the jtvnw v2 URL.
That misses face emotes Twitch has reassigned to large IDs in their
modern emote system — most notably <3 (now 555555584) and several
others.
Switched to a name-based set (':)', ':D', ':O', ';)', 'B)', '<3', 'O_o',
…) gated on a Twitch CDN URL — so BTTV/FFZ/7TV emotes that share the
same name still render at full size (they live on different domains).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
We rendered emotes at ~28–32px CSS but were fetching the smallest CDN variant (Twitch /1.0 = 28px, BTTV /1x ≈ 28px, FFZ urls['1'], 7TV 1x.webp). On any hi-DPI display the browser had to upscale, so emotes came out blurry vs vanilla Twitch (which requests larger natives). Switched all providers to prefer the 2x variant (Twitch /2.0, BTTV /2x, FFZ urls['2'], 7TV 2x.webp) with graceful fallback to 1x where 2x isn't published. Browser now downscales — sharp result. .emote-tiny face emotes still shrink to ~20px CSS so quality remains crisp even when /2.0 of an OG face is a server upscale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnosed from the log: roschei had _chatUsers entry {color:"#008000",
_fromGQL:true} despite Twitch DOM ground truth being rgb(0,179,0). Same
user could appear in two shades depending on lookup timing — the IRC
echo path was overwriting prev resolved color whenever msg.color was set.
Two paths needed the fix (only one was patched in 3.28.8):
- The "track color + badges before dedup" block (~5103) used a
keepGqlColor heuristic gated on msg._needsColorLookup. When IRC sent
an explicit color tag, msg._needsColorLookup was false → resolved
color got clobbered. Now: if prev._fromGQL, always keep prev.color.
- _upgradeOptimistic also overwrote with a bare 4-field object losing
prev resolved state. Now spreads prev and uses same preserve rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hover action cluster (copy + reply + pin) sat on top of the message text — covering inline emotes the user wanted to hover for the preview card. Now smaller (20×18 buttons, 11px svg) and offset above the message (top: -14px) so emotes stay reachable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The browser-native title= tooltip popped up on every emote hover and visually competed with our own emote-preview card (which already shows the name + source label). Dropped — alt= remains for accessibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. GQL fallback set _fromGQL=true even when GQL returned no color and we were keeping the raw IRC hex from prev. _enqueueTwitchColorLookup then skipped that entry forever, leaving roschei et al. stuck on #008000 even though Twitch DOM had rgb(0,179,0). Now: only mark _fromGQL when GQL actually produced a color. 2. Existing chrome.storage.local.uc_user_colors entries from earlier buggy builds carry the bad combo (raw hex + _fromGQL true). On hydrate, sanitize: drop _fromGQL when color is still a #rrggbb so the lookup queue re-attempts on the next message. 3. Emote heights bumped (1.75em → 2em small, 2em → 2.25em medium, 2.2em → 2.5em large) — vanilla Twitch renders ~28px chat emotes and our previous height visibly read smaller side-by-side. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raid notifications used a thin red border-left + small right-side pill, easy to miss in fast chat. Now styled like Announcements: bilateral gradient border (orange→red→yellow), glow, and a header bar with pulsing rocket icon, RAID label and viewer count pill. /uc mock commands rewritten to cover everything we render: raid (with viewer count), announcement [PRIMARY|BLUE|GREEN|ORANGE|PURPLE], sub, resub (with months + streak), subgift, giftbundle [N], redeem [name] [cost], highlight, timeout [s], ban, delete. raider/first/sus kept. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Twitch sends msg-param-sub-plan="Prime" for free Prime subs; we mapped it to "Tier 1" via the lookup default. Now: when subPlan is Prime, the sub event gets a crown SVG (instead of star), the body reads "Subscribed with Prime." (gradient cyan/blue), and the row uses Twitch Prime's signature blue accent + glow. /uc prime mock added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ier-tinted accents Tier 1 sub previously rendered as a thin purple left-border + light bg; Prime got the prominent gradient/glow treatment in 3.30.1 so paid subs looked underwhelming next to it. Now all sub events use the bilateral border + glow + tinted icon callout style — Tier 1 stays purple, Tier 2 picks up cyan, Tier 3 picks up gold, Prime stays Twitch-Prime blue. /uc sub2 and /uc sub3 mocks added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.8 diagnostic Pin dump odhalil skutečnou Twitch pin footer
strukturu:
<p>
<span>[3 badge imgs]</span>
<span class="chatter-name">
<span class="CoreText">
<span style="color: rgb(218, 165, 32);">Meewile</span>
</span>
</span>
odesláno v 08:34 PM
</p>
Author je UVNITŘ time container, ne vedle. v3.38.9 secondary
heuristic hledal timeEl v span/a only + walked previous siblings.
Ale timeEl je <p>, sibs.indexOf(<p>) v spans/a = -1 → loop
přeskočen → authorEl zůstal null.
Fix:
1. Accept <p>/<div>/<span>/<a> jako timeEl kandidáta.
2. Primary: search INSIDE timeEl pro styled short-text span,
prefer leaf-first (innermost colored span = exact author).
3. Fallback: walk previous siblings (pro layouts kde author
je next to time, ne uvnitř).
Badges path z v3.38.9 by pak fungoval — hop up z authorEl
dokud nenajde flex/grid row a scan badge imgs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
….11) User report: pin banner se neukáže když vanilla Twitch chat je collapsed (ikdyž v UC panel by user chtěl vidět). Root cause: Twitch v collapsed mode nerenderuje .community-highlight-stack__card full cards, místo toho compact "!N" badge na video playeru. Content script HIGHLIGHT_SELECTORS nenašly nic. Změny: 1. Přidané selektory: [class*="community-highlights"], [data-a-target="community-highlights"], .pinned-chat__highlight-card. 2. Rate-limited diagnostic (1× / 30s) — když snapshotHighlights nenajde nic ale body obsahuje pin/highlight-related elementy, dumpne outerHTML prvních 6 kandidátů jako HighlightDiag log. Další iterace použije skutečnou collapsed-mode DOM strukturu. Follow-up plán: až budeme mít HighlightDiag dump, extractor se adaptuje. Long-term možnost: GQL fallback přes CHECK_PIN handler co už existuje (channel.pinnedChatMessages query), funguje bez ohledu na chat collapsed state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…8.12) Pin diagnostic dump z v3.38.8 Cohh session ukázal: pinnedBy="Suzunahara" ✓ bodySegs=9 ✓ author=null ✗ authorBadges=0 ✗ timeText=null ✗ html=<div class="pinned-chat__highlight-card__collapsed ..."> Root cause: v collapsed stavu (chat hidden/collapsed) Twitch vůbec nerenderuje author footer v pin card — jen pinner + body. Extractor nemá co vytáhnout. Fix: content script před extract detekuje __collapsed class + klikne expand button (aria-label rozbalit/expand nebo aria-expanded=false) real-event sequence (pointerdown/mousedown/ pointerup/mouseup/click) — obejde Vue synthetic click resistance. Twitch card expanduje → author row mountne → extractor dostane full data. _ucExpanded flag prevent re-click cycling. Sidepanel render už byl graceful (footer skip když !author && !timeText), takže ani nouzovému case nevadí. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: chat by neměl být collapsnutý, jen schovaný — všechno by se mělo renderovat. Správná diagnóza: width:0 / min-width:0 / max-width:0 triggerovalo Twitch interní ResizeObserver na chat column. Když Twitch detekoval 0-width, přepnul rendering režim: - Pin cards → .pinned-chat__highlight-card__collapsed (jen pinner + body, bez author footeru) - Community highlights → compact "!N" badge na playeru místo full cards Mirror scraper neměl co vytáhnout. Fix: místo width:0 používáme position:fixed + right:-9999px + height:100vh. Element je mimo viewport (uživatel nevidí) ale offsetWidth zůstává na natural hodnotě (~340px) — Twitch's collapse detection nefire-uje, takže všechno se rendruje full-mode jako by chat byl otevřený. visibility:hidden + pointer-events:none zajišťují že hidden chat neblokuje klikání na to co je pod ním. v3.38.12 expand-click workaround zůstává jako fallback pro scénář kdy user má vanilla "Sbalit" aktivní. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Correct fix for "pin se nezobrazuje když chat je schovaný":
User's insight: pin zpráva EXISTUJE na Twitch serveru bez ohledu
na to, jestli UI komponenta je mountovaná. Nespoléhat na DOM
scraper — fetch pin data přímo z Twitch GQL.
Implementace:
1. Background: nový FETCH_PINS handler volá
channel.pinnedChatMessages GQL query. Vrací:
- pinnedBy (displayName / login)
- sender (displayName, login, chatColor, id)
- senderBadges (setID + version, mapované na URL v sidepanel
přes _twitchBadges cache)
- content.fragments (text + emote IDs → CDN URL)
- pinnedAt timestamp
- pinId
2. Sidepanel: _startPinPoll() volá FETCH_PINS každých 8s,
transformuje pins do highlight card formátu a pushuje jako
kind:'pin' do highlights banneru. _handleHighlights merguje
DOM cards (hype-train/gift/raid) + GQL pins. DOM pin cards
jsou filtrované out jako redundantní.
3. Channel switch resetuje _gqlPinCards + _lastDomHighlightCards.
4. Hide CSS vrácena na stabilní width:0 z v3.27.4. Pin data
už nezávisí na tom, co Twitch UI zrovna mountuje — funguje
i když je chat column "collapsed".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnostic Pin log z v3.38.14 (díky FetchPins GQL errors log):
Cannot query field "pinnedAt" on PinnedChatMessage
Cannot query field "sender" on PinnedChatMessage
Cannot query field "senderBadges" on PinnedChatMessage
Cannot query field "content" on PinnedChatMessage
Cannot query field "emoteID" on Emote
Cannot query field "bitsAmount"/"tier" on Cheermote
Schema guess byl špatný. Skutečné schéma:
PinnedChatMessage {
id, startsAt (NE pinnedAt!), endsAt,
pinnedBy { id, login, displayName },
message { # nested!
id,
sender { id, login, displayName, chatColor },
content {
text,
fragments {
text,
content {
... on Emote { id, token } # NE emoteID
... on Cheermote { prefix } # no bitsAmount/tier
}
}
}
}
}
Query rewritten, mapper pulluje z node.message.*. Badges no longer
direct on PinnedChatMessage — sidepanel může padnout zpět na
_twitchBadges cache via sender login (recent-chat mapping).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.15 log: "Cannot query field 'message' on type
PinnedChatMessage" — ani message field neexistuje, nejen sender/
content.
Instead of další guess-work nad GQL schema, poslat introspection
query jednou per session: __type(name: "PinnedChatMessage") {
fields { name type { ... } } }. Výsledek se loguje jako Pin tag —
uvidíme VŠECHNA skutečná pole.
Minimal working query teď: id / startsAt / endsAt / pinnedBy
(confirmed funkční z předchozích diagnostic). Sender + content
fields přidáme v další iteraci podle introspection výstupu.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.16 log ukázal že Twitch zakázal GQL introspection:
__type(name: "PinnedChatMessage") vrátil {"data":{}}.
Probe loop iteruje 18 candidate field names — každý v
samostatném query. Pole co selže s "Cannot query field"
neexistuje; pole co projde (nebo vyžaduje subselection) se
loguje jako PROBE EXISTS.
Kandidáti: type, text, body, contentText, rawText, html,
messageID, messageId, pinnedMessageID, chatMessageID, sender,
user, author, fromUser, sourceUser, fragments, emotes.
Runs once per session. Další iterace: podle PROBE výsledků
postavíme funkční query pro content + author.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3.38.18)
v3.38.17 probe log ukázal že všech 17 kandidátů vrátilo:
"Variable \"n\" has invalid value null" — moje query použila
$n ale variables měly { name: channel }. Všechny queries tedy
failovaly PŘED schema check, žádná užitečná info.
Fixy:
1. Query variable přejmenován na $name (match s variables obj).
2. Error classifier: "Cannot query field" = field neexistuje
(skip), "requires subselection" = object type (log EXISTS),
čistý success = scalar (log EXISTS + sample).
Next iteration: podle PROBE výstupů postavíme query na sender /
content nebo ekvivalent.
Na dotaz "není toto v Twitch docs": Twitch GQL je NEdokumentované
interní API — jen Helix REST je public. Nejspolehlivější schema
zdroj je Chrome DevTools Network tab na Twitch UI (filter gql,
find pin-related operation, copy query).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.18 probe confirmed: PinnedChatMessage v anonymní GQL má jen scalar fields (id, startsAt, endsAt, type="MOD") + pinnedBy. Žádné sender, content, message reference. Merge strategy: - DOM mirror (TW_HIGHLIGHTS) má přednost — full sender, content, emotes, badges. Platí když chat je visible. - GQL fallback — jen metadata (pinner + time + "Pinned" placeholder body). Platí když chat je hidden a DOM pin stack je unmountnutý. Pokud chcete full content při hidden chat: potřebujeme autoritativní Twitch GQL schéma. User může zachytit z Network tab na Twitch UI (filter "gql" → pin-related request → copy payload) a pošleme správnou query. Probe loop odstraněn — known results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User captured the real Twitch web UI response from Network tab.
Real schema (operation: GetPinnedChat):
PinnedChatMessage {
id, type, startsAt, endsAt, updatedAt,
pinnedBy { id, displayName },
pinnedMessage { # <-- NOT "message"
id, sentAt,
content {
text,
fragments {
text,
content { ... on Emote { emoteID } }
}
},
sender {
id, chatColor, displayName,
displayBadges { setID, version }
}
}
}
Query přepsaná na real schema. Mapper pulluje:
- segments z fragments (emote URLs z emoteID)
- sender.chatColor pro author color
- sender.displayBadges → _twitchBadges cache pro URLs + titles
- sentAt jako timestamp
Pin banner má teď full obsah (pinner, author, badges, colored
name, text s emotes, timestamp) bez ohledu na to, jestli je
vanilla chat visible nebo skrytý.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log: "Cannot query field 'emoteID' on type Emote".
User's browser DOES get emoteID — Twitch UI auto-adds
Client-Integrity header. Our anonymous fetch doesn't → whole
query fails → NO data (not just emote — sentAt, sender, badges
too). That's why v3.38.20 UC banner was still empty.
Fix:
1. Drop `... on Emote { emoteID }` spread. Keep only
`__typename` on fragment content. Query survives, sentAt +
sender + displayBadges arrive correctly.
2. Sidepanel resolves emote URL by NAME against local maps
(twitchNative / channel7tv / global7tv / bttvEmotes /
ffzEmotes). Unknown emote → render as plain text fallback.
3. Poll interval 8s → 4s. Plus visibilitychange listener fires
immediate tick on panel focus so tab switching shows fresh
pin instantly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs shown in user's screenshots:
1. STACKING DUPLICITY (15+ "Připnuto uživatelem Suzunahara"
cards piled up after ~1 min). _rerenderHighlights merged
_lastDomHighlightCards + _gqlPinCards. _handleHighlights
then stashed merged result back as _lastDomHighlightCards.
Each 4s poll tick = one more pin copy accumulated.
Fix: _lastDomHighlightCards holds ONLY non-pin cards
(filter). Pins come fresh each tick from either DOM or GQL
source, never from cache. Plus _rerenderTag identity so
rerender msgs skip the caching block entirely.
2. MOD BADGE 2× in author footer. DOM extractPinDetails walked
authorEl.parentElement up until flex/grid container — but
Twitch's card root is flex, which includes BOTH the pin
header (pinner + pinner's mod badge) and footer (author +
3 author badges). 4 total imgs collected, first mod appears
twice.
Fix: authorRow = authorEl.closest('p') — strictly scopes
to the footer <p class="jPfhdt">. Plus hard dedup on badge
src URL as a belt-and-suspenders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: - Pin se "collapsne co pár vteřin" (banner se re-mountoval každé 4s pin poll, ztrácel user's chevron toggle click) - Po reloadu addon: emoty + badges chybí, až po chvíli naskočí Fixy: 1. Idempotent rerender v _handleHighlights. Hash zahrnuje visible data (kind/text/pinId/author/color/badges urls/ segment types+URLs/timeText). Pokud hash matches předchozí, SKIP DOM re-mount. Žádný flicker, žádný collapse reset. 2. Preserve collapsed state přes re-render. Při teardown: iterate přes existující pin wraps, collect (pinKey → collapsed boolean) do _pendingPinCollapse. V _buildPinCard: restore — pokud wasCollapsed === false, user ho expandnul, ne-nastavuj collapsed; jinak default collapsed. Emotes resolve issue: anonymní Twitch GQL nevrací subscriber emotes (Client-Integrity gate) → pin body segments mají jen text name, ne URL. Fallback: DOM scraper (po pár vteřin až chat subtree mountne) nebo IRC learning dodá real URLs. Display transition plaintext → full emotes je accepted tradeoff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log ukázal: [Pin] extract missing=author with class
"pinned-chat__highlight-card__collapsed". Twitch auto-flipuje
pin card mezi expanded/collapsed każdou pár vteřin. Collapsed
stav nemounne author footer → DOM extract vrátí author=null,
badges=0, timeText=null.
Předchozí DOM-wins logika pak downgradeovala banner na
partial DOM místo GQL fallback. Z plných emotes+badges dolů
na "cohhWow YOU YES YOU... CohhilitionBot odesláno v 18:10".
Nový _mergePinCard(domCard, gqlCard):
for each field (pinnedBy, author, authorColor, authorBadges,
bodySegments, timeText):
use DOM this tick if non-empty
else use _lastGoodPinCache
else use GQL fallback
_lastGoodPinCache refresh only when DOM extract had complete
footer (author + timeText + bodySegments all present).
Tím: jednou získaná expanded data drží se přes další collapse
cykly. Emote URLs z DOM zůstávají. GQL author/badges vyplní
collapsed DOM mezery.
Channel switch resetuje _lastGoodPinCache + _lastHighlightsHash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: "druhý render je nejlepší (badge + emoty), jen mu to trvá než se objeví". Root cause: DOM TW_HIGHLIGHTS příjde až když MutationObserver v content script tick-ne na changes — může být pár sekund po boot než pin card se objeví / mění. Mezitím sidepanel rendruje jen GQL fallback. Fix: proactive scan na demand. 1. Content script: nový SCAN_HIGHLIGHTS_NOW handler (forcibly resets lastHighlightHash + calls relayHighlights). 2. Sidepanel: _kickDomHighlightScan() sendMessage do všech Twitch tabs. Volá se: - na _startPinPoll boot - každý 4s pin poll tick - na visibilitychange → visible Round-trip content→sidepanel je ~10-30ms vs. několik-sekundový wait na next MutationObserver tick. Expanded DOM pin (s plnými emotes + badges) se objeví okamžitě. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…v3.38.26) User report: "emoty probliknou a hned zmiznou!". Root cause: _rerenderHighlights vytvářelo msg.cards = [...non-pin, ...gqlPinCards]. _handleHighlights pak filtroval kind:'pin' a dostal GQL pin **jako by byl DOM pin**: const domPins = domCards.filter((c) => c?.kind === 'pin'); // ^ contained gqlPin, not actual DOM pin! const mergedPin = this._mergePinCard(gqlPin, gqlPin); // Both args identical → bodySegments picked from d (=gql) // plaintext names, overriding cached DOM URLs. Každý 4s rerender tick → banner downgrade na plaintext i když cache měla plné DOM URLs. Fix: 1. _rerenderHighlights posílá msg.cards = [...non-pin] only — NO GQL pins injected. 2. _handleHighlights pulluje gqlPin vždy přímo z this._gqlPinCards[0] (separate source od DOM channel). 3. isRerender guard prevent _lastDomHighlightCards overwrite během rerender tick (kde msg.cards neobsahuje žádné DOM data). Timing teď: - T=0: GQL → render 1 (plaintext, cache empty, DOM not yet) - T=50ms: DOM → render 2 (FULL URLs, cache fresh) - T=4s tick: rerender se jen non-pin cards, merge pulls cached DOM URLs → render stays FULL. Identical hash → no re-mount. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Po sérii iterací (v3.38.0 → v3.38.26) user explicit potvrdil že pin banner funguje. Požádal o "checkpoint a ne že to budeš měnit". Memory lock: - checkpoint_v3_38_26_pin_stable.md — detailní stable flow (fetchPins query shape, _mergePinCard per-field order, _rerenderHighlights semantic, _lastGoodPinCache conditions, isRerender guard, emote resolve strategy, per-channel caps, proactive DOM scan) - MEMORY.md index: 🔒 marker pro prioritní načtení v další session - CLAUDE-HISTORY.md Changelog: záznam o zamčení Pravidla pro další session: 1. Číst checkpoint PŘED jakoukoli změnou pin komponent 2. NIKDY neměnit: fetchPins query struktura, _mergePinCard order, _rerenderHighlights cards (jen non-pin), cache update conditions, isRerender guard 3. Pokud user chce změnu pin flow, EXPLICITNĚ se zeptat zda chápe riziko regrese Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: "Udělat jméno lépe čitelné, některé barvy jsou blbě čitelné... celkově tu pinnutou zprávu více fancy, víc výrazně." Změny JEN vizuální — core pin flow (v3.38.26 stable) se nemění. CSS: - Bilaterální gradient border-image (zlaté tóny) - Radial + linear gradient pozadí - Top highlight line přes .hl-card.hl-pin::before - Layered glow box-shadow + 5s pulse animace (hl-pin-glow) - Pulsing pin icon — 2.6s scale + drop-shadow (hl-pin-icon-pulse) - Author footer divider — fading amber line přes ::before - Badges wrapped v semi-transparent chip container - Timestamp jako pill s borderem a amber-tinted backgroundem - Body text — větší (14.5px), stronger shadow, 1.7em emotes - Hover state pro btny: translateY(-1px) + warm glow - Head label uppercase s letter-spacing JS: - readableColor(authorColor) na .hl-pin-author — tmavé hex jako #B22222 se zesvětlí pro guarantee contrast proti amber pozadí (stejný algoritmus co používáme pro chat usernames) - Plus stacked text-shadow pro universal readability napříč všemi barvami Core flow (_mergePinCard, _rerenderHighlights, _handleHighlights, _lastGoodPinCache, isRerender guard, GQL query struktura) zůstává v3.38.26 stable — respekt user pokynu "nerozhrabávej". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: badges + author name + timestamp teď všechno na jedné řádce (předtím timestamp wrap-oval pod). Fix: flex-wrap: nowrap na .hl-pin-foot, ellipsis truncation na .hl-pin-author (pro extrémně dlouhé nicky), flex-shrink: 0 na timestamp pill aby se nezmačkl. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: "chci aby to jméno nebylo zkráceno, klidně použijme to místo pod obrázkem piny". - .hl-pin-body padding-left 42px → 14px (recover the empty gutter under the pin icon) - .hl-pin-author: dropped ellipsis truncation, now nowrap + flex-shrink: 0 → full name renders no matter how long. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User request: "udělej zase mockup command /uc pin 'Obsah pinnuté zprávy'". Pushes a mock pin card into _gqlPinCards + triggers _rerenderHighlights — goes through the real merge pipeline so the visual matches actual pins 1:1. Body tokenization on word boundaries with emote name resolution (channel 7TV → global 7TV → BTTV → FFZ → Twitch native) so mocks with real emote tokens (cohhWow, etc.) render as images. Pinner + author = current user; color picked from _platformColors.twitch; badges from _myBadgesCache mapped via _twitchBadges. Timestamp = current local HH:MM. Plus 'pin' entry in UC_CMDS autocomplete + help message. Core pin flow (v3.38.26 lock) zůstává nedotčený — jen injection source. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.30 bug: args v _handleUcCommand je raw string (text
po "/uc "), ne array. args.slice(1).join(' ') vracel
".slice" na stringu (= text s prvním znakem odstraněným) —
body byl vždy empty nebo zmrzačený.
Fix: parts.slice(1).join(' ') — parts je split array
vytvořená na začátku _handleUcCommand.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.38.31 stále nefungoval. Root cause: mock pushed do
_gqlPinCards → _rerenderHighlights → _mergePinCard:
domPin = undefined (rerender path nezahrnuje pins)
gqlPin = mockPin
cache = předchozí reálný pin (cohh)
pick priority: DOM → CACHE → GQL. Cache měl full data z
reálného pinu před mock → cache won, mock was never rendered.
Fix: injekt mock jako DOM-highlights card přes direct
_handleHighlights({cards:[mockPin, ...cached-non-pin]}) +
_lastHighlightsHash = '' bust. Non-rerender path:
domPins = [mockPin]
isRerender = false
_mergePinCard(mockPin, gqlPin) — DOM wins everything
cache refreshes from mock (all fields present)
banner rendruje mock
Příští real DOM scan nebo pin poll mock nahradí — to je
expected pro mock command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock /uc pin teď není destruktivní vůči real pinu. Place do separate _mockPinCards array; banner stackne real (merged DOM+GQL) + mock pin samostatně. Real pin polling pokračuje normálně, mock auto-expire po 30s. Channel switch resetuje _mockPinCards + clearTimeout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
….38.35)
Diagnostic dump z UC-pinned zprávy ukázal:
<p class="iQZxnA">
[textNode "Připnuto uživatelem "]
<span><div><img mod-badge></div></span>
[textNode "Jouki728"]
</p>
<p class="pinned-chat__message">peepoHey ⠀</p>
V card-root div je textContent = "Připnuto uživatelem Jouki728peepoHey ⠀"
(38 chars, pod 60 limit). Greedy regex [\p{L}\p{N}_-]+ zachytil
"Jouki728peepoHey" jako pinnedBy → bug "Jouki728pee...".
Fix #1 (pinnedBy):
- Scan DIRECT child text nodes elementu (skip descendant text)
- Pokud první přímý text matchuje label, pinner je extracted z
same-element direct text za labelem
- Tight regex [A-Za-z0-9_]{2,25}\b (Twitch username chars + length cap)
Fix #2 (body):
- Selector [class*="pinned-chat-message"] (hyphen) NEnematchoval
real Twitch class "pinned-chat__message" (double underscore)
- Přidány .pinned-chat__message + [class*="pinned-chat__message"]
- Body extract teď najde correct element s emote fragments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User report: 1. peepoHey emote nezresolvněn (zobrazí se jako text) 2. Ostrý okraj místo zaoblených rohů Fix #1 (emote resolve): Twitch's pin DOM keeps body as <span class="text-fragment">peepoHey ⠀</span> plain text. 7TV/BTTV/FFZ Vue replacer doesn't penetrate pin subtree (only chat-line containers). Body segments arrive as text type. _buildPinCard teď tokenizuje text segmenty (split na whitespace) a resolve každý token proti emote library order: twitchNative → channel7tv → global7tv → bttvEmotes → ffzEmotes → kickNative → ucEmotes. Resolved emotes render jako <img>, unresolved fallback na linkified text. Plus strip UC marker (Braille blank ⠀). Fix #2 (rounded corners): border-image: linear-gradient(...) v Chrome ignoruje border-radius (spec implementation-defined behaviour). Rohy byly ostré navzdory border-radius: 10px. Nahrazeno: solid amber border + multi-layer box-shadow (combination outer glow + inner ring + bottom-edge accent) which together replicate the gradient ring effect WHILE border-radius drží. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: peepoHey stále renderoval jako text. Bug: emote maps (channel7tv, global7tv, bttvEmotes, ffzEmotes, twitchNative, kickNative, ucEmotes) ukládají URL **string přímo**: this.channel7tv.set(emote.name, url); this.bttvEmotes.set(e.code, `https://cdn...`); Můj v3.38.36 resolver dělal entry?.url — accessing .url na string vrací undefined. Falsy check failed → emote rendered jako text vždy. Fix: const url = resolveEmote(name); if (typeof url === 'string' && url) — rendrue img s tím URL. Stejný fix v /uc pin mocku. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User: po klik UC pin button se objevily 2 karty — jedna stará generic, druhá nová fancy. Root cause: 2 paralelní pin paths. - Legacy v3.37.0: PIN_MESSAGE mutation → _showPinnedBanner → separate #pinned-banner element + _startPinWatcher - v3.38.x: FETCH_PINS poll → _gqlPinCards → #highlights-banner Oba renderovaly stejný pin paralelně. Fix: po úspěšné PIN_MESSAGE mutation: 1. _hidePinnedBanner — schovat legacy element 2. Trigger immediate FETCH_PINS (instead of waiting up to 4s for next poll tick) 3. Pin se objeví jen v fancy #highlights-banner Plus refactor: _pinFromGql(p) extracted as method (jeden zdroj pravdy pro GQL pin → highlight card transform). Volá ho periodic poll tick i post-mutation fast-fetch. Plus opraveno entry?.url → typeof url === 'string' (v3.38.37 fix consistent applied here). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Big release — 121 commitů od posledního master deploye (v3.26.4). Hlavní oblasti:
🛡️ Stability
requestIdleCallback, lazy-load 250 zpráv při bootu, scroll-up načte další 150 batches📌 Pin systém (v3.38.0–v3.38.38)
GetPinnedChatfallback — pin se zobrazí i když je vanilla chat hidden_lastGoodPinCachedrží gold standard data)#pinned-banner→#highlights-banner)/uc pin [text]mock command (stackuje s real pinem)🎨 Twitch events + UI
Cheer{N}→ tier emote + colored bit count)🖱️ UX
⚡ Perf
SCAN_HIGHLIGHTS_NOW) — 10-30ms místo waiting na MutationObserver_bootMarkper fáze + heap size)🐛 Misc
extension/backup.html) — export/import storageTest plan
/uc pin Test→ mock card stackuje s reálným🤖 Generated with Claude Code