mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
refactor: improve superchat UI and gift membership handling
This commit is contained in:
@@ -9,7 +9,7 @@ const MAX_MESSAGES = 500;
|
|||||||
export async function startBackend() {
|
export async function startBackend() {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: {
|
logger: {
|
||||||
level: 'info'
|
level: 'warn', // Only show warnings and errors, not every request
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ export async function startBackend() {
|
|||||||
ingestion = await bootstrapInnertube(parsedLiveId);
|
ingestion = await bootstrapInnertube(parsedLiveId);
|
||||||
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
||||||
ingestion.emitter.on('message', (message) => {
|
ingestion.emitter.on('message', (message) => {
|
||||||
console.log(`[Chat] ${message.author}: ${message.text}`);
|
|
||||||
store.push(message);
|
store.push(message);
|
||||||
if (store.length > MAX_MESSAGES) {
|
if (store.length > MAX_MESSAGES) {
|
||||||
store.splice(0, store.length - MAX_MESSAGES);
|
store.splice(0, store.length - MAX_MESSAGES);
|
||||||
@@ -106,7 +105,6 @@ export async function startBackend() {
|
|||||||
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
||||||
|
|
||||||
ingestion.emitter.on('message', (message) => {
|
ingestion.emitter.on('message', (message) => {
|
||||||
console.log(`[Chat] ${message.author}: ${message.text}`);
|
|
||||||
store.push(message);
|
store.push(message);
|
||||||
if (store.length > MAX_MESSAGES) {
|
if (store.length > MAX_MESSAGES) {
|
||||||
store.splice(0, store.length - MAX_MESSAGES);
|
store.splice(0, store.length - MAX_MESSAGES);
|
||||||
|
|||||||
@@ -164,13 +164,15 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
|
|||||||
'liveChatPaidStickerRenderer'
|
'liveChatPaidStickerRenderer'
|
||||||
]);
|
]);
|
||||||
const typeName = String(item.type || item.item_type || item.renderer || '').trim();
|
const typeName = String(item.type || item.item_type || item.renderer || '').trim();
|
||||||
const isSuper = superTypes.has(typeName) || !!(item.purchase_amount_text || item.purchaseAmountText);
|
const isSuper = superTypes.has(typeName) || !!(item.purchase_amount);
|
||||||
if (!isSuper) return undefined;
|
if (!isSuper) return undefined;
|
||||||
|
|
||||||
let amountText = '';
|
let amountText = '';
|
||||||
const candidates = [
|
const candidates = [
|
||||||
item.purchase_amount_text,
|
item.purchase_amount, // Correct property name from youtubei.js docs
|
||||||
|
item.purchase_amount_text, // Fallback
|
||||||
item.purchaseAmountText,
|
item.purchaseAmountText,
|
||||||
|
item.header?.purchase_amount,
|
||||||
item.header?.purchase_amount_text,
|
item.header?.purchase_amount_text,
|
||||||
item.header?.purchaseAmountText,
|
item.header?.purchaseAmountText,
|
||||||
item.amount,
|
item.amount,
|
||||||
@@ -183,6 +185,9 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
|
|||||||
if (typeof v === 'number') return String(v);
|
if (typeof v === 'number') return String(v);
|
||||||
if (typeof v.simpleText === 'string') return v.simpleText;
|
if (typeof v.simpleText === 'string') return v.simpleText;
|
||||||
if (Array.isArray(v.runs)) return v.runs.map((r: any) => r.text ?? '').join('');
|
if (Array.isArray(v.runs)) return v.runs.map((r: any) => r.text ?? '').join('');
|
||||||
|
if (typeof v.toString === 'function' && v.toString !== Object.prototype.toString) {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,22 +196,27 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
|
|||||||
if (amountText) break;
|
if (amountText) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount = 'Super Chat';
|
let amount = '';
|
||||||
let currency = '';
|
let currency = '';
|
||||||
|
|
||||||
if (amountText) {
|
if (amountText) {
|
||||||
// Regex to capture currency symbol/code and amount
|
// Regex to capture currency symbol/code and amount
|
||||||
// Handles: $5.00, €5,00, 5,00 €, 5.00 USD, TRY5.00
|
// Handles: $5.00, €5,00, 5,00 €, 5.00 USD, TRY5.00, TRY 55, etc.
|
||||||
const match = amountText.match(/([\$\€\£\¥\₹\₺A-Z]+)?\s*([\d,\.,]+)\s*([\$\€\£\¥\₹\₺A-Z]+)?/);
|
const match = amountText.match(/([\$\€\£\¥\₹\₺]|[A-Z]{2,3})?\s*([\d,\.]+)\s*([\$\€\£\¥\₹\₺]|[A-Z]{2,3})?/);
|
||||||
if (match) {
|
if (match) {
|
||||||
// Prefer currency symbol before the number, fallback to after
|
// Prefer currency symbol/code before the number, fallback to after
|
||||||
currency = match[1] || match[3] || '';
|
currency = match[1] || match[3] || '';
|
||||||
amount = match[2];
|
amount = match[2];
|
||||||
} else {
|
} else {
|
||||||
// Fallback for cases where only the number is present
|
// Fallback: use the whole text if no pattern matches
|
||||||
amount = amountText;
|
amount = amountText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we still don't have an amount, use a default
|
||||||
|
if (!amount) {
|
||||||
|
amount = 'Super Chat';
|
||||||
|
}
|
||||||
|
|
||||||
let rawColor = item.body_background_color ?? item.bodyBackgroundColor ?? item.headerBackgroundColor;
|
let rawColor = item.body_background_color ?? item.bodyBackgroundColor ?? item.headerBackgroundColor;
|
||||||
let color = '#1e3a8a';
|
let color = '#1e3a8a';
|
||||||
@@ -237,13 +247,38 @@ function normalizeAction(action: any): ChatMessage | null {
|
|||||||
|
|
||||||
// Normalize various live chat events
|
// Normalize various live chat events
|
||||||
const itemType = String(item.type || '').trim();
|
const itemType = String(item.type || '').trim();
|
||||||
|
const messageText = resolveMessageText(item).toLowerCase();
|
||||||
|
|
||||||
const isText = itemType === 'LiveChatTextMessage';
|
const isText = itemType === 'LiveChatTextMessage';
|
||||||
const isPaid = !!extractSuperChatInfo(item);
|
const isPaid = !!extractSuperChatInfo(item);
|
||||||
const isMembership = itemType === 'LiveChatMembershipItem';
|
const isMembership = itemType === 'LiveChatMembershipItem';
|
||||||
const isGiftPurchase = itemType === 'LiveChatGiftMembershipsPurchase' || itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement';
|
const isGiftPurchase = itemType === 'LiveChatSponsorshipsGiftPurchaseAnnouncement';
|
||||||
const isGiftReceived = itemType === 'LiveChatGiftMembershipReceived';
|
const isGiftReceived = itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement';
|
||||||
|
|
||||||
|
// Check if the message text indicates it's a gift recipient message
|
||||||
|
const isGiftRecipientMessage =
|
||||||
|
messageText.includes('received a gift membership') ||
|
||||||
|
messageText.includes('received a membership gift') ||
|
||||||
|
messageText.includes('received a gift') ||
|
||||||
|
/received\s+a\s+.*membership.*by/i.test(messageText);
|
||||||
|
|
||||||
if (isText || isPaid || isMembership || isGiftPurchase || isGiftReceived) {
|
// Log all gift-related messages for debugging
|
||||||
|
if (isGiftPurchase || isGiftReceived || isGiftRecipientMessage) {
|
||||||
|
console.log(`[Gift Debug] Type: ${itemType}, Author: ${item.author?.name}, Text: "${messageText}", Purchase: ${isGiftPurchase}, Received: ${isGiftReceived}, TextPattern: ${isGiftRecipientMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore gift received messages - we only care about the purchaser
|
||||||
|
if (isGiftReceived || isGiftRecipientMessage) {
|
||||||
|
console.log(`[Gift] Filtering out recipient message from: ${item.author?.name}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log gift purchase messages for debugging
|
||||||
|
if (isGiftPurchase) {
|
||||||
|
console.log(`[Chat] Gift purchase detected - Author: ${item.author?.name}, Type: ${itemType}, Text: ${messageText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isText || isPaid || isMembership || isGiftPurchase) {
|
||||||
const badges = extractBadges(item);
|
const badges = extractBadges(item);
|
||||||
const isModerator = badges.some(b => b.type === 'moderator');
|
const isModerator = badges.some(b => b.type === 'moderator');
|
||||||
const isMember = badges.some(b => b.type === 'member');
|
const isMember = badges.some(b => b.type === 'member');
|
||||||
@@ -251,7 +286,7 @@ function normalizeAction(action: any): ChatMessage | null {
|
|||||||
|
|
||||||
// Extract membership level for new members
|
// Extract membership level for new members
|
||||||
let membershipLevel: string | undefined;
|
let membershipLevel: string | undefined;
|
||||||
if (isMembership || isGiftPurchase || isGiftReceived || isPaid) {
|
if (isMembership || isGiftPurchase || isPaid) {
|
||||||
membershipLevel = item.header_subtext?.toString() ||
|
membershipLevel = item.header_subtext?.toString() ||
|
||||||
item.header_primary_text?.toString() ||
|
item.header_primary_text?.toString() ||
|
||||||
(isPaid ? undefined : 'New member');
|
(isPaid ? undefined : 'New member');
|
||||||
@@ -263,8 +298,9 @@ function normalizeAction(action: any): ChatMessage | null {
|
|||||||
const text = resolveMessageText(item);
|
const text = resolveMessageText(item);
|
||||||
const headerText = item.header_primary_text?.toString() || item.header_subtext?.toString() || '';
|
const headerText = item.header_primary_text?.toString() || item.header_subtext?.toString() || '';
|
||||||
const combinedText = text + ' ' + headerText;
|
const combinedText = text + ' ' + headerText;
|
||||||
// Try to extract number from text like "Gifted 5 memberships" or "5 memberships"
|
|
||||||
const countMatch = combinedText.match(/(\d+)\s*(?:membership|memberships|member|members)/i);
|
// Try to extract number from text like "Gifted 5 memberships", "Sent 5 gift memberships", or "5 memberships"
|
||||||
|
const countMatch = combinedText.match(/(?:sent|gifted)?\s*(\d+)\s*(?:gift\s*)?(?:membership|memberships|member|members)/i);
|
||||||
if (countMatch) {
|
if (countMatch) {
|
||||||
giftCount = parseInt(countMatch[1], 10);
|
giftCount = parseInt(countMatch[1], 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,15 +435,15 @@ function ChatItem({ message, isSelected, onSelect }: ChatItemProps) {
|
|||||||
{badge.type === 'verified' && '✓'}
|
{badge.type === 'verified' && '✓'}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
{message.superChat && (
|
||||||
|
<span className="chatItem__superchat-inline" style={{ backgroundColor: message.superChat.color }}>
|
||||||
|
{message.superChat.currency}{message.superChat.currency ? ' ' : ''}{message.superChat.amount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<time className="chatItem__time">{new Date(message.publishedAt).toLocaleTimeString()}</time>
|
<time className="chatItem__time">{new Date(message.publishedAt).toLocaleTimeString()}</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{message.superChat && (
|
|
||||||
<div className="chatItem__superchat" style={{ backgroundColor: message.superChat.color }}>
|
|
||||||
💰 {message.superChat.currency}{message.superChat.amount}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="chatItem__text">{message.text}</p>
|
<p className="chatItem__text">{message.text}</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -463,7 +463,7 @@ function MemberItem({ message, isSelected, onSelect }: ChatItemProps) {
|
|||||||
<span className="memberItem__author">{message.author}</span>
|
<span className="memberItem__author">{message.author}</span>
|
||||||
<span className="memberItem__level">
|
<span className="memberItem__level">
|
||||||
{message.membershipGiftPurchase && message.giftCount
|
{message.membershipGiftPurchase && message.giftCount
|
||||||
? `Gifted ${message.giftCount} membership${message.giftCount > 1 ? 's' : ''}`
|
? `Sent ${message.giftCount} gift membership${message.giftCount > 1 ? 's' : ''}`
|
||||||
: message.membershipLevel || 'New member'}
|
: message.membershipLevel || 'New member'}
|
||||||
</span>
|
</span>
|
||||||
<time className="memberItem__time">{new Date(message.publishedAt).toLocaleTimeString()}</time>
|
<time className="memberItem__time">{new Date(message.publishedAt).toLocaleTimeString()}</time>
|
||||||
|
|||||||
@@ -686,6 +686,17 @@ main {
|
|||||||
align-self: flex-start; /* Align to the left */
|
align-self: flex-start; /* Align to the left */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chatItem__superchat-inline {
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.chatItem__membership {
|
.chatItem__membership {
|
||||||
padding: 0.6rem 1rem;
|
padding: 0.6rem 1rem;
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
@@ -944,6 +955,53 @@ main {
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-name {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-separator {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-amount {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__superchat-text {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: left;
|
||||||
|
word-break: break-word;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay__membership {
|
.overlay__membership {
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-32
@@ -67,39 +67,54 @@ export default function OverlayPage() {
|
|||||||
<main className="overlay">
|
<main className="overlay">
|
||||||
{message ? (
|
{message ? (
|
||||||
<div className={`overlay__card ${fadingOut ? 'overlay__card--fadeOut' : ''}`}>
|
<div className={`overlay__card ${fadingOut ? 'overlay__card--fadeOut' : ''}`}>
|
||||||
<div className="overlay__header">
|
{message.superChat ? (
|
||||||
{message.authorPhoto && (
|
<>
|
||||||
<img src={message.authorPhoto} alt={message.author} className="overlay__avatar" />
|
<div className="overlay__superchat-header" style={{ backgroundColor: message.superChat.color }}>
|
||||||
)}
|
{message.authorPhoto && (
|
||||||
<div>
|
<img src={message.authorPhoto} alt={message.author} className="overlay__superchat-avatar" />
|
||||||
<div className="overlay__authorLine">
|
)}
|
||||||
<span className="overlay__author">{message.author}</span>
|
<span className="overlay__superchat-name">{message.author}</span>
|
||||||
{message.badges && message.badges.map((badge, i) => (
|
<span className="overlay__superchat-separator"> - </span>
|
||||||
<span key={i} className={`overlay__badge overlay__badge--${badge.type}`} title={badge.label}>
|
<span className="overlay__superchat-amount">
|
||||||
{badge.type === 'moderator' && '🛡️'}
|
{message.superChat.currency}{message.superChat.currency ? ' ' : ''}{message.superChat.amount}
|
||||||
{badge.type === 'member' && '⭐'}
|
</span>
|
||||||
{badge.type === 'verified' && '✓'}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{message.text && (
|
||||||
</div>
|
<p className="overlay__superchat-text">{message.text}</p>
|
||||||
{message.superChat && (
|
)}
|
||||||
<div className="overlay__superchat" style={{ backgroundColor: message.superChat.color }}>
|
</>
|
||||||
💰 {message.superChat.currency}{message.superChat.amount}
|
) : (
|
||||||
</div>
|
<>
|
||||||
)}
|
<div className="overlay__header">
|
||||||
{(message.membershipGift || message.membershipGiftPurchase) && (
|
{message.authorPhoto && (
|
||||||
<div className="overlay__membership">
|
<img src={message.authorPhoto} alt={message.author} className="overlay__avatar" />
|
||||||
{message.membershipGiftPurchase && message.giftCount
|
)}
|
||||||
? `🎁 Gifted ${message.giftCount} Membership${message.giftCount > 1 ? 's' : ''}!`
|
<div>
|
||||||
: message.membershipGiftPurchase
|
<div className="overlay__authorLine">
|
||||||
? '🎁 Gift Purchase'
|
<span className="overlay__author">{message.author}</span>
|
||||||
: '🎁 New Member!'}
|
{message.badges && message.badges.map((badge, i) => (
|
||||||
</div>
|
<span key={i} className={`overlay__badge overlay__badge--${badge.type}`} title={badge.label}>
|
||||||
)}
|
{badge.type === 'moderator' && '🛡️'}
|
||||||
{(!message.superChat && !message.membershipGift && !message.membershipGiftPurchase) && message.text && (
|
{badge.type === 'member' && '⭐'}
|
||||||
<p className="overlay__text">{message.text}</p>
|
{badge.type === 'verified' && '✓'}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(message.membershipGift || message.membershipGiftPurchase) && (
|
||||||
|
<div className="overlay__membership">
|
||||||
|
{message.membershipGiftPurchase && message.giftCount
|
||||||
|
? `🎁 Sent ${message.giftCount} Gift Membership${message.giftCount > 1 ? 's' : ''}!`
|
||||||
|
: message.membershipGiftPurchase
|
||||||
|
? '🎁 Gift Purchase'
|
||||||
|
: '🎁 New Member!'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{message.text && (
|
||||||
|
<p className="overlay__text">{message.text}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user