Skip to content
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
1 change: 1 addition & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const safelistSelectors = [
'body',
'stroke-primary',
'mode-dark',
'line-through',
// Components with custom color prop might need its color to be whitelisted too
'bg-blue-500',
'hover:bg-blue-400'
Expand Down
24 changes: 13 additions & 11 deletions src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
chatUserActionsItems,
ChatUserActions
} from '../ts/chat-constants';
import { isAllEmoji, isChatMessage, isPrivileged, responseIsAction } from '../ts/chat-utils';
import { buildDeletedObj, isAllEmoji, isChatMessage, isPrivileged, responseIsAction } from '../ts/chat-utils';
import { handleReplyThreadResponse } from '../ts/chat-actions';
import Button from 'smelte/src/components/Button';
import {
Expand Down Expand Up @@ -216,14 +216,15 @@
};

const onDelete = (deletion: Ytc.ParsedDeleted) => {
messageActions.some((action) => {
const changed = messageActions.some((action) => {
if (isWelcome(action)) return false;
if (action.message.messageId === deletion.messageId) {
action.deleted = { replace: deletion.replacedMessage };
action.deleted = buildDeletedObj(deletion, action.message.message);
return true;
}
return false;
});
if (changed) messageActions = messageActions;
};

const onChatAction = (action: Chat.Actions, isInitial = false) => {
Expand Down Expand Up @@ -320,20 +321,21 @@
$ytDark = response.dark;
break;
case 'chatUserActionResponse':
if (response.success && response.action === ChatUserActions.DELETE_MESSAGE) {
onDelete({
messageId: response.message.messageId,
replacedMessage: [],
pending: true
});
break;
}
$alertDialog = {
title: response.success ? 'Success!' : 'Error',
message: chatUserActionsItems.find(v => v.value === response.action)
?.messages[response.success ? 'success' : 'error'] ?? '',
color: response.success ? 'primary' : 'error'
};
if (response.success) {
if (response.action === ChatUserActions.DELETE_MESSAGE) {
onDelete({
messageId: response.message.messageId,
replacedMessage: [{ text: '[message retracted]' }]
});
break;
}
messageActions = messageActions.filter(
(a) => {
if (isWelcome(a)) return true;
Expand Down Expand Up @@ -465,7 +467,7 @@
{#if $enableStickySuperchatBar}
<StickyBar />
{/if}
<div class="w-full min-h-0 flex justify-end flex-col relative">
<div class="w-full min-h-0 flex-1 flex justify-end flex-col relative">
<div bind:this={div} on:scroll={checkAtBottom} class="content overflow-y-scroll">
<div style="height: {topBarSize}px;" />
{#each messageActions as action (action.message.messageId)}
Expand Down
43 changes: 33 additions & 10 deletions src/components/Message.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,17 @@
});
$: nameColorClass = generateNameColorClass(member, moderator, owner, forceDark);

$: if (deleted != null) {
message.message = deleted.replace;
let showOriginal = false;
$: displayRuns = deleted != null && !showOriginal ? deleted.replace : message.message;
// If showing original text, swap the first text run to 'hide'.
let toggleLabelRuns: Ytc.ParsedRun[] | undefined;
$: {
let swapped = !showOriginal;
toggleLabelRuns = deleted?.viewOriginalText?.map((r) => {
if (swapped || r.type !== 'text') return r;
swapped = true;
return { ...r, text: 'Hide deleted message' };
});
}
$: displayAuthorName = formatAuthorName(message.author.name);

Expand All @@ -71,10 +80,10 @@

$: isSelf = message.author.id === $selfChannelId;
$: visibleActions = chatUserActionsItems.filter((d) => {
if (isSelf) {
return d.value === ChatUserActions.DELETE_MESSAGE && message.params != null;
if (d.value === ChatUserActions.DELETE_MESSAGE) {
return (isSelf || message.canDelete) && message.params != null && deleted == null;
}
return d.value !== ChatUserActions.DELETE_MESSAGE;
return !isSelf;
});
$: menuItems = visibleActions.map((d) => ({
icon: d.icon,
Expand All @@ -92,13 +101,13 @@
</script>

<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div
<div
class="inline-flex flex-row gap-2 break-words w-full overflow-visible"
>
{#if !hideName && $showProfileIcons}
<a
href={message.author.url}
class="flex-shrink-0 {message.author.url ? 'cursor-pointer' : 'cursor-auto'}"
class="flex-shrink-0 {message.author.url ? 'cursor-pointer' : 'cursor-auto'} {deleted != null ? 'opacity-50' : ''}"
target="_blank"
>
<img
Expand All @@ -108,7 +117,7 @@
/>
</a>
{/if}
<div>
<div class={deleted != null ? 'opacity-50' : ''}>
{#if !hideName}
<span
class="text-xs mr-1 text-gray-700 dark:text-gray-600 align-middle"
Expand Down Expand Up @@ -177,12 +186,26 @@
</span>
{/if}
<MessageRun
runs={message.message}
runs={displayRuns}
{forceDark}
deleted={deleted != null}
{forceTLColor}
class={message.membershipGiftRedeem ? 'text-gray-700 dark:text-gray-600 italic font-medium' : ''}
class="{message.membershipGiftRedeem ? 'text-gray-700 dark:text-gray-600 italic font-medium' : ''} {deleted?.pending || showOriginal ? 'line-through' : ''}"
/>
{#if deleted?.viewOriginalText}
<button
type="button"
on:click={() => (showOriginal = !showOriginal)}
class="ml-1 align-middle text-xs cursor-pointer text-deleted-light dark:text-deleted-dark bg-transparent border-0 p-0"
>
<MessageRun
runs={toggleLabelRuns}
{forceDark}
{forceTLColor}
class="underline cursor-pointer"
/>
</button>
{/if}
{#if message.membershipGiftRedeem}
<svg
height="1em"
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/Menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
class={menuClasses}
transition:fade={{ duration: 150 }}
bind:this={listDiv}
style="max-width: 20em; font-size: 0.9em; {offsetYStyle}"
style="max-width: 20em; font-size: 1em; {offsetYStyle}"
>
<List
select
Expand All @@ -98,7 +98,7 @@
on:click={() => onItemClick(item)}
style="padding: 0.5em 1em"
>
<Icon class="pr-6">
<Icon class="pr-2">
{item.icon}
</Icon>
<span>{item.text}</span>
Expand Down
2 changes: 2 additions & 0 deletions src/scripts/chat-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const chatLoaded = async (): Promise<void> => {
const visitorId = (ytcfg as any)?.data_?.VISITOR_DATA ?? baseContext?.client?.visitorData;
const clientName = (ytcfg as any)?.data_?.INNERTUBE_CLIENT_NAME;
const clientVersion = (ytcfg as any)?.data_?.INNERTUBE_CLIENT_VERSION;
const pageId = (ytcfg as any)?.data_?.DELEGATED_SESSION_ID;
return {
headers: {
'Content-Type': 'application/json',
Expand All @@ -140,6 +141,7 @@ const chatLoaded = async (): Promise<void> => {
...(visitorId != null ? { 'X-Goog-Visitor-Id': String(visitorId) } : {}),
...(clientName != null ? { 'X-Youtube-Client-Name': String(clientName) } : {}),
...(clientVersion != null ? { 'X-Youtube-Client-Version': String(clientVersion) } : {}),
...(pageId != null ? { 'X-Goog-PageId': String(pageId) } : {}),
'X-Origin': currentDomain,
...(auth != null ? { Authorization: auth } : {})
},
Expand Down
18 changes: 16 additions & 2 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ const parseAddChatItemAction = (action: Ytc.AddChatItemAction, isReplay = false,
};
const channelId = renderer.authorExternalChannelId;

const canDelete = messageRenderer.inlineActionButtons?.some(
(b) => b.buttonRenderer?.icon?.iconType === 'DELETE'
) ?? false;

const item: Ytc.ParsedMessage = {
author: {
// It's apparently possible for there to be no author name (and only an author photo).
Expand All @@ -257,7 +261,8 @@ const parseAddChatItemAction = (action: Ytc.AddChatItemAction, isReplay = false,
timestamp: isReplay && timestampText != null ? timestampText : formatTimestamp(timestampUsec),
showtime: isReplay ? liveTimeoutOrReplayMs : liveShowtimeMs,
messageId: renderer.id,
params: messageRenderer.contextMenuEndpoint?.liveChatItemContextMenuEndpoint.params
params: messageRenderer.contextMenuEndpoint?.liveChatItemContextMenuEndpoint.params,
canDelete
};
if (channelId != null) {
item.author.url = `${currentDomain}/channel/${channelId}`;
Expand Down Expand Up @@ -341,7 +346,10 @@ const parseAuthorBonkedAction = (action: Ytc.AuthorBonkedAction): Ytc.ParsedBonk
const parseMessageDeletedAction = (action: Ytc.MessageDeletedAction): Ytc.ParsedDeleted | undefined => {
return {
replacedMessage: parseMessageRuns(action.deletedStateMessage.runs),
messageId: action.targetItemId
messageId: action.targetItemId,
viewOriginalText: action.showOriginalContentMessage
? parseMessageRuns(action.showOriginalContentMessage.runs)
: undefined
};
};

Expand Down Expand Up @@ -469,6 +477,12 @@ const processLiveAction = (action: Ytc.Action, isReplay: boolean, liveTimeoutMs:
return parseAuthorBonkedAction(action.markChatItemsByAuthorAsDeletedAction);
} else if (action.markChatItemAsDeletedAction) {
return parseMessageDeletedAction(action.markChatItemAsDeletedAction);
} else if (action.removeChatItemAction) {
return {
replacedMessage: [],
messageId: action.removeChatItemAction.targetItemId,
pending: true
};
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/ts/chat-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,12 @@ export const stripYoutubePlayerShell = (): void => {
}
};


export const buildDeletedObj = (
deletion: Ytc.ParsedDeleted,
originalRuns: Ytc.ParsedRun[]
): Chat.MessageDeletedObj => ({
replace: deletion.pending ? originalRuns : deletion.replacedMessage,
viewOriginalText: deletion.viewOriginalText,
pending: deletion.pending
});
3 changes: 2 additions & 1 deletion src/ts/queue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parseChatResponse } from './chat-parser';
import { buildDeletedObj } from './chat-utils';

interface QueueItem<T> { data: T, next?: QueueItem<T> }
export interface Queue<T> {
Expand Down Expand Up @@ -195,7 +196,7 @@ export function ytcQueue(isReplay = false): YtcQueue {
}
for (const d of deletions) {
if (message.messageId !== d.messageId) continue;
messageAction.deleted = { replace: d.replacedMessage };
messageAction.deleted = buildDeletedObj(d, message.message);
return;
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/ts/typings/chat.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
declare namespace Chat {
interface MessageDeletedObj {
replace: Ytc.ParsedRun[];
viewOriginalText?: Ytc.ParsedRun[];
pending?: boolean;
}

interface MessageAction {
Expand Down
18 changes: 18 additions & 0 deletions src/ts/typings/ytc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ declare namespace Ytc {
replayChatItemAction?: ReplayChatItemAction;
markChatItemsByAuthorAsDeletedAction?: AuthorBonkedAction;
markChatItemAsDeletedAction?: MessageDeletedAction;
removeChatItemAction?: RemoveChatItemAction;
}

/*
Expand Down Expand Up @@ -85,6 +86,11 @@ declare namespace Ytc {
externalChannelId: string;
}

/** YTC removeChatItemAction object */
interface RemoveChatItemAction {
targetItemId: string;
}

/** YTC markChatItemAsDeletedAction object. */
interface MessageDeletedAction extends IDeleted {
/** ID of message to be deleted */
Expand Down Expand Up @@ -224,6 +230,12 @@ declare namespace Ytc {
params: string;
};
};
/** Mod-only quick-action buttons (Remove/Timeout/Hide). */
inlineActionButtons?: Array<{
buttonRenderer?: {
icon?: { iconType?: string };
};
}>;
/** Reply-to-superchat button on normal text messages. */
beforeContentButtons?: Array<{
buttonViewModel?: ReplyButtonViewModel;
Expand Down Expand Up @@ -423,6 +435,8 @@ declare namespace Ytc {
interface IDeleted {
/** Message to replace deleted messages. */
deletedStateMessage: RunsObj;
/** Mod-only "View deleted message" affordance. */
showOriginalContentMessage?: RunsObj;
}

/** Integer formatted as string for whatever reason */
Expand Down Expand Up @@ -512,6 +526,7 @@ declare namespace Ytc {
params?: string;
membershipGiftPurchase?: ParsedMembershipGiftPurchase;
membershipGiftRedeem?: boolean;
canDelete?: boolean;
/** Reply context when this message is a reply to a Super Chat. */
replyToSuperchat?: ParsedReplyToSuperchat;
/** Opaque get_panel params for fetching this message's own reply thread (set on SCs). */
Expand Down Expand Up @@ -543,6 +558,9 @@ declare namespace Ytc {
interface ParsedDeleted {
replacedMessage: ParsedRun[];
messageId: string;
viewOriginalText?: ParsedRun[];
/** No replacement text from YT — keep original text and mark as awaiting retraction (line-through). */
pending?: boolean;
}

interface ParsedPinned {
Expand Down
Loading