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"; "SettingsDataClearMediaDone" = "Media cache cleared";
"LeaveGroupTitle" = "Leave {group}?"; "LeaveGroupTitle" = "Leave {group}?";
"LeaveGroupDescription" = "If you leave, **{nextOwner}** will become the new owner of **{group}** in 1 week."; "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"; "LeaveGroupAppointOwner" = "Appoint Another Owner";
"LeaveGroupAdmins" = "GROUP ADMINS"; "LeaveGroupAdmins" = "GROUP ADMINS";
"LeaveGroupMembers" = "GROUP MEMBERS"; "LeaveGroupMembers" = "GROUP MEMBERS";

View File

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

View File

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

View File

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

View File

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

View File

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