Delete Chat Modal: Support safe leave basic group (#6759)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2026-03-31 11:28:43 +02:00
parent ed638330d4
commit aa44302ed4
6 changed files with 100 additions and 33 deletions

View File

@ -2679,6 +2679,7 @@
"SettingsDataClearMediaDone" = "Media cache cleared";
"LeaveGroupTitle" = "Leave {group}?";
"LeaveGroupDescription" = "If you leave, **{nextOwner}** will become the new owner of **{group}** in 1 week.";
"LeaveBasicGroupDescription" = "If you leave, **{nextOwner}** will immediately become the new owner of **{group}**.";
"LeaveGroupAppointOwner" = "Appoint Another Owner";
"LeaveGroupAdmins" = "GROUP ADMINS";
"LeaveGroupMembers" = "GROUP MEMBERS";

View File

@ -63,6 +63,7 @@ const DeleteChatModal = ({
}: OwnProps & StateProps) => {
const {
leaveChannel,
leaveBasicGroup,
deleteHistory,
deleteSavedHistory,
deleteChannel,
@ -114,8 +115,8 @@ const DeleteChatModal = ({
leaveChannel({ chatId: chat.id });
onClose();
} else if (isBasicGroup && chat.isCreator) {
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
deleteChatUser({ chatId: chat.id, userId: currentUserId! });
leaveBasicGroup({ chatId: chat.id });
onClose();
} else {
handleDeleteChat();
}

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatFullInfo, ApiPeer } from '../../../api/types';
import type { GlobalState, TabState } from '../../../global/types';
import { isChatBasicGroup } from '../../../global/helpers';
import { getPeerTitle } from '../../../global/helpers/peers';
import { selectChat, selectChatFullInfo, selectPeer } from '../../../global/selectors';
@ -46,7 +47,7 @@ const LeaveGroupModal = ({
chatFullInfo,
}: OwnProps & StateProps) => {
const {
closeLeaveGroupModal, leaveChannel, loadMoreMembers, loadFullChat,
closeLeaveGroupModal, leaveChannel, leaveBasicGroup, loadMoreMembers, loadFullChat,
transferChatOwnership, verifyTransferOwnership, openTwoFaCheckModal,
} = getActions();
const lang = useLang();
@ -58,6 +59,7 @@ const LeaveGroupModal = ({
const isOpen = Boolean(modal);
const renderingChat = useCurrentOrPrev(chat);
const isBasicGroup = renderingChat && isChatBasicGroup(renderingChat);
const renderingCurrentUser = useCurrentOrPrev(currentUser);
useEffect(() => {
@ -171,15 +173,17 @@ const LeaveGroupModal = ({
const chatId = modal?.chatId;
if (!chatId) return;
const leaveAction = isBasicGroup ? leaveBasicGroup : leaveChannel;
if (isOwnerChanged && newOwnerId) {
transferChatOwnership({
chatId,
userId: newOwnerId,
password,
onSuccess: () => leaveChannel({ chatId, shouldSkipOwnershipCheck: true }),
onSuccess: () => leaveAction({ chatId, shouldSkipOwnershipCheck: true }),
});
} else {
leaveChannel({ chatId, shouldSkipOwnershipCheck: true });
leaveAction({ chatId, shouldSkipOwnershipCheck: true });
}
closeLeaveGroupModal();
});
@ -220,7 +224,7 @@ const LeaveGroupModal = ({
)}
<h3>{lang('LeaveGroupTitle', { group: chatTitle })}</h3>
<p>
{lang('LeaveGroupDescription', {
{lang(isBasicGroup ? 'LeaveBasicGroupDescription' : 'LeaveGroupDescription', {
nextOwner: newOwnerName,
group: chatTitle,
}, {

View File

@ -57,7 +57,7 @@ import {
isUserBot,
} from '../../helpers';
import {
addActionHandler, getGlobal, setGlobal,
addActionHandler, getActions, getGlobal, setGlobal,
} from '../../index';
import {
addChatListIds,
@ -888,6 +888,56 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
void callApi('deleteChat', { chatId: chat.id });
});
async function checkFutureCreatorAndOpenModal(
chat: ApiChat,
shouldSkipOwnershipCheck: boolean | undefined,
tabId: number,
): Promise<boolean> {
if (shouldSkipOwnershipCheck || !chat.isCreator) {
return false;
}
const futureCreator = await callApi('fetchFutureCreatorAfterLeave', { chat });
if (!futureCreator) {
return false;
}
const global = getGlobal();
const hasPassword = global.settings.byKey.hasPassword;
const actions = getActions();
if (!hasPassword) {
actions.openTwoFaCheckModal({ tabId });
return true;
}
actions.openLeaveGroupModal({ chatId: chat.id, nextOwnerId: futureCreator.id, tabId });
return true;
}
function cleanupAfterLeave(chatId: string, tabId: number) {
let global = getGlobal();
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
getActions().openChat({ id: undefined, tabId });
}
global = getGlobal();
const chatMessages = selectChatMessages(global, chatId);
if (!chatMessages) {
return;
}
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
if (!localMessageIds.length) {
return;
}
global = deleteChatMessages(global, chatId, localMessageIds);
setGlobal(global);
}
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
const { chatId, shouldSkipOwnershipCheck, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
@ -895,35 +945,39 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise<void>
return;
}
if (!shouldSkipOwnershipCheck && chat.isCreator) {
const futureCreator = await callApi('fetchFutureCreatorAfterLeave', { chat });
if (futureCreator) {
global = getGlobal();
const hasPassword = global.settings.byKey.hasPassword;
if (!hasPassword) {
actions.openTwoFaCheckModal({ tabId });
return;
}
actions.openLeaveGroupModal({ chatId, nextOwnerId: futureCreator.id, tabId });
return;
}
const isModalOpen = await checkFutureCreatorAndOpenModal(chat, shouldSkipOwnershipCheck, tabId);
if (isModalOpen) {
return;
}
global = getGlobal();
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
actions.openChat({ id: undefined, tabId });
}
await callApi('leaveChannel', { chat });
cleanupAfterLeave(chatId, tabId);
});
addActionHandler('leaveBasicGroup', async (global, actions, payload): Promise<void> => {
const { chatId, shouldSkipOwnershipCheck, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
const currentUserId = global.currentUserId;
if (!chat || !currentUserId) {
return;
}
const isModalOpen = await checkFutureCreatorAndOpenModal(chat, shouldSkipOwnershipCheck, tabId);
if (isModalOpen) {
return;
}
actions.deleteHistory({ chatId, shouldDeleteForAll: false, tabId });
global = getGlobal();
const chatMessages = selectChatMessages(global, chatId);
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
global = deleteChatMessages(global, chatId, localMessageIds);
setGlobal(global);
const user = selectUser(global, currentUserId);
if (user) {
await callApi('deleteChatUser', { chat, user, shouldRevokeHistory: false });
}
cleanupAfterLeave(chatId, tabId);
});
addActionHandler('verifyTransferOwnership', async (global, actions, payload): Promise<void> => {
@ -973,8 +1027,7 @@ addActionHandler('transferChatOwnership', async (global, actions, payload): Prom
const chat = selectChat(global, chatId);
const user = selectUser(global, userId);
if (!chat || !user) {
if (!chat || !user?.accessHash) {
return;
}

View File

@ -405,6 +405,10 @@ export interface ActionPayloads {
chatId: string;
shouldSkipOwnershipCheck?: boolean;
} & WithTabId;
leaveBasicGroup: {
chatId: string;
shouldSkipOwnershipCheck?: boolean;
} & WithTabId;
deleteChannel: { chatId: string } & WithTabId;
toggleChatPinned: {
id: string;

View File

@ -3493,6 +3493,10 @@ export interface LangPairWithVariables<V = LangVariable> {
'nextOwner': V;
'group': V;
};
'LeaveBasicGroupDescription': {
'nextOwner': V;
'group': V;
};
'LeaveGroupJoinedDate': {
'date': V;
};