Skip to content
Open
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
23 changes: 13 additions & 10 deletions src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ChatUserActions
} from '../ts/chat-constants';
import {
buildDeletedObj,
isAllEmoji,
isChatMessage,
isPrivileged,
Expand Down Expand Up @@ -249,14 +250,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 @@ -353,20 +355,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 @@ -515,7 +518,7 @@
{#if $enableStickySuperchatBar}
<StickyBar />
{/if}
<div class="w-screen min-h-0 flex justify-end flex-col relative">
<div class="w-screen 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
41 changes: 32 additions & 9 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 @@ -98,7 +107,7 @@
{#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
18 changes: 16 additions & 2 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,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 @@ -259,7 +263,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 @@ -343,7 +348,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 @@ -471,6 +479,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 @@ -131,3 +131,12 @@ export const useReconnect = <T extends Chat.Port>(connect: () => Promise<T>): Re
}
};
};

export const buildDeletedObj = (
deletion: Ytc.ParsedDeleted,
originalRuns: Ytc.ParsedRun[]
): Chat.MessageDeletedObj => ({
replace: deletion.pending ? originalRuns : deletion.replacedMessage,
viewOriginalText: deletion.viewOriginalText,
pending: deletion.pending
});
2 changes: 2 additions & 0 deletions src/ts/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const buildInnertubeHeaders = (ytcfg: YtCfg) => {
const visitorId = (ytcfg as any)?.data_?.VISITOR_DATA ?? ytcfg.data_.INNERTUBE_CONTEXT?.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 @@ -74,6 +75,7 @@ const buildInnertubeHeaders = (ytcfg: YtCfg) => {
...(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
3 changes: 2 additions & 1 deletion src/ts/queue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parseChatResponse } from './chat-parser';
import type { Chat } from './typings/chat';
import { buildDeletedObj } from './chat-utils';

interface QueueItem<T> { data: T, next?: QueueItem<T> }
export interface Queue<T> {
Expand Down Expand Up @@ -196,7 +197,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
Expand Up @@ -4,6 +4,8 @@ import type { Unsubscriber, YtcQueue } from '../queue';
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 @@ -62,6 +62,7 @@ declare namespace Ytc {
replayChatItemAction?: ReplayChatItemAction;
markChatItemsByAuthorAsDeletedAction?: AuthorBonkedAction;
markChatItemAsDeletedAction?: MessageDeletedAction;
removeChatItemAction?: RemoveChatItemAction;
}

/*
Expand Down Expand Up @@ -89,6 +90,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 @@ -223,6 +229,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 @@ -422,6 +434,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 @@ -511,6 +525,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 @@ -542,6 +557,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