From eb1fa02df687d2724f04b7eec6f37c9bf7465097 Mon Sep 17 00:00:00 2001 From: FlaminSarge Date: Mon, 16 Dec 2024 07:18:56 -0800 Subject: [PATCH] Implement rudimentary redirect banner Supports send and receive, uses the urlEndpoint or watchEndpoint link to open a new tab with the given url if the button is clicked This also adds support for bold text (but not deemphasized text yet) to MessageRun This does not implement contextMenuButton at all yet, only inlineActionButton. --- src/components/Hyperchat.svelte | 13 +++- src/components/MessageRuns.svelte | 8 ++- src/components/RedirectBanner.svelte | 88 ++++++++++++++++++++++++++++ src/ts/chat-parser.ts | 34 ++++++++++- src/ts/typings/ytc.d.ts | 67 +++++++++++++++++---- 5 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 src/components/RedirectBanner.svelte diff --git a/src/components/Hyperchat.svelte b/src/components/Hyperchat.svelte index 6fe727d9..5eb8eadc 100644 --- a/src/components/Hyperchat.svelte +++ b/src/components/Hyperchat.svelte @@ -46,6 +46,7 @@ ytDark } from '../ts/storage'; import { version } from '../manifest.json'; + import RedirectBanner from './RedirectBanner.svelte'; const welcome = { welcome: true, message: { messageId: 'welcome' } }; type Welcome = typeof welcome; @@ -56,6 +57,8 @@ const messageKeys = new Set(); let pinned: Ytc.ParsedPinned | null; let summary: Ytc.ParsedSummary | null; + let redirect: Ytc.ParsedRedirect | null; + $: hasBanner = pinned || redirect || (summary && $showChatSummary); let div: HTMLElement; let isAtBottom = true; let truncateInterval: number; @@ -189,6 +192,9 @@ case 'summary': summary = action; break; + case 'redirect': + redirect = action; + break; case 'pin': pinned = action; break; @@ -399,13 +405,18 @@ {/each} - {#if (summary && $showChatSummary) || pinned} + {#if hasBanner}
{#if summary && $showChatSummary}
{/if} + {#if redirect} +
+ +
+ {/if} {#if pinned}
diff --git a/src/components/MessageRuns.svelte b/src/components/MessageRuns.svelte index e3bf1b6d..58ec8171 100644 --- a/src/components/MessageRuns.svelte +++ b/src/components/MessageRuns.svelte @@ -45,7 +45,13 @@ {#if deleted} {run.text} {:else} - + {#if run.styles?.includes('bold')} + + + + {:else} + + {/if} {/if} {:else if run.type === 'link'} + import { slide, fade } from 'svelte/transition'; + import MessageRun from './MessageRuns.svelte'; + import Tooltip from './common/Tooltip.svelte'; + import Icon from 'smelte/src/components/Icon'; + import { Theme } from '../ts/chat-constants'; + import { createEventDispatcher } from 'svelte'; + import { showProfileIcons } from '../ts/storage'; + import Button from 'smelte/src/components/Button'; + + export let redirect: Ytc.ParsedRedirect; + + let dismissed = false; + let shorten = false; + let autoHideTimeout: NodeJS.Timeout | null = null; + const classes = 'rounded inline-flex flex-col overflow-visible ' + + 'bg-secondary-900 p-2 w-full text-white z-10 shadow'; + + const onShorten = () => { + shorten = !shorten; + if (autoHideTimeout) { + clearTimeout(autoHideTimeout); + autoHideTimeout = null; + } + }; + + $: if (redirect) { + dismissed = false; + shorten = false; + if (redirect.showtime) { + autoHideTimeout = setTimeout(() => { shorten = true; }, redirect.showtime); + } + } + + const dispatch = createEventDispatcher(); + $: dismissed, shorten, dispatch('resize'); + + +{#if !dismissed} +
+
+
+ + + {#if shorten} + expand_more + {:else} + expand_less + {/if} + + + Redirect Notice +
+
+ + { dismissed = true; }} + > + close + + Dismiss + +
+
+ {#if !shorten && !dismissed} +
+ {#if $showProfileIcons} + {redirect.item.profileIcon.alt} + {/if} + +
+
+ +
+ {/if} +
+{/if} diff --git a/src/ts/chat-parser.ts b/src/ts/chat-parser.ts index 1a1b53fe..b70cf635 100644 --- a/src/ts/chat-parser.ts +++ b/src/ts/chat-parser.ts @@ -44,6 +44,7 @@ const parseMessageRuns = (runs?: Ytc.MessageRun[]): Ytc.ParsedRun[] => { } else if (run.text != null) { parsedRuns.push({ type: 'text', + styles: (run.bold ? ['bold'] : []).concat(run.deemphasize ? ['deemphasize'] : []), text: decodeURIComponent(escape(unescape(encodeURIComponent( run.text )))) @@ -106,6 +107,34 @@ const parseChatSummary = (renderer: Ytc.AddChatItem, showtime: number): Ytc.Pars return item; } +const parseRedirectBanner = (renderer: Ytc.AddChatItem, showtime: number): Ytc.ParsedRedirect | undefined => { + if (!renderer.liveChatBannerRedirectRenderer) { + return; + } + const baseRenderer = renderer.liveChatBannerRedirectRenderer!; + const profileIcon = { + src: fixUrl(baseRenderer.authorPhoto?.thumbnails[0].url ?? ''), + alt: 'Redirect profile icon' + }; + const url = baseRenderer.inlineActionButton?.buttonRenderer.command.urlEndpoint?.url || + (baseRenderer.inlineActionButton?.buttonRenderer.command.watchEndpoint?.videoId ? + "/watch?v=" + baseRenderer.inlineActionButton?.buttonRenderer.command.watchEndpoint?.videoId + : ''); + const item: Ytc.ParsedRedirect = { + type: 'redirect', + item: { + message: parseMessageRuns(baseRenderer.bannerMessage.runs), + profileIcon: profileIcon, + action: { + url: fixUrl(url), + text: parseMessageRuns(baseRenderer.inlineActionButton?.buttonRenderer.text?.runs), + } + }, + showtime: showtime, + }; + return item; +} + const parseAddChatItemAction = (action: Ytc.AddChatItemAction, isReplay = false, liveTimeoutOrReplayMs = 0): Ytc.ParsedMessage | undefined => { const actionItem = action.item; const renderer = actionItem.liveChatTextMessageRenderer ?? @@ -228,7 +257,7 @@ const parseMessageDeletedAction = (action: Ytc.MessageDeletedAction): Ytc.Parsed }; }; -const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | Ytc.ParsedSummary | undefined => { +const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedMisc | undefined => { const baseRenderer = action.bannerRenderer.liveChatBannerRenderer; // fold both auto-disappear and auto-collapse into just collapse for showtime @@ -239,6 +268,9 @@ const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | Ytc. if (baseRenderer.contents.liveChatBannerChatSummaryRenderer) { return parseChatSummary(baseRenderer.contents, showtime); } + if (baseRenderer.contents.liveChatBannerRedirectRenderer) { + return parseRedirectBanner(baseRenderer.contents, showtime); + } const parsedContents = parseAddChatItemAction( { item: baseRenderer.contents }, true ); diff --git a/src/ts/typings/ytc.d.ts b/src/ts/typings/ytc.d.ts index 73ef19ac..d86114ae 100644 --- a/src/ts/typings/ytc.d.ts +++ b/src/ts/typings/ytc.d.ts @@ -145,16 +145,14 @@ declare namespace Ytc { } interface ThumbnailsWithLabel extends Thumbnails { - accessibility?: { - accessibilityData: { - label: string; - }; - }; + accessibility?: AccessibilityObj; } /** Message run object */ interface MessageRun { text?: string; + bold?: boolean; + deemphasize?: boolean; navigationEndpoint?: { commandMetadata: { webCommandMetadata: { @@ -185,11 +183,7 @@ declare namespace Ytc { /** Unlocalized string */ iconType: string; }; - accessibility?: { - accessibilityData: { - label: string; - }; - }; + accessibility?: AccessibilityObj; }; } @@ -248,6 +242,35 @@ declare namespace Ytc { }; } + interface RedirectRenderer { + bannerMessage: RunsObj; + authorPhoto?: Thumbnails; + inlineActionButton?: { + buttonRenderer: ButtonRenderer; + } + contextMenuButton?: { + buttonRenderer: ButtonRenderer; + } + } + + interface ButtonRenderer { + style?: string; + size?: string; + icon?: string; + accessibility?: AccessibilityObj; + isDisabled?: boolean; + text?: RunsObj; + command: { + urlEndpoint?: { + url: string; + target: string; + } + watchEndpoint?: { + videoId: string; + } + } + } + interface PlaceholderRenderer { // No idea what the purpose of this is id: string; timestampUsec: IntString; @@ -271,6 +294,8 @@ declare namespace Ytc { liveChatSponsorshipsGiftRedemptionAnnouncementRenderer?: TextMessageRenderer; /** AI Chat Summary */ liveChatBannerChatSummaryRenderer?: ChatSummaryRenderer; + /** Redirects */ + liveChatBannerRedirectRenderer?: RedirectRenderer; /** ??? */ liveChatPlaceholderItemRenderer?: PlaceholderRenderer; } @@ -299,6 +324,12 @@ declare namespace Ytc { runs: MessageRun[]; } + interface AccessibilityObj { + accessibilityData: { + label: string; + } + } + /* * Parsed objects */ @@ -310,6 +341,7 @@ declare namespace Ytc { interface ParsedTextRun { type: 'text'; text: string; + styles?: string[]; } interface ParsedLinkRun { @@ -400,13 +432,26 @@ declare namespace Ytc { showtime: number; } + interface ParsedRedirect { + type: 'redirect'; + item: { + message: ParsedRun[]; + profileIcon: ParsedImage; + action: { + url: string; + text: ParsedRun[]; + } + }; + showtime: number; + } + interface ParsedTicker extends ParsedMessage { type: 'ticker'; tickerDuration: number; detailText?: string; } - type ParsedMisc = ParsedPinned | ParsedSummary | { type: 'unpin' }; + type ParsedMisc = ParsedPinned | ParsedSummary | ParsedRedirect | { type: 'unpin' }; type ParsedTimedItem = ParsedMessage | ParsedTicker;