Add for leave family
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Loader2,
|
||||
LogOut,
|
||||
Mic,
|
||||
Paperclip,
|
||||
Pin,
|
||||
@@ -84,6 +85,7 @@ import {
|
||||
fetchFamilyPresence,
|
||||
forwardChatMessages,
|
||||
getApiErrorMessage,
|
||||
leaveFamilyGroup,
|
||||
markChatRoomRead,
|
||||
removeChatRoomMember,
|
||||
removeFamilyMember,
|
||||
@@ -278,6 +280,9 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
||||
const [presenceMembers, setPresenceMembers] = useState<FamilyPresenceMember[]>([]);
|
||||
const [deleteFamilyDialogOpen, setDeleteFamilyDialogOpen] = useState(false);
|
||||
const [leaveFamilyDialogOpen, setLeaveFamilyDialogOpen] = useState(false);
|
||||
const [leavingFamily, setLeavingFamily] = useState(false);
|
||||
const [selectedNewOwnerUserId, setSelectedNewOwnerUserId] = useState<string | null>(null);
|
||||
const [deletingFamily, setDeletingFamily] = useState(false);
|
||||
const [deleteMessagesDialogOpen, setDeleteMessagesDialogOpen] = useState(false);
|
||||
const [pendingDeleteMessageIds, setPendingDeleteMessageIds] = useState<string[]>([]);
|
||||
@@ -381,6 +386,16 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
);
|
||||
const myMembership = activeRoom?.members.find((member) => member.userId === user?.id);
|
||||
const isFamilyOwner = group?.ownerId === user?.id;
|
||||
const humanMembers = useMemo(
|
||||
() => (group?.members ?? []).filter((member) => !member.isBot && member.role !== 'bot'),
|
||||
[group?.members]
|
||||
);
|
||||
const otherHumanMembers = useMemo(
|
||||
() => humanMembers.filter((member) => member.userId !== user?.id),
|
||||
[humanMembers, user?.id]
|
||||
);
|
||||
const ownerMustTransfer = Boolean(isFamilyOwner && otherHumanMembers.length > 0);
|
||||
const ownerLeaveDeletesFamily = Boolean(isFamilyOwner && otherHumanMembers.length === 0);
|
||||
const isChatCreator = activeRoom?.createdById === user?.id;
|
||||
const canManageChatMembers = Boolean(isFamilyOwner || isChatCreator || activeRoomIsBot);
|
||||
const canManageActiveBot = Boolean(
|
||||
@@ -609,6 +624,31 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
if (event.type === 'family_group_deleted' && payload?.groupId === groupId) {
|
||||
showToast('Семья была удалена');
|
||||
router.push('/family');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'family_left_self' && payload?.groupId === groupId) {
|
||||
router.push('/family');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'family_member_left' && payload?.groupId === groupId) {
|
||||
const userName = typeof payload?.userName === 'string' ? payload.userName : 'Участник';
|
||||
showToast(`${userName} покинул(а) семью`);
|
||||
void loadGroup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'family_member_removed' && payload?.groupId === groupId) {
|
||||
const userName = typeof payload?.userName === 'string' ? payload.userName : 'Участник';
|
||||
showToast(`${userName} удалён(а) из семьи`);
|
||||
void loadGroup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'family_ownership_transferred' && payload?.groupId === groupId) {
|
||||
showToast('Вы стали главой семьи');
|
||||
void loadGroup();
|
||||
}
|
||||
});
|
||||
}, [
|
||||
@@ -821,12 +861,6 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
if (!token) return;
|
||||
try {
|
||||
await removeFamilyMember(memberId, token);
|
||||
const removedMember = group?.members?.find((item) => item.id === memberId);
|
||||
if (removedMember?.userId === user?.id) {
|
||||
showToast('Вы вышли из семьи');
|
||||
router.push('/family');
|
||||
return;
|
||||
}
|
||||
await loadGroup();
|
||||
setMembersOpen(false);
|
||||
setFamilyMembersOpen(false);
|
||||
@@ -836,6 +870,35 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
function openLeaveFamilyDialog() {
|
||||
setSelectedNewOwnerUserId(otherHumanMembers[0]?.userId ?? null);
|
||||
setLeaveFamilyDialogOpen(true);
|
||||
}
|
||||
|
||||
async function confirmLeaveFamily() {
|
||||
if (!token) return;
|
||||
if (ownerMustTransfer && !selectedNewOwnerUserId) {
|
||||
showToast('Выберите нового главу семьи');
|
||||
return;
|
||||
}
|
||||
setLeavingFamily(true);
|
||||
try {
|
||||
const result = await leaveFamilyGroup(
|
||||
groupId,
|
||||
token,
|
||||
ownerMustTransfer ? selectedNewOwnerUserId ?? undefined : undefined
|
||||
);
|
||||
setLeaveFamilyDialogOpen(false);
|
||||
setFamilyMembersOpen(false);
|
||||
showToast(result.deleted ? 'Семья удалена' : 'Вы покинули семью');
|
||||
router.push('/family');
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось покинуть семью') ?? 'Ошибка');
|
||||
} finally {
|
||||
setLeavingFamily(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function createSecretChatWithMember(memberUserId: string) {
|
||||
if (!token) return;
|
||||
try {
|
||||
@@ -1456,6 +1519,11 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="flex min-w-0 items-center gap-1 truncate font-medium">
|
||||
<span className="truncate">{user ? roomDisplayLabel(room, user.id) : room.name}</span>
|
||||
{room.peerLeftFamily ? (
|
||||
<span className="shrink-0 rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">
|
||||
не в семье
|
||||
</span>
|
||||
) : null}
|
||||
{room.isPinned ? <Pin className="h-3.5 w-3.5 shrink-0 fill-[#8a8f9e] text-[#8a8f9e]" /> : null}
|
||||
</p>
|
||||
{room.lastMessage ? (
|
||||
@@ -1528,9 +1596,19 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
{activeRoomIsBot ? (
|
||||
<>🤖 Бот · {activeRoom.members.length} участников</>
|
||||
) : activeRoomIsE2E ? (
|
||||
<>🔒 E2E · секретный чат</>
|
||||
<>
|
||||
🔒 E2E · секретный чат
|
||||
{activeRoom.peerLeftFamily ? (
|
||||
<span className="ml-1 text-amber-600">· собеседник не в семье</span>
|
||||
) : null}
|
||||
</>
|
||||
) : activeRoom.type === 'DIRECT' ? (
|
||||
<>Личный чат · {activeRoom.members.length} участников</>
|
||||
<>
|
||||
Личный чат · {activeRoom.members.length} участников
|
||||
{activeRoom.peerLeftFamily ? (
|
||||
<span className="ml-1 text-amber-600">· собеседник не в семье</span>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{onlineCount > 0 ? (
|
||||
@@ -2220,8 +2298,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
<div className="mt-3 max-h-[420px] space-y-2 overflow-y-auto">
|
||||
{(group?.members ?? []).map((member) => {
|
||||
const isSelf = member.userId === user?.id;
|
||||
const canRemove =
|
||||
member.role !== 'owner' && (isFamilyOwner || isSelf);
|
||||
const canKick = !isSelf && isFamilyOwner && member.role !== 'owner' && !member.isBot;
|
||||
const presence = presenceByUserId.get(member.userId);
|
||||
|
||||
return (
|
||||
@@ -2272,7 +2349,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
🔒 E2E
|
||||
</Button>
|
||||
) : null}
|
||||
{canRemove ? (
|
||||
{canKick ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -2282,7 +2359,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
void handleRemoveFromFamily(member.id);
|
||||
}}
|
||||
>
|
||||
{isSelf ? 'Выйти' : 'Удалить'}
|
||||
Удалить
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -2296,6 +2373,17 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Пригласить в семью
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-2 w-full rounded-xl"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setFamilyMembersOpen(false);
|
||||
openLeaveFamilyDialog();
|
||||
}}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Покинуть семью
|
||||
</Button>
|
||||
{isFamilyOwner ? (
|
||||
<Button
|
||||
className="mt-2 w-full rounded-xl bg-red-600 hover:bg-red-700"
|
||||
@@ -2311,6 +2399,87 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={leaveFamilyDialogOpen} onOpenChange={setLeaveFamilyDialogOpen}>
|
||||
<DialogContent className="rounded-[28px] sm:max-w-[480px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Покинуть семью «{group?.name}»?</DialogTitle>
|
||||
</DialogHeader>
|
||||
{ownerMustTransfer ? (
|
||||
<>
|
||||
<p className="text-sm leading-relaxed text-[#667085]">
|
||||
Вы создатель семьи. Перед выходом передайте управление другому участнику — он станет новым главой
|
||||
семьи.
|
||||
</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>
|
||||
</>
|
||||
) : ownerLeaveDeletesFamily ? (
|
||||
<p className="text-sm leading-relaxed text-[#667085]">
|
||||
В семье больше нет других участников. Семья и все чаты будут полностью удалены без возможности
|
||||
восстановления.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm leading-relaxed text-[#667085]">
|
||||
Семья исчезнет из вашего профиля вместе со всеми чатами. У остальных участников переписка с вами
|
||||
сохранится с пометкой, что вы больше не состоите в семье.
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-6 flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
disabled={leavingFamily}
|
||||
onClick={() => setLeaveFamilyDialogOpen(false)}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 bg-red-600 hover:bg-red-700"
|
||||
disabled={leavingFamily || (ownerMustTransfer && !selectedNewOwnerUserId)}
|
||||
onClick={() => void confirmLeaveFamily()}
|
||||
>
|
||||
{leavingFamily ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Выходим...
|
||||
</>
|
||||
) : (
|
||||
'Покинуть семью'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={deleteFamilyDialogOpen} onOpenChange={setDeleteFamilyDialogOpen}>
|
||||
<DialogContent className="rounded-[28px] sm:max-w-[440px]">
|
||||
<DialogHeader>
|
||||
@@ -2366,7 +2535,14 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
className="h-10 w-10"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium">{member.displayName}</p>
|
||||
<p className="truncate font-medium">
|
||||
{member.displayName}
|
||||
{member.inFamily === false ? (
|
||||
<span className="ml-1.5 rounded-md bg-[#eceef4] px-1.5 py-0.5 text-[10px] font-medium text-[#667085]">
|
||||
не в семье
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p className="text-xs text-[#667085]">{memberRoleLabel(member)}</p>
|
||||
<p className={cn('text-xs', presence?.online ? 'font-medium text-emerald-600' : 'text-[#a8adbc]')}>
|
||||
{presence ? formatPresenceStatus(presence) : 'Статус неизвестен'}
|
||||
|
||||
@@ -859,6 +859,7 @@ export interface ChatRoomMember {
|
||||
pinnedAt?: string;
|
||||
familyRole?: string;
|
||||
isChatCreator?: boolean;
|
||||
inFamily?: boolean;
|
||||
}
|
||||
|
||||
export interface ChatRoom {
|
||||
@@ -874,6 +875,7 @@ export interface ChatRoom {
|
||||
updatedAt: string;
|
||||
isPinned?: boolean;
|
||||
pinnedAt?: string;
|
||||
peerLeftFamily?: boolean;
|
||||
members: ChatRoomMember[];
|
||||
lastMessage?: ChatMessage;
|
||||
}
|
||||
@@ -978,6 +980,17 @@ export async function removeFamilyMember(memberId: string, token?: string | null
|
||||
return apiFetch<{ count: number }>(`/family/members/${memberId}`, { method: 'DELETE' }, token);
|
||||
}
|
||||
|
||||
export async function leaveFamilyGroup(groupId: string, token?: string | null, newOwnerUserId?: string) {
|
||||
return apiFetch<{ count: number; deleted?: boolean }>(
|
||||
`/family/groups/${groupId}/leave`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(newOwnerUserId ? { newOwnerUserId } : {})
|
||||
},
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchChatMessages(roomId: string, token?: string | null, beforeMessageId?: string) {
|
||||
const query = beforeMessageId ? `?beforeMessageId=${beforeMessageId}` : '';
|
||||
return apiFetch<{ messages?: ChatMessage[] }>(`/chat/rooms/${roomId}/messages${query}`, {}, token);
|
||||
|
||||
Reference in New Issue
Block a user