Channel Admin: Add leave channel modal (#6656)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
parent
c22d035308
commit
6c3f009388
@ -116,6 +116,7 @@ Convesion from and to Api* objects is done by `apiBuilders` (function name start
|
|||||||
const result = await callApi('methodName', { /* params */ });
|
const result = await callApi('methodName', { /* params */ });
|
||||||
```
|
```
|
||||||
* Always check for `undefined` before proceeding.
|
* Always check for `undefined` before proceeding.
|
||||||
|
* **IMPORTANT: Do not pass `accessHash` directly to API methods.** Methods that accept separate `id` and `accessHash` parameters are outdated. Instead, pass the full `ApiPeer`, `ApiChat`, or `ApiUser` object. The `buildInput*` functions in `gramjsBuilders` will extract the necessary fields.
|
||||||
|
|
||||||
## 4. Example
|
## 4. Example
|
||||||
|
|
||||||
|
|||||||
@ -556,6 +556,22 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
|
|||||||
price: buildApiCurrencyAmount(price),
|
price: buildApiCurrencyAmount(price),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action instanceof GramJs.MessageActionNewCreatorPending) {
|
||||||
|
const { newCreatorId } = action;
|
||||||
|
return {
|
||||||
|
mediaType: 'action',
|
||||||
|
type: 'newCreatorPending',
|
||||||
|
newCreatorId: buildApiPeerId(newCreatorId, 'user'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action instanceof GramJs.MessageActionChangeCreator) {
|
||||||
|
const { newCreatorId } = action;
|
||||||
|
return {
|
||||||
|
mediaType: 'action',
|
||||||
|
type: 'changeCreator',
|
||||||
|
newCreatorId: buildApiPeerId(newCreatorId, 'user'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return UNSUPPORTED_ACTION;
|
return UNSUPPORTED_ACTION;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,6 +117,11 @@ export function wrapError<T extends Error>(error: T): WrappedError<T> {
|
|||||||
key: 'ErrorPasswordFresh',
|
key: 'ErrorPasswordFresh',
|
||||||
variables: { time: formatWait(error.seconds) },
|
variables: { time: formatWait(error.seconds) },
|
||||||
};
|
};
|
||||||
|
} else if (error instanceof errors.SessionFreshError) {
|
||||||
|
messageKey = {
|
||||||
|
key: 'ErrorSessionFresh',
|
||||||
|
variables: { time: formatWait(error.seconds) },
|
||||||
|
};
|
||||||
} else if (error instanceof errors.RPCError) {
|
} else if (error instanceof errors.RPCError) {
|
||||||
messageKey = {
|
messageKey = {
|
||||||
key: ERROR_KEYS[error.errorMessage],
|
key: ERROR_KEYS[error.errorMessage],
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { RPCError } from '../../../lib/gramjs/errors';
|
import { PasswordFreshError, RPCError, SessionFreshError } from '../../../lib/gramjs/errors';
|
||||||
|
|
||||||
import type { ChatListType, ThreadReadState } from '../../../types';
|
import type { ChatListType, ThreadReadState } from '../../../types';
|
||||||
import {
|
import {
|
||||||
@ -82,13 +82,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
addPhotoToLocalDb,
|
addPhotoToLocalDb,
|
||||||
} from '../helpers/localDb';
|
} from '../helpers/localDb';
|
||||||
import { isChatFolder } from '../helpers/misc';
|
import { checkErrorType, isChatFolder, wrapError } from '../helpers/misc';
|
||||||
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
|
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
|
||||||
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
|
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
|
||||||
import {
|
import {
|
||||||
applyState, updateChannelState,
|
applyState, updateChannelState,
|
||||||
} from '../updates/updateManager';
|
} from '../updates/updateManager';
|
||||||
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
|
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
|
||||||
|
import { getPassword } from './twoFaSettings';
|
||||||
|
|
||||||
type FullChatData = {
|
type FullChatData = {
|
||||||
fullInfo: ApiChatFullInfo;
|
fullInfo: ApiChatFullInfo;
|
||||||
@ -960,18 +961,88 @@ export function deleteChat({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function leaveChannel({
|
export function leaveChannel({ chat }: { chat: ApiChat }) {
|
||||||
channelId, accessHash,
|
|
||||||
}: {
|
|
||||||
channelId: string; accessHash: string;
|
|
||||||
}) {
|
|
||||||
return invokeRequest(new GramJs.channels.LeaveChannel({
|
return invokeRequest(new GramJs.channels.LeaveChannel({
|
||||||
channel: buildInputChannel(channelId, accessHash),
|
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||||
}), {
|
}), {
|
||||||
shouldReturnTrue: true,
|
shouldReturnTrue: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchFutureCreatorAfterLeave({ chat }: { chat: ApiChat }) {
|
||||||
|
const result = await invokeRequest(new GramJs.channels.GetFutureCreatorAfterLeave({
|
||||||
|
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiUser(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyTransferOwnership({
|
||||||
|
chat, user,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
user: ApiUser;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await invokeRequest(new GramJs.channels.EditCreator({
|
||||||
|
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||||
|
userId: buildInputUser(user.id, user.accessHash),
|
||||||
|
password: new GramJs.InputCheckPasswordEmpty(),
|
||||||
|
}), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
shouldThrow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { canTransfer: true };
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!checkErrorType(err)) return undefined;
|
||||||
|
|
||||||
|
if (err instanceof RPCError && err.errorMessage === 'PASSWORD_HASH_INVALID') return { canTransfer: true };
|
||||||
|
|
||||||
|
if (err instanceof PasswordFreshError) return { errorMessage: 'PASSWORD_TOO_FRESH' };
|
||||||
|
if (err instanceof SessionFreshError) return { errorMessage: 'SESSION_TOO_FRESH' };
|
||||||
|
if (err instanceof RPCError && err.errorMessage === 'PASSWORD_MISSING') return { errorMessage: 'PASSWORD_MISSING' };
|
||||||
|
|
||||||
|
return wrapError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editChannelCreator({
|
||||||
|
chat, user, password,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
user: ApiUser;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const passwordCheck = await getPassword(password);
|
||||||
|
|
||||||
|
if (!passwordCheck) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('error' in passwordCheck) {
|
||||||
|
return passwordCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.channels.EditCreator({
|
||||||
|
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||||
|
userId: buildInputUser(user.id, user.accessHash),
|
||||||
|
password: passwordCheck,
|
||||||
|
}), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
shouldThrow: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!checkErrorType(err)) return undefined;
|
||||||
|
return wrapError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteChannel({
|
export function deleteChannel({
|
||||||
channelId, accessHash,
|
channelId, accessHash,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -346,6 +346,16 @@ export interface ApiMessageActionStarGiftPurchaseOfferDeclined extends ActionMed
|
|||||||
price: ApiTypeCurrencyAmount;
|
price: ApiTypeCurrencyAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiMessageActionNewCreatorPending extends ActionMediaType {
|
||||||
|
type: 'newCreatorPending';
|
||||||
|
newCreatorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiMessageActionChangeCreator extends ActionMediaType {
|
||||||
|
type: 'changeCreator';
|
||||||
|
newCreatorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiMessageActionUnsupported extends ActionMediaType {
|
export interface ApiMessageActionUnsupported extends ActionMediaType {
|
||||||
type: 'unsupported';
|
type: 'unsupported';
|
||||||
}
|
}
|
||||||
@ -366,4 +376,5 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
|
|||||||
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
||||||
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
||||||
| ApiMessageActionTodoAppendTasks | ApiMessageActionStarGiftPurchaseOffer
|
| ApiMessageActionTodoAppendTasks | ApiMessageActionStarGiftPurchaseOffer
|
||||||
| ApiMessageActionStarGiftPurchaseOfferDeclined;
|
| ApiMessageActionStarGiftPurchaseOfferDeclined | ApiMessageActionNewCreatorPending
|
||||||
|
| ApiMessageActionChangeCreator;
|
||||||
|
|||||||
@ -707,6 +707,7 @@
|
|||||||
"ErrorPhoneBanned" = "This phone number is banned";
|
"ErrorPhoneBanned" = "This phone number is banned";
|
||||||
"ErrorFloodTime" = "Too many attempts, please try again in {time}";
|
"ErrorFloodTime" = "Too many attempts, please try again in {time}";
|
||||||
"ErrorPasswordFresh" = "The password was modified recently, try again in {time}";
|
"ErrorPasswordFresh" = "The password was modified recently, try again in {time}";
|
||||||
|
"ErrorSessionFresh" = "For security reasons, please wait {time} or use an older session";
|
||||||
"ErrorUnexpected" = "Unexpected error";
|
"ErrorUnexpected" = "Unexpected error";
|
||||||
"ErrorUnexpectedMessage" = "Unexpected error: {error}";
|
"ErrorUnexpectedMessage" = "Unexpected error: {error}";
|
||||||
"ErrorEmailUnconfirmed" = "Email not confirmed";
|
"ErrorEmailUnconfirmed" = "Email not confirmed";
|
||||||
@ -966,6 +967,12 @@
|
|||||||
"ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin.";
|
"ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin.";
|
||||||
"EditAdminRank" = "Custom title";
|
"EditAdminRank" = "Custom title";
|
||||||
"EditAdminRemoveAdmin" = "Dismiss Admin";
|
"EditAdminRemoveAdmin" = "Dismiss Admin";
|
||||||
|
"EditAdminTransferChannelOwnership" = "Transfer Channel Ownership";
|
||||||
|
"EditAdminTransferGroupOwnership" = "Transfer Group Ownership";
|
||||||
|
"EditAdminTransferOwnershipText" = "This will transfer **full owner rights** for **{chat}** to **{user}**. The new owner will be free to remove any of your admin privileges or even ban you.";
|
||||||
|
"EditAdminTransferChangeOwner" = "Change Owner";
|
||||||
|
"EditAdminTransferChannelOwnershipSuccess" = "{user} is now the owner of the channel.";
|
||||||
|
"EditAdminTransferGroupOwnershipSuccess" = "{user} is now the owner of the group.";
|
||||||
"ChannelAdminDismiss" = "Dismiss Admin";
|
"ChannelAdminDismiss" = "Dismiss Admin";
|
||||||
"ChannelPermissionsHeader" = "What can members of this group do?";
|
"ChannelPermissionsHeader" = "What can members of this group do?";
|
||||||
"UserRestrictionsSend" = "Send Messages";
|
"UserRestrictionsSend" = "Send Messages";
|
||||||
@ -2388,6 +2395,8 @@
|
|||||||
"StarGiftAuctionBidRefundedTransaction" = "Refunded Auction Bid";
|
"StarGiftAuctionBidRefundedTransaction" = "Refunded Auction Bid";
|
||||||
"ActionStarGiftPrepaidUpgraded" = "{user} turned the gift into a unique collectible";
|
"ActionStarGiftPrepaidUpgraded" = "{user} turned the gift into a unique collectible";
|
||||||
"ActionStarGiftPrepaidUpgradedYou" = "You turned the gift into a unique collectible";
|
"ActionStarGiftPrepaidUpgradedYou" = "You turned the gift into a unique collectible";
|
||||||
|
"ActionNewCreatorPending" = "{user} will become the new owner in 7 days if {from} does not return.";
|
||||||
|
"ActionChangeCreator" = "{from} has transferred ownership of the group to {user}.";
|
||||||
"UserNoteTitle" = "Notes";
|
"UserNoteTitle" = "Notes";
|
||||||
"UserNoteHint" = "only visible to you";
|
"UserNoteHint" = "only visible to you";
|
||||||
"EditUserNoteHint" = "Notes are only visible to you.";
|
"EditUserNoteHint" = "Notes are only visible to you.";
|
||||||
@ -2609,6 +2618,21 @@
|
|||||||
"SettingsDataClearMediaCache" = "Clear Media Cache";
|
"SettingsDataClearMediaCache" = "Clear Media Cache";
|
||||||
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
|
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
|
||||||
"SettingsDataClearMediaDone" = "Media cache cleared";
|
"SettingsDataClearMediaDone" = "Media cache cleared";
|
||||||
|
"LeaveGroupTitle" = "Leave {group}?";
|
||||||
|
"LeaveGroupDescription" = "If you leave, **{nextOwner}** will become the new owner of **{group}** in 1 week.";
|
||||||
|
"LeaveGroupAppointOwner" = "Appoint Another Owner";
|
||||||
|
"LeaveGroupAdmins" = "GROUP ADMINS";
|
||||||
|
"LeaveGroupMembers" = "GROUP MEMBERS";
|
||||||
|
"LeaveGroupJoinedDate" = "joined {date}";
|
||||||
|
"SecurityCheck" = "Security Check";
|
||||||
|
"SecurityCheckInfo" = "You can only confirm this action if:";
|
||||||
|
"SecurityCheckTwoStepEnabled" = "2-Step verification is active and has been enabled for your account for more than **7 days**.";
|
||||||
|
"SecurityCheckTwoStepNotChanged" = "Your 2-Step Verification password has not been changed the last **7 days**.";
|
||||||
|
"SecurityCheckLoggedIn" = "You have been logged in on this device for more than **24 hours**.";
|
||||||
|
"SecurityCheckEnableTwoStep" = "Enable 2-Step Verification";
|
||||||
|
"EnterPassword" = "Enter Password";
|
||||||
|
"EnterPasswordDescription" = "Please enter your Two-Step Verification password to complete this action.";
|
||||||
|
"Transfer" = "Transfer";
|
||||||
"TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}";
|
"TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}";
|
||||||
"TranslateMenuCocoonLinkText" = "How does it work?";
|
"TranslateMenuCocoonLinkText" = "How does it work?";
|
||||||
"CocoonTitle" = "Cocoon";
|
"CocoonTitle" = "Cocoon";
|
||||||
|
|||||||
@ -107,5 +107,7 @@ export { default as FrozenAccountModal } from '../components/modals/frozenAccoun
|
|||||||
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
|
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
|
||||||
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
|
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
|
||||||
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
|
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
|
||||||
|
export { default as LeaveGroupModal } from '../components/modals/leaveGroup/LeaveGroupModal';
|
||||||
|
export { default as TwoFaCheckModal } from '../components/modals/twoFaCheck/TwoFaCheckModal';
|
||||||
export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal';
|
export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal';
|
||||||
export { default as CocoonModal } from '../components/modals/cocoon/CocoonModal';
|
export { default as CocoonModal } from '../components/modals/cocoon/CocoonModal';
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ type StateProps = {
|
|||||||
contactName?: string;
|
contactName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
const DeleteChatModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
chat,
|
chat,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
@ -61,7 +60,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
|||||||
contactName,
|
contactName,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
leaveChannel,
|
leaveChannel,
|
||||||
deleteHistory,
|
deleteHistory,
|
||||||
|
|||||||
4
src/components/common/PasswordConfirmModal.module.scss
Normal file
4
src/components/common/PasswordConfirmModal.module.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.description {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
102
src/components/common/PasswordConfirmModal.tsx
Normal file
102
src/components/common/PasswordConfirmModal.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { memo, useState } from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { GlobalState } from '../../global/types';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
|
import PasswordForm from './PasswordForm';
|
||||||
|
import PasswordMonkey from './PasswordMonkey';
|
||||||
|
|
||||||
|
import styles from './PasswordConfirmModal.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
description?: string;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
onSubmit: (password: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
error?: GlobalState['twoFaSettings']['errorKey'];
|
||||||
|
hint?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PasswordConfirmModal = ({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
confirmLabel,
|
||||||
|
description,
|
||||||
|
error,
|
||||||
|
hint,
|
||||||
|
isLoading,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const { checkPassword, clearTwoFaError } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [shouldShowPassword, setShouldShowPassword] = useState(false);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const handlePasswordChange = useLastCallback((value: string) => {
|
||||||
|
setPassword(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = useLastCallback(() => {
|
||||||
|
checkPassword({
|
||||||
|
currentPassword: password,
|
||||||
|
onSuccess: () => {
|
||||||
|
onSubmit(password);
|
||||||
|
setPassword('');
|
||||||
|
setShouldShowPassword(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
onClose();
|
||||||
|
clearTwoFaError();
|
||||||
|
setPassword('');
|
||||||
|
setShouldShowPassword(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={title || lang('EnterPassword')}
|
||||||
|
confirmLabel={confirmLabel || lang('AutoDeleteConfirm')}
|
||||||
|
confirmHandler={handleSubmit}
|
||||||
|
confirmIsDestructive
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
|
||||||
|
{description && <p className={styles.description}>{description}</p>}
|
||||||
|
<PasswordForm
|
||||||
|
error={error && lang.withRegular(error)}
|
||||||
|
hint={hint}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isPasswordVisible={shouldShowPassword}
|
||||||
|
onChangePasswordVisibility={setShouldShowPassword}
|
||||||
|
onClearError={clearTwoFaError}
|
||||||
|
onInputChange={handlePasswordChange}
|
||||||
|
/>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
const { errorKey, hint, isLoading } = global.twoFaSettings;
|
||||||
|
return {
|
||||||
|
error: errorKey,
|
||||||
|
hint,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(PasswordConfirmModal));
|
||||||
14
src/components/common/TransferBetweenPeers.module.scss
Normal file
14
src/components/common/TransferBetweenPeers.module.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
30
src/components/common/TransferBetweenPeers.tsx
Normal file
30
src/components/common/TransferBetweenPeers.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiPeer } from '../../api/types';
|
||||||
|
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
|
|
||||||
|
import Avatar from './Avatar';
|
||||||
|
import Icon from './icons/Icon';
|
||||||
|
|
||||||
|
import styles from './TransferBetweenPeers.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
fromPeer?: ApiPeer;
|
||||||
|
toPeer?: ApiPeer;
|
||||||
|
avatarSize?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_AVATAR_SIZE = 4 * REM;
|
||||||
|
|
||||||
|
const TransferBetweenPeers = ({ fromPeer, toPeer, avatarSize = DEFAULT_AVATAR_SIZE }: OwnProps) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Avatar peer={fromPeer} size={avatarSize} />
|
||||||
|
<Icon name="next" className={styles.arrow} />
|
||||||
|
<Avatar peer={toPeer} size={avatarSize} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TransferBetweenPeers);
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type React from '../../../lib/teact/teact';
|
import type { TeactNode } from '../../../lib/teact/teact';
|
||||||
import {
|
import {
|
||||||
memo, useCallback, useEffect, useMemo, useRef,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
@ -58,10 +58,25 @@ type MultipleModeProps<CategoryType extends string> = {
|
|||||||
onSelectedIdsChange?: (Ids: string[]) => void;
|
onSelectedIdsChange?: (Ids: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PeerPickerSection = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemIdsProps = {
|
||||||
|
itemIds: string[];
|
||||||
|
sections?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SectionsProps = {
|
||||||
|
sections: PeerPickerSection[];
|
||||||
|
itemIds?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
type OwnProps<CategoryType extends string> = {
|
type OwnProps<CategoryType extends string> = {
|
||||||
className?: string;
|
className?: string;
|
||||||
categories?: UniqueCustomPeer<CategoryType>[];
|
categories?: UniqueCustomPeer<CategoryType>[];
|
||||||
itemIds: string[];
|
|
||||||
lockedUnselectedSubtitle?: string;
|
lockedUnselectedSubtitle?: string;
|
||||||
filterValue?: string;
|
filterValue?: string;
|
||||||
filterPlaceholder?: string;
|
filterPlaceholder?: string;
|
||||||
@ -81,7 +96,7 @@ type OwnProps<CategoryType extends string> = {
|
|||||||
onFilterChange?: (value: string) => void;
|
onFilterChange?: (value: string) => void;
|
||||||
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
||||||
onLoadMore?: () => void;
|
onLoadMore?: () => void;
|
||||||
} & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
|
} & (ItemIdsProps | SectionsProps) & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
|
||||||
|
|
||||||
const MAX_FULL_ITEMS = 10;
|
const MAX_FULL_ITEMS = 10;
|
||||||
const ALWAYS_FULL_ITEMS_COUNT = 5;
|
const ALWAYS_FULL_ITEMS_COUNT = 5;
|
||||||
@ -91,7 +106,8 @@ const ITEM_CLASS_NAME = 'PeerPickerItem';
|
|||||||
const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||||
className,
|
className,
|
||||||
categories,
|
categories,
|
||||||
itemIds,
|
itemIds: itemIdsProp,
|
||||||
|
sections,
|
||||||
categoryPlaceholderKey,
|
categoryPlaceholderKey,
|
||||||
filterValue,
|
filterValue,
|
||||||
filterPlaceholder,
|
filterPlaceholder,
|
||||||
@ -117,6 +133,11 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
|||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const itemIds = useMemo(() => {
|
||||||
|
if (itemIdsProp) return itemIdsProp;
|
||||||
|
return sections.flatMap((section) => section.ids);
|
||||||
|
}, [itemIdsProp, sections]);
|
||||||
|
|
||||||
const allowMultiple = optionalProps.allowMultiple;
|
const allowMultiple = optionalProps.allowMultiple;
|
||||||
const lockedSelectedIds = allowMultiple ? optionalProps.lockedSelectedIds : undefined;
|
const lockedSelectedIds = allowMultiple ? optionalProps.lockedSelectedIds : undefined;
|
||||||
const lockedUnselectedIds = allowMultiple ? optionalProps.lockedUnselectedIds : undefined;
|
const lockedUnselectedIds = allowMultiple ? optionalProps.lockedUnselectedIds : undefined;
|
||||||
@ -344,6 +365,28 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
|||||||
);
|
);
|
||||||
}, [categories, categoryPlaceholderKey, oldLang, renderItem]);
|
}, [categories, categoryPlaceholderKey, oldLang, renderItem]);
|
||||||
|
|
||||||
|
const renderItems = useCallback(() => {
|
||||||
|
if (!sections) {
|
||||||
|
return viewportIds?.map((id) => renderItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: TeactNode[] = [];
|
||||||
|
sections.forEach((section) => {
|
||||||
|
if (section.ids.length === 0) return;
|
||||||
|
result.push(
|
||||||
|
<div key={section.key} className={styles.sectionHeader}>{section.title}</div>,
|
||||||
|
);
|
||||||
|
section.ids.forEach((id) => {
|
||||||
|
result.push(renderItem(id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [sections, viewportIds, renderItem]);
|
||||||
|
|
||||||
|
const hasContent = sections
|
||||||
|
? sections.some((s) => s.ids.length > 0)
|
||||||
|
: Boolean(viewportIds?.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName(styles.container, className)}>
|
<div className={buildClassName(styles.container, className)}>
|
||||||
{isSearchable && (
|
{isSearchable && (
|
||||||
@ -389,7 +432,7 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewportIds?.length ? (
|
{hasContent ? (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
|
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
|
||||||
items={viewportIds}
|
items={viewportIds}
|
||||||
@ -398,7 +441,7 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
|||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
noScrollRestore={noScrollRestore}
|
noScrollRestore={noScrollRestore}
|
||||||
>
|
>
|
||||||
{viewportIds.map((id) => renderItem(id))}
|
{renderItems()}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
) : !isLoading && viewportIds && !viewportIds.length ? (
|
) : !isLoading && viewportIds && !viewportIds.length ? (
|
||||||
<p className={styles.noResults}>{notFoundText || 'Sorry, nothing found.'}</p>
|
<p className={styles.noResults}>{notFoundText || 'Sorry, nothing found.'}</p>
|
||||||
|
|||||||
@ -45,6 +45,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.peerChip {
|
.peerChip {
|
||||||
max-width: calc(50% - 0.5rem);
|
max-width: calc(50% - 0.5rem);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|||||||
@ -1045,6 +1045,22 @@ const ActionMessageText = ({
|
|||||||
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
|
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
|
||||||
return lang(getCallMessageKey(action, isOutgoing));
|
return lang(getCallMessageKey(action, isOutgoing));
|
||||||
|
|
||||||
|
case 'newCreatorPending': {
|
||||||
|
const { newCreatorId } = action;
|
||||||
|
const newCreator = selectPeer(global, newCreatorId);
|
||||||
|
const newCreatorTitle = (newCreator && getPeerTitle(lang, newCreator)) || userFallbackText;
|
||||||
|
const newCreatorLink = renderPeerLink(newCreator?.id, newCreatorTitle, asPreview);
|
||||||
|
return lang('ActionNewCreatorPending', { user: newCreatorLink, from: senderLink }, { withNodes: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'changeCreator': {
|
||||||
|
const { newCreatorId } = action;
|
||||||
|
const newCreator = selectPeer(global, newCreatorId);
|
||||||
|
const newCreatorTitle = (newCreator && getPeerTitle(lang, newCreator)) || userFallbackText;
|
||||||
|
const newCreatorLink = renderPeerLink(newCreator?.id, newCreatorTitle, asPreview);
|
||||||
|
return lang('ActionChangeCreator', { user: newCreatorLink, from: senderLink }, { withNodes: true });
|
||||||
|
}
|
||||||
|
|
||||||
case 'starGiftPurchaseOffer': {
|
case 'starGiftPurchaseOffer': {
|
||||||
const { gift, price } = action;
|
const { gift, price } = action;
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import GiftInfoValueModal from './gift/value/GiftInfoValueModal.async';
|
|||||||
import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async';
|
import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async';
|
||||||
import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
||||||
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
|
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
|
||||||
|
import LeaveGroupModal from './leaveGroup/LeaveGroupModal.async';
|
||||||
import LocationAccessModal from './locationAccess/LocationAccessModal.async';
|
import LocationAccessModal from './locationAccess/LocationAccessModal.async';
|
||||||
import MapModal from './map/MapModal.async';
|
import MapModal from './map/MapModal.async';
|
||||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||||
@ -66,6 +67,7 @@ import StealthModeModal from './storyStealthMode/StealthModeModal.async';
|
|||||||
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
|
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
|
||||||
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
|
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
|
||||||
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
|
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
|
||||||
|
import TwoFaCheckModal from './twoFaCheck/TwoFaCheckModal.async';
|
||||||
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
|
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
|
||||||
import WebAppModal from './webApp/WebAppModal.async';
|
import WebAppModal from './webApp/WebAppModal.async';
|
||||||
|
|
||||||
@ -131,6 +133,8 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'storyStealthModal' |
|
'storyStealthModal' |
|
||||||
'isPasskeyModalOpen' |
|
'isPasskeyModalOpen' |
|
||||||
'birthdaySetupModal' |
|
'birthdaySetupModal' |
|
||||||
|
'leaveGroupModal' |
|
||||||
|
'isTwoFaCheckModalOpen' |
|
||||||
'isQuickChatPickerOpen' |
|
'isQuickChatPickerOpen' |
|
||||||
'isCocoonModalOpen'
|
'isCocoonModalOpen'
|
||||||
>;
|
>;
|
||||||
@ -208,6 +212,8 @@ const MODALS: ModalRegistry = {
|
|||||||
storyStealthModal: StealthModeModal,
|
storyStealthModal: StealthModeModal,
|
||||||
isPasskeyModalOpen: PasskeyModal,
|
isPasskeyModalOpen: PasskeyModal,
|
||||||
birthdaySetupModal: BirthdaySetupModal,
|
birthdaySetupModal: BirthdaySetupModal,
|
||||||
|
leaveGroupModal: LeaveGroupModal,
|
||||||
|
isTwoFaCheckModalOpen: TwoFaCheckModal,
|
||||||
isQuickChatPickerOpen: QuickChatPickerModal,
|
isQuickChatPickerOpen: QuickChatPickerModal,
|
||||||
isCocoonModalOpen: CocoonModal,
|
isCocoonModalOpen: CocoonModal,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,18 +6,14 @@ import type { TabState } from '../../../../global/types';
|
|||||||
|
|
||||||
import { getPeerTitle } from '../../../../global/helpers/peers';
|
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||||
import { selectPeer } from '../../../../global/selectors';
|
import { selectPeer } from '../../../../global/selectors';
|
||||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
|
||||||
|
|
||||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Avatar from '../../../common/Avatar';
|
import TransferBetweenPeers from '../../../common/TransferBetweenPeers';
|
||||||
import Icon from '../../../common/icons/Icon';
|
|
||||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||||
|
|
||||||
import styles from './GiftAuctionChangeRecipientModal.module.scss';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
modal: TabState['giftAuctionChangeRecipientModal'];
|
modal: TabState['giftAuctionChangeRecipientModal'];
|
||||||
};
|
};
|
||||||
@ -27,8 +23,6 @@ type StateProps = {
|
|||||||
newPeer?: ApiPeer;
|
newPeer?: ApiPeer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVATAR_SIZE = 4 * REM;
|
|
||||||
|
|
||||||
const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps & StateProps) => {
|
const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps & StateProps) => {
|
||||||
const { closeGiftAuctionChangeRecipientModal, openGiftAuctionBidModal } = getActions();
|
const { closeGiftAuctionChangeRecipientModal, openGiftAuctionBidModal } = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -60,11 +54,7 @@ const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps &
|
|||||||
confirmLabel={lang('Continue')}
|
confirmLabel={lang('Continue')}
|
||||||
confirmHandler={handleConfirm}
|
confirmHandler={handleConfirm}
|
||||||
>
|
>
|
||||||
<div className={styles.preview}>
|
<TransferBetweenPeers fromPeer={renderingOldPeer} toPeer={renderingNewPeer} />
|
||||||
<Avatar peer={renderingOldPeer} size={AVATAR_SIZE} />
|
|
||||||
<Icon name="next" className={styles.arrow} />
|
|
||||||
<Avatar peer={renderingNewPeer} size={AVATAR_SIZE} />
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
{lang('GiftAuctionChangeRecipientDescription', {
|
{lang('GiftAuctionChangeRecipientDescription', {
|
||||||
oldPeer: getPeerTitle(lang, renderingOldPeer),
|
oldPeer: getPeerTitle(lang, renderingOldPeer),
|
||||||
|
|||||||
14
src/components/modals/leaveGroup/LeaveGroupModal.async.tsx
Normal file
14
src/components/modals/leaveGroup/LeaveGroupModal.async.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { OwnProps } from './LeaveGroupModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const LeaveGroupModalAsync = (props: OwnProps) => {
|
||||||
|
const { modal } = props;
|
||||||
|
const LeaveGroupModal = useModuleLoader(Bundles.Extra, 'LeaveGroupModal', !modal);
|
||||||
|
|
||||||
|
return LeaveGroupModal ? <LeaveGroupModal key={modal?.chatId} {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeaveGroupModalAsync;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
.dialog {
|
||||||
|
max-width: 22rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordDescription {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
317
src/components/modals/leaveGroup/LeaveGroupModal.tsx
Normal file
317
src/components/modals/leaveGroup/LeaveGroupModal.tsx
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import {
|
||||||
|
memo, useCallback, useEffect, useMemo, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiChatFullInfo, ApiPeer } from '../../../api/types';
|
||||||
|
import type { GlobalState, TabState } from '../../../global/types';
|
||||||
|
|
||||||
|
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||||
|
import { selectChat, selectChatFullInfo, selectPeer } from '../../../global/selectors';
|
||||||
|
|
||||||
|
import useSelector from '../../../hooks/data/useSelector';
|
||||||
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import usePeerSearch, { prepareChatMemberSearch } from '../../../hooks/usePeerSearch';
|
||||||
|
|
||||||
|
import PasswordConfirmModal from '../../common/PasswordConfirmModal';
|
||||||
|
import PeerPicker, { type PeerPickerSection } from '../../common/pickers/PeerPicker';
|
||||||
|
import PickerModal from '../../common/pickers/PickerModal';
|
||||||
|
import TransferBetweenPeers from '../../common/TransferBetweenPeers';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
|
||||||
|
import styles from './LeaveGroupModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: TabState['leaveGroupModal'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
currentUser?: ApiPeer;
|
||||||
|
currentUserId?: string;
|
||||||
|
nextOwner?: ApiPeer;
|
||||||
|
chatFullInfo?: ApiChatFullInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LeaveGroupModal = ({
|
||||||
|
modal,
|
||||||
|
chat,
|
||||||
|
currentUser,
|
||||||
|
currentUserId,
|
||||||
|
nextOwner,
|
||||||
|
chatFullInfo,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
closeLeaveGroupModal, leaveChannel, loadMoreMembers, loadFullChat,
|
||||||
|
transferChannelOwnership, verifyTransferOwnership, openTwoFaCheckModal,
|
||||||
|
} = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [isPickerOpen, openPicker, closePicker] = useFlag();
|
||||||
|
const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag();
|
||||||
|
const [newOwnerId, setNewOwnerId] = useState<string | undefined>(modal?.nextOwnerId);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const isOpen = Boolean(modal);
|
||||||
|
const renderingChat = useCurrentOrPrev(chat);
|
||||||
|
const renderingCurrentUser = useCurrentOrPrev(currentUser);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chat && !chatFullInfo) {
|
||||||
|
loadFullChat({ chatId: chat.id });
|
||||||
|
}
|
||||||
|
}, [chat, chatFullInfo]);
|
||||||
|
|
||||||
|
const newOwner = (!newOwnerId || newOwnerId === modal?.nextOwnerId) ? nextOwner : undefined;
|
||||||
|
|
||||||
|
const renderingNewOwner = useCurrentOrPrev(newOwner);
|
||||||
|
|
||||||
|
const selectNewOwnerPeer = useCallback((global: GlobalState) => {
|
||||||
|
return newOwnerId ? selectPeer(global, newOwnerId) : undefined;
|
||||||
|
}, [newOwnerId]);
|
||||||
|
const selectedNewOwnerPeer = useSelector(selectNewOwnerPeer);
|
||||||
|
const newOwnerPeer = selectedNewOwnerPeer || renderingNewOwner;
|
||||||
|
|
||||||
|
const { adminIds, memberIds, allIds } = useMemo(() => {
|
||||||
|
if (!currentUserId) {
|
||||||
|
return { adminIds: [], memberIds: [], allIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminMembersById = chatFullInfo?.adminMembersById;
|
||||||
|
const allMembers = chatFullInfo?.members;
|
||||||
|
|
||||||
|
const adminUserIds = adminMembersById
|
||||||
|
? Object.values(adminMembersById)
|
||||||
|
.filter((member) => member.userId !== currentUserId && !member.isOwner)
|
||||||
|
.map((member) => member.userId)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const memberUserIds = (allMembers || [])
|
||||||
|
.filter((member) => {
|
||||||
|
return member.userId !== currentUserId
|
||||||
|
&& !member.isOwner
|
||||||
|
&& !member.isAdmin;
|
||||||
|
})
|
||||||
|
.map((member) => member.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
adminIds: adminUserIds,
|
||||||
|
memberIds: memberUserIds,
|
||||||
|
allIds: [...adminUserIds, ...memberUserIds],
|
||||||
|
};
|
||||||
|
}, [currentUserId, chatFullInfo]);
|
||||||
|
|
||||||
|
const hasAdmins = adminIds.length > 0;
|
||||||
|
|
||||||
|
const isOwnerChanged = Boolean(newOwnerId && newOwnerId !== modal?.nextOwnerId);
|
||||||
|
|
||||||
|
const memberSearchFn = useMemo(() => {
|
||||||
|
return chat ? prepareChatMemberSearch(chat) : undefined;
|
||||||
|
}, [chat]);
|
||||||
|
|
||||||
|
const { result: searchResults, isLoading: isSearchLoading } = usePeerSearch({
|
||||||
|
query: search,
|
||||||
|
queryFn: memberSearchFn,
|
||||||
|
defaultValue: allIds,
|
||||||
|
isDisabled: !chat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sections, filteredIds } = useMemo(() => {
|
||||||
|
if (search) {
|
||||||
|
const filtered = (searchResults || []).filter((id) => id !== currentUserId && allIds.includes(id));
|
||||||
|
return { sections: undefined, filteredIds: filtered };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAdmins) {
|
||||||
|
return { sections: undefined, filteredIds: allIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMembers = memberIds.length > 0;
|
||||||
|
if (!hasMembers) {
|
||||||
|
return {
|
||||||
|
sections: undefined,
|
||||||
|
filteredIds: adminIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sections: [
|
||||||
|
{ key: 'admins', title: lang('LeaveGroupAdmins'), ids: adminIds },
|
||||||
|
{ key: 'members', title: lang('LeaveGroupMembers'), ids: memberIds },
|
||||||
|
] as PeerPickerSection[],
|
||||||
|
filteredIds: allIds,
|
||||||
|
};
|
||||||
|
}, [search, searchResults, currentUserId, allIds, hasAdmins, adminIds, memberIds, lang]);
|
||||||
|
|
||||||
|
const pickerTitle = lang('LeaveGroupAppointOwner');
|
||||||
|
|
||||||
|
const handleLeave = useLastCallback(() => {
|
||||||
|
const chatId = modal?.chatId;
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
if (isOwnerChanged && newOwnerId) {
|
||||||
|
verifyTransferOwnership({
|
||||||
|
chatId,
|
||||||
|
userId: newOwnerId,
|
||||||
|
onSuccess: openPasswordModal,
|
||||||
|
onPasswordMissing: openTwoFaCheckModal,
|
||||||
|
onPasswordTooFresh: openTwoFaCheckModal,
|
||||||
|
onSessionTooFresh: openTwoFaCheckModal,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openPasswordModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLeaveAndTransfer = useLastCallback((password: string) => {
|
||||||
|
const chatId = modal?.chatId;
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
if (isOwnerChanged && newOwnerId) {
|
||||||
|
transferChannelOwnership({
|
||||||
|
chatId,
|
||||||
|
userId: newOwnerId,
|
||||||
|
password,
|
||||||
|
onSuccess: () => leaveChannel({ chatId, shouldSkipOwnershipCheck: true }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
leaveChannel({ chatId, shouldSkipOwnershipCheck: true });
|
||||||
|
}
|
||||||
|
closeLeaveGroupModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAppointOwner = useLastCallback(() => {
|
||||||
|
openPicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelectNewOwner = useLastCallback((userId: string) => {
|
||||||
|
setNewOwnerId(userId);
|
||||||
|
closePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClosePicker = useLastCallback(() => {
|
||||||
|
closePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLoadMore = useLastCallback(() => {
|
||||||
|
if (modal?.chatId) {
|
||||||
|
loadMoreMembers({ chatId: modal.chatId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!renderingChat || !renderingCurrentUser) return undefined;
|
||||||
|
|
||||||
|
const chatTitle = getPeerTitle(lang, renderingChat);
|
||||||
|
const newOwnerName = newOwnerPeer ? getPeerTitle(lang, newOwnerPeer) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen && !isPickerOpen && !isPasswordModalOpen}
|
||||||
|
dialogClassName={styles.dialog}
|
||||||
|
onClose={closeLeaveGroupModal}
|
||||||
|
>
|
||||||
|
{newOwnerPeer && (
|
||||||
|
<TransferBetweenPeers fromPeer={renderingCurrentUser} toPeer={newOwnerPeer} />
|
||||||
|
)}
|
||||||
|
<h3>{lang('LeaveGroupTitle', { group: chatTitle })}</h3>
|
||||||
|
<p>
|
||||||
|
{lang('LeaveGroupDescription', {
|
||||||
|
nextOwner: newOwnerName,
|
||||||
|
group: chatTitle,
|
||||||
|
}, {
|
||||||
|
withNodes: true,
|
||||||
|
withMarkdown: true,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<div className="dialog-buttons-column">
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
className="confirm-dialog-button"
|
||||||
|
isText
|
||||||
|
onClick={handleLeave}
|
||||||
|
>
|
||||||
|
{lang('GroupLeaveGroup')}
|
||||||
|
</Button>
|
||||||
|
{allIds.length > 0 && (
|
||||||
|
<Button
|
||||||
|
className="confirm-dialog-button"
|
||||||
|
isText
|
||||||
|
onClick={handleAppointOwner}
|
||||||
|
>
|
||||||
|
{lang('LeaveGroupAppointOwner')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={() => closeLeaveGroupModal()}>
|
||||||
|
{lang('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<PickerModal
|
||||||
|
isOpen={isPickerOpen}
|
||||||
|
title={pickerTitle}
|
||||||
|
hasCloseButton
|
||||||
|
shouldAdaptToSearch
|
||||||
|
withFixedHeight
|
||||||
|
onClose={handleClosePicker}
|
||||||
|
>
|
||||||
|
{sections ? (
|
||||||
|
<PeerPicker
|
||||||
|
sections={sections}
|
||||||
|
filterValue={search}
|
||||||
|
filterPlaceholder={lang('Search')}
|
||||||
|
isSearchable
|
||||||
|
isLoading={isSearchLoading}
|
||||||
|
withStatus
|
||||||
|
withDefaultPadding
|
||||||
|
onFilterChange={setSearch}
|
||||||
|
onSelectedIdChange={handleSelectNewOwner}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PeerPicker
|
||||||
|
itemIds={filteredIds}
|
||||||
|
filterValue={search}
|
||||||
|
filterPlaceholder={lang('Search')}
|
||||||
|
isSearchable
|
||||||
|
isLoading={isSearchLoading}
|
||||||
|
withStatus
|
||||||
|
withDefaultPadding
|
||||||
|
onFilterChange={setSearch}
|
||||||
|
onSelectedIdChange={handleSelectNewOwner}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PickerModal>
|
||||||
|
<PasswordConfirmModal
|
||||||
|
isOpen={isPasswordModalOpen}
|
||||||
|
title={lang('EnterPassword')}
|
||||||
|
confirmLabel={lang('Transfer')}
|
||||||
|
description={lang('EnterPasswordDescription')}
|
||||||
|
onClose={closePasswordModal}
|
||||||
|
onSubmit={handleLeaveAndTransfer}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { modal }): Complete<StateProps> => {
|
||||||
|
const chat = modal?.chatId ? selectChat(global, modal.chatId) : undefined;
|
||||||
|
const currentUser = global.currentUserId ? selectPeer(global, global.currentUserId) : undefined;
|
||||||
|
const nextOwner = modal?.nextOwnerId ? selectPeer(global, modal.nextOwnerId) : undefined;
|
||||||
|
const fullInfo = modal?.chatId ? selectChatFullInfo(global, modal.chatId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
currentUser,
|
||||||
|
currentUserId: global.currentUserId,
|
||||||
|
nextOwner,
|
||||||
|
chatFullInfo: fullInfo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(LeaveGroupModal));
|
||||||
14
src/components/modals/twoFaCheck/TwoFaCheckModal.async.tsx
Normal file
14
src/components/modals/twoFaCheck/TwoFaCheckModal.async.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { OwnProps } from './TwoFaCheckModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const TwoFaCheckModalAsync = (props: OwnProps) => {
|
||||||
|
const { modal } = props;
|
||||||
|
const TwoFaCheckModal = useModuleLoader(Bundles.Extra, 'TwoFaCheckModal', !modal);
|
||||||
|
|
||||||
|
return TwoFaCheckModal ? <TwoFaCheckModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TwoFaCheckModalAsync;
|
||||||
18
src/components/modals/twoFaCheck/TwoFaCheckModal.module.scss
Normal file
18
src/components/modals/twoFaCheck/TwoFaCheckModal.module.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.dialog :global(.modal-dialog) {
|
||||||
|
max-width: 22rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-block: 1.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/modals/twoFaCheck/TwoFaCheckModal.tsx
Normal file
62
src/components/modals/twoFaCheck/TwoFaCheckModal.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { memo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { TabState } from '../../../global/types';
|
||||||
|
import { SettingsScreens } from '../../../types';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
|
||||||
|
import styles from './TwoFaCheckModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: TabState['isTwoFaCheckModalOpen'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
hasPassword?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TwoFaCheckModal = ({ modal, hasPassword }: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
closeTwoFaCheckModal, openSettingsScreen,
|
||||||
|
} = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const isOpen = Boolean(modal);
|
||||||
|
|
||||||
|
const handleEnableTwoFa = useLastCallback(() => {
|
||||||
|
openSettingsScreen({ screen: SettingsScreens.TwoFaDisabled });
|
||||||
|
closeTwoFaCheckModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={lang('SecurityCheck')}
|
||||||
|
className={styles.dialog}
|
||||||
|
confirmLabel={hasPassword ? lang('OK') : lang('SecurityCheckEnableTwoStep')}
|
||||||
|
confirmHandler={hasPassword ? closeTwoFaCheckModal : handleEnableTwoFa}
|
||||||
|
isOnlyConfirm={hasPassword}
|
||||||
|
areButtonsInColumn={!hasPassword}
|
||||||
|
onClose={closeTwoFaCheckModal}
|
||||||
|
>
|
||||||
|
<p>{lang('SecurityCheckInfo')}</p>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
<li>{lang('SecurityCheckTwoStepEnabled', undefined, { withNodes: true, withMarkdown: true })}</li>
|
||||||
|
<li>{lang('SecurityCheckTwoStepNotChanged', undefined, { withNodes: true, withMarkdown: true })}</li>
|
||||||
|
<li>{lang('SecurityCheckLoggedIn', undefined, { withNodes: true, withMarkdown: true })}</li>
|
||||||
|
</ul>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
hasPassword: global.settings.byKey.hasPassword,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(TwoFaCheckModal));
|
||||||
@ -8,13 +8,15 @@ import type {
|
|||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import { ManagementScreens } from '../../../types';
|
import { ManagementScreens } from '../../../types';
|
||||||
|
|
||||||
import { getUserFullName, isChatBasicGroup, isChatChannel } from '../../../global/helpers';
|
import { getUserFullName, isChatBasicGroup, isChatChannel, isUserBot } from '../../../global/helpers';
|
||||||
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import PasswordConfirmModal from '../../common/PasswordConfirmModal';
|
||||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
@ -58,12 +60,17 @@ const ManageGroupAdminRights = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
}: OwnProps & StateProps) => {
|
}: OwnProps & StateProps) => {
|
||||||
const { updateChatAdmin } = getActions();
|
const {
|
||||||
|
updateChatAdmin, transferChannelOwnership, showNotification,
|
||||||
|
openTwoFaCheckModal, verifyTransferOwnership,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
|
const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
|
||||||
const [isTouched, setIsTouched] = useState(Boolean(isNewAdmin));
|
const [isTouched, setIsTouched] = useState(Boolean(isNewAdmin));
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag();
|
const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag();
|
||||||
|
const [isTransferDialogOpen, openTransferDialog, closeTransferDialog] = useFlag();
|
||||||
|
const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag();
|
||||||
const [customTitle, setCustomTitle] = useState('');
|
const [customTitle, setCustomTitle] = useState('');
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -196,6 +203,52 @@ const ManageGroupAdminRights = ({
|
|||||||
setIsTouched(true);
|
setIsTouched(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleStartTransfer = useLastCallback(() => {
|
||||||
|
if (!selectedUserId) return;
|
||||||
|
|
||||||
|
verifyTransferOwnership({
|
||||||
|
chatId: chat.id,
|
||||||
|
userId: selectedUserId,
|
||||||
|
onSuccess: openTransferDialog,
|
||||||
|
onPasswordMissing: openTwoFaCheckModal,
|
||||||
|
onPasswordTooFresh: openTwoFaCheckModal,
|
||||||
|
onSessionTooFresh: openTwoFaCheckModal,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleConfirmTransfer = useLastCallback(() => {
|
||||||
|
closeTransferDialog();
|
||||||
|
openPasswordModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTransferOwnership = useLastCallback((password: string) => {
|
||||||
|
if (!selectedUserId) return;
|
||||||
|
|
||||||
|
const user = usersById[selectedUserId];
|
||||||
|
const userName = user ? getUserFullName(user) : '';
|
||||||
|
|
||||||
|
transferChannelOwnership({
|
||||||
|
chatId: chat.id,
|
||||||
|
userId: selectedUserId,
|
||||||
|
password,
|
||||||
|
onSuccess: () => {
|
||||||
|
showNotification({
|
||||||
|
message: lang(
|
||||||
|
isChannel ? 'EditAdminTransferChannelOwnershipSuccess' : 'EditAdminTransferGroupOwnershipSuccess',
|
||||||
|
{ user: userName },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
closePasswordModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedUser = selectedUserId ? usersById[selectedUserId] : undefined;
|
||||||
|
const canTransferOwnership = Boolean(
|
||||||
|
chat.isCreator && selectedUser && !isUserBot(selectedUser) && selectedUserId !== currentUserId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedChatMember) {
|
if (!selectedChatMember) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -395,6 +448,11 @@ const ManageGroupAdminRights = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canTransferOwnership && currentUserId !== selectedUserId && !isFormFullyDisabled && !isNewAdmin && (
|
||||||
|
<ListItem icon="key" ripple onClick={handleStartTransfer}>
|
||||||
|
{lang(isChannel ? 'EditAdminTransferChannelOwnership' : 'EditAdminTransferGroupOwnership')}
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
{currentUserId !== selectedUserId && !isFormFullyDisabled && !isNewAdmin && (
|
{currentUserId !== selectedUserId && !isFormFullyDisabled && !isNewAdmin && (
|
||||||
<ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
|
<ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
|
||||||
{lang('EditAdminRemoveAdmin')}
|
{lang('EditAdminRemoveAdmin')}
|
||||||
@ -422,6 +480,24 @@ const ManageGroupAdminRights = ({
|
|||||||
confirmIsDestructive
|
confirmIsDestructive
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isTransferDialogOpen}
|
||||||
|
onClose={closeTransferDialog}
|
||||||
|
title={lang(isChannel ? 'EditAdminTransferChannelOwnership' : 'EditAdminTransferGroupOwnership')}
|
||||||
|
textParts={lang('EditAdminTransferOwnershipText', {
|
||||||
|
chat: chat.title,
|
||||||
|
user: selectedUserId ? getUserFullName(usersById[selectedUserId]) : '',
|
||||||
|
}, { withNodes: true, withMarkdown: true })}
|
||||||
|
confirmLabel={lang('EditAdminTransferChangeOwner')}
|
||||||
|
confirmHandler={handleConfirmTransfer}
|
||||||
|
/>
|
||||||
|
<PasswordConfirmModal
|
||||||
|
isOpen={isPasswordModalOpen}
|
||||||
|
title={lang(isChannel ? 'EditAdminTransferChannelOwnership' : 'EditAdminTransferGroupOwnership')}
|
||||||
|
confirmLabel={lang('EditAdminTransferChangeOwner')}
|
||||||
|
onClose={closePasswordModal}
|
||||||
|
onSubmit={handleTransferOwnership}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -889,12 +889,28 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
|
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
|
||||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
const { chatId, shouldSkipOwnershipCheck, tabId = getCurrentTabId() } = payload;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shouldSkipOwnershipCheck && chat.isCreator && chat.accessHash) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
global = leaveChat(global, chatId);
|
global = leaveChat(global, chatId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
@ -902,15 +918,76 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise<void>
|
|||||||
actions.openChat({ id: undefined, tabId });
|
actions.openChat({ id: undefined, tabId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id: channelId, accessHash } = chat;
|
await callApi('leaveChannel', { chat });
|
||||||
if (channelId && accessHash) {
|
global = getGlobal();
|
||||||
await callApi('leaveChannel', { channelId, accessHash });
|
const chatMessages = selectChatMessages(global, chatId);
|
||||||
global = getGlobal();
|
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||||
const chatMessages = selectChatMessages(global, chatId);
|
global = deleteChatMessages(global, chatId, localMessageIds);
|
||||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
setGlobal(global);
|
||||||
global = deleteChatMessages(global, chatId, localMessageIds);
|
});
|
||||||
setGlobal(global);
|
|
||||||
|
addActionHandler('verifyTransferOwnership', async (global, actions, payload): Promise<void> => {
|
||||||
|
const {
|
||||||
|
chatId, userId, onSuccess, onPasswordMissing, onPasswordTooFresh, onSessionTooFresh,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
if (!chat || !user) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await callApi('verifyTransferOwnership', {
|
||||||
|
chat,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('canTransfer' in result) {
|
||||||
|
onSuccess?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.errorMessage) {
|
||||||
|
case 'PASSWORD_MISSING':
|
||||||
|
onPasswordMissing?.();
|
||||||
|
break;
|
||||||
|
case 'PASSWORD_TOO_FRESH':
|
||||||
|
onPasswordTooFresh?.();
|
||||||
|
break;
|
||||||
|
case 'SESSION_TOO_FRESH':
|
||||||
|
onSessionTooFresh?.();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('transferChannelOwnership', async (global, actions, payload): Promise<void> => {
|
||||||
|
const {
|
||||||
|
chatId, userId, password, onSuccess,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
if (!chat?.accessHash || !user?.accessHash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await callApi('editChannelCreator', {
|
||||||
|
chat,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => {
|
addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => {
|
||||||
|
|||||||
@ -952,6 +952,29 @@ addCallback((global: GlobalState) => {
|
|||||||
prevBlurredTabsCount = blurredTabsCount;
|
prevBlurredTabsCount = blurredTabsCount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openLeaveGroupModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { chatId, nextOwnerId, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
leaveGroupModal: {
|
||||||
|
chatId,
|
||||||
|
nextOwnerId,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addTabStateResetterAction('closeLeaveGroupModal', 'leaveGroupModal');
|
||||||
|
|
||||||
|
addActionHandler('openTwoFaCheckModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
isTwoFaCheckModalOpen: true,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addTabStateResetterAction('closeTwoFaCheckModal', 'isTwoFaCheckModalOpen');
|
||||||
|
|
||||||
addActionHandler('openQuickChatPicker', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openQuickChatPicker', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
|||||||
@ -401,7 +401,10 @@ export interface ActionPayloads {
|
|||||||
joinChannel: {
|
joinChannel: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
leaveChannel: { chatId: string } & WithTabId;
|
leaveChannel: {
|
||||||
|
chatId: string;
|
||||||
|
shouldSkipOwnershipCheck?: boolean;
|
||||||
|
} & WithTabId;
|
||||||
deleteChannel: { chatId: string } & WithTabId;
|
deleteChannel: { chatId: string } & WithTabId;
|
||||||
toggleChatPinned: {
|
toggleChatPinned: {
|
||||||
id: string;
|
id: string;
|
||||||
@ -2556,6 +2559,29 @@ export interface ActionPayloads {
|
|||||||
openGiftRecipientPicker: WithTabId | undefined;
|
openGiftRecipientPicker: WithTabId | undefined;
|
||||||
closeGiftRecipientPicker: WithTabId | undefined;
|
closeGiftRecipientPicker: WithTabId | undefined;
|
||||||
|
|
||||||
|
openLeaveGroupModal: {
|
||||||
|
chatId: string;
|
||||||
|
nextOwnerId?: string;
|
||||||
|
} & WithTabId;
|
||||||
|
closeLeaveGroupModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
openTwoFaCheckModal: WithTabId | undefined;
|
||||||
|
closeTwoFaCheckModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
verifyTransferOwnership: {
|
||||||
|
chatId: string;
|
||||||
|
userId: string;
|
||||||
|
onSuccess?: VoidFunction;
|
||||||
|
onPasswordMissing?: VoidFunction;
|
||||||
|
onPasswordTooFresh?: VoidFunction;
|
||||||
|
onSessionTooFresh?: VoidFunction;
|
||||||
|
};
|
||||||
|
transferChannelOwnership: {
|
||||||
|
chatId: string;
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
onSuccess?: VoidFunction;
|
||||||
|
} & WithTabId;
|
||||||
openQuickChatPicker: WithTabId | undefined;
|
openQuickChatPicker: WithTabId | undefined;
|
||||||
closeQuickChatPicker: WithTabId | undefined;
|
closeQuickChatPicker: WithTabId | undefined;
|
||||||
|
|
||||||
|
|||||||
@ -971,6 +971,13 @@ export type TabState = {
|
|||||||
|
|
||||||
isPasskeyModalOpen?: boolean;
|
isPasskeyModalOpen?: boolean;
|
||||||
|
|
||||||
|
leaveGroupModal?: {
|
||||||
|
chatId: string;
|
||||||
|
nextOwnerId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
isTwoFaCheckModalOpen?: true;
|
||||||
|
|
||||||
isWaitingForStarGiftUpgrade?: true;
|
isWaitingForStarGiftUpgrade?: true;
|
||||||
isWaitingForStarGiftTransfer?: true;
|
isWaitingForStarGiftTransfer?: true;
|
||||||
insertingPeerIdMention?: string;
|
insertingPeerIdMention?: string;
|
||||||
|
|||||||
@ -148,6 +148,18 @@ export class PasswordFreshError extends BadRequestError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SessionFreshError extends BadRequestError {
|
||||||
|
public seconds: number;
|
||||||
|
|
||||||
|
constructor(args: any) {
|
||||||
|
const seconds = Number(args.capture || 0);
|
||||||
|
super(`Session is fresh, please try again in ${seconds} seconds.`, args.request);
|
||||||
|
|
||||||
|
this.message = `Session is fresh, please try again in ${seconds} seconds.`;
|
||||||
|
this.seconds = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PasskeyLoginRequestedError extends Error {
|
export class PasskeyLoginRequestedError extends Error {
|
||||||
public credentialJson: PublicKeyCredentialJSON;
|
public credentialJson: PublicKeyCredentialJSON;
|
||||||
|
|
||||||
@ -185,6 +197,7 @@ export const rpcErrorRe = new Map<RegExp, any>([
|
|||||||
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
|
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
|
||||||
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
||||||
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
|
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
|
||||||
|
[/SESSION_TOO_FRESH_(\d+)/, SessionFreshError],
|
||||||
[/^Timeout$/, TimedOutError],
|
[/^Timeout$/, TimedOutError],
|
||||||
[/PASSKEY_CREDENTIAL_NOT_FOUND/, PasskeyCredentialNotFoundError],
|
[/PASSKEY_CREDENTIAL_NOT_FOUND/, PasskeyCredentialNotFoundError],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -1836,6 +1836,7 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
|
|||||||
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
||||||
|
channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates;
|
||||||
channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers;
|
channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers;
|
||||||
channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;
|
channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;
|
||||||
channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;
|
channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;
|
||||||
@ -1852,6 +1853,7 @@ channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:fla
|
|||||||
channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates;
|
channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood;
|
channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood;
|
||||||
channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool;
|
channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool;
|
||||||
|
channels.getFutureCreatorAfterLeave#a00918af channel:InputChannel = User;
|
||||||
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
|
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
|
||||||
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
||||||
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
||||||
|
|||||||
@ -295,6 +295,7 @@
|
|||||||
"channels.togglePreHistoryHidden",
|
"channels.togglePreHistoryHidden",
|
||||||
"channels.getGroupsForDiscussion",
|
"channels.getGroupsForDiscussion",
|
||||||
"channels.setDiscussionGroup",
|
"channels.setDiscussionGroup",
|
||||||
|
"channels.editCreator",
|
||||||
"channels.getSendAs",
|
"channels.getSendAs",
|
||||||
"channels.toggleJoinToSend",
|
"channels.toggleJoinToSend",
|
||||||
"channels.toggleJoinRequest",
|
"channels.toggleJoinRequest",
|
||||||
@ -396,6 +397,7 @@
|
|||||||
"channels.toggleForum",
|
"channels.toggleForum",
|
||||||
"channels.toggleParticipantsHidden",
|
"channels.toggleParticipantsHidden",
|
||||||
"channels.toggleViewForumAsMessages",
|
"channels.toggleViewForumAsMessages",
|
||||||
|
"channels.getFutureCreatorAfterLeave",
|
||||||
"photos.uploadContactProfilePhoto",
|
"photos.uploadContactProfilePhoto",
|
||||||
"messages.getMessagesViews",
|
"messages.getMessagesViews",
|
||||||
"chatlists.exportChatlistInvite",
|
"chatlists.exportChatlistInvite",
|
||||||
|
|||||||
46
src/types/language.d.ts
vendored
46
src/types/language.d.ts
vendored
@ -844,6 +844,9 @@ export interface LangPair {
|
|||||||
'ChannelEditAdminCannotEdit': undefined;
|
'ChannelEditAdminCannotEdit': undefined;
|
||||||
'EditAdminRank': undefined;
|
'EditAdminRank': undefined;
|
||||||
'EditAdminRemoveAdmin': undefined;
|
'EditAdminRemoveAdmin': undefined;
|
||||||
|
'EditAdminTransferChannelOwnership': undefined;
|
||||||
|
'EditAdminTransferGroupOwnership': undefined;
|
||||||
|
'EditAdminTransferChangeOwner': undefined;
|
||||||
'ChannelAdminDismiss': undefined;
|
'ChannelAdminDismiss': undefined;
|
||||||
'ChannelPermissionsHeader': undefined;
|
'ChannelPermissionsHeader': undefined;
|
||||||
'UserRestrictionsSend': undefined;
|
'UserRestrictionsSend': undefined;
|
||||||
@ -1917,6 +1920,18 @@ export interface LangPair {
|
|||||||
'SettingsDataClearMediaCache': undefined;
|
'SettingsDataClearMediaCache': undefined;
|
||||||
'SettingsDataClearMediaCacheDescription': undefined;
|
'SettingsDataClearMediaCacheDescription': undefined;
|
||||||
'SettingsDataClearMediaDone': undefined;
|
'SettingsDataClearMediaDone': undefined;
|
||||||
|
'LeaveGroupAppointOwner': undefined;
|
||||||
|
'LeaveGroupAdmins': undefined;
|
||||||
|
'LeaveGroupMembers': undefined;
|
||||||
|
'SecurityCheck': undefined;
|
||||||
|
'SecurityCheckInfo': undefined;
|
||||||
|
'SecurityCheckTwoStepEnabled': undefined;
|
||||||
|
'SecurityCheckTwoStepNotChanged': undefined;
|
||||||
|
'SecurityCheckLoggedIn': undefined;
|
||||||
|
'SecurityCheckEnableTwoStep': undefined;
|
||||||
|
'EnterPassword': undefined;
|
||||||
|
'EnterPasswordDescription': undefined;
|
||||||
|
'Transfer': undefined;
|
||||||
'TranslateMenuCocoonLinkText': undefined;
|
'TranslateMenuCocoonLinkText': undefined;
|
||||||
'CocoonTitle': undefined;
|
'CocoonTitle': undefined;
|
||||||
'CocoonDescription': undefined;
|
'CocoonDescription': undefined;
|
||||||
@ -2101,6 +2116,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'ErrorPasswordFresh': {
|
'ErrorPasswordFresh': {
|
||||||
'time': V;
|
'time': V;
|
||||||
};
|
};
|
||||||
|
'ErrorSessionFresh': {
|
||||||
|
'time': V;
|
||||||
|
};
|
||||||
'ErrorUnexpectedMessage': {
|
'ErrorUnexpectedMessage': {
|
||||||
'error': V;
|
'error': V;
|
||||||
};
|
};
|
||||||
@ -2150,6 +2168,16 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'AreYouSureDeleteThisChatWithGroup': {
|
'AreYouSureDeleteThisChatWithGroup': {
|
||||||
'chat': V;
|
'chat': V;
|
||||||
};
|
};
|
||||||
|
'EditAdminTransferOwnershipText': {
|
||||||
|
'chat': V;
|
||||||
|
'user': V;
|
||||||
|
};
|
||||||
|
'EditAdminTransferChannelOwnershipSuccess': {
|
||||||
|
'user': V;
|
||||||
|
};
|
||||||
|
'EditAdminTransferGroupOwnershipSuccess': {
|
||||||
|
'user': V;
|
||||||
|
};
|
||||||
'LinkExpiresIn': {
|
'LinkExpiresIn': {
|
||||||
'time': V;
|
'time': V;
|
||||||
};
|
};
|
||||||
@ -3274,6 +3302,14 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'ActionStarGiftPrepaidUpgraded': {
|
'ActionStarGiftPrepaidUpgraded': {
|
||||||
'user': V;
|
'user': V;
|
||||||
};
|
};
|
||||||
|
'ActionNewCreatorPending': {
|
||||||
|
'user': V;
|
||||||
|
'from': V;
|
||||||
|
};
|
||||||
|
'ActionChangeCreator': {
|
||||||
|
'from': V;
|
||||||
|
'user': V;
|
||||||
|
};
|
||||||
'FileTransferProgress': {
|
'FileTransferProgress': {
|
||||||
'currentSize': V;
|
'currentSize': V;
|
||||||
'totalSize': V;
|
'totalSize': V;
|
||||||
@ -3375,6 +3411,16 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'status': V;
|
'status': V;
|
||||||
'onlineCount': V;
|
'onlineCount': V;
|
||||||
};
|
};
|
||||||
|
'LeaveGroupTitle': {
|
||||||
|
'group': V;
|
||||||
|
};
|
||||||
|
'LeaveGroupDescription': {
|
||||||
|
'nextOwner': V;
|
||||||
|
'group': V;
|
||||||
|
};
|
||||||
|
'LeaveGroupJoinedDate': {
|
||||||
|
'date': V;
|
||||||
|
};
|
||||||
'TranslateMenuCocoon': {
|
'TranslateMenuCocoon': {
|
||||||
'link': V;
|
'link': V;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -71,6 +71,8 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
|
|||||||
ADMIN_RANK_EMOJI_NOT_ALLOWED: 'An admin rank cannot contain emojis',
|
ADMIN_RANK_EMOJI_NOT_ALLOWED: 'An admin rank cannot contain emojis',
|
||||||
ADMIN_RANK_INVALID: 'The specified admin rank is invalid',
|
ADMIN_RANK_INVALID: 'The specified admin rank is invalid',
|
||||||
FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet',
|
FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet',
|
||||||
|
SESSION_TOO_FRESH: 'Session is fresh, please try again later',
|
||||||
|
SESSION_IS_FRESH: 'Session is fresh, please try again later',
|
||||||
INPUT_USER_DEACTIVATED: 'Can\'t do this action to a deleted account',
|
INPUT_USER_DEACTIVATED: 'Can\'t do this action to a deleted account',
|
||||||
BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired',
|
BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired',
|
||||||
PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired',
|
PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user