fix and update
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user