From ec48592503eba99ab924232240833cfe7487f1cd Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 00:43:53 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20public-mode=20profile=20shows=20letter?= =?UTF-8?q?=20X=20=E2=80=94=20GQL=20query=20referenced=20authed-only=20fie?= =?UTF-8?q?ld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the X-fallback in the new profile header when the app runs without Twitch credentials ("public mode"): the GQL query in fetchPublicStreamerProfile asked for `broadcasterType`, which exists on the AUTHENTICATED Twitch GQL schema but NOT on the public one. The public endpoint returned `errors[]` with "Cannot query field broadcasterType on type User", which fetchPublicTwitchGql correctly treats as a complete failure and returns null. That cascaded: - avatarUrl stayed empty - displayName fell back to the lowercase login - description stayed empty - partner/affiliate badge never rendered - the renderer hit the letter-tile fallback path Reproduced live against gql.twitch.tv with a curl-equivalent: the exact query worked when broadcasterType was swapped for the public- schema field roles{isPartner isAffiliate}. xrohat correctly comes back as Partner, with the full 150x150 avatar URL, real displayName "xRohat", and 1.25M follower count. The 4.6.18 data-URL fetch fix is still right (Electron renderer img loading against the Twitch CDN was its own minor headache) — it just never got exercised because we never had a URL to fetch in the first place. With this fix the data-URL path now activates on every public-mode profile load, and avatars actually render. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5bfc087..5bf85d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2393,7 +2393,7 @@ interface PublicProfileQueryResult { displayName: string; description: string | null; profileImageURL: string | null; - broadcasterType: string | null; + roles?: { isPartner: boolean; isAffiliate: boolean } | null; followers?: { totalCount: number } | null; } | null; } @@ -2405,6 +2405,13 @@ async function fetchPublicStreamerProfile(login: string): Promise<{ broadcasterType: '' | 'partner' | 'affiliate'; followerCount: number | null; } | null> { + // The public (unauthenticated) GQL schema does NOT expose + // `broadcasterType` directly — querying it returns an errors[] response + // which the upstream helper treats as a complete failure (null data), + // which in turn left the avatar empty and the user's whole profile + // fell through to the fallback letter tile. Use `roles{isPartner + // isAffiliate}` instead, which the public schema does expose, and + // derive broadcasterType locally. const data = await fetchPublicTwitchGql( `query($login: String!) { user(login: $login) { @@ -2413,19 +2420,22 @@ async function fetchPublicStreamerProfile(login: string): Promise<{ displayName description profileImageURL(width: 150) - broadcasterType + roles { isPartner isAffiliate } followers { totalCount } } }`, { login } ); if (!data?.user) return null; - const bt = (data.user.broadcasterType || '').toLowerCase(); + const roles = data.user.roles; + const broadcasterType: '' | 'partner' | 'affiliate' = roles?.isPartner + ? 'partner' + : (roles?.isAffiliate ? 'affiliate' : ''); return { displayName: data.user.displayName || login, avatarUrl: data.user.profileImageURL || '', description: data.user.description || '', - broadcasterType: (bt === 'partner' || bt === 'affiliate') ? bt : '', + broadcasterType, followerCount: typeof data.user.followers?.totalCount === 'number' ? data.user.followers.totalCount : null }; }