Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rudimentary redirect banner #155

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import Message from './Message.svelte';
import PinnedMessage from './PinnedMessage.svelte';
import ChatSummary from './ChatSummary.svelte';
import RedirectBanner from './RedirectBanner.svelte';
import PaidMessage from './PaidMessage.svelte';
import MembershipItem from './MembershipItem.svelte';
import ReportBanDialog from './ReportBanDialog.svelte';
Expand Down Expand Up @@ -56,6 +57,8 @@
const messageKeys = new Set<string>();
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;
Expand Down Expand Up @@ -189,6 +192,9 @@
case 'summary':
summary = action;
break;
case 'redirect':
redirect = action;
break;
case 'pin':
pinned = action;
break;
Expand Down Expand Up @@ -399,13 +405,18 @@
</div>
{/each}
</div>
{#if (summary && $showChatSummary) || pinned}
{#if hasBanner}
<div class="absolute top-0 w-full" bind:this={topBar}>
{#if summary && $showChatSummary}
<div class="mx-1.5 mt-1.5">
<ChatSummary summary={summary} on:resize={topBarResized} />
</div>
{/if}
{#if redirect}
<div class="mx-1.5 mt-1.5">
<RedirectBanner redirect={redirect} on:resize={topBarResized} />
</div>
{/if}
{#if pinned}
<div class="mx-1.5 mt-1.5">
<PinnedMessage pinned={pinned} on:resize={topBarResized} />
Expand Down
8 changes: 7 additions & 1 deletion src/components/MessageRuns.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@
{#if deleted}
<span>{run.text}</span>
{:else}
<TranslatedMessage text={run.text} {forceTLColor} />
{#if run.styles?.includes('bold')}
<strong>
<TranslatedMessage text={run.text} {forceTLColor} />
</strong>
{:else}
<TranslatedMessage text={run.text} {forceTLColor} />
{/if}
{/if}
{:else if run.type === 'link'}
<a
Expand Down
88 changes: 88 additions & 0 deletions src/components/RedirectBanner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
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');
</script>

{#if !dismissed}
<div
class={classes}
transition:fade={{ duration: 250 }}
>
<div class="flex flex-row items-center cursor-pointer" on:click={onShorten}>
<div class="font-medium tracking-wide text-white flex-1">
<span class="mr-1 inline-block" style="transform: translateY(3px);">
<Icon small>
{#if shorten}
expand_more
{:else}
expand_less
{/if}
</Icon>
</span>
<span class="align-middle">Redirect Notice</span>
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
<Icon
slot="activator"
class="cursor-pointer text-lg"
on:click={() => { dismissed = true; }}
>
close
</Icon>
Dismiss
</Tooltip>
</div>
</div>
{#if !shorten && !dismissed}
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
{#if $showProfileIcons}
<img
class="h-5 w-5 inline align-middle rounded-full flex-none"
src={redirect.item.profileIcon.src}
alt={redirect.item.profileIcon.alt}
/>
{/if}
<MessageRun runs={redirect.item.message} forceDark forceTLColor={Theme.DARK}/>
</div>
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
<Button href={redirect.item.action.url} target="_blank" small>
<MessageRun runs={redirect.item.action.text} forceDark forceTLColor={Theme.DARK}/>
</Button>
</div>
{/if}
</div>
{/if}
34 changes: 33 additions & 1 deletion src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
))))
Expand Down Expand Up @@ -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
Comment on lines +119 to +121
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handling should probably be cleaned up at some point.

: '');
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 ??
Expand Down Expand Up @@ -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
Expand All @@ -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
);
Expand Down
2 changes: 1 addition & 1 deletion src/ts/chat-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const isValidFrameInfo = (f: Chat.UncheckedFrameInfo, port?: Chat.Port):
return check;
};

const actionTypes = new Set(['messages', 'bonk', 'delete', 'pin', 'unpin', 'summary', 'playerProgress', 'forceUpdate']);
const actionTypes = new Set(['messages', 'bonk', 'delete', 'pin', 'unpin', 'summary', 'redirect', 'playerProgress', 'forceUpdate']);
export const responseIsAction = (r: Chat.BackgroundResponse): r is Chat.Actions =>
actionTypes.has(r.type);

Expand Down
67 changes: 56 additions & 11 deletions src/ts/typings/ytc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -185,11 +183,7 @@ declare namespace Ytc {
/** Unlocalized string */
iconType: string;
};
accessibility?: {
accessibilityData: {
label: string;
};
};
accessibility?: AccessibilityObj;
};
}

Expand Down Expand Up @@ -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;
Expand All @@ -271,6 +294,8 @@ declare namespace Ytc {
liveChatSponsorshipsGiftRedemptionAnnouncementRenderer?: TextMessageRenderer;
/** AI Chat Summary */
liveChatBannerChatSummaryRenderer?: ChatSummaryRenderer;
/** Redirects */
liveChatBannerRedirectRenderer?: RedirectRenderer;
/** ??? */
liveChatPlaceholderItemRenderer?: PlaceholderRenderer;
}
Expand Down Expand Up @@ -299,6 +324,12 @@ declare namespace Ytc {
runs: MessageRun[];
}

interface AccessibilityObj {
accessibilityData: {
label: string;
}
}

/*
* Parsed objects
*/
Expand All @@ -310,6 +341,7 @@ declare namespace Ytc {
interface ParsedTextRun {
type: 'text';
text: string;
styles?: string[];
}

interface ParsedLinkRun {
Expand Down Expand Up @@ -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;

Expand Down