Новый дизайн, удаление

This commit is contained in:
Халимов Рустам
2026-03-12 22:54:15 +03:00
parent 1ec6b438d2
commit 9daafae9c5
158 changed files with 1511 additions and 254 deletions

View File

@@ -16,7 +16,7 @@ export default function App() {
return (
<div className="h-full flex items-center justify-center bg-surface">
<div className="flex flex-col items-center gap-4">
<VortexLoader />
<AppLoader />
<p className="text-zinc-500 text-sm">Загрузка...</p>
</div>
</div>
@@ -37,16 +37,16 @@ export default function App() {
);
}
function VortexLoader() {
function AppLoader() {
return (
<div className="relative w-12 h-12">
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-vortex-500 animate-spin" />
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin" />
<div
className="absolute inset-1 rounded-full border-2 border-transparent border-t-vortex-400 animate-spin"
className="absolute inset-1 rounded-full border-2 border-transparent border-t-accent/70 animate-spin"
style={{ animationDuration: '0.8s', animationDirection: 'reverse' }}
/>
<div
className="absolute inset-2 rounded-full border-2 border-transparent border-t-vortex-300 animate-spin"
className="absolute inset-2 rounded-full border-2 border-transparent border-t-accent/40 animate-spin"
style={{ animationDuration: '0.6s' }}
/>
</div>

View File

@@ -17,6 +17,7 @@ import {
Forward,
Bookmark,
ArrowLeft,
MessageSquare,
} from 'lucide-react';
import { useChatStore } from '../stores/chatStore';
import { useAuthStore } from '../stores/authStore';
@@ -317,10 +318,10 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
if (!activeChat || !chat) {
return (
<div className="flex-1 flex items-center justify-center bg-surface-secondary/50 rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl relative z-0 backdrop-blur-3xl group">
<div className="flex-1 h-full flex items-center justify-center bg-surface-secondary/50 rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl relative z-0 backdrop-blur-3xl group">
{/* Slowly pulsing purple background as requested */}
<div className="absolute inset-0 pointer-events-none overflow-hidden transition-opacity duration-[10000ms]">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-vortex-600/10 rounded-full blur-[120px] animate-[pulse_8s_ease-in-out_infinite]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-accent/10 rounded-full blur-[120px] animate-[pulse_8s_ease-in-out_infinite]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-purple-600/15 rounded-full blur-[100px] animate-[pulse_12s_ease-in-out_infinite_reverse]" />
</div>
@@ -331,18 +332,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="w-28 h-28 mx-auto mb-8 rounded-[2rem] bg-gradient-to-br from-vortex-500/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_60px_-15px_var(--color-accent)] ring-1 ring-white/10 backdrop-blur-2xl relative"
className="w-28 h-28 mx-auto mb-8 rounded-[2rem] bg-gradient-to-br from-accent/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_60px_-15px_var(--color-accent)] ring-1 ring-white/10 backdrop-blur-2xl relative"
>
<div className="absolute inset-0 rounded-[2rem] bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
<img src="/logo.png" alt="Vortex" className="w-16 h-16 rounded-2xl object-cover shadow-2xl transform hover:scale-105 transition-transform" />
<MessageSquare className="w-16 h-16 text-accent shadow-2xl transform hover:scale-105 transition-transform" />
</motion.div>
<motion.h2
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-vortex-400 via-fuchsia-400 to-indigo-400 mb-4 drop-shadow-lg tracking-tight"
className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent via-fuchsia-400 to-indigo-400 mb-4 drop-shadow-lg tracking-tight"
>
Vortex Messenger
SelfHost Messenger
</motion.h2>
<motion.p
initial={{ y: 20, opacity: 0 }}

View File

@@ -1,6 +1,8 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Phone, PhoneOff, Video, VideoOff, Mic, MicOff, Monitor, MonitorOff, Minimize2, Volume2, ShieldCheck, ShieldOff, ChevronUp } from 'lucide-react';
import { useChatStore } from '../stores/chatStore';
import { useAuthStore } from '../stores/authStore';
import { getSocket } from '../lib/socket';
import { api } from '../lib/api';
import { useLang } from '../lib/i18n';
@@ -10,6 +12,7 @@ interface ParticipantInfo {
username: string;
displayName?: string;
avatar?: string | null;
isSharingScreen?: boolean;
}
interface PeerState {
@@ -65,6 +68,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
const [microphones, setMicrophones] = useState<MediaDeviceInfo[]>([]);
const [activeMicId, setActiveMicId] = useState<string>('');
const [showMicMenu, setShowMicMenu] = useState(false);
const [sharingUserId, setSharingUserId] = useState<string | null>(null);
const localStreamRef = useRef<MediaStream | null>(null);
const screenStreamRef = useRef<MediaStream | null>(null);
@@ -289,6 +293,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
}
}
setIsScreenSharing(false);
const socket = getSocket();
socket?.emit('screen_share_stopped', chatId);
} else {
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
@@ -328,6 +334,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
};
setIsScreenSharing(true);
const socket = getSocket();
socket?.emit('screen_share_started', chatId);
} catch { console.error('Screen share failed'); }
}
}, [isScreenSharing, chatId]);
@@ -521,6 +529,9 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
const newMap = new Map<string, ParticipantInfo>();
for (const p of data.participants) {
newMap.set(p.id, p);
if (p.isSharingScreen && p.id !== useAuthStore.getState().user?.id) {
setSharingUserId(p.id);
}
// Create peer connection to each existing participant (we are the initiator)
if (!peersRef.current.has(p.id)) {
await createPeerConnection(p.id, true);
@@ -605,6 +616,38 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
}
};
const onScreenShareStarted = (data: { userId: string }) => {
const isMe = data.userId === useAuthStore.getState().user?.id;
if (!isMe) {
setSharingUserId(data.userId);
}
setParticipants(prev => {
const next = new Map(prev);
const p = next.get(data.userId);
if (p) next.set(data.userId, { ...p, isSharingScreen: true });
return next;
});
};
const onScreenShareStopped = (data: { userId: string }) => {
const isMe = data.userId === useAuthStore.getState().user?.id;
if (!isMe && sharingUserId === data.userId) {
setSharingUserId(null);
} else if (isMe) {
// Just in case
}
// If the person who stopped was the sharing user, clear it
setSharingUserId(current => current === data.userId ? null : current);
setParticipants(prev => {
const next = new Map(prev);
const p = next.get(data.userId);
if (p) next.set(data.userId, { ...p, isSharingScreen: false });
return next;
});
};
socket.on('group_call_participants', onParticipants);
socket.on('group_call_user_joined', onUserJoined);
socket.on('group_call_user_left', onUserLeft);
@@ -613,6 +656,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
socket.on('group_ice_candidate', onIceCandidate);
socket.on('group_call_renegotiate', onRenegotiate);
socket.on('group_call_renegotiate_answer', onRenegotiateAnswer);
socket.on('screen_share_started', onScreenShareStarted);
socket.on('screen_share_stopped', onScreenShareStopped);
return () => {
socket.off('group_call_participants', onParticipants);
@@ -623,6 +668,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
socket.off('group_ice_candidate', onIceCandidate);
socket.off('group_call_renegotiate', onRenegotiate);
socket.off('group_call_renegotiate_answer', onRenegotiateAnswer);
socket.off('screen_share_started', onScreenShareStarted);
socket.off('screen_share_stopped', onScreenShareStopped);
};
}, [isOpen, chatId, createPeerConnection]);
@@ -856,6 +903,16 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
</div>
</div>
{/* Screen share indicator */}
{sharingUserId && (
<div className="absolute top-6 left-6 z-20 flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-500/20 border border-blue-500/30 backdrop-blur-md">
<Monitor size={14} className="text-blue-400 animate-pulse" />
<span className="text-xs font-medium text-blue-100">
{participants.get(sharingUserId)?.displayName || 'Кто-то'} транслирует экран
</span>
</div>
)}
{/* Controls */}
<div className="px-8 pb-8 pt-4 flex items-center justify-center gap-4 flex-wrap">
{/* Mic with dropdown */}
@@ -882,12 +939,16 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
{isVideoOff ? <VideoOff size={18} /> : <Video size={18} />}
</button>
<button
onClick={toggleScreenShare}
className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isScreenSharing ? 'bg-vortex-500/30 text-vortex-400' : 'bg-white/10 text-white hover:bg-white/20'}`}
title={isScreenSharing ? t('stopScreenShare') : t('screenShare')}
>
{isScreenSharing ? <MonitorOff size={18} /> : <Monitor size={18} />}
</button>
onClick={toggleScreenShare}
disabled={!!sharingUserId && !isScreenSharing}
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${isScreenSharing
? 'bg-blue-500 text-white shadow-lg shadow-blue-500/40'
: 'bg-white/10 text-white hover:bg-white/15'
} ${!!sharingUserId && !isScreenSharing ? 'opacity-30 cursor-not-allowed' : ''}`}
title={sharingUserId ? 'Кто-то уже транслирует экран' : ''}
>
{isScreenSharing ? <MonitorOff size={20} /> : <Monitor size={20} />}
</button>
<button
onClick={e => { e.stopPropagation(); setShowVolumeSlider(!showVolumeSlider); }}
className="w-11 h-11 rounded-full flex items-center justify-center transition-colors bg-white/10 text-white hover:bg-white/20"

View File

@@ -55,6 +55,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
try {
const chat = await api.createPersonalChat(selectedUser.id);
addChat(chat);
import('../lib/socket').then(({ getSocket }) => getSocket()?.emit('join_chat', chat.id));
setActiveChat(chat.id);
loadMessages(chat.id);
onClose();
@@ -75,14 +76,15 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
if (!groupName.trim() || selectedUsers.length === 0) return;
setIsCreating(true);
try {
const chat = await api.createGroupChat(
groupName.trim(),
selectedUsers.map((u) => u.id)
);
addChat(chat);
setActiveChat(chat.id);
loadMessages(chat.id);
onClose();
const chat = await api.createGroupChat(
groupName.trim(),
selectedUsers.map((u) => u.id)
);
addChat(chat);
import('../lib/socket').then(({ getSocket }) => getSocket()?.emit('join_chat', chat.id));
setActiveChat(chat.id);
loadMessages(chat.id);
onClose();
} catch (e) {
console.error(e);
} finally {

View File

@@ -303,7 +303,7 @@ export default function SideMenu({ isOpen, onClose }: SideMenuProps) {
{ icon: Users, label: t('friends'), onClick: () => changeView('friends'), badge: friendRequests.length > 0 ? friendRequests.length : undefined },
{ icon: Settings, label: t('settings'), onClick: () => changeView('settings') },
{ divider: true },
{ icon: Info, label: t('aboutApp'), subtitle: 'Vortex Messenger v1.0', onClick: () => changeView('about') },
{ icon: Info, label: t('aboutApp'), subtitle: 'SelfHost Messenger v1.0', onClick: () => changeView('about') },
];
// Slide direction for animations
@@ -664,13 +664,13 @@ export default function SideMenu({ isOpen, onClose }: SideMenuProps) {
</div>
<div className="px-5 py-3">
<h4 className="text-xs text-zinc-500 uppercase tracking-wide mb-3">{t('about')}</h4>
<div className="flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50">
<Info size={18} className="text-zinc-400" />
<div className="flex-1 min-w-0">
<p className="text-sm text-zinc-200">Vortex Messenger</p>
<p className="text-xs text-zinc-500">{t('version')} 1.0.0</p>
</div>
</div>
<div className="flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50">
<Info size={18} className="text-zinc-400" />
<div className="flex-1 min-w-0">
<p className="text-sm text-zinc-200">SelfHost Messenger</p>
<p className="text-xs text-zinc-500">{t('version')} 1.0.0</p>
</div>
</div>
</div>
</div>
</motion.div>

View File

@@ -86,8 +86,10 @@ export default function Sidebar() {
<Menu size={20} />
</button>
<div className="flex items-center gap-2 flex-1 min-w-0">
<img src="/logo.png" alt="Vortex" className="w-8 h-8 rounded-lg object-cover" />
<h1 className="text-lg font-bold gradient-text truncate">Vortex</h1>
<div className="w-8 h-8 rounded-lg bg-accent flex items-center justify-center text-white">
<MessageSquare size={18} />
</div>
<h1 className="text-lg font-bold gradient-text truncate">SelfHost</h1>
</div>
<button
onClick={() => setShowNewChat(true)}
@@ -128,8 +130,8 @@ export default function Sidebar() {
onClick={() => setShowCreateStory(true)}
className="flex flex-col items-center gap-1 flex-shrink-0 group"
>
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 border-dashed border-zinc-600 flex items-center justify-center group-hover:border-vortex-400 transition-colors">
<Plus size={18} className="text-zinc-400 group-hover:text-vortex-400 transition-colors" />
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 border-dashed border-zinc-600 flex items-center justify-center group-hover:border-accent transition-colors">
<Plus size={18} className="text-zinc-400 group-hover:text-accent transition-colors" />
</div>
<span className="text-[10px] text-zinc-500 truncate w-12 sm:w-14 text-center">{t('newStory')}</span>
</button>
@@ -145,7 +147,7 @@ export default function Sidebar() {
>
<div className={`w-14 h-14 rounded-full p-[2.5px] transition-transform group-hover:scale-105 ${
group.hasUnviewed
? 'bg-gradient-to-tr from-vortex-400 via-purple-500 to-pink-500 shadow-lg shadow-vortex-500/25'
? 'bg-gradient-to-tr from-accent via-purple-500 to-pink-500 shadow-lg shadow-accent/25'
: isMine
? 'bg-gradient-to-tr from-zinc-500 to-zinc-600'
: 'bg-zinc-700'

View File

@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { X, ChevronLeft, ChevronRight, Eye, Trash2, Plus, ChevronUp } from 'lucide-react';
import { useAuthStore } from '../stores/authStore';
import { api } from '../lib/api';
import { getSocket } from '../lib/socket';
import { useLang } from '../lib/i18n';
import { getInitials, generateAvatarColor } from '../lib/utils';
import Avatar from './Avatar';
@@ -124,16 +125,49 @@ export default function StoryViewer({ stories, initialUserIndex, onClose, onRefr
};
}, [storyIndex, userIndex, paused, goNext]);
// Keyboard
// Keyboard and Socket
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowRight') goNext();
if (e.key === 'ArrowLeft') goPrev();
};
const socket = getSocket();
const handleStoryViewed = (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; viewedAt: string; viewCount: number }) => {
if (!currentStory || data.storyId !== currentStory.id) return;
setViewOverrides(prev => ({
...prev,
[data.storyId]: {
viewCount: data.viewCount,
viewed: prev[data.storyId]?.viewed || false
}
}));
// Update viewers list if visible
if (showViewers) {
setViewers(prev => {
if (prev.some(v => v.userId === data.userId)) return prev;
return [...prev, {
userId: data.userId,
username: data.username,
displayName: data.displayName,
avatar: data.avatar,
viewedAt: data.viewedAt
}].sort((a, b) => new Date(b.viewedAt).getTime() - new Date(a.viewedAt).getTime());
});
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [goNext, goPrev, onClose]);
socket?.on('story_viewed', handleStoryViewed);
return () => {
window.removeEventListener('keydown', onKey);
socket?.off('story_viewed', handleStoryViewed);
};
}, [goNext, goPrev, onClose, currentStory?.id, showViewers]);
const handleDelete = async () => {
if (!currentStory) return;

View File

@@ -170,7 +170,7 @@ const translations = {
removeAvatar: 'Удалить аватар',
namePlaceholder: 'Имя',
notSpecified: 'Не указано',
onVortexSince: 'На Vortex с',
onVortexSince: 'На SelfHost с',
cancel: 'Отмена',
confirm: 'Подтвердить',
save: 'Сохранить',
@@ -292,7 +292,7 @@ const translations = {
version: 'Version',
modernMessenger: 'A modern messenger focused',
onPrivacy: 'on privacy and convenience',
modernMessengerShort: 'Modern messenger',
modernMessengerShort: 'SelfHost Messenger',
searchChats: 'Search chats...',
chat: 'Chat',
group: 'Group',
@@ -425,7 +425,7 @@ const translations = {
removeAvatar: 'Remove avatar',
namePlaceholder: 'Name',
notSpecified: 'Not specified',
onVortexSince: 'On Vortex since',
onVortexSince: 'On SelfHost since',
cancel: 'Cancel',
confirm: 'Confirm',
save: 'Save',

View File

@@ -2,7 +2,7 @@ import { useState, FormEvent } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuthStore } from '../stores/authStore';
import { useLang } from '../lib/i18n';
import { Eye, EyeOff, ArrowRight, UserPlus, LogIn } from 'lucide-react';
import { Eye, EyeOff, ArrowRight, UserPlus, LogIn, MessageSquare } from 'lucide-react';
export default function AuthPage() {
const [isLogin, setIsLogin] = useState(true);
@@ -44,9 +44,9 @@ export default function AuthPage() {
{/* Анимированный фон */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] opacity-20">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-vortex-600/30 to-purple-600/30 blur-[120px] animate-pulse" />
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-accent/30 to-purple-600/30 blur-[120px] animate-pulse" />
</div>
<div className="absolute top-20 left-20 w-72 h-72 bg-vortex-500/10 rounded-full blur-[100px]" />
<div className="absolute top-20 left-20 w-72 h-72 bg-accent/10 rounded-full blur-[100px]" />
<div className="absolute bottom-20 right-20 w-96 h-96 bg-purple-500/10 rounded-full blur-[100px]" />
</div>
@@ -57,7 +57,7 @@ export default function AuthPage() {
transition={{ duration: 0.4, ease: 'easeOut' }}
className="relative z-10 w-full max-w-md mx-4"
>
<div className="glass-strong rounded-3xl p-8 shadow-2xl shadow-vortex-500/5">
<div className="glass-strong rounded-3xl p-8 shadow-2xl shadow-accent/5">
{/* Заголовок */}
<div className="flex flex-col items-center mb-8">
@@ -65,14 +65,11 @@ export default function AuthPage() {
initial={{ rotate: -180, scale: 0 }}
animate={{ rotate: 0, scale: 1 }}
transition={{ duration: 0.6, type: 'spring', bounce: 0.4 }}
className="w-20 h-20 rounded-2xl bg-accent flex items-center justify-center text-white shadow-lg shadow-accent/30"
>
<img
src="/logo.png"
alt="Vortex"
className="w-20 h-20 rounded-2xl shadow-lg shadow-vortex-500/30 object-cover"
/>
<MessageSquare size={40} />
</motion.div>
<h1 className="text-2xl font-bold gradient-text mt-4">Vortex</h1>
<h1 className="text-2xl font-bold gradient-text mt-4">SelfHost</h1>
<p className="text-zinc-500 text-sm mt-1">
{isLogin ? t('login') : t('register')}
</p>
@@ -104,7 +101,7 @@ export default function AuthPage() {
value={username}
onChange={(e) => setUsername(e.target.value.replace(/[^a-zA-Z0-9_]/g, ''))}
placeholder="username"
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden"
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all outline-hidden"
required
autoFocus
autoComplete="off"
@@ -144,7 +141,7 @@ export default function AuthPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('passwordPlaceholder')}
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all pr-12 outline-hidden"
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all pr-12 outline-hidden"
required
autoComplete={isLogin ? 'current-password' : 'new-password'}
minLength={8}
@@ -159,7 +156,7 @@ export default function AuthPage() {
</div>
{!isLogin && (
<p className="mt-1.5 text-xs text-zinc-500 flex items-center gap-1.5">
<div className="w-1 h-1 rounded-full bg-vortex-500" />
<div className="w-1 h-1 rounded-full bg-accent" />
{t('passwordRequirements')}
</p>
)}
@@ -179,7 +176,7 @@ export default function AuthPage() {
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder={t('bioPlaceholder')}
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden"
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all outline-hidden"
/>
</motion.div>
)}
@@ -190,7 +187,7 @@ export default function AuthPage() {
whileTap={{ scale: 0.99 }}
disabled={isSubmitting}
type="submit"
className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-vortex-500 to-purple-600 text-white font-medium shadow-lg shadow-vortex-500/25 hover:shadow-vortex-500/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-accent to-purple-600 text-white font-medium shadow-lg shadow-accent/25 hover:shadow-accent/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{isSubmitting ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@@ -215,7 +212,7 @@ export default function AuthPage() {
setDisplayName('');
setBio('');
}}
className="text-vortex-400 hover:text-vortex-300 font-medium transition-colors ml-1"
className="text-accent hover:text-accent font-medium transition-colors ml-1"
>
{isLogin ? t('registerNow') : t('loginNow')}
</button>

View File

@@ -30,6 +30,7 @@ export default function ChatPage() {
setPinnedMessage,
removePinnedMessage,
clearStore,
addChat,
} = useChatStore();
const { user } = useAuthStore();
const initialized = useRef(false);
@@ -134,6 +135,11 @@ export default function ChatPage() {
updateMessage(message);
});
socket.on('new_chat', (chat: any) => {
addChat(chat);
socket.emit('join_chat', chat.id);
});
socket.on('message_deleted', (data: { messageId: string; chatId: string }) => {
removeMessage(data.messageId, data.chatId);
});
@@ -215,6 +221,7 @@ export default function ChatPage() {
socket.off('new_message');
socket.off('scheduled_delivered');
socket.off('message_edited');
socket.off('new_chat');
socket.off('message_deleted');
socket.off('messages_deleted');
socket.off('messages_hidden');

View File

@@ -435,7 +435,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
addChat: (chat) => {
set((state) => {
if (state.chats.some((c) => c.id === chat.id)) return state;
const existing = state.chats.find((c) => c.id === chat.id);
if (existing) {
return {
chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...chat } : c)),
};
}
return { chats: [chat, ...state.chats] };
});
},