fix and update

This commit is contained in:
lendry
2026-06-25 16:01:41 +03:00
parent ce58e6f4c1
commit 3880c68d59
40 changed files with 1715 additions and 215 deletions

View File

@@ -23,7 +23,10 @@ import {
Volume2,
VolumeX
} from 'lucide-react';
import { AvatarWithPresence } from '@/components/family/avatar-with-presence';
import { FamilyInviteDialog } from '@/components/family/family-invite-dialog';
import { VoiceMessagePlayer } from '@/components/chat/voice-message-player';
import { UserAvatar } from '@/components/id/user-avatar';
import { useAuth } from '@/components/id/auth-provider';
import { useToast } from '@/components/id/toast-provider';
import { useRealtime } from '@/components/notifications/realtime-provider';
@@ -48,7 +51,6 @@ import {
deleteFamilyGroup,
editChatMessage,
FamilyGroup,
FamilyInviteCandidate,
FamilyPresenceMember,
fetchChatMessages,
fetchChatRooms,
@@ -58,9 +60,7 @@ import {
markChatRoomRead,
removeChatRoomMember,
removeFamilyMember,
searchFamilyInviteUsers,
sendChatMessage,
sendFamilyInvite,
setChatRoomMuted,
updateFamilyGroup,
uploadMediaObject,
@@ -132,18 +132,6 @@ function messagePreview(message?: ChatMessage) {
return message.content ?? '';
}
function OnlineDot({ online, className }: { online?: boolean; className?: string }) {
if (!online) return null;
return (
<span
className={cn(
'absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-emerald-500 shadow-sm',
className
)}
/>
);
}
function FamilyAvatar({ groupId, name, hasAvatar, token }: { groupId: string; name: string; hasAvatar?: boolean; token: string | null }) {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
@@ -206,10 +194,6 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const [membersOpen, setMembersOpen] = useState(false);
const [familyMembersOpen, setFamilyMembersOpen] = useState(false);
const [addMembersOpen, setAddMembersOpen] = useState(false);
const [inviteQuery, setInviteQuery] = useState('');
const [inviteResults, setInviteResults] = useState<FamilyInviteCandidate[]>([]);
const [selectedInviteUser, setSelectedInviteUser] = useState<FamilyInviteCandidate | null>(null);
const [inviteSearching, setInviteSearching] = useState(false);
const [renameValue, setRenameValue] = useState('');
const [newRoomName, setNewRoomName] = useState('');
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
@@ -579,39 +563,6 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
}
}
async function submitInvite() {
if (!token || !selectedInviteUser) return;
try {
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
setInviteOpen(false);
setInviteQuery('');
setInviteResults([]);
setSelectedInviteUser(null);
showToast('Приглашение отправлено');
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось отправить приглашение') ?? 'Ошибка');
}
}
useEffect(() => {
if (!inviteOpen || !token || selectedInviteUser) {
return;
}
const query = inviteQuery.trim();
if (query.length < 2) {
setInviteResults([]);
return;
}
const timer = window.setTimeout(() => {
setInviteSearching(true);
void searchFamilyInviteUsers(groupId, query, token)
.then((response) => setInviteResults(response.users ?? []))
.catch(() => setInviteResults([]))
.finally(() => setInviteSearching(false));
}, 300);
return () => window.clearTimeout(timer);
}, [groupId, inviteOpen, inviteQuery, selectedInviteUser, token]);
async function submitCreateRoom() {
if (!token || !newRoomName.trim()) return;
try {
@@ -941,7 +892,19 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const isEditing = editingMessageId === message.id;
const canEdit = mine && !message.isDeleted && (message.type === 'TEXT' || message.type === 'EMOJI');
return (
<div key={message.id} className={cn('group flex', mine ? 'justify-end' : 'justify-start')}>
<div key={message.id} className={cn('group flex gap-2', mine ? 'justify-end' : 'justify-start')}>
{!mine ? (
<UserAvatar
userId={message.senderId}
displayName={message.senderName}
hasAvatar={message.senderHasAvatar}
token={token}
isVerified={message.senderIsVerified}
verificationIcon={message.senderVerificationIcon}
className="h-8 w-8 self-end"
badgeSize="xs"
/>
) : null}
<div
className={cn(
'relative max-w-[78%] rounded-[18px] px-3 py-2 shadow-sm',
@@ -1176,74 +1139,12 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
event.target.value = '';
}} />
<Dialog open={inviteOpen} onOpenChange={(open) => {
setInviteOpen(open);
if (!open) {
setInviteQuery('');
setInviteResults([]);
setSelectedInviteUser(null);
}
}}>
<DialogContent className="rounded-[28px] sm:max-w-[420px]">
<DialogHeader><DialogTitle>Пригласить в семью</DialogTitle></DialogHeader>
{selectedInviteUser ? (
<div className="rounded-2xl bg-[#f4f5f8] px-4 py-3">
<p className="font-medium">{selectedInviteUser.displayName}</p>
<p className="text-sm text-[#667085]">
{selectedInviteUser.email ?? selectedInviteUser.phone ?? selectedInviteUser.username ?? 'Контакт не указан'}
</p>
<button type="button" className="mt-2 text-sm text-[#667085] underline" onClick={() => setSelectedInviteUser(null)}>
Выбрать другого пользователя
</button>
</div>
) : (
<>
<Input
placeholder="ФИО, почта, телефон или логин"
value={inviteQuery}
onChange={(event) => setInviteQuery(event.target.value)}
/>
<div className="mt-2 max-h-56 overflow-y-auto rounded-2xl border border-[#eceef4]">
{inviteSearching ? (
<div className="flex items-center gap-2 px-4 py-3 text-sm text-[#667085]">
<Loader2 className="h-4 w-4 animate-spin" />
Ищем пользователей...
</div>
) : inviteQuery.trim().length < 2 ? (
<p className="px-4 py-3 text-sm text-[#667085]">Введите минимум 2 символа для поиска</p>
) : inviteResults.length ? (
inviteResults.map((candidate) => (
<button
key={candidate.id}
type="button"
className="flex w-full items-center gap-3 border-b border-[#eceef4] px-4 py-3 text-left last:border-b-0 hover:bg-[#fafbfd]"
onClick={() => {
setSelectedInviteUser(candidate);
setInviteResults([]);
}}
>
<Avatar className="h-9 w-9">
<AvatarFallback>{candidate.displayName.slice(0, 1)}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="truncate font-medium">{candidate.displayName}</p>
<p className="truncate text-sm text-[#667085]">
{candidate.email ?? candidate.phone ?? candidate.username ?? 'Без контактов'}
</p>
</div>
</button>
))
) : (
<p className="px-4 py-3 text-sm text-[#667085]">Пользователи не найдены</p>
)}
</div>
</>
)}
<Button className="mt-4 w-full rounded-xl" disabled={!selectedInviteUser} onClick={() => void submitInvite()}>
Отправить приглашение
</Button>
</DialogContent>
</Dialog>
<FamilyInviteDialog
groupId={groupId}
open={inviteOpen}
onOpenChange={setInviteOpen}
onInvited={() => void loadGroup()}
/>
<Dialog open={familyMembersOpen} onOpenChange={setFamilyMembersOpen}>
<DialogContent className="rounded-[28px] sm:max-w-[480px]">
@@ -1262,21 +1163,27 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
return (
<div key={member.id} className="flex items-center gap-3 rounded-2xl bg-[#f4f5f8] px-3 py-3">
<div className="relative shrink-0">
<Avatar className="h-10 w-10">
<AvatarFallback>{member.displayName.slice(0, 1)}</AvatarFallback>
</Avatar>
<OnlineDot online={presence?.online} />
</div>
<AvatarWithPresence
userId={member.userId}
displayName={member.displayName}
hasAvatar={member.hasAvatar}
token={token}
isVerified={member.isVerified}
verificationIcon={member.verificationIcon}
online={isSelf ? undefined : presence?.online}
className="h-10 w-10"
/>
<div className="min-w-0 flex-1">
<p className="truncate font-medium">
{member.displayName}
{isSelf ? <span className="ml-1 text-xs font-normal text-[#667085]">(Вы)</span> : null}
</p>
<p className="text-xs text-[#667085]">{familyMemberRoleLabel(member.role)}</p>
<p className={cn('text-xs', presence?.online ? 'font-medium text-emerald-600' : 'text-[#a8adbc]')}>
{presence ? formatPresenceStatus(presence) : 'Статус неизвестен'}
</p>
{!isSelf ? (
<p className={cn('text-xs', presence?.online ? 'font-medium text-emerald-600' : 'text-[#a8adbc]')}>
{presence ? formatPresenceStatus(presence) : 'Статус неизвестен'}
</p>
) : null}
</div>
{canRemove ? (
<Button
@@ -1357,12 +1264,16 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
return (
<div key={member.id} className="flex items-center gap-3 rounded-2xl bg-[#f4f5f8] px-3 py-3">
<div className="relative shrink-0">
<Avatar className="h-10 w-10">
<AvatarFallback>{member.displayName.slice(0, 1)}</AvatarFallback>
</Avatar>
<OnlineDot online={presence?.online} />
</div>
<AvatarWithPresence
userId={member.userId}
displayName={member.displayName}
hasAvatar={member.hasAvatar}
token={token}
isVerified={member.isVerified}
verificationIcon={member.verificationIcon}
online={presence?.online}
className="h-10 w-10"
/>
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{member.displayName}</p>
<p className="text-xs text-[#667085]">{memberRoleLabel(member)}</p>
@@ -1404,22 +1315,32 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<DialogHeader><DialogTitle>Добавить в чат</DialogTitle></DialogHeader>
<div className="space-y-2">
{familyMembersNotInRoom.length ? (
familyMembersNotInRoom.map((member) => (
familyMembersNotInRoom.map((member) => {
const presence = presenceByUserId.get(member.userId);
return (
<button
key={member.id}
type="button"
className="flex w-full items-center gap-3 rounded-2xl bg-[#f4f5f8] px-3 py-3 text-left transition hover:bg-[#eceef4]"
onClick={() => void handleAddRoomMember(member.userId)}
>
<Avatar className="h-9 w-9">
<AvatarFallback>{member.displayName.slice(0, 1)}</AvatarFallback>
</Avatar>
<AvatarWithPresence
userId={member.userId}
displayName={member.displayName}
hasAvatar={member.hasAvatar}
token={token}
isVerified={member.isVerified}
verificationIcon={member.verificationIcon}
online={presence?.online}
className="h-9 w-9"
/>
<div>
<p className="font-medium">{member.displayName}</p>
<p className="text-xs text-[#667085]">{member.role === 'owner' ? 'Создатель семьи' : 'Участник семьи'}</p>
</div>
</button>
))
);
})
) : (
<p className="py-6 text-center text-sm text-[#667085]">Все участники семьи уже в этом чате</p>
)}