Мобилка

This commit is contained in:
Халимов Рустам
2026-03-11 17:59:02 +03:00
parent 1da4b26f5b
commit 50d4d75c04
4 changed files with 82 additions and 70 deletions
+14 -13
View File
@@ -1574,14 +1574,15 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
animate={{ scale: 1, opacity: 1, y: 0 }} animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }} exit={{ scale: 0.9, opacity: 0, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }} transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className={`relative w-full mx-4 rounded-[2.5rem] glass-strong shadow-2xl shadow-black/50 overflow-hidden border border-white/5 ${showVideoArea ? 'max-w-5xl' : 'max-w-md' className={`relative w-full h-full sm:h-auto sm:mx-4 sm:rounded-[2.5rem] glass-strong shadow-2xl shadow-black/50 overflow-hidden border border-white/5 flex flex-col ${
}`} showVideoArea ? 'sm:max-w-5xl' : 'sm:max-w-md'
}`}
> >
{/* Mic selector popup */} {/* Mic selector popup */}
{showMicMenu && microphones.length > 0 && ( {showMicMenu && microphones.length > 0 && (
<> <>
<div className="fixed inset-0 z-[200]" onClick={() => setShowMicMenu(false)} /> <div className="fixed inset-0 z-[200]" onClick={() => setShowMicMenu(false)} />
<div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[320px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2"> <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] sm:w-[320px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2">
<div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('selectMicrophone')}</div> <div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('selectMicrophone')}</div>
{microphones.map((mic, i) => ( {microphones.map((mic, i) => (
<button <button
@@ -1603,7 +1604,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
<> <>
{/* Backdrop to close on click outside */} {/* Backdrop to close on click outside */}
<div className="fixed inset-0 z-[200]" onClick={() => setShowCameraMenu(false)} /> <div className="fixed inset-0 z-[200]" onClick={() => setShowCameraMenu(false)} />
<div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2"> <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] sm:w-[300px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2">
<div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('switchCamera')}</div> <div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('switchCamera')}</div>
{cameras.map((cam, i) => ( {cameras.map((cam, i) => (
<button <button
@@ -1624,7 +1625,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
{showVolumeSlider && ( {showVolumeSlider && (
<> <>
<div className="fixed inset-0 z-[200]" onClick={() => setShowVolumeSlider(false)} /> <div className="fixed inset-0 z-[200]" onClick={() => setShowVolumeSlider(false)} />
<div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[250px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl p-4" onClick={(e) => e.stopPropagation()}> <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] sm:w-[250px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl p-4" onClick={(e) => e.stopPropagation()}>
<div className="text-xs text-zinc-400 uppercase tracking-wider mb-3">{t('volume')}</div> <div className="text-xs text-zinc-400 uppercase tracking-wider mb-3">{t('volume')}</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Volume2 size={16} className="text-zinc-400 shrink-0" /> <Volume2 size={16} className="text-zinc-400 shrink-0" />
@@ -1644,7 +1645,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
)} )}
{/* === Main Content Area === */} {/* === Main Content Area === */}
<div className="flex-1 flex flex-col relative overflow-hidden bg-black min-h-[400px]"> <div className="flex-1 flex flex-col relative overflow-hidden bg-black min-h-[300px] sm:min-h-[400px]">
{showVideoArea ? ( {showVideoArea ? (
<div <div
ref={videoContainerRef} ref={videoContainerRef}
@@ -1664,9 +1665,9 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
<div className="relative mb-6"> <div className="relative mb-6">
<div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-ping" /> <div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-ping" />
{displayAvatar ? ( {displayAvatar ? (
<img src={displayAvatar} alt="" className="relative w-32 h-32 rounded-full object-cover border-4 border-vortex-500/30" /> <img src={displayAvatar} alt="" className="relative w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover border-4 border-vortex-500/30" />
) : ( ) : (
<div className="relative w-32 h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white text-4xl font-bold border-4 border-vortex-500/30"> <div className="relative w-24 h-24 sm:w-32 sm:h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white text-3xl sm:text-4xl font-bold border-4 border-vortex-500/30">
{initials} {initials}
</div> </div>
)} )}
@@ -1709,7 +1710,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
{/* Local video PIP (bottom-right) */} {/* Local video PIP (bottom-right) */}
{hasLocalVideo && ( {hasLocalVideo && (
<div className="absolute bottom-3 right-3 w-48 rounded-xl overflow-hidden border-2 border-white/20 shadow-lg bg-black z-20" <div className="absolute bottom-4 right-4 w-28 sm:w-48 rounded-xl overflow-hidden border-2 border-white/20 shadow-lg bg-black z-20"
style={{ aspectRatio: '16 / 9' }} style={{ aspectRatio: '16 / 9' }}
> >
<video <video
@@ -1772,9 +1773,9 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
)} )}
<div className="relative z-10 p-1.5 rounded-full bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 shadow-2xl cursor-pointer"> <div className="relative z-10 p-1.5 rounded-full bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 shadow-2xl cursor-pointer">
{displayAvatar ? ( {displayAvatar ? (
<img src={displayAvatar} alt="" className="w-32 h-32 rounded-full object-cover shadow-inner" /> <img src={displayAvatar} alt="" className="w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover shadow-inner" />
) : ( ) : (
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-4xl shadow-inner"> <div className="w-24 h-24 sm:w-32 sm:h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-3xl sm:text-4xl shadow-inner">
{initials} {initials}
</div> </div>
)} )}
@@ -1806,7 +1807,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
</div> </div>
{/* === Controls === */} {/* === Controls === */}
<div className="px-8 pb-10 flex items-center justify-center gap-4 relative z-10 flex-wrap"> <div className="px-4 py-6 sm:px-8 sm:pb-10 flex items-center justify-center gap-3 sm:gap-4 relative z-10 flex-wrap">
{callState === 'incoming' && ( {callState === 'incoming' && (
<> <>
<button <button
@@ -1887,7 +1888,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
</button> </button>
<button <button
onClick={endCallSafe} onClick={endCallSafe}
className="w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl shadow-red-500/30 transition-all hover:scale-105 ml-2" className="w-12 h-12 sm:w-14 sm:h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl shadow-red-500/30 transition-all hover:scale-105"
> >
<PhoneOff size={22} /> <PhoneOff size={22} />
</button> </button>
+55 -50
View File
@@ -16,6 +16,7 @@ import {
Pin, Pin,
Forward, Forward,
Bookmark, Bookmark,
ArrowLeft,
} from 'lucide-react'; } from 'lucide-react';
import { useChatStore } from '../stores/chatStore'; import { useChatStore } from '../stores/chatStore';
import { useAuthStore } from '../stores/authStore'; import { useAuthStore } from '../stores/authStore';
@@ -421,19 +422,17 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
<div <div
ref={chatViewRef} ref={chatViewRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
className={`flex-1 flex flex-col h-full rounded-3xl overflow-hidden shadow-[0_0_120px_-20px_rgba(0,0,0,0.5)] border border-border/50 relative z-0 chat-theme-${chatTheme} transition-colors duration-500`} className={`flex-1 flex flex-col h-full sm:rounded-[2rem] overflow-hidden shadow-[0_0_120px_-20px_rgba(0,0,0,0.5)] border-x sm:border border-border/50 relative z-0 chat-theme-${chatTheme} transition-colors duration-500`}
> >
{/* Шапка чата */}
{selectionMode ? ( {selectionMode ? (
<div className="h-[76px] flex items-center justify-between px-6 border-b border-border/40 bg-surface-secondary/80 backdrop-blur-xl z-20 flex-shrink-0 animate-in slide-in-from-top-2"> <div className="h-[76px] flex items-center justify-between px-4 sm:px-6 border-b border-border/40 bg-surface-secondary/80 backdrop-blur-xl z-20 flex-shrink-0 animate-in slide-in-from-top-2">
<div className="flex items-center gap-4 text-white"> <div className="flex items-center gap-2 sm:gap-4 text-white">
<button onClick={() => { setSelectionMode(false); setSelectedMessages(new Set()); }} className="p-2 -ml-2 rounded-full hover:bg-white/10 transition"> <button onClick={() => { setSelectionMode(false); setSelectedMessages(new Set()); }} className="p-2 -ml-2 rounded-full hover:bg-white/10 transition">
<X size={20} className="text-zinc-300" /> <X size={20} className="text-zinc-300" />
</button> </button>
<span className="font-medium text-[15px]">{selectedMessages.size} {t('selected') || 'выбрано'}</span> <span className="font-medium text-[15px]">{selectedMessages.size} {t('selected') || 'выбрано'}</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Кнопка удаления с выпадающим меню */}
<div className="relative" ref={deleteMenuRef}> <div className="relative" ref={deleteMenuRef}>
<button <button
disabled={selectedMessages.size === 0} disabled={selectedMessages.size === 0}
@@ -482,52 +481,59 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
</div> </div>
</div> </div>
) : ( ) : (
<div className="h-[76px] flex items-center justify-between px-6 border-b border-border/40 bg-surface-secondary/80 backdrop-blur-xl z-20 flex-shrink-0"> <div className="h-[76px] flex items-center justify-between px-3 sm:px-6 border-b border-border/40 bg-surface-secondary/80 backdrop-blur-xl z-20 flex-shrink-0 gap-2">
<button <div className="flex items-center gap-2 min-w-0 flex-1">
className="flex items-center gap-3 min-w-0 flex-1 group transition-all" <button
onClick={() => { onClick={() => setActiveChat(null)}
if (chat.type === 'personal' && otherMember) { className="sm:hidden p-2 -ml-1 rounded-xl hover:bg-surface-hover text-zinc-400 transition-colors"
setProfileUserId(otherMember.user.id); >
} else if (chat.type === 'group') { <ArrowLeft size={22} />
setShowGroupSettings(true); </button>
} <button
}} className="flex items-center gap-3 min-w-0 flex-1 group transition-all"
> onClick={() => {
<div className="relative flex-shrink-0 transform transition-transform duration-300 group-hover:scale-105"> if (chat.type === 'personal' && otherMember) {
{isFavorites ? ( setProfileUserId(otherMember.user.id);
<div className="w-11 h-11 rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center shadow-lg ring-2 ring-transparent group-hover:ring-accent/30 transition-all duration-300"> } else if (chat.type === 'group') {
<Bookmark size={20} className="text-white" /> setShowGroupSettings(true);
</div> }
) : ( }}
<Avatar >
src={chatAvatar} <div className="relative flex-shrink-0 transform transition-transform duration-300 group-hover:scale-105">
name={chatName} {isFavorites ? (
size="md" <div className="w-11 h-11 rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center shadow-lg ring-2 ring-transparent group-hover:ring-accent/30 transition-all duration-300">
online={isOnline ? true : undefined} <Bookmark size={20} className="text-white" />
className="ring-2 ring-transparent group-hover:ring-accent/30 transition-all duration-300 rounded-full" </div>
/> ) : (
)} <Avatar
</div> src={chatAvatar}
<div className="min-w-0 text-left"> name={chatName}
<h3 className="text-base font-semibold text-white truncate drop-shadow-sm group-hover:text-accent/90 transition-colors">{chatName}</h3> size="md"
<p className="text-xs text-zinc-400 truncate"> online={isOnline ? true : undefined}
{isFavorites className="ring-2 ring-transparent group-hover:ring-accent/30 transition-all duration-300 rounded-full"
? t('favoritesDescription') />
: typingInChat.length > 0 )}
? <span className="text-accent font-medium">{t('typing')}</span> </div>
: isOnline <div className="min-w-0 text-left">
? <span className="text-emerald-400">{t('online')}</span> <h3 className="text-base font-semibold text-white truncate drop-shadow-sm group-hover:text-accent/90 transition-colors">{chatName}</h3>
: chat.type === 'personal' && otherMember?.user.lastSeen <p className="text-xs text-zinc-400 truncate">
? `${t('lastSeenAt')} ${formatLastSeen(otherMember.user.lastSeen, lang)}` {isFavorites
: chat.type === 'group' ? t('favoritesDescription')
? `${chat.members.length} ${t('members')}` : typingInChat.length > 0
: ''} ? <span className="text-accent font-medium">{t('typing')}</span>
</p> : isOnline
</div> ? <span className="text-emerald-400">{t('online')}</span>
</button> : chat.type === 'personal' && otherMember?.user.lastSeen
? `${t('lastSeenAt')} ${formatLastSeen(otherMember.user.lastSeen, lang)}`
: chat.type === 'group'
? `${chat.members.length} ${t('members')}`
: ''}
</p>
</div>
</button>
</div>
<div className="flex items-center gap-1.5 ml-4"> <div className="flex items-center gap-1.5 ml-4">
{/* Поиск */}
<AnimatePresence> <AnimatePresence>
{showSearch && ( {showSearch && (
<motion.div <motion.div
@@ -590,7 +596,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
</> </>
)} )}
{/* Меню */}
<div className="relative" ref={topMenuRef}> <div className="relative" ref={topMenuRef}>
<button <button
onClick={() => setShowTopMenu(!showTopMenu)} onClick={() => setShowTopMenu(!showTopMenu)}
+4 -4
View File
@@ -68,7 +68,7 @@ export default function Sidebar() {
return ( return (
<> <>
<div className="w-[340px] h-full flex flex-col bg-surface-secondary rounded-3xl overflow-hidden border border-border/50 shadow-2xl relative z-10"> <div className="w-full h-full flex flex-col bg-surface-secondary sm:rounded-[2rem] overflow-hidden border-x sm:border border-border/50 shadow-2xl relative z-10">
{/* Шапка */} {/* Шапка */}
<div className="h-[76px] px-4 flex items-center gap-3 border-b border-border/40 bg-surface-secondary flex-shrink-0"> <div className="h-[76px] px-4 flex items-center gap-3 border-b border-border/40 bg-surface-secondary flex-shrink-0">
<button <button
@@ -121,10 +121,10 @@ export default function Sidebar() {
onClick={() => setShowCreateStory(true)} onClick={() => setShowCreateStory(true)}
className="flex flex-col items-center gap-1 flex-shrink-0 group" className="flex flex-col items-center gap-1 flex-shrink-0 group"
> >
<div className="w-14 h-14 rounded-full border-2 border-dashed border-zinc-600 flex items-center justify-center group-hover:border-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-vortex-400 transition-colors">
<Plus size={20} className="text-zinc-400 group-hover:text-vortex-400 transition-colors" /> <Plus size={18} className="text-zinc-400 group-hover:text-vortex-400 transition-colors" />
</div> </div>
<span className="text-[10px] text-zinc-500 truncate w-14 text-center">{t('newStory')}</span> <span className="text-[10px] text-zinc-500 truncate w-12 sm:w-14 text-center">{t('newStory')}</span>
</button> </button>
{storyGroups.map((group, idx) => { {storyGroups.map((group, idx) => {
+9 -3
View File
@@ -257,15 +257,21 @@ export default function ChatPage() {
setGroupCallOpen(false); setGroupCallOpen(false);
}; };
const activeChat = useChatStore((state) => state.activeChat);
return ( return (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="h-full flex bg-surface p-3 gap-3 overflow-hidden" className="h-full flex bg-surface sm:p-3 sm:gap-3 overflow-hidden"
> >
<Sidebar /> <div className={`${activeChat ? 'hidden sm:block' : 'block'} w-full sm:w-[340px] flex-shrink-0`}>
<ChatView onStartCall={handleStartCall} onStartGroupCall={handleStartGroupCall} /> <Sidebar />
</div>
<div className={`${activeChat ? 'block' : 'hidden sm:block'} flex-1 h-full min-w-0`}>
<ChatView onStartCall={handleStartCall} onStartGroupCall={handleStartGroupCall} />
</div>
<CallModal <CallModal
key={callSessionId} key={callSessionId}
isOpen={callOpen} isOpen={callOpen}