diff --git a/src/components/Hyperchat.svelte b/src/components/Hyperchat.svelte index 33755b1e..ee4dbb0c 100644 --- a/src/components/Hyperchat.svelte +++ b/src/components/Hyperchat.svelte @@ -1,4 +1,5 @@ + - + { + scrollToBottom(); + topBarResized(); +}} on:load={onLoad} />
-
+ {#if $enableStickySuperchatBar} + + {/if} +
+
{#each messageActions as action (action.message.messageId)}
{/if}
{/each}
+ {#if pinned} +
+
+ +
+
+ {/if} + {#if !isAtBottom} +
+
+ {/if}
- {#if pinned} -
- -
- {/if} - {#if !isAtBottom} -
-
- {/if}
diff --git a/src/components/StickyBar.svelte b/src/components/StickyBar.svelte new file mode 100644 index 00000000..fb013e36 --- /dev/null +++ b/src/components/StickyBar.svelte @@ -0,0 +1,84 @@ + + + +{#if open} +
+
+ {#each $stickySuperchats as sc (sc.messageId)} + + + + {/each} +
+
+{/if} + + diff --git a/src/components/SuperchatViewDialog.svelte b/src/components/SuperchatViewDialog.svelte new file mode 100644 index 00000000..2fbf21e8 --- /dev/null +++ b/src/components/SuperchatViewDialog.svelte @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/components/TimedItem.svelte b/src/components/TimedItem.svelte new file mode 100644 index 00000000..9757260c --- /dev/null +++ b/src/components/TimedItem.svelte @@ -0,0 +1,25 @@ + + +{#if ('superChat' in item || 'superSticker' in item)} + +{:else} + +{/if} diff --git a/src/components/WelcomeMessage.svelte b/src/components/WelcomeMessage.svelte index 6cf2ea0f..70c37a31 100644 --- a/src/components/WelcomeMessage.svelte +++ b/src/components/WelcomeMessage.svelte @@ -87,7 +87,7 @@ e.preventDefault(); }} class="inline-block align-middle cursor-pointer pt-0.5 h-fit"> - + close diff --git a/src/components/changelog/Changelog.svelte b/src/components/changelog/Changelog.svelte index a565846b..0c5e04f1 100644 --- a/src/components/changelog/Changelog.svelte +++ b/src/components/changelog/Changelog.svelte @@ -1,15 +1,29 @@
    On today's KFP menu:
  • - You can now block and report users! - It took months to reverse-engineer this feature. Enjoy! + Sticky superchats are finally supported! + Tickers for superchats and membership joins/milestones + will be visible at the top of the screen (like in default YTC). + The bar can also be disabled in the settings menu. +
  • +
  • + Superchats and membership messages now properly display + profile pictures if enabled. +
  • +
  • + Fixed issues with the settings menu not opening + properly on select devices +
  • +
  • + Fixed pinned messages hindering visibility when + scrolled to the top of the chat +
  • +
  • + Fixed occasional freezing issues
    What's still cooking in the usual room: -
  • - Sticky superchats -
  • Updated screenshots, store listing, and trailer video
  • diff --git a/src/components/settings/InterfaceSettings.svelte b/src/components/settings/InterfaceSettings.svelte index b86781a3..07f4edb6 100644 --- a/src/components/settings/InterfaceSettings.svelte +++ b/src/components/settings/InterfaceSettings.svelte @@ -8,9 +8,11 @@ showUserBadges, emojiRenderMode, autoLiveChat, - useSystemEmojis + useSystemEmojis, + isDark, + enableStickySuperchatBar } from '../../ts/storage'; - import { Theme, themeItems, emojiRenderItems } from '../../ts/chat-constants'; + import { themeItems, emojiRenderItems } from '../../ts/chat-constants'; import Card from '../common/Card.svelte'; import Radio from '../common/RadioGroupStore.svelte'; import Checkbox from '../common/CheckboxStore.svelte'; @@ -23,20 +25,7 @@ ); const darkStore = dark(); - $: switch ($theme) { - case Theme.DARK: - darkStore.set(true); - break; - case Theme.LIGHT: - darkStore.set(false); - break; - case Theme.YOUTUBE: - if (window.location.search.includes('dark')) darkStore.set(true); - else darkStore.set(false); - break; - default: - break; - } + $: darkStore.set($isDark); $: console.debug({ theme: $theme, @@ -44,6 +33,13 @@ showTimestamps: $showTimestamps, showUsernames: $showUsernames }); + + const superchatBarWasDisabled = !$enableStickySuperchatBar; + let superchatBarWasToggled: boolean | null = null; + const updateSuperchatBarToggle = () => { + superchatBarWasToggled = superchatBarWasToggled !== null; + }; + $: $enableStickySuperchatBar, updateSuperchatBarToggle(); @@ -51,6 +47,10 @@
    Theme:
+ + {#if (superchatBarWasToggled || superchatBarWasDisabled) && $enableStickySuperchatBar} + The superchat bar will appear upon reload or when the next superchat arrives. + {/if} diff --git a/src/manifest.json b/src/manifest.json index 9531d6bc..a986de42 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,7 +3,7 @@ "name": "HyperChat by LiveTL", "homepage_url": "https://livetl.app/en/hyperchat/", "description": "YouTube chat, but it's fast and sleek!", - "version": "2.5.6", + "version": "2.6.0", "permissions": [ "storage" ], diff --git a/src/scripts/chat-background.ts b/src/scripts/chat-background.ts index 6b047d84..809fd1ac 100644 --- a/src/scripts/chat-background.ts +++ b/src/scripts/chat-background.ts @@ -306,11 +306,11 @@ const setInitialData = (port: Chat.Port, message: Chat.JsonMsg): void => { /** * Updates the player progress of the queue of the interceptor. */ -const updatePlayerProgress = (port: Chat.Port, playerProgress: number): void => { +const updatePlayerProgress = (port: Chat.Port, playerProgress: number, isFromYt?: boolean): void => { const interceptor = findInterceptorFromPort(port, { playerProgress }); if (!interceptor || !isYtcInterceptor(interceptor)) return; - interceptor.queue.updatePlayerProgress(playerProgress); + interceptor.queue.updatePlayerProgress(playerProgress, isFromYt); }; /** @@ -390,7 +390,7 @@ chrome.runtime.onConnect.addListener((port) => { setInitialData(port, message); break; case 'updatePlayerProgress': - updatePlayerProgress(port, message.playerProgress); + updatePlayerProgress(port, message.playerProgress, message.isFromYt); break; case 'setTheme': setTheme(port, message.dark); @@ -428,9 +428,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (request.type === 'createPopup') { chrome.windows.create({ url: request.url, - type: 'popup', - height: 420, - width: 690 + type: 'popup' }, () => {}); } }); diff --git a/src/scripts/chat-interceptor.ts b/src/scripts/chat-interceptor.ts index 61fff45e..9ed720bb 100644 --- a/src/scripts/chat-interceptor.ts +++ b/src/scripts/chat-interceptor.ts @@ -213,7 +213,8 @@ const chatLoaded = async (): Promise => { if (d.data['yt-player-video-progress'] != null) { port.postMessage({ type: 'updatePlayerProgress', - playerProgress: d.data['yt-player-video-progress'] + playerProgress: d.data['yt-player-video-progress'], + isFromYt: true }); } }); diff --git a/src/stylesheets/scrollbar.css b/src/stylesheets/scrollbar.css new file mode 100644 index 00000000..cac8a2da --- /dev/null +++ b/src/stylesheets/scrollbar.css @@ -0,0 +1,21 @@ +* ::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +* ::-webkit-scrollbar-track { + background: transparent; +} + +* ::-webkit-scrollbar-thumb { + background: #888; +} + +* ::-webkit-scrollbar-thumb:hover { + background: #555; +} + +* { + scrollbar-width: thin; + scrollbar-color: #888 transparent; +} diff --git a/src/ts/chat-parser.ts b/src/ts/chat-parser.ts index cecb3a56..15e5e338 100644 --- a/src/ts/chat-parser.ts +++ b/src/ts/chat-parser.ts @@ -181,13 +181,38 @@ const parsePinnedMessageAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned }; }; -const processCommonAction = (action: Ytc.ReplayAction, isReplay: boolean, liveTimeoutOrReplayMs: number): Ytc.ParsedMessage | Ytc.ParsedMisc | undefined => { +const parseTickerAction = (action: Ytc.AddTickerAction, isReplay: boolean, liveTimeoutOrReplayMs: number): Ytc.ParsedTicker | undefined => { + const baseRenderer = action.item.liveChatTickerPaidMessageItemRenderer ?? action.item.liveChatTickerSponsorItemRenderer; + if (!baseRenderer) return; + const parsedMessage = parseAddChatItemAction({ + item: baseRenderer.showItemEndpoint.showLiveChatItemEndpoint.renderer + }, isReplay, liveTimeoutOrReplayMs); + if (!parsedMessage) return; + return { + type: 'ticker', + ...parsedMessage, + tickerDuration: baseRenderer.fullDurationSec ?? baseRenderer.durationSec, + detailText: 'detailText' in baseRenderer + ? ( + 'simpleText' in baseRenderer.detailText ? baseRenderer.detailText.simpleText : baseRenderer.detailText.runs[0].text + ) + : undefined + }; +}; + +const processCommonAction = ( + action: Ytc.ReplayAction, + isReplay: boolean, + liveTimeoutOrReplayMs: number +): Ytc.ParsedTimedItem | Ytc.ParsedMisc | undefined => { if (action.addChatItemAction) { return parseAddChatItemAction(action.addChatItemAction, isReplay, liveTimeoutOrReplayMs); } else if (action.addBannerToLiveChatCommand) { return parsePinnedMessageAction(action.addBannerToLiveChatCommand); } else if (action.removeBannerForLiveChatCommand) { return { type: 'unpin' } as const; + } else if (action.addLiveChatTickerItemAction) { + return parseTickerAction(action.addLiveChatTickerItemAction, isReplay, liveTimeoutOrReplayMs); } }; @@ -202,8 +227,8 @@ const processLiveAction = (action: Ytc.Action, isReplay: boolean, liveTimeoutMs: } }; -const sortAction = (action: Ytc.ParsedAction, messageArray: Ytc.ParsedMessage[], bonkArray: Ytc.ParsedBonk[], deleteArray: Ytc.ParsedDeleted[], miscArray: Ytc.ParsedMisc[]): void => { - if ('message' in action) { +const sortAction = (action: Ytc.ParsedAction, messageArray: Ytc.ParsedTimedItem[], bonkArray: Ytc.ParsedBonk[], deleteArray: Ytc.ParsedDeleted[], miscArray: Ytc.ParsedMisc[]): void => { + if ('message' in action || 'tickerDuration' in action) { messageArray.push(action); } else if ('replacedMessage' in action && 'authorId' in action) { bonkArray.push(action); @@ -233,7 +258,7 @@ export const parseChatResponse = (response: string, isReplay: boolean): Ytc.Pars return; } - const messageArray: Ytc.ParsedMessage[] = []; + const messageArray: Ytc.ParsedTimedItem[] = []; const bonkArray: Ytc.ParsedBonk[] = []; const deleteArray: Ytc.ParsedDeleted[] = []; const miscArray: Ytc.ParsedMisc[] = []; diff --git a/src/ts/queue.ts b/src/ts/queue.ts index 02026930..85390304 100644 --- a/src/ts/queue.ts +++ b/src/ts/queue.ts @@ -93,7 +93,7 @@ export interface YtcQueue { latestAction: Subscribable; getInitialData: () => Chat.Actions[]; addJsonToQueue: (json: string, isInitial: boolean, interceptor: Chat.Interceptor, forceDisplay?: boolean) => void; - updatePlayerProgress: (timeMs: number) => void; + updatePlayerProgress: (timeMs: number, isFromYt?: boolean) => void; cleanUp: () => void; selfChannel: Subscribable; } @@ -173,7 +173,9 @@ export function ytcQueue(isReplay = false): YtcQueue { * Normally called by the live polling interval that runs every 250 ms. */ const updateLiveProgress = (): void => { - onVideoProgress(Date.now() / 1000); + const t = Date.now() / 1000; + onVideoProgress(t); + updatePlayerProgress(t); }; /** @@ -269,8 +271,8 @@ export function ytcQueue(isReplay = false): YtcQueue { * Update player progress to given time. * Only effective on VODs. */ - const updatePlayerProgress = (timeMs: number): void => { - latestAction.set({ type: 'playerProgress', playerProgress: timeMs }); + const updatePlayerProgress = (timeMs: number, isFromYt?: boolean): void => { + if (isReplay || isFromYt == null) latestAction.set({ type: 'playerProgress', playerProgress: timeMs }); if (livePolling != null || !isReplay) return; onVideoProgress(timeMs); }; diff --git a/src/ts/storage.ts b/src/ts/storage.ts index 40a0bd0b..3999e804 100644 --- a/src/ts/storage.ts +++ b/src/ts/storage.ts @@ -1,5 +1,5 @@ import { webExtStores } from 'svelte-webext-stores'; -import { readable, writable } from 'svelte/store'; +import { derived, readable, writable } from 'svelte/store'; import type { Writable } from 'svelte/store'; import { getClient, AvailableLanguages } from 'iframe-translator'; import type { IframeTranslatorClient, AvailableLanguageCodes } from 'iframe-translator'; @@ -55,6 +55,7 @@ export const emojiRenderMode = stores.addSyncStore('hc.emojiRenderMode', Youtube export const autoLiveChat = stores.addSyncStore('hc.autoLiveChat', false); export const useSystemEmojis = stores.addSyncStore('hc.useSystemEmojis', false); export const hoveredItem = writable(null as null | Chat.MessageAction['message']['messageId']); +export const focusedSuperchat = writable(null as null | Ytc.ParsedTimedItem); export const port = writable(null as null | Chat.Port); export const selfChannelId = writable(null as null | string); export const reportDialog = writable(null as null | { @@ -66,3 +67,11 @@ export const alertDialog = writable(null as null | { message: string; color: string; }); +export const stickySuperchats = writable([] as Ytc.ParsedTicker[]); +export const isDark = derived(theme, ($theme) => { + return $theme === Theme.DARK || ( + $theme === Theme.YOUTUBE && window.location.search.includes('dark') + ); +}); +export const currentProgress = writable(null as null | number); +export const enableStickySuperchatBar = stores.addSyncStore('hc.enableStickySuperchatBar', true); diff --git a/src/ts/typings/chat.d.ts b/src/ts/typings/chat.d.ts index ab9b4806..8fc2a1a6 100644 --- a/src/ts/typings/chat.d.ts +++ b/src/ts/typings/chat.d.ts @@ -4,7 +4,7 @@ declare namespace Chat { } interface MessageAction { - message: Ytc.ParsedMessage; + message: Ytc.ParsedTimedItem; deleted?: MessageDeletedObj; } @@ -26,6 +26,7 @@ declare namespace Chat { interface PlayerProgressAction { type: 'playerProgress'; playerProgress: number; + isFromYt?: boolean; } interface ForceUpdate { @@ -113,6 +114,7 @@ declare namespace Chat { interface updatePlayerProgressMsg { type: 'updatePlayerProgress'; playerProgress: number; + isFromYt?: boolean; } interface setThemeMsg { diff --git a/src/ts/typings/ytc.d.ts b/src/ts/typings/ytc.d.ts index 7db979e4..a4486b1f 100644 --- a/src/ts/typings/ytc.d.ts +++ b/src/ts/typings/ytc.d.ts @@ -93,12 +93,29 @@ declare namespace Ytc { interface AddTickerAction { item: { liveChatTickerSponsorItemRenderer?: TickerRenderer & { - detailText: RunsObj; + detailText: RunsObj | SimpleTextObj; detailTextColor: number; + startBackgroundColor: number; + endBackgroundColor: number; + showItemEndpoint: { + showLiveChatItemEndpoint: { + renderer: { + liveChatMembershipItemRenderer: MembershipRenderer; + }; + }; + }; }; liveChatTickerPaidMessageItemRenderer?: TickerRenderer & { amount: SimpleTextObj; amountTextColor: number; + durationSec: number; + showItemEndpoint: { + showLiveChatItemEndpoint: { + renderer: { + liveChatPaidMessageRenderer: PaidMessageRenderer; + }; + }; + }; }; }; durationSec: IntString; @@ -319,9 +336,17 @@ declare namespace Ytc { }; } + interface ParsedTicker extends ParsedMessage { + type: 'ticker'; + tickerDuration: number; + detailText?: string; + } + type ParsedMisc = ParsedPinned | { type: 'unpin'}; - type ParsedAction = ParsedMessage | ParsedBonk | ParsedDeleted | ParsedMisc; + type ParsedTimedItem = ParsedMessage | ParsedTicker; + + type ParsedAction = ParsedTimedItem | ParsedBonk | ParsedDeleted | ParsedMisc; interface ParsedChunk { messages: ParsedMessage[];