more fix and update

This commit is contained in:
lendry
2026-06-24 20:15:19 +03:00
parent dcab6557d3
commit 9727cf3f35
53 changed files with 3479 additions and 494 deletions

View File

@@ -31,14 +31,17 @@ import {
ChatRoom,
createChatRoom,
FamilyGroup,
FamilyInviteCandidate,
fetchChatMessages,
fetchChatRooms,
fetchFamilyGroup,
getApiErrorMessage,
searchFamilyInviteUsers,
sendChatMessage,
sendFamilyInvite,
setChatRoomMuted,
updateFamilyGroup,
uploadMediaObject,
voteChatPoll
} from '@/lib/api';
import { cn } from '@/lib/utils';
@@ -63,6 +66,8 @@ const EMOJIS = ['😀', '😂', '❤️', '👍', '🎉', '🔥', '😍', '🙏'
interface PresignedUploadResponse {
uploadUrl: string;
storageKey: string;
expiresAt?: string;
uploadToken?: string;
}
interface LoadedChatMedia {
@@ -108,7 +113,10 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const [inviteOpen, setInviteOpen] = useState(false);
const [createRoomOpen, setCreateRoomOpen] = useState(false);
const [pollOpen, setPollOpen] = useState(false);
const [inviteTarget, setInviteTarget] = useState('');
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[]>([]);
@@ -300,7 +308,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
{ method: 'POST', body: JSON.stringify({ contentType: file.type }) },
token
);
await fetch(presigned.uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file });
await uploadMediaObject(presigned, file, file.type);
await apiFetch(`/media/families/${groupId}/avatar/confirm`, {
method: 'POST',
body: JSON.stringify({ storageKey: presigned.storageKey })
@@ -313,17 +321,38 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
}
async function submitInvite() {
if (!token || !inviteTarget.trim()) return;
if (!token || !selectedInviteUser) return;
try {
await sendFamilyInvite(groupId, inviteTarget.trim(), token);
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
setInviteOpen(false);
setInviteTarget('');
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 {
@@ -377,11 +406,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
},
token
);
await fetch(presigned.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: file
});
await uploadMediaObject(presigned, file, contentType);
const message = await sendChatMessage(
activeRoomId,
{
@@ -747,11 +772,72 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
event.target.value = '';
}} />
<Dialog open={inviteOpen} onOpenChange={setInviteOpen}>
<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>
<Input placeholder="Email, телефон или логин" value={inviteTarget} onChange={(event) => setInviteTarget(event.target.value)} />
<Button className="mt-4 w-full rounded-xl" onClick={() => void submitInvite()}>Отправить приглашение</Button>
{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>