Skip to content
Merged
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
56 changes: 28 additions & 28 deletions src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import PinnedMessage from './PinnedMessage.svelte';
import ChatSummary from './ChatSummary.svelte';
import RedirectBanner from './RedirectBanner.svelte';
import PollResults from './PollResults.svelte';
import PaidMessage from './PaidMessage.svelte';
import MembershipItem from './MembershipItem.svelte';
import ReportBanDialog from './ReportBanDialog.svelte';
Expand Down Expand Up @@ -55,35 +56,11 @@
const TRUNCATE_SIZE = 20;
let messageActions: (Chat.MessageAction | Welcome)[] = [];
const messageKeys = new Set<string>();
let poll: Ytc.ParsedPoll | null;
let pinned: Ytc.ParsedPinned | null;
let summary: Ytc.ParsedSummary | null;
let redirect: Ytc.ParsedRedirect | null;
// = {
// type: 'redirect',
// item: {
// message: [
// {
// type: 'text',
// text: 'Don\'t miss out! People are going to watch something from someone',
// },
// ],
// profileIcon: {
// src: 'https://picsum.photos/32',
// alt: 'Redirect profile photo',
// },
// action: {
// url: 'https://example.com/',
// text: [
// {
// type: 'text',
// text: 'Go Now',
// },
// ],
// },
// },
// showtime: 5000,
// };
$: hasBanner = pinned ?? redirect ?? (summary && $showChatSummary);
$: hasBanner = poll ?? pinned ?? redirect ?? (summary && $showChatSummary);
let div: HTMLElement;
let isAtBottom = true;
let truncateInterval: number;
Expand Down Expand Up @@ -214,6 +191,9 @@
case 'delete':
onDelete(action.deletion);
break;
case 'poll':
poll = action;
break;
case 'summary':
summary = action;
break;
Expand All @@ -224,7 +204,22 @@
pinned = action;
break;
case 'unpin':
pinned = null;
if (action.targetActionId) {
if (action.targetActionId === pinned?.actionId) {
pinned = null;
}
if (action.targetActionId === summary?.actionId) {
summary = null;
}
if (action.targetActionId === poll?.actionId) {
poll = null;
}
if (action.targetActionId === redirect?.actionId) {
redirect = null;
}
} else {
pinned = null;
}
break;
case 'playerProgress':
$currentProgress = action.playerProgress;
Expand Down Expand Up @@ -376,7 +371,7 @@
}
}, 350);
};
$: $enableStickySuperchatBar, pinned, topBarResized();
$: $enableStickySuperchatBar, hasBanner, topBarResized();

const isMention = (msg: Ytc.ParsedMessage) => {
return $selfChannelName && msg.message.map(run => {
Expand Down Expand Up @@ -432,6 +427,11 @@
</div>
{#if hasBanner}
<div class="absolute top-0 w-full" bind:this={topBar}>
{#if poll}
<div class="mx-1.5 mt-1.5">
<PollResults poll={poll} on:resize={topBarResized} />
</div>
{/if}
{#if summary && $showChatSummary}
<div class="mx-1.5 mt-1.5">
<ChatSummary summary={summary} on:resize={topBarResized} />
Expand Down
90 changes: 90 additions & 0 deletions src/components/PollResults.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<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 ProgressLinear from 'smelte/src/components/ProgressLinear';

export let poll: Ytc.ParsedPoll;

let dismissed = false;
let shorten = false;
let prevId: string | 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 (poll.actionId !== prevId) {
dismissed = false;
shorten = false;
prevId = poll.actionId;
}

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>
{#if $showProfileIcons}
<img
class="h-5 w-5 inline align-middle rounded-full flex-none"
src={poll.item.profileIcon.src}
alt={poll.item.profileIcon.alt}
/>
{/if}
{#each poll.item.header as run}
{#if run.type === 'text'}
<span class="align-middle">{run.text}</span>
{/if}
{/each}
</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 inline-flex flex-row gap-2 break-words w-full overflow-visible" transition:slide|local={{ duration: 300 }}>
<MessageRun runs={poll.item.question} forceDark forceTLColor={Theme.DARK}/>
</div>
{#each poll.item.choices as choice}
<div class="mt-1 w-full whitespace-pre-line flex justify-start items-end" transition:slide|global={{ duration: 300 }}>
<MessageRun runs={choice.text} forceDark forceTLColor={Theme.DARK} />
<span class="ml-auto" transition:slide|global={{ duration: 300 }}>
{choice.percentage}
</span>
</div>
<ProgressLinear progress={(choice.ratio || 0.001) * 100} color="gray"/>
{/each}
{/if}
</div>
{/if}
55 changes: 49 additions & 6 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const splitRunsByNewline = (runs: Ytc.ParsedRun[], maxSplit: number = -1): Ytc.P
return acc;
}, [[]]);

const parseChatSummary = (renderer: Ytc.AddChatItem, showtime: number): Ytc.ParsedSummary | undefined => {
const parseChatSummary = (renderer: Ytc.AddChatItem, actionId: string, showtime: number): Ytc.ParsedSummary | undefined => {
if (!renderer.liveChatBannerChatSummaryRenderer) {
return;
}
Expand All @@ -96,18 +96,18 @@ const parseChatSummary = (renderer: Ytc.AddChatItem, showtime: number): Ytc.Pars
});
const item: Ytc.ParsedSummary = {
type: 'summary',
actionId: baseRenderer.liveChatSummaryId,
item: {
header: splitRuns[0],
subheader: subheader,
message: splitRuns[2],
},
id: baseRenderer.liveChatSummaryId,
showtime: showtime,
};
return item;
}

const parseRedirectBanner = (renderer: Ytc.AddChatItem, showtime: number): Ytc.ParsedRedirect | undefined => {
const parseRedirectBanner = (renderer: Ytc.AddChatItem, actionId: string, showtime: number): Ytc.ParsedRedirect | undefined => {
if (!renderer.liveChatBannerRedirectRenderer) {
return;
}
Expand All @@ -122,6 +122,7 @@ const parseRedirectBanner = (renderer: Ytc.AddChatItem, showtime: number): Ytc.P
: '');
const item: Ytc.ParsedRedirect = {
type: 'redirect',
actionId: actionId,
item: {
message: parseMessageRuns(baseRenderer.bannerMessage.runs),
profileIcon: profileIcon,
Expand Down Expand Up @@ -257,19 +258,55 @@ const parseMessageDeletedAction = (action: Ytc.MessageDeletedAction): Ytc.Parsed
};
};

const parsePollRenderer = (baseRenderer: Ytc.PollRenderer): Ytc.ParsedPoll | undefined => {
if (!baseRenderer) {
return;
}
const profileIcon = {
src: fixUrl(baseRenderer.header.pollHeaderRenderer.thumbnail?.thumbnails[0].url ?? ''),
alt: 'Poll profile icon'
};
// TODO implement 'selected' field? YT doesn't use it in results.
return {
type: 'poll',
actionId: baseRenderer.liveChatPollId,
item: {
profileIcon: profileIcon,
header: parseMessageRuns(baseRenderer.header.pollHeaderRenderer.metadataText.runs),
question: parseMessageRuns(baseRenderer.header.pollHeaderRenderer.pollQuestion.runs),
choices: baseRenderer.choices.map((choice) => {
return {
text: parseMessageRuns(choice.text.runs),
selected: choice.selected,
ratio: choice.voteRatio,
percentage: choice.votePercentage?.simpleText
};
}),
}
};
}

const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedMisc | undefined => {
const baseRenderer = action.bannerRenderer.liveChatBannerRenderer;

// polls can come through banner or through update actions, send both to the same parser
// actionId isn't needed as the pollRenderer contains liveChatPollId
if (baseRenderer.contents.pollRenderer) {
return parsePollRenderer(baseRenderer.contents.pollRenderer);
}

const actionId = baseRenderer.actionId;

// fold both auto-disappear and auto-collapse into just collapse for showtime
const showtime = action.bannerProperties?.isEphemeral
? (action.bannerProperties?.bannerTimeoutMs || 0)
: 1000 * (action.bannerProperties?.autoCollapseDelay?.seconds || baseRenderer.bannerProperties?.autoCollapseDelay?.seconds || 0);

if (baseRenderer.contents.liveChatBannerChatSummaryRenderer) {
return parseChatSummary(baseRenderer.contents, showtime);
return parseChatSummary(baseRenderer.contents, actionId, showtime);
}
if (baseRenderer.contents.liveChatBannerRedirectRenderer) {
return parseRedirectBanner(baseRenderer.contents, showtime);
return parseRedirectBanner(baseRenderer.contents, actionId, showtime);
}
const parsedContents = parseAddChatItemAction(
{ item: baseRenderer.contents }, true
Expand All @@ -279,6 +316,7 @@ const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedMisc | undefi
}
return {
type: 'pin',
actionId: actionId,
item: {
header: parseMessageRuns(
baseRenderer.header.liveChatBannerHeaderRenderer.text.runs
Expand Down Expand Up @@ -318,9 +356,14 @@ const processCommonAction = (
} else if (action.addBannerToLiveChatCommand) {
return parseBannerAction(action.addBannerToLiveChatCommand);
} else if (action.removeBannerForLiveChatCommand) {
return { type: 'unpin' } as const;
return {
type: 'unpin',
targetActionId: action.removeBannerForLiveChatCommand.targetActionId,
} as Ytc.ParsedRemoveBanner;
} else if (action.addLiveChatTickerItemAction) {
return parseTickerAction(action.addLiveChatTickerItemAction, isReplay, liveTimeoutOrReplayMs);
} else if (action.updateLiveChatPollAction) {
return parsePollRenderer(action.updateLiveChatPollAction.pollToUpdate.pollRenderer);
}
};

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

Expand Down
Loading