global update and global fix

This commit is contained in:
lendry
2026-06-25 23:48:57 +03:00
parent 3880c68d59
commit b0ea87e898
121 changed files with 13759 additions and 663 deletions

View File

@@ -1,22 +1,34 @@
'use client';
import { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { Bot, Loader2 } from 'lucide-react';
import { AvatarWithPresence } from '@/components/family/avatar-with-presence';
import { useAuth } from '@/components/id/auth-provider';
import { useToast } from '@/components/id/toast-provider';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { FamilyInviteCandidate, getApiErrorMessage, searchFamilyInviteUsers, sendFamilyInvite } from '@/lib/api';
import {
FamilyGroup,
FamilyInviteCandidate,
ManagedBot,
addBotFatherToFamily,
addBotToFamily,
fetchMyBots,
getApiErrorMessage,
searchFamilyInviteUsers,
sendFamilyInvite
} from '@/lib/api';
export function FamilyInviteDialog({
groupId,
group,
open,
onOpenChange,
onInvited
}: {
groupId: string;
group?: FamilyGroup | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onInvited?: () => void;
@@ -28,14 +40,39 @@ export function FamilyInviteDialog({
const [inviteSearching, setInviteSearching] = useState(false);
const [selectedInviteUser, setSelectedInviteUser] = useState<FamilyInviteCandidate | null>(null);
const [submitting, setSubmitting] = useState(false);
const [myBots, setMyBots] = useState<ManagedBot[]>([]);
const [myBotsLoading, setMyBotsLoading] = useState(false);
const [addingBotId, setAddingBotId] = useState<string | null>(null);
const familyBotUsernames = useMemo(
() => new Set((group?.members ?? []).map((member) => member.botUsername).filter(Boolean) as string[]),
[group?.members]
);
const availableMyBots = useMemo(
() => myBots.filter((bot) => !familyBotUsernames.has(bot.username)),
[familyBotUsernames, myBots]
);
useEffect(() => {
if (!open) {
setInviteQuery('');
setInviteResults([]);
setSelectedInviteUser(null);
setMyBots([]);
return;
}
if (!token) return;
setMyBotsLoading(true);
fetchMyBots(token)
.then((response) => setMyBots(response.bots ?? []))
.catch(() => setMyBots([]))
.finally(() => setMyBotsLoading(false));
}, [open, token]);
useEffect(() => {
if (!open) return;
if (!token || inviteQuery.trim().length < 2) {
setInviteResults([]);
return;
@@ -52,21 +89,71 @@ export function FamilyInviteDialog({
return () => window.clearTimeout(timer);
}, [groupId, inviteQuery, open, token]);
async function addBotById(botId: string, displayName: string) {
if (!token) return;
setAddingBotId(botId);
try {
await addBotToFamily(groupId, botId, token);
showToast(`${displayName} добавлен в семью`);
onOpenChange(false);
onInvited?.();
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось добавить бота') ?? 'Ошибка');
} finally {
setAddingBotId(null);
}
}
async function submitInvite() {
if (!token || !selectedInviteUser) return;
setSubmitting(true);
try {
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
showToast('Приглашение отправлено');
if (selectedInviteUser.isBot) {
if (selectedInviteUser.botId) {
await addBotToFamily(groupId, selectedInviteUser.botId, token);
showToast(`${selectedInviteUser.displayName} добавлен в семью`);
} else {
await addBotFatherToFamily(groupId, token);
showToast('BotFather добавлен в семью');
}
} else {
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
showToast('Приглашение отправлено');
}
onOpenChange(false);
onInvited?.();
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось отправить приглашение') ?? 'Ошибка');
showToast(
getApiErrorMessage(
error,
selectedInviteUser.isBot ? 'Не удалось добавить бота' : 'Не удалось отправить приглашение'
) ?? 'Ошибка'
);
} finally {
setSubmitting(false);
}
}
function botCandidateLabel(candidate: FamilyInviteCandidate) {
if (!candidate.isBot) {
return candidate.email ?? candidate.phone ?? candidate.username ?? 'Без контактов';
}
const handle = `@${candidate.botUsername ?? 'bot'}`;
if (candidate.ownerDisplayName) {
return `${handle} · владелец: ${candidate.ownerDisplayName}`;
}
if (candidate.botUsername === 'BotFather_bot') {
return `${handle} — создавайте ботов через чат`;
}
return handle;
}
function submitButtonLabel() {
if (!selectedInviteUser?.isBot) return 'Отправить приглашение';
if (selectedInviteUser.botId) return 'Добавить бота';
return 'Добавить BotFather';
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="rounded-[28px] sm:max-w-[420px]">
@@ -75,33 +162,70 @@ export function FamilyInviteDialog({
</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 className="font-medium">
{selectedInviteUser.displayName}
{selectedInviteUser.isBot ? ' 🤖' : ''}
</p>
<p className="text-sm text-[#667085]">{botCandidateLabel(selectedInviteUser)}</p>
<button type="button" className="mt-2 text-sm text-[#667085] underline" onClick={() => setSelectedInviteUser(null)}>
Выбрать другого пользователя
Выбрать другого
</button>
</div>
) : (
<>
<Input
placeholder="ФИО, почта, телефон или логин"
placeholder="ФИО, почта, телефон, логин или @бот"
value={inviteQuery}
onChange={(event) => setInviteQuery(event.target.value)}
/>
{inviteQuery.trim().length < 2 ? (
<div className="mt-3">
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-[#667085]">Мои боты</p>
<div className="max-h-40 overflow-y-auto rounded-2xl border border-[#eceef4]">
{myBotsLoading ? (
<div className="flex items-center gap-2 px-4 py-3 text-sm text-[#667085]">
<Loader2 className="h-4 w-4 animate-spin" />
Загрузка ботов...
</div>
) : availableMyBots.length ? (
availableMyBots.map((bot) => (
<button
key={bot.id}
type="button"
disabled={addingBotId === bot.id}
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] disabled:opacity-60"
onClick={() => void addBotById(bot.id, bot.name)}
>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[#eef4ff] text-[#3390ec]">
{addingBotId === bot.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Bot className="h-4 w-4" />}
</div>
<div className="min-w-0">
<p className="truncate font-medium">{bot.name}</p>
<p className="truncate text-sm text-[#667085]">@{bot.username}</p>
</div>
</button>
))
) : (
<p className="px-4 py-3 text-sm text-[#667085]">
{myBots.length ? 'Все ваши боты уже в семье' : 'У вас пока нет ботов. Создайте через BotFather.'}
</p>
)}
</div>
<p className="mt-2 text-xs text-[#667085]">Или найдите пользователя / бота другого владельца через поиск выше</p>
</div>
) : null}
<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>
<p className="px-4 py-3 text-sm text-[#667085]">Введите минимум 2 символа для поиска пользователей и ботов</p>
) : inviteResults.length ? (
inviteResults.map((candidate) => (
<button
key={candidate.id}
key={`${candidate.id}-${candidate.botId ?? 'user'}`}
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={() => {
@@ -109,31 +233,38 @@ export function FamilyInviteDialog({
setInviteResults([]);
}}
>
<AvatarWithPresence
userId={candidate.id}
displayName={candidate.displayName}
hasAvatar={candidate.hasAvatar}
token={token}
isVerified={candidate.isVerified}
verificationIcon={candidate.verificationIcon}
className="h-9 w-9"
/>
{candidate.isBot ? (
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[#eef4ff] text-[#3390ec]">
<Bot className="h-4 w-4" />
</div>
) : (
<AvatarWithPresence
userId={candidate.id}
displayName={candidate.displayName}
hasAvatar={candidate.hasAvatar}
token={token}
isVerified={candidate.isVerified}
verificationIcon={candidate.verificationIcon}
className="h-9 w-9"
/>
)}
<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 className="truncate font-medium">
{candidate.displayName}
{candidate.isBot ? ' 🤖' : ''}
</p>
<p className="truncate text-sm text-[#667085]">{botCandidateLabel(candidate)}</p>
</div>
</button>
))
) : (
<p className="px-4 py-3 text-sm text-[#667085]">Пользователи не найдены</p>
<p className="px-4 py-3 text-sm text-[#667085]">Ничего не найдено</p>
)}
</div>
</>
)}
<Button className="mt-4 w-full rounded-xl" disabled={!selectedInviteUser || submitting} onClick={() => void submitInvite()}>
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Отправить приглашение'}
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : submitButtonLabel()}
</Button>
</DialogContent>
</Dialog>