Аватары заглушки

This commit is contained in:
Халимов Рустам
2026-04-02 15:26:05 +03:00
parent a42007df2d
commit 757142eda7
10 changed files with 100 additions and 80 deletions

View File

@@ -98,6 +98,7 @@ const translations = {
sendError: 'Ошибка отправки',
// New chat
newChat: 'Новый чат',
add: 'Добавить',
menu: 'Меню',
// Date picker
months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
@@ -384,6 +385,7 @@ const translations = {
fileBtn: 'File',
sendError: 'Send error',
newChat: 'New chat',
add: 'Add',
menu: 'Menu',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
weekDays: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],

View File

@@ -31,16 +31,16 @@ function AvatarInner({ src, name, size = 'md', className = '', online }: AvatarP
const gradientClass = generateAvatarColor(name || '');
return (
<div className={`relative shrink-0 ${className} rounded-full border-2 border-outline-variant/10 overflow-hidden`}>
<div className={`relative shrink-0 ${className} rounded-2xl border-2 border-outline-variant/10 overflow-hidden`}>
{src ? (
<img
src={src}
alt={name}
className={`${sizeClass} rounded-full object-cover`}
className={`${sizeClass} rounded-2xl object-cover`}
/>
) : (
<div
className={`${sizeClass} rounded-full ${gradientClass} flex items-center justify-center text-on-primary font-black tracking-tighter shadow-inner`}
className={`${sizeClass} rounded-2xl ${gradientClass} flex items-center justify-center text-on-primary font-black tracking-tighter shadow-inner`}
>
{initials}
</div>

View File

@@ -45,10 +45,10 @@ export default function GlobalNavBar({ activeTab, onTabChange }: GlobalNavBarPro
</div>
<div className="mt-auto group cursor-pointer relative">
<div className="absolute -inset-1 bg-primary/20 rounded-full opacity-0 group-hover:opacity-100 blur transition-opacity" />
<div className="absolute -inset-1 bg-primary/20 rounded-2xl opacity-0 group-hover:opacity-100 blur transition-opacity" />
<img
alt="User Profile"
className="relative w-10 h-10 rounded-full border-2 border-outline-variant/10 hover:border-primary/50 transition-colors object-cover"
className="relative w-10 h-10 rounded-2xl border-2 border-outline-variant/10 hover:border-primary/50 transition-colors object-cover"
src={user?.avatar || `https://ui-avatars.com/api/?name=${user?.username || 'user'}&background=3096e5&color=fff`}
/>
</div>

View File

@@ -40,6 +40,8 @@ import DatePicker from '../components/ui/DatePicker';
import TelegramImportModal from '../../../modules/users/presentation/components/TelegramImportModal';
import type { User as UserType, UserPresence, FriendRequest, FriendWithId } from '../../domain/types';
import { getInitials } from '../../utils/utils';
type SideView = 'main' | 'profile' | 'settings' | 'about' | 'themes' | 'friends';
interface SideMenuProps {
@@ -135,12 +137,7 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
onClose();
};
const initials = (user?.displayName || user?.userName || user?.username || '??')
.split(' ')
.map((w: string) => w[0])
.join('')
.slice(0, 2)
.toUpperCase();
const initials = getInitials(user?.displayName || user?.userName || user?.username || '??');
const menuItems = [
{ icon: User, label: t('myProfile'), onClick: () => { onClose(); onOpenProfile(); } },
@@ -173,12 +170,12 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
<div className="flex items-start justify-between mb-5">
{/* Avatar with glow ring */}
<div className="relative group cursor-pointer" onClick={() => { onClose(); onOpenProfile(); }}>
<div className="absolute -inset-1 bg-gradient-to-r from-accent via-purple-500 to-accent rounded-full opacity-60 blur group-hover:opacity-90 transition duration-500 animate-[spin_4s_linear_infinite]" />
<div className="absolute -inset-1 bg-gradient-to-r from-accent via-purple-500 to-accent rounded-2xl opacity-60 blur group-hover:opacity-90 transition duration-500 animate-[spin_4s_linear_infinite]" />
<div className="relative">
{user?.avatar ? (
<img src={user.avatar} alt="" className="w-[72px] h-[72px] rounded-full object-cover ring-[3px] ring-surface" />
<img src={user.avatar} alt="" className="w-[72px] h-[72px] rounded-2xl object-cover ring-[3px] ring-surface" />
) : (
<div className="w-[72px] h-[72px] rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center ring-[3px] ring-surface relative overflow-hidden">
<div className="w-[72px] h-[72px] rounded-2xl bg-linear-to-br from-primary/80 to-primary flex items-center justify-center ring-[3px] ring-surface relative overflow-hidden">
<span className="relative z-10 text-2xl font-black text-on-primary drop-shadow-md">{initials}</span>
</div>
)}
@@ -518,10 +515,10 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
{friendSearchResults.map((u) => (
<div key={u.id} className="flex items-center gap-3 p-3 rounded-xl bg-white/5 border border-border/50">
{u.avatar ? (
<img src={u.avatar} alt="" className="w-10 h-10 rounded-full object-cover" />
<img src={u.avatar} alt="" className="w-10 h-10 rounded-xl object-cover" />
) : (
<div className="w-10 h-10 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{(u.displayName || u.username || '?')[0].toUpperCase()}
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{getInitials(u.displayName || u.username || '?')}
</div>
)}
<div className="flex-1 min-w-0">
@@ -553,10 +550,10 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
{friendRequests.map((req) => (
<div key={req.id} className="flex items-center gap-3 p-3 rounded-xl bg-white/5 border border-border/50">
{req.user.avatar ? (
<img src={req.user.avatar} alt="" className="w-10 h-10 rounded-full object-cover" />
<img src={req.user.avatar} alt="" className="w-10 h-10 rounded-xl object-cover" />
) : (
<div className="w-10 h-10 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{(req.user.displayName || req.user.username || '?')[0].toUpperCase()}
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{getInitials(req.user.displayName || req.user.username || '?')}
</div>
)}
<div className="flex-1 min-w-0">
@@ -598,10 +595,10 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
<div key={friend.id} className="flex items-center gap-3 p-3 rounded-xl hover:bg-white/5 transition-colors group/friend">
<div className="relative">
{friend.avatar ? (
<img src={friend.avatar} alt="" className="w-10 h-10 rounded-full object-cover" />
<img src={friend.avatar} alt="" className="w-10 h-10 rounded-xl object-cover" />
) : (
<div className="w-10 h-10 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{(friend.displayName || friend.username || '?')[0].toUpperCase()}
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary font-black text-sm">
{getInitials(friend.displayName || friend.username || '?')}
</div>
)}
{friend.isOnline && (

View File

@@ -122,38 +122,44 @@ export default function Sidebar() {
</div>
</div>
{/* Story circles */}
{(storyGroups.length > 0 || true) && (
<div className="flex items-center gap-4 px-6 py-2 pb-6 overflow-x-auto scrollbar-hide flex-shrink-0">
{/* Add story circle */}
<button
onClick={() => setShowCreateStory(true)}
className="flex flex-col items-center gap-1.5 flex-shrink-0 group w-[60px]"
>
<div className="w-[50px] h-[50px] rounded-full border border-dashed border-zinc-600 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-colors">
<Plus size={20} className="text-accent transition-colors" />
</div>
<span className="text-[11px] text-zinc-400 truncate w-full text-center">{t('newStory')}</span>
</button>
{/* Stories Section */}
<div className="px-6 mb-2">
<h2 className="text-[11px] font-black text-on-surface-variant/40 uppercase tracking-[0.12em] select-none">
{t('stories') || 'ИСТОРИИ'}
</h2>
</div>
{storyGroups.map((group, idx) => {
const avatar = group.user.avatarUrl || group.user.avatar;
const avatarUrl = avatar ? `${API_URL}${avatar}` : null;
const isMine = group.user.id === user?.id;
return (
<button
key={group.user.id}
onClick={() => openViewer(idx)}
className="flex flex-col items-center gap-1.5 flex-shrink-0 group w-[60px]"
>
<div className={`w-[50px] h-[50px] rounded-full flex items-center justify-center transition-transform group-hover:scale-[1.03] ${
group.hasUnviewed
? 'border-[2px] border-accent'
: isMine
? 'border border-zinc-600'
: 'border border-zinc-700'
}`}>
<div className="w-[44px] h-[44px] rounded-full flex items-center justify-center overflow-hidden">
<div className="flex items-center gap-4 px-6 py-2 pb-6 overflow-x-auto scrollbar-hide flex-shrink-0">
{/* Add story item */}
<button
onClick={() => setShowCreateStory(true)}
className="flex flex-col items-center gap-2 flex-shrink-0 group w-[62px]"
>
<div className="w-[56px] h-[56px] rounded-2xl border-2 border-dashed border-white/10 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/5 transition-all">
<Plus size={22} className="text-accent/80 group-hover:text-accent transition-colors" />
</div>
<span className="text-[12px] font-medium text-zinc-400 group-hover:text-white transition-colors truncate w-full text-center">
{t('add')}
</span>
</button>
{storyGroups.map((group, idx) => {
const avatar = group.user.avatarUrl || group.user.avatar;
const avatarUrl = avatar ? `${API_URL}${avatar}` : null;
const isMine = group.user.id === user?.id;
return (
<button
key={group.user.id}
onClick={() => openViewer(idx)}
className="flex flex-col items-center gap-2 flex-shrink-0 group w-[62px]"
>
<div className={`relative p-[1.5px] rounded-[1.25rem] transition-all duration-300 ${
group.hasUnviewed
? 'bg-accent shadow-[0_0_15px_rgba(48,150,229,0.2)] scale-[1.02]'
: 'bg-white/5'
}`}>
<div className="w-[53px] h-[53px] rounded-[1.125rem] bg-surface-container-low p-1 flex items-center justify-center">
<div className="relative w-full h-full rounded-[0.9rem] overflow-hidden">
<Avatar
src={avatarUrl}
name={group.user.displayName || group.user.userName || group.user.username || '?'}
@@ -161,14 +167,14 @@ export default function Sidebar() {
/>
</div>
</div>
<span className="text-[11px] text-zinc-400 truncate w-full text-center">
{isMine ? t('myStory') : (group.user.displayName || group.user.userName || group.user.username || '?').split(' ')[0]}
</span>
</button>
);
})}
</div>
)}
</div>
<span className="text-[12px] font-medium text-zinc-400 group-hover:text-white transition-colors truncate w-full text-center">
{isMine ? t('myStory') : (group.user.displayName || group.user.userName || group.user.username || '?').split(' ')[0]}
</span>
</button>
);
})}
</div>
{/* Список чатов */}
<div className="flex-1 overflow-y-auto px-2 custom-scrollbar">

View File

@@ -148,7 +148,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
{/* Аватар */}
<div className="relative flex-shrink-0">
{isFavorites ? (
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-primary-container flex items-center justify-center shadow-lg shadow-primary/10 border-2 border-outline-variant/10">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary to-primary-container flex items-center justify-center shadow-lg shadow-primary/10 border-2 border-outline-variant/10">
<span className="material-symbols-outlined text-on-primary-container text-[24px]">bookmark</span>
</div>
) : (

View File

@@ -427,7 +427,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
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"
className="w-24 h-24 mb-6 rounded-[2rem] bg-[#1c1c1c] flex items-center justify-center shadow-[0_0_80px_rgba(48,150,229,0.15)] border border-white/5"
>
<span className="material-symbols-outlined text-[#3096e5] text-5xl select-none" style={{ fontVariationSettings: "'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48" }}>
forum
@@ -613,7 +613,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
>
<div className="relative flex-shrink-0 transform transition-all duration-500 group-hover:scale-105 active:scale-95">
{isFavorites ? (
<div className="w-11 h-11 rounded-full bg-gradient-to-br from-primary to-primary-container flex items-center justify-center shadow-lg shadow-primary/10 border-2 border-outline-variant/10">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-primary to-primary-container flex items-center justify-center shadow-lg shadow-primary/10 border-2 border-outline-variant/10">
<span className="material-symbols-outlined text-on-primary-container text-[20px]">bookmark</span>
</div>
) : (

View File

@@ -24,7 +24,7 @@ import { useAuthStore } from '../../../auth/application/authStore';
import { useChatStore } from '../../application/chatStore';
import { getSocket } from '../../../../core/infrastructure/socket';
import { useLang } from '../../../../core/infrastructure/i18n';
import { extractWaveform, getMediaUrl, generateAvatarColor } from '../../../../core/utils/utils';
import { extractWaveform, getMediaUrl, generateAvatarColor, getInitials } from '../../../../core/utils/utils';
import type { Message, MediaItem, Reaction, ChatMember } from '../../../../core/domain/types';
import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox';
import LinkPreview from './LinkPreview';
@@ -313,7 +313,7 @@ function MessageBubble({
if (reactionGroups[r.emoji].avatars.length < 3) {
reactionGroups[r.emoji].avatars.push({
url: r.user?.avatar,
initials: displayName[0].toUpperCase(),
initials: getInitials(displayName),
colorClass: 'from-primary/80 to-primary-container'
});
}
@@ -398,10 +398,10 @@ function MessageBubble({
{showAvatar ? (
<button onClick={() => onViewProfile?.(message.senderId)}>
{senderAvatar ? (
<img src={senderAvatar} alt="" className="w-8 h-8 rounded-full object-cover" />
<img src={senderAvatar} alt="" className="w-8 h-8 rounded-lg object-cover" />
) : (
<div className="w-8 h-8 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-xs font-black shadow-inner">
{senderName[0]?.toUpperCase() || '?'}
<div className="w-8 h-8 rounded-lg bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-xs font-black shadow-inner">
{getInitials(senderName)}
</div>
)}
</button>
@@ -845,10 +845,10 @@ function MessageBubble({
{showAvatar ? (
<button onClick={() => onViewProfile?.(message.senderId)}>
{senderAvatar ? (
<img src={senderAvatar} alt="" className="w-8 h-8 rounded-full object-cover" />
<img src={senderAvatar} alt="" className="w-8 h-8 rounded-lg object-cover" />
) : (
<div className="w-8 h-8 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-xs font-black shadow-inner">
{senderName[0]?.toUpperCase() || '?'}
<div className="w-8 h-8 rounded-lg bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-[10px] font-black shadow-inner">
{getInitials(senderName)}
</div>
)}
</button>

View File

@@ -9,6 +9,8 @@ import { useAuthStore } from '../../../auth/application/authStore';
import { useLang } from '../../../../core/infrastructure/i18n';
import type { UserPresence, FriendWithId } from '../../../../core/domain/types';
import { getInitials } from '../../../../core/utils/utils';
interface NewChatModalProps {
onClose: () => void;
}
@@ -55,6 +57,18 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
const handleSelectUser = async (selectedUser: UserPresence) => {
if (mode === 'personal') {
try {
// Check if personal chat with this user already exists
const existingChat = useChatStore.getState().chats.find(
(c) => c.type === 'personal' && c.members.some((m) => m.user.id === selectedUser.id)
);
if (existingChat) {
setActiveChat(existingChat.id);
loadMessages(existingChat.id);
onClose();
return;
}
const chat = await ChatApi.createPersonalChat(selectedUser.id);
addChat(chat);
import('../../../../core/infrastructure/socket').then(({ getSocket }) => getSocket()?.emit('join_chat', chat.id));
@@ -170,10 +184,10 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-knot-500/20 border border-knot-500/30"
>
{u.avatar ? (
<img src={u.avatar} alt="" className="w-5 h-5 rounded-full object-cover" />
<img src={u.avatar} alt="" className="w-5 h-5 rounded-lg object-cover" />
) : (
<div className="w-5 h-5 rounded-full bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-[9px] font-black shadow-inner">
{(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase()}
<div className="w-5 h-5 rounded-lg bg-linear-to-br from-primary/80 to-primary flex items-center justify-center text-on-primary text-[9px] font-black shadow-inner">
{getInitials(u.displayName || u.userName || u.username || '?')}
</div>
)}
<span className="text-xs text-white">{u.displayName || u.userName || u.username || ''}</span>
@@ -279,7 +293,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
<img src={u.avatar} alt="" className="w-11 h-11 rounded-2xl object-cover" />
) : (
<div className="w-11 h-11 rounded-2xl bg-linear-to-br from-primary to-primary-container flex items-center justify-center text-on-primary font-black text-sm shadow-inner">
{(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase() || '?'}
{getInitials(u.displayName || u.userName || u.username || '?')}
</div>
)}
{u.isOnline && (
@@ -331,7 +345,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
<img src={u.avatar} alt="" className="w-11 h-11 rounded-2xl object-cover" />
) : (
<div className="w-11 h-11 rounded-2xl bg-linear-to-br from-primary to-primary-container flex items-center justify-center text-on-primary font-black text-sm shadow-inner">
{(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase() || '?'}
{getInitials(u.displayName || u.userName || u.username || '?')}
</div>
)}
{u.isOnline && (

View File

@@ -15,6 +15,7 @@ import { httpClient } from '../../../../core/infrastructure/httpClient';
import { Loader2 } from 'lucide-react';
import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox';
import type { FriendWithId, FriendRequest } from '../../../../core/domain/types';
import { getInitials } from '../../../../core/utils/utils';
interface UserProfileProps {
userId: string;
@@ -225,12 +226,12 @@ export default function UserProfile({ userId, onClose, onMessage, isSelf: isSelf
{/* Profile Card */}
<div className="flex flex-col items-center mb-10">
<div className="relative mb-8 group">
<div className="absolute -inset-4 bg-primary/20 blur-3xl rounded-full opacity-40 group-hover:opacity-60 transition-opacity" />
<div className="absolute -inset-4 bg-primary/20 blur-3xl rounded-[3rem] opacity-40 group-hover:opacity-60 transition-opacity" />
<div className="relative w-40 h-40 rounded-[3rem] bg-zinc-900 border-2 border-white/10 overflow-hidden shadow-2xl">
{user.avatar ? (
<img src={resolveUrl(user.avatar)} className="w-full h-full object-cover" alt="" />
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/30 to-primary-container/10 text-5xl font-black text-primary">{user.displayName?.[0] || 'U'}</div>
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/30 to-primary-container/10 text-5xl font-black text-primary">{getInitials(user.displayName)}</div>
)}
</div>
</div>