mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
feat: implement message transition handling in overlay component for smoother display changes
This commit is contained in:
+70
-46
@@ -12,9 +12,12 @@ type SelectionPayload = {
|
|||||||
|
|
||||||
export default function OverlayPage() {
|
export default function OverlayPage() {
|
||||||
const [message, setMessage] = useState<ChatMessage | null>(null);
|
const [message, setMessage] = useState<ChatMessage | null>(null);
|
||||||
|
const [displayMessage, setDisplayMessage] = useState<ChatMessage | null>(null);
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [fadingOut, setFadingOut] = useState(false);
|
const [fadingOut, setFadingOut] = useState(false);
|
||||||
|
const [switching, setSwitching] = useState(false);
|
||||||
const connectionRef = useRef<EventSource | null>(null);
|
const connectionRef = useRef<EventSource | null>(null);
|
||||||
|
const switchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent multiple connections
|
// Prevent multiple connections
|
||||||
@@ -30,22 +33,7 @@ export default function OverlayPage() {
|
|||||||
const onSelection = (event: MessageEvent) => {
|
const onSelection = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const payload: SelectionPayload = JSON.parse(event.data);
|
const payload: SelectionPayload = JSON.parse(event.data);
|
||||||
|
setMessage(payload.message);
|
||||||
setMessage((prevMessage) => {
|
|
||||||
if (payload.message === null && prevMessage !== null) {
|
|
||||||
// Trigger fade out animation before clearing
|
|
||||||
setFadingOut(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setMessage(null);
|
|
||||||
setFadingOut(false);
|
|
||||||
}, 300);
|
|
||||||
return prevMessage; // Keep current message during fade
|
|
||||||
} else {
|
|
||||||
setFadingOut(false);
|
|
||||||
return payload.message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('overlay: failed to parse payload', error);
|
console.error('overlay: failed to parse payload', error);
|
||||||
@@ -61,28 +49,64 @@ export default function OverlayPage() {
|
|||||||
source.removeEventListener('selection', onSelection as EventListener);
|
source.removeEventListener('selection', onSelection as EventListener);
|
||||||
source.close();
|
source.close();
|
||||||
connectionRef.current = null;
|
connectionRef.current = null;
|
||||||
|
if (switchTimeoutRef.current) {
|
||||||
|
clearTimeout(switchTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []); // Empty dependency array - only connect once
|
}, []); // Empty dependency array - only connect once
|
||||||
|
|
||||||
|
// Handle message transitions
|
||||||
|
useEffect(() => {
|
||||||
|
// Clear any pending timeout
|
||||||
|
if (switchTimeoutRef.current) {
|
||||||
|
clearTimeout(switchTimeoutRef.current);
|
||||||
|
switchTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === null && displayMessage !== null) {
|
||||||
|
// Deselecting - fade out
|
||||||
|
setFadingOut(true);
|
||||||
|
setSwitching(false);
|
||||||
|
switchTimeoutRef.current = setTimeout(() => {
|
||||||
|
setDisplayMessage(null);
|
||||||
|
setFadingOut(false);
|
||||||
|
}, 300);
|
||||||
|
} else if (message !== null && displayMessage !== null && message.id !== displayMessage.id) {
|
||||||
|
// Switching between messages - fade out then fade in
|
||||||
|
setSwitching(true);
|
||||||
|
setFadingOut(true);
|
||||||
|
switchTimeoutRef.current = setTimeout(() => {
|
||||||
|
setDisplayMessage(message);
|
||||||
|
setFadingOut(false);
|
||||||
|
setSwitching(false);
|
||||||
|
}, 300);
|
||||||
|
} else if (message !== null && displayMessage === null) {
|
||||||
|
// First message - just show it
|
||||||
|
setDisplayMessage(message);
|
||||||
|
setFadingOut(false);
|
||||||
|
setSwitching(false);
|
||||||
|
}
|
||||||
|
}, [message, displayMessage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="overlay">
|
<main className="overlay">
|
||||||
{message ? (
|
{displayMessage ? (
|
||||||
<div className={`overlay__card ${fadingOut ? 'overlay__card--fadeOut' : ''}`}>
|
<div className={`overlay__card ${fadingOut ? 'overlay__card--fadeOut' : ''}`}>
|
||||||
{message.superChat ? (
|
{displayMessage.superChat ? (
|
||||||
<>
|
<>
|
||||||
<div className="overlay__superchat-header" style={{ backgroundColor: message.superChat.color }}>
|
<div className="overlay__superchat-header" style={{ backgroundColor: displayMessage.superChat.color }}>
|
||||||
{message.authorPhoto && (
|
{displayMessage.authorPhoto && (
|
||||||
<img src={proxyImageUrl(message.authorPhoto)} alt={message.author} className="overlay__superchat-avatar" />
|
<img src={proxyImageUrl(displayMessage.authorPhoto)} alt={displayMessage.author} className="overlay__superchat-avatar" />
|
||||||
)}
|
)}
|
||||||
<span className="overlay__superchat-name">{message.author}</span>
|
<span className="overlay__superchat-name">{displayMessage.author}</span>
|
||||||
<span className="overlay__superchat-separator"> - </span>
|
<span className="overlay__superchat-separator"> - </span>
|
||||||
<span className="overlay__superchat-amount">
|
<span className="overlay__superchat-amount">
|
||||||
{message.superChat.currency}{message.superChat.currency ? ' ' : ''}{message.superChat.amount}
|
{displayMessage.superChat.currency}{displayMessage.superChat.currency ? ' ' : ''}{displayMessage.superChat.amount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{message.runs?.length ? (
|
{displayMessage.runs?.length ? (
|
||||||
<p className="overlay__superchat-text">
|
<p className="overlay__superchat-text">
|
||||||
{message.runs.map((r, i) =>
|
{displayMessage.runs.map((r, i) =>
|
||||||
r.emojiUrl ? (
|
r.emojiUrl ? (
|
||||||
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
||||||
) : (
|
) : (
|
||||||
@@ -90,29 +114,29 @@ export default function OverlayPage() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : message.text && message.text !== 'N/A' && (
|
) : displayMessage.text && displayMessage.text !== 'N/A' && (
|
||||||
<p className="overlay__superchat-text">{message.text}</p>
|
<p className="overlay__superchat-text">{displayMessage.text}</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (message.membershipGift || message.membershipGiftPurchase) ? (
|
) : (displayMessage.membershipGift || displayMessage.membershipGiftPurchase) ? (
|
||||||
<>
|
<>
|
||||||
<div className="overlay__membership-header">
|
<div className="overlay__membership-header">
|
||||||
{message.authorPhoto && (
|
{displayMessage.authorPhoto && (
|
||||||
<img src={proxyImageUrl(message.authorPhoto)} alt={message.author} className="overlay__membership-avatar" />
|
<img src={proxyImageUrl(displayMessage.authorPhoto)} alt={displayMessage.author} className="overlay__membership-avatar" />
|
||||||
)}
|
)}
|
||||||
<div className="overlay__membership-info">
|
<div className="overlay__membership-info">
|
||||||
<span className="overlay__membership-name">{message.author}</span>
|
<span className="overlay__membership-name">{displayMessage.author}</span>
|
||||||
<span className="overlay__membership-separator"> - </span>
|
<span className="overlay__membership-separator"> - </span>
|
||||||
<span className="overlay__membership-level">
|
<span className="overlay__membership-level">
|
||||||
{message.membershipGiftPurchase && message.giftCount
|
{displayMessage.membershipGiftPurchase && displayMessage.giftCount
|
||||||
? `Sent ${message.giftCount} Gift Membership${message.giftCount > 1 ? 's' : ''}`
|
? `Sent ${displayMessage.giftCount} Gift Membership${displayMessage.giftCount > 1 ? 's' : ''}`
|
||||||
: message.membershipLevel || 'New Member'}
|
: displayMessage.membershipLevel || 'New Member'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{message.runs?.length ? (
|
{displayMessage.runs?.length ? (
|
||||||
<p className="overlay__membership-text">
|
<p className="overlay__membership-text">
|
||||||
{message.runs.map((r, i) =>
|
{displayMessage.runs.map((r, i) =>
|
||||||
r.emojiUrl ? (
|
r.emojiUrl ? (
|
||||||
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
||||||
) : (
|
) : (
|
||||||
@@ -120,20 +144,20 @@ export default function OverlayPage() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : message.text && message.text !== 'N/A' && (
|
) : displayMessage.text && displayMessage.text !== 'N/A' && (
|
||||||
<p className="overlay__membership-text">{message.text}</p>
|
<p className="overlay__membership-text">{displayMessage.text}</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="overlay__header">
|
<div className="overlay__header">
|
||||||
{message.authorPhoto && (
|
{displayMessage.authorPhoto && (
|
||||||
<img src={proxyImageUrl(message.authorPhoto)} alt={message.author} className="overlay__avatar" />
|
<img src={proxyImageUrl(displayMessage.authorPhoto)} alt={displayMessage.author} className="overlay__avatar" />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="overlay__authorLine">
|
<div className="overlay__authorLine">
|
||||||
<span className="overlay__author">{message.author}</span>
|
<span className="overlay__author">{displayMessage.author}</span>
|
||||||
{message.badges && message.badges.map((badge, i) => (
|
{displayMessage.badges && displayMessage.badges.map((badge, i) => (
|
||||||
badge.imageUrl ? (
|
badge.imageUrl ? (
|
||||||
<img key={i} src={proxyImageUrl(badge.imageUrl)} alt={badge.label} className="overlay__badge overlay__badge--image" title={badge.label} />
|
<img key={i} src={proxyImageUrl(badge.imageUrl)} alt={badge.label} className="overlay__badge overlay__badge--image" title={badge.label} />
|
||||||
) : (
|
) : (
|
||||||
@@ -147,9 +171,9 @@ export default function OverlayPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{message.runs?.length ? (
|
{displayMessage.runs?.length ? (
|
||||||
<p className="overlay__text">
|
<p className="overlay__text">
|
||||||
{message.runs.map((r, i) =>
|
{displayMessage.runs.map((r, i) =>
|
||||||
r.emojiUrl ? (
|
r.emojiUrl ? (
|
||||||
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
<img key={i} src={proxyImageUrl(r.emojiUrl)} alt={r.emojiAlt || 'emoji'} className="overlay__emoji" />
|
||||||
) : (
|
) : (
|
||||||
@@ -157,8 +181,8 @@ export default function OverlayPage() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : message.text && message.text !== 'N/A' && (
|
) : displayMessage.text && displayMessage.text !== 'N/A' && (
|
||||||
<p className="overlay__text">{message.text}</p>
|
<p className="overlay__text">{displayMessage.text}</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user