Add for leave family

This commit is contained in:
lendry
2026-06-27 00:15:36 +03:00
parent 1a30e7e21c
commit 4b86c64cc4
17 changed files with 323 additions and 31 deletions

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
ArrowLeft,
ArrowRightLeft,
BarChart3,
CalendarDays,
Camera,
@@ -95,6 +96,7 @@ import {
setChatRoomPinned,
setChatRoomMuted,
toggleChatMessageReaction,
transferFamilyOwnership,
updateFamilyGroup,
uploadMediaObject,
voteChatPoll
@@ -281,7 +283,9 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const [presenceMembers, setPresenceMembers] = useState<FamilyPresenceMember[]>([]);
const [deleteFamilyDialogOpen, setDeleteFamilyDialogOpen] = useState(false);
const [leaveFamilyDialogOpen, setLeaveFamilyDialogOpen] = useState(false);
const [transferOwnershipDialogOpen, setTransferOwnershipDialogOpen] = useState(false);
const [leavingFamily, setLeavingFamily] = useState(false);
const [transferringOwnership, setTransferringOwnership] = useState(false);
const [selectedNewOwnerUserId, setSelectedNewOwnerUserId] = useState<string | null>(null);
const [deletingFamily, setDeletingFamily] = useState(false);
const [deleteMessagesDialogOpen, setDeleteMessagesDialogOpen] = useState(false);
@@ -396,6 +400,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
);
const ownerMustTransfer = Boolean(isFamilyOwner && otherHumanMembers.length > 0);
const ownerLeaveDeletesFamily = Boolean(isFamilyOwner && otherHumanMembers.length === 0);
const mobileChatOpen = Boolean(activeRoomId);
const isChatCreator = activeRoom?.createdById === user?.id;
const canManageChatMembers = Boolean(isFamilyOwner || isChatCreator || activeRoomIsBot);
const canManageActiveBot = Boolean(
@@ -875,6 +880,30 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
setLeaveFamilyDialogOpen(true);
}
function openTransferOwnershipDialog() {
setSelectedNewOwnerUserId(otherHumanMembers[0]?.userId ?? null);
setTransferOwnershipDialogOpen(true);
}
async function confirmTransferOwnership() {
if (!token || !selectedNewOwnerUserId) {
showToast('Выберите нового главу семьи');
return;
}
setTransferringOwnership(true);
try {
const updated = await transferFamilyOwnership(groupId, selectedNewOwnerUserId, token);
setGroup(updated);
setTransferOwnershipDialogOpen(false);
setFamilyMembersOpen(false);
showToast('Управление семьёй передано');
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось передать управление семьёй') ?? 'Ошибка');
} finally {
setTransferringOwnership(false);
}
}
async function confirmLeaveFamily() {
if (!token) return;
if (ownerMustTransfer && !selectedNewOwnerUserId) {
@@ -1447,8 +1476,20 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
}
return (
<div className="flex h-[calc(100vh-120px)] min-h-[640px] overflow-hidden rounded-[28px] border border-[#eceef4] bg-[#eef2f8]">
<aside className="flex w-[300px] shrink-0 flex-col border-r border-[#e4e8ef] bg-white">
<div
className={cn(
'flex overflow-hidden bg-[#eef2f8]',
'h-[calc(100dvh-3.5rem-env(safe-area-inset-bottom)-4.5rem)] min-h-0 lg:h-[calc(100vh-120px)] lg:min-h-[640px]',
'rounded-none border-0 lg:rounded-[28px] lg:border lg:border-[#eceef4]'
)}
>
<aside
className={cn(
'flex shrink-0 flex-col border-r border-[#e4e8ef] bg-white',
'w-full lg:w-[300px]',
mobileChatOpen ? 'hidden lg:flex' : 'flex'
)}
>
<div className="border-b border-[#eceef4] p-4">
<button type="button" className="mb-3 flex items-center gap-2 text-sm text-[#667085]" onClick={() => router.push('/family')}>
<ArrowLeft className="h-4 w-4" />
@@ -1557,11 +1598,24 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
</div>
</aside>
<section className="flex min-w-0 flex-1 flex-col bg-[#e6ebf2]">
<section
className={cn(
'min-w-0 flex-1 flex-col bg-[#e6ebf2]',
mobileChatOpen ? 'flex' : 'hidden lg:flex'
)}
>
{activeRoom ? (
<>
<div className="flex items-center justify-between border-b border-[#dce3ec] bg-white px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<div className="flex items-center justify-between gap-2 border-b border-[#dce3ec] bg-white px-3 py-3 sm:px-4">
<div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
<button
type="button"
aria-label="Назад к списку чатов"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-[#667085] transition hover:bg-[#f4f5f8] lg:hidden"
onClick={() => setActiveRoomId(null)}
>
<ArrowLeft className="h-5 w-5" />
</button>
{(activeRoom.type === 'GROUP' || activeRoom.type === 'GENERAL') ? (
<label className="group/room-avatar relative block shrink-0 cursor-pointer overflow-hidden rounded-full">
<ChatRoomAvatarDisplay room={activeRoom} viewerUserId={user?.id ?? ''} token={token} />
@@ -1623,13 +1677,13 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
</p>
</button>
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-1 sm:gap-2">
{!activeRoomIsBot ? (
<>
<Button
size="sm"
variant="secondary"
className="rounded-xl"
className="h-9 w-9 rounded-xl p-0 sm:h-auto sm:w-auto sm:px-3"
aria-label="Поиск по сообщениям"
onClick={() => {
setChatToolsTab('search');
@@ -1641,7 +1695,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<Button
size="sm"
variant="secondary"
className="rounded-xl"
className="hidden h-9 w-9 rounded-xl p-0 sm:flex sm:h-auto sm:w-auto sm:px-3"
aria-label="Перейти к дате"
onClick={() => {
setChatToolsTab('calendar');
@@ -2095,7 +2149,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
onDelete={() => void handleBulkDelete()}
/>
) : (
<div className="border-t border-[#dce3ec] bg-white px-3 py-3">
<div className="border-t border-[#dce3ec] bg-white px-2 py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] sm:px-3 sm:py-3">
{editingMessageId ? (
<ChatMessageEditBanner
preview={draft.trim() || messages.find((item) => item.id === editingMessageId)?.content || undefined}
@@ -2126,7 +2180,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<Paperclip className="h-4 w-4" />
</Button>
{!activeRoomIsE2E ? (
<Button size="sm" variant="ghost" className="h-10 w-10 rounded-xl p-0" onClick={() => setPollOpen(true)}>
<Button size="sm" variant="ghost" className="hidden h-10 w-10 rounded-xl p-0 sm:flex" onClick={() => setPollOpen(true)}>
<BarChart3 className="h-4 w-4" />
</Button>
) : null}
@@ -2373,6 +2427,19 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<UserPlus className="mr-2 h-4 w-4" />
Пригласить в семью
</Button>
{isFamilyOwner && otherHumanMembers.length > 0 ? (
<Button
className="mt-2 w-full rounded-xl"
variant="secondary"
onClick={() => {
setFamilyMembersOpen(false);
openTransferOwnershipDialog();
}}
>
<ArrowRightLeft className="mr-2 h-4 w-4" />
Передать управление
</Button>
) : null}
<Button
className="mt-2 w-full rounded-xl"
variant="secondary"
@@ -2399,6 +2466,73 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
</DialogContent>
</Dialog>
<Dialog open={transferOwnershipDialogOpen} onOpenChange={setTransferOwnershipDialogOpen}>
<DialogContent className="max-h-[90dvh] overflow-y-auto rounded-[28px] sm:max-w-[480px]">
<DialogHeader>
<DialogTitle>Передать управление семьёй</DialogTitle>
</DialogHeader>
<p className="text-sm leading-relaxed text-[#667085]">
Выберите участника, который станет новым главой семьи «{group?.name}». Вы останетесь участником и сможете
покинуть семью в любой момент.
</p>
<div className="mt-4 max-h-[280px] space-y-2 overflow-y-auto">
{otherHumanMembers.map((member) => {
const selected = selectedNewOwnerUserId === member.userId;
return (
<button
key={member.id}
type="button"
onClick={() => setSelectedNewOwnerUserId(member.userId)}
className={cn(
'flex w-full items-center gap-3 rounded-2xl px-3 py-3 text-left transition',
selected ? 'bg-[#3390ec]/10 ring-2 ring-[#3390ec]/40' : 'bg-[#f4f5f8] hover:bg-[#eceef4]'
)}
>
<AvatarWithPresence
userId={member.userId}
displayName={member.displayName}
hasAvatar={member.hasAvatar}
token={token}
isVerified={member.isVerified}
verificationIcon={member.verificationIcon}
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]">{familyMemberRoleLabel(member.role)}</p>
</div>
{selected ? <Check className="h-5 w-5 shrink-0 text-[#3390ec]" /> : null}
</button>
);
})}
</div>
<div className="mt-6 flex flex-col-reverse gap-3 sm:flex-row">
<Button
variant="secondary"
className="flex-1"
disabled={transferringOwnership}
onClick={() => setTransferOwnershipDialogOpen(false)}
>
Отмена
</Button>
<Button
className="flex-1"
disabled={transferringOwnership || !selectedNewOwnerUserId}
onClick={() => void confirmTransferOwnership()}
>
{transferringOwnership ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Передаём...
</>
) : (
'Передать управление'
)}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={leaveFamilyDialogOpen} onOpenChange={setLeaveFamilyDialogOpen}>
<DialogContent className="rounded-[28px] sm:max-w-[480px]">
<DialogHeader>