By using this service, you agree to our Terms of Service and acknowledge our disclaimers.
diff --git a/client/components/LeftPanel.tsx b/client/components/LeftPanel.tsx
index 81c8099..f4f2f73 100644
--- a/client/components/LeftPanel.tsx
+++ b/client/components/LeftPanel.tsx
@@ -3,6 +3,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { MediaModal } from '@/components/MediaModal';
+import { AnimatedAvatar } from '@/components/AnimatedAvatar';
import {
Users,
Circle,
@@ -41,6 +42,7 @@ interface LeftPanelProps {
isVisible: boolean;
className?: string;
users?: ActiveUser[];
+ currentUser?: ActiveUser | null;
mediaFiles?: MediaFile[];
onFileUpload?: (files: FileList) => void;
onFileDelete?: (fileId: string) => void;
@@ -50,6 +52,7 @@ interface LeftPanelProps {
export const LeftPanel: React.FC = ({
isVisible,
users = [],
+ currentUser,
mediaFiles = [],
onFileDelete,
onModalStateChange
@@ -280,115 +283,8 @@ export const LeftPanel: React.FC = ({
isVisible ? 'transform-none' : '-translate-x-full'
}`}
>
- {/* Users Panel */}
-
-
-
-
- Users
-
-
-
-
- {activeUsers.length === 0 ? (
-
- ) : (
- activeUsers.map((user) => {
- const { status, color } = getStatusIndicator(user);
- return (
-
-
-
-
-
-
- {user.name.charAt(0).toUpperCase()}
-
-
-
-
-
- {user.name}
-
-
- {formatLastSeen(user.lastSeen)}
-
-
-
-
- {status}
-
-
-
- {user.currentLine && (
-
-
- Line {user.currentLine}
-
- {user.isTyping && (
-
- )}
-
- )}
-
-
- );
- })
- )}
-
-
-
-
-
- {activeUsers.filter(u => getStatusIndicator(u).status === 'online').length} online
-
-
- {activeUsers.filter(u => u.isTyping).length} typing
-
-
-
-
-
{/* Media Panel */}
-
+
@@ -543,6 +439,118 @@ export const LeftPanel: React.FC = ({
+ {/* Users Panel */}
+
+
+
+
+ Users
+
+
+
+
+ {activeUsers.length === 0 ? (
+
+ ) : (
+ activeUsers.map((user) => {
+ const { status, color } = getStatusIndicator(user);
+ const isCurrentUser = currentUser && user.id === currentUser.id;
+ return (
+
+
+
+
+
+ {isCurrentUser ? (
+
+ ) : (
+
+ {user.name.charAt(0).toUpperCase()}
+
+ )}
+
+
+
+
+ {isCurrentUser ? 'You' : user.name}
+
+
+ {formatLastSeen(user.lastSeen)}
+
+
+
+
+ {status}
+
+
+
+ {user.currentLine && (
+
+
+ Line {user.currentLine}
+
+ {user.isTyping && (
+
+ )}
+
+ )}
+
+
+ );
+ })
+ )}
+
+
+
+
+
+ {activeUsers.filter(u => getStatusIndicator(u).status === 'online').length} online
+
+
+ {activeUsers.filter(u => u.isTyping).length} typing
+
+
+
+
+
{/* Media Modal */}
= ({
return (
-
+ e.stopPropagation()}>
e.stopPropagation()}
diff --git a/client/components/ui/BetterHoverCard.tsx b/client/components/ui/BetterHoverCard.tsx
new file mode 100644
index 0000000..40dc2fa
--- /dev/null
+++ b/client/components/ui/BetterHoverCard.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import * as React from "react";
+import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card";
+import { cn } from "@/lib/utils";
+
+interface CustomHoverCardProps {
+ trigger: React.ReactNode;
+ children: React.ReactNode;
+ holdDelay?: number;
+ contentClassName?: string;
+}
+
+// Context for managing mutually exclusive hover cards
+const HoverCardContext = React.createContext<{
+ openId: string | null;
+ setOpenId: (id: string | null) => void;
+}>({
+ openId: null,
+ setOpenId: () => {},
+});
+
+export const BetterHoverCard = ({ trigger, children, holdDelay = 600, contentClassName }: CustomHoverCardProps) => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [isTouchHeld, setIsTouchHeld] = React.useState(false);
+ const timerRef = React.useRef();
+ const isTouchDevice = React.useMemo(() => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0), []);
+ const idRef = React.useRef(Math.random().toString(36).substr(2, 9));
+ const { openId, setOpenId } = React.useContext(HoverCardContext);
+
+ const handlePointerDown = (e: React.PointerEvent) => {
+ if (isTouchDevice && e.pointerType === 'touch') {
+ e.currentTarget.addEventListener('contextmenu', (e) => e.preventDefault(), { once: true });
+ timerRef.current = setTimeout(() => {
+ setIsOpen(true);
+ setIsTouchHeld(true);
+ setOpenId(idRef.current);
+ }, holdDelay);
+ }
+ };
+
+ const handlePointerUp = (e: React.PointerEvent) => {
+ if (isTouchDevice && e.pointerType === 'touch') {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ }
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ if (isTouchDevice && open && !isTouchHeld) {
+ return; // Ignore open on tap for touch devices
+ }
+ if (open) {
+ setOpenId(idRef.current);
+ } else if (openId === idRef.current) {
+ setOpenId(null);
+ }
+ setIsOpen(open);
+ if (!open) {
+ setIsTouchHeld(false);
+ }
+ };
+
+ // Sync isOpen with context
+ React.useEffect(() => {
+ setIsOpen(openId === idRef.current);
+ }, [openId]);
+
+ return (
+
+
+ {trigger}
+
+
+ {children}
+
+
+ );
+};
+
+// Provider component to wrap the app or relevant section
+export const HoverCardProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [openId, setOpenId] = React.useState(null);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/components/ui/hover-card.tsx b/client/components/ui/hover-card.tsx
index a66613a..2a736de 100644
--- a/client/components/ui/hover-card.tsx
+++ b/client/components/ui/hover-card.tsx
@@ -5,7 +5,9 @@ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
-const HoverCard = HoverCardPrimitive.Root
+const HoverCard = ({ openDelay = 200, ...props }: React.ComponentPropsWithoutRef) => (
+
+);
const HoverCardTrigger = HoverCardPrimitive.Trigger
diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts
index 795d550..4f075c6 100644
--- a/client/tailwind.config.ts
+++ b/client/tailwind.config.ts
@@ -87,6 +87,11 @@ const config = {
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
+ "gradient-flow": {
+ '0%': { backgroundPosition: '0% 50%' },
+ '50%': { backgroundPosition: '100% 50%' },
+ '100%': { backgroundPosition: '0% 50%' },
+ },
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
@@ -97,6 +102,7 @@ const config = {
},
},
animation: {
+ "gradient-flow": "gradient-flow 3s ease-in-out infinite",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},