Дизайн
This commit is contained in:
@@ -41,6 +41,9 @@ const translations = {
|
||||
noChats: 'Нет чатов — создайте первый!',
|
||||
nothingFound: 'Ничего не найдено',
|
||||
selectChat: 'Выберите чат для начала общения',
|
||||
selectChatTitle: 'Выберите чат',
|
||||
selectChatSubtext: 'Начните общение прямо сейчас. Все ваши сообщения защищены сквозным шифрованием.',
|
||||
newMessage: 'Новое сообщение',
|
||||
noMessages: 'Нет сообщений — напишите первое!',
|
||||
typing: 'печатает...',
|
||||
online: 'в сети',
|
||||
@@ -328,6 +331,9 @@ const translations = {
|
||||
noChats: 'No chats — create one!',
|
||||
nothingFound: 'Nothing found',
|
||||
selectChat: 'Select a chat to start messaging',
|
||||
selectChatTitle: 'Select a chat',
|
||||
selectChatSubtext: 'Start communicating right now. All your messages are protected with end-to-end encryption.',
|
||||
newMessage: 'New message',
|
||||
noMessages: 'No messages — write the first one!',
|
||||
typing: 'typing...',
|
||||
online: 'online',
|
||||
|
||||
@@ -59,10 +59,14 @@ export default function Sidebar() {
|
||||
};
|
||||
|
||||
socket?.on('story_viewed', onStoryViewed);
|
||||
|
||||
const handleOpenNewChat = () => setShowNewChat(true);
|
||||
window.addEventListener('OPEN_NEW_CHAT', handleOpenNewChat);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
socket?.off('story_viewed', onStoryViewed);
|
||||
window.removeEventListener('OPEN_NEW_CHAT', handleOpenNewChat);
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
@@ -185,7 +189,7 @@ export default function Sidebar() {
|
||||
{/* Float Action Button equivalent for Web */}
|
||||
<button
|
||||
onClick={() => setShowNewChat(true)}
|
||||
className="absolute bottom-6 right-6 w-14 h-14 rounded-2xl bg-primary text-on-primary-container shadow-lg shadow-primary/20 flex items-center justify-center hover:scale-105 active:scale-95 transition-transform slide-on-ice"
|
||||
className="absolute bottom-6 right-6 w-14 h-14 rounded-2xl bg-primary text-black shadow-lg shadow-primary/20 flex items-center justify-center hover:scale-105 active:scale-95 transition-transform slide-on-ice"
|
||||
title={t('newChat')}
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl">add</span>
|
||||
|
||||
@@ -61,32 +61,16 @@ export function stripMarkdown(text: string): string {
|
||||
}
|
||||
|
||||
export function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((part) => part[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
if (!name) return '?';
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
return parts[0].slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
export function generateAvatarColor(name: string): string {
|
||||
const colors = [
|
||||
'bg-linear-to-br from-blue-400 to-blue-600',
|
||||
'bg-linear-to-br from-sky-400 to-sky-600',
|
||||
'bg-linear-to-br from-cyan-400 to-blue-600',
|
||||
'bg-linear-to-br from-blue-500 to-indigo-600',
|
||||
'bg-linear-to-br from-emerald-400 to-blue-600',
|
||||
'bg-linear-to-br from-blue-600 to-primary-container',
|
||||
'bg-linear-to-br from-primary to-primary-container',
|
||||
'bg-linear-to-br from-blue-400 to-cyan-500',
|
||||
];
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
export function generateAvatarColor(_name: string): string {
|
||||
return 'bg-gradient-to-br from-[#3096e5] to-[#1e6fb3]';
|
||||
}
|
||||
|
||||
// Waveform cache so we don't decode the same audio twice
|
||||
|
||||
@@ -381,7 +381,7 @@ export default function ChatPage() {
|
||||
|
||||
{/* Selected Chat View (Main Area) */}
|
||||
<div
|
||||
className={`${activeChat ? 'block' : 'hidden lg:block'} flex-1 h-full min-w-0 bg-surface-container-lowest m-4 rounded-3xl overflow-hidden relative group slide-on-ice`}
|
||||
className={`${activeChat ? 'block' : 'hidden lg:block'} flex-1 h-full min-w-0 bg-surface-container-lowest relative group slide-on-ice`}
|
||||
>
|
||||
<ChatView onStartCall={handleStartCall} onStartGroupCall={handleStartGroupCall} />
|
||||
</div>
|
||||
@@ -394,7 +394,7 @@ export default function ChatPage() {
|
||||
</div>
|
||||
|
||||
{/* Right side placeholder / Profile detail */}
|
||||
<div className="hidden lg:flex flex-1 items-center justify-center bg-surface-base m-4 rounded-3xl overflow-hidden border border-white/5 relative slide-on-ice">
|
||||
<div className="hidden lg:flex flex-1 items-center justify-center bg-surface-base h-full relative slide-on-ice">
|
||||
<div className="flex flex-col items-center gap-6 max-w-sm text-center">
|
||||
<div className="w-24 h-24 rounded-3xl bg-primary/10 flex items-center justify-center text-primary shadow-inner">
|
||||
<Users size={48} className="knot-logo-spin opacity-50" />
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
Forward,
|
||||
Bookmark,
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
MessagesSquare,
|
||||
Pencil,
|
||||
} from 'lucide-react';
|
||||
import { useChatStore } from '../../application/chatStore';
|
||||
import { useAuthStore } from '../../../auth/application/authStore';
|
||||
@@ -25,7 +26,7 @@ import { ChatApi } from '../../infrastructure/chatApi';
|
||||
import { getSocket } from '../../../../core/infrastructure/socket';
|
||||
import { isChatMuted, toggleMuteChat } from '../../../../core/utils/sounds';
|
||||
import { useLang } from '../../../../core/infrastructure/i18n';
|
||||
import { formatLastSeen } from '../../../../core/utils/utils';
|
||||
import { formatLastSeen, getInitials, generateAvatarColor } from '../../../../core/utils/utils';
|
||||
import type { UserBasic, Message } from '../../../../core/domain/types';
|
||||
import MessageBubble from './MessageBubble';
|
||||
import MessageInput from './MessageInput';
|
||||
@@ -391,43 +392,72 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
if (!activeChat || !chat) {
|
||||
return (
|
||||
<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">
|
||||
<div className="text-center relative z-10 w-full max-w-sm px-6 mx-auto">
|
||||
<section className="flex-1 h-full flex flex-col items-center justify-center bg-[#010101] relative overflow-hidden">
|
||||
{/* Background Knot Texture - Correct Horizontal Unclosed Infinity SVG */}
|
||||
<div className="absolute inset-0 opacity-[0.04] pointer-events-none flex items-center justify-center">
|
||||
<svg
|
||||
width="1000"
|
||||
height="500"
|
||||
viewBox="0 0 600 300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-[120vw] h-auto text-white select-none"
|
||||
>
|
||||
{/* Left part of the infinity loop */}
|
||||
<path
|
||||
d="M300 150 C 240 230 150 230 150 150 C 150 70 240 70 300 150"
|
||||
stroke="currentColor"
|
||||
strokeWidth="45"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Right part of the infinity loop with a small gap at the crossing */}
|
||||
<path
|
||||
d="M315 170 C 375 250 465 250 465 170 C 465 90 375 90 315 170"
|
||||
stroke="currentColor"
|
||||
strokeWidth="45"
|
||||
strokeLinecap="round"
|
||||
transform="translate(-15, -20)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Chat Empty State View */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center z-10 p-12 text-center relative">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="w-24 h-24 mx-auto mb-6 rounded-full bg-surface-hover flex items-center justify-center border border-border/40 backdrop-blur-md relative"
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="w-24 h-24 mb-6 rounded-full bg-[#1c1c1c] flex items-center justify-center shadow-[0_0_80px_rgba(48,150,229,0.15)] border border-white/5"
|
||||
>
|
||||
<MessageSquare className="w-10 h-10 text-zinc-500 transform hover:scale-105 transition-transform" />
|
||||
<span className="material-symbols-outlined text-[#3096e5] text-5xl select-none" style={{ fontVariationSettings: "'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48" }}>
|
||||
forum
|
||||
</span>
|
||||
</motion.div>
|
||||
<motion.h2
|
||||
|
||||
<h2 className="text-3xl font-black text-[#e5e2e1] tracking-tight mb-2 leading-tight">
|
||||
{t('selectChatTitle')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm font-medium text-[#c1c6d7] max-w-sm leading-relaxed opacity-40 px-4">
|
||||
{t('selectChatSubtext')}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
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-2xl font-semibold text-white mb-2 tracking-tight"
|
||||
transition={{ delay: 0.2 }}
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('OPEN_NEW_CHAT'))}
|
||||
className="mt-10 px-9 py-3.5 bg-gradient-to-tr from-[#3096e5] to-[#9acbff] text-black font-extrabold text-base rounded-full shadow-[0_15px_40px_rgba(48,150,229,0.2)] hover:scale-105 active:scale-95 transition-all flex items-center gap-2.5"
|
||||
>
|
||||
Knot Messenger
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-sm text-zinc-400 bg-surface-hover backdrop-blur-lg py-2 px-4 rounded-full inline-flex border border-border/50"
|
||||
>
|
||||
{t('selectChat')}
|
||||
</motion.p>
|
||||
<span className="material-symbols-outlined text-black font-bold text-xl">edit</span>
|
||||
{t('newMessage')}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const initials = (chatName || '??')
|
||||
.split(' ')
|
||||
.map((w: string) => w[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
const initials = getInitials(chatName || '??');
|
||||
|
||||
const handleToggleSelect = (msgId: string) => {
|
||||
const newMap = new Set(selectedMessages);
|
||||
@@ -445,6 +475,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
setSelectedMessages(new Set([msgId]));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleForward = (targetChatId: string) => {
|
||||
const socket = getSocket();
|
||||
if (!socket || !activeChat) return;
|
||||
|
||||
@@ -885,7 +885,7 @@ export default function MessageInput({ chatId }: MessageInputProps) {
|
||||
onClick={() => handleSend()}
|
||||
onContextMenu={(e) => { e.preventDefault(); setScheduleStep('presets'); setShowSchedule(true); }}
|
||||
disabled={isSending}
|
||||
className="w-11 h-11 flex items-center justify-center rounded-2xl bg-primary text-white hover:brightness-110 active:scale-95 transition-all shadow-[0_4px_15px_rgba(48,150,229,0.3)] disabled:opacity-50"
|
||||
className="w-11 h-11 flex items-center justify-center rounded-2xl bg-primary text-black hover:brightness-110 active:scale-95 transition-all shadow-[0_4px_15px_rgba(48,150,229,0.3)] disabled:opacity-50"
|
||||
>
|
||||
<Send size={20} strokeWidth={2.5} className="translate-x-[1px]" />
|
||||
</button>
|
||||
@@ -985,7 +985,7 @@ export default function MessageInput({ chatId }: MessageInputProps) {
|
||||
) : (
|
||||
<button
|
||||
onClick={startRecording}
|
||||
className="w-11 h-11 flex items-center justify-center rounded-2xl bg-primary text-white hover:brightness-110 active:scale-95 transition-all shadow-[0_4px_15px_rgba(48,150,229,0.3)]"
|
||||
className="w-11 h-11 flex items-center justify-center rounded-2xl bg-primary text-black hover:brightness-110 active:scale-95 transition-all shadow-[0_4px_15px_rgba(48,150,229,0.3)]"
|
||||
>
|
||||
<Mic size={22} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user