p-stream/src/components/player/atoms/WatchPartyStatus.tsx
2025-12-27 21:32:22 -07:00

166 lines
6.1 KiB
TypeScript

import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useWatchPartySync } from "@/hooks/useWatchPartySync";
import { useAuthStore } from "@/stores/auth";
import { getProgressPercentage } from "@/stores/progress";
import { useWatchPartyStore } from "@/stores/watchParty";
export function WatchPartyStatus() {
const { t } = useTranslation();
const { enabled, roomCode, isHost, showStatusOverlay } = useWatchPartyStore();
const [expanded, setExpanded] = useState(false);
const [showNotification, setShowNotification] = useState(false);
const [lastUserCount, setLastUserCount] = useState(1);
const account = useAuthStore((s) => s.account);
const backendUrl = useBackendUrl();
const backendHostname = backendUrl ? new URL(backendUrl).hostname : null;
const {
roomUsers,
hostUser,
isBehindHost,
isAheadOfHost,
timeDifferenceFromHost,
syncWithHost,
isSyncing,
userCount,
} = useWatchPartySync();
// Show notification when users join
useEffect(() => {
if (userCount > lastUserCount) {
setShowNotification(true);
const timer = setTimeout(() => setShowNotification(false), 3000);
return () => clearTimeout(timer);
}
setLastUserCount(userCount);
}, [userCount, lastUserCount]);
// If watch party is not enabled or overlay is hidden, don't show anything
if (!enabled || !roomCode || !showStatusOverlay) return null;
// Toggle expanded state
const handleToggleExpanded = () => {
setExpanded(!expanded);
};
// Get display name for a user (nickname if it's the current user, otherwise truncated userId)
const getDisplayName = (userId: string) => {
if (account?.userId === userId && account?.nickname) {
return account.nickname;
}
return `${userId.substring(0, 12)}...`;
};
return (
<div
className={`absolute top-4 right-4 z-50 p-2 bg-mediaCard-shadow bg-opacity-70 backdrop-blur-sm rounded-md text-white text-xs
flex flex-col items-end gap-1 max-w-[260px] transition-all duration-300
${showNotification ? "ring-1 ring-buttons-purple shadow-lg shadow-buttons-purple" : ""}`}
>
<div className="flex gap-2 w-full justify-between items-center">
<div className="flex items-center gap-2">
<Icon icon={Icons.WATCH_PARTY} className="w-4 h-4" />
<span className="font-bold pr-1">
{isHost ? t("watchParty.hosting") : t("watchParty.watching")}
</span>
</div>
<span className="text-type-logo font-mono tracking-wider">
{roomCode}
</span>
</div>
{backendHostname && (
<div className="w-full text-xs text-type-secondary text-center">
{t("watchParty.activeBackend", { backend: backendHostname })}
</div>
)}
<div className="w-full text-type-secondary flex justify-between items-center space-x-2">
<div className="cursor-pointer" onClick={handleToggleExpanded}>
<Icon
icon={expanded ? Icons.CHEVRON_DOWN : Icons.CHEVRON_RIGHT}
className="w-3 h-3"
/>
</div>
<span>
{roomUsers.length <= 1
? t("watchParty.alone")
: t("watchParty.withCount", { count: roomUsers.length - 1 })}
</span>
{/* Sync status indicator */}
{!isHost && hostUser && (
<div className="flex items-center gap-1">
<div
className={`w-2 h-2 rounded-full ${
isBehindHost || isAheadOfHost ? "bg-red-500" : "bg-green-500"
}`}
/>
<span className="text-xs">
{isBehindHost || isAheadOfHost
? t("watchParty.status.outOfSync")
: t("watchParty.status.inSync")}
</span>
</div>
)}
</div>
{expanded && roomUsers.length > 1 && (
<div className="w-full mt-1 border-t border-mediaCard-hoverBackground pt-1">
<div className="text-xs text-type-secondary mb-1">Viewers:</div>
<div className="space-y-1 max-h-24 overflow-y-auto">
{roomUsers.map((user) => (
<div
key={user.userId}
className="flex items-center justify-between text-xs"
>
<span className="flex items-center gap-1">
<Icon
icon={user.isHost ? Icons.RISING_STAR : Icons.USER}
className={`w-3 h-3 ${user.isHost ? "text-onboarding-best" : ""}`}
/>
<span className={user.isHost ? "text-onboarding-best" : ""}>
{getDisplayName(user.userId)}
</span>
</span>
<span className="text-type-secondary">
{user.player.duration > 0
? `${Math.floor(getProgressPercentage(user.player.time, user.player.duration))}%`
: `${Math.floor(user.player.time)}s`}
</span>
</div>
))}
</div>
</div>
)}
{!isHost && hostUser && (isBehindHost || isAheadOfHost) && (
<div className="mt-1 w-full">
<Button
theme="secondary"
className="text-xs py-1 px-2 bg-buttons-purple bg-opacity-50 hover:bg-buttons-purpleHover hover:bg-opacity-80 w-full flex items-center justify-center gap-1"
onClick={syncWithHost}
disabled={isSyncing}
>
<Icon icon={Icons.CLOCK} className="w-3 h-3" />
<span className="whitespace-nowrap">
{isSyncing
? t("watchParty.syncing")
: isBehindHost
? t("watchParty.behindHost", {
seconds: Math.abs(Math.round(timeDifferenceFromHost)),
})
: t("watchParty.aheadOfHost", {
seconds: Math.round(timeDifferenceFromHost),
})}
</span>
</Button>
</div>
)}
</div>
);
}