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 */ });
|
||||
```
|
||||
* 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
|
||||
|
||||
|
||||
@ -556,6 +556,22 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
|
||||
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;
|
||||
}
|
||||
|
||||
@ -117,6 +117,11 @@ export function wrapError<T extends Error>(error: T): WrappedError<T> {
|
||||
key: 'ErrorPasswordFresh',
|
||||
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) {
|
||||
messageKey = {
|
||||
key: ERROR_KEYS[error.errorMessage],
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@ -82,13 +82,14 @@ import {
|
||||
import {
|
||||
addPhotoToLocalDb,
|
||||
} from '../helpers/localDb';
|
||||
import { isChatFolder } from '../helpers/misc';
|
||||
import { checkErrorType, isChatFolder, wrapError } from '../helpers/misc';
|
||||
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
|
||||
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
|
||||
import {
|
||||
applyState, updateChannelState,
|
||||
} from '../updates/updateManager';
|
||||
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
|
||||
import { getPassword } from './twoFaSettings';
|
||||
|
||||
type FullChatData = {
|
||||
fullInfo: ApiChatFullInfo;
|
||||
@ -960,18 +961,88 @@ export function deleteChat({
|
||||
});
|
||||
}
|
||||
|
||||
export function leaveChannel({
|
||||
channelId, accessHash,
|
||||
}: {
|
||||
channelId: string; accessHash: string;
|
||||
}) {
|
||||
export function leaveChannel({ chat }: { chat: ApiChat }) {
|
||||
return invokeRequest(new GramJs.channels.LeaveChannel({
|
||||
channel: buildInputChannel(channelId, accessHash),
|
||||
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||
}), {
|
||||
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({
|
||||
channelId, accessHash,
|
||||
}: {
|
||||
|
||||
@ -346,6 +346,16 @@ export interface ApiMessageActionStarGiftPurchaseOfferDeclined extends ActionMed
|
||||
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 {
|
||||
type: 'unsupported';
|
||||
}
|
||||
@ -366,4 +376,5 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
|
||||
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
||||
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
||||
| ApiMessageActionTodoAppendTasks | ApiMessageActionStarGiftPurchaseOffer
|
||||
| ApiMessageActionStarGiftPurchaseOfferDeclined;
|
||||
| ApiMessageActionStarGiftPurchaseOfferDeclined | ApiMessageActionNewCreatorPending
|
||||
| ApiMessageActionChangeCreator;
|
||||
|
||||
@ -707,6 +707,7 @@
|
||||
"ErrorPhoneBanned" = "This phone number is banned";
|
||||
"ErrorFloodTime" = "Too many attempts, please 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";
|
||||
"ErrorUnexpectedMessage" = "Unexpected error: {error}";
|
||||
"ErrorEmailUnconfirmed" = "Email not confirmed";
|
||||
@ -966,6 +967,12 @@
|
||||
"ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin.";
|
||||
"EditAdminRank" = "Custom title";
|
||||
"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";
|
||||
"ChannelPermissionsHeader" = "What can members of this group do?";
|
||||
"UserRestrictionsSend" = "Send Messages";
|
||||
@ -2388,6 +2395,8 @@
|
||||
"StarGiftAuctionBidRefundedTransaction" = "Refunded Auction Bid";
|
||||
"ActionStarGiftPrepaidUpgraded" = "{user} 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";
|
||||
"UserNoteHint" = "only visible to you";
|
||||
"EditUserNoteHint" = "Notes are only visible to you.";
|
||||
@ -2609,6 +2618,21 @@
|
||||
"SettingsDataClearMediaCache" = "Clear Media Cache";
|
||||
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
|
||||
"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}";
|
||||
"TranslateMenuCocoonLinkText" = "How does it work?";
|
||||
"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 QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
|
||||
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 CocoonModal } from '../components/modals/cocoon/CocoonModal';
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
@ -46,7 +45,7 @@ type StateProps = {
|
||||
contactName?: string;
|
||||
};
|
||||
|
||||
const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
||||
const DeleteChatModal = ({
|
||||
isOpen,
|
||||
chat,
|
||||
isSavedDialog,
|
||||
@ -61,7 +60,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
||||
contactName,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
leaveChannel,
|
||||
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 {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
@ -58,10 +58,25 @@ type MultipleModeProps<CategoryType extends string> = {
|
||||
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> = {
|
||||
className?: string;
|
||||
categories?: UniqueCustomPeer<CategoryType>[];
|
||||
itemIds: string[];
|
||||
lockedUnselectedSubtitle?: string;
|
||||
filterValue?: string;
|
||||
filterPlaceholder?: string;
|
||||
@ -81,7 +96,7 @@ type OwnProps<CategoryType extends string> = {
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
} & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
|
||||
} & (ItemIdsProps | SectionsProps) & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
|
||||
|
||||
const MAX_FULL_ITEMS = 10;
|
||||
const ALWAYS_FULL_ITEMS_COUNT = 5;
|
||||
@ -91,7 +106,8 @@ const ITEM_CLASS_NAME = 'PeerPickerItem';
|
||||
const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||
className,
|
||||
categories,
|
||||
itemIds,
|
||||
itemIds: itemIdsProp,
|
||||
sections,
|
||||
categoryPlaceholderKey,
|
||||
filterValue,
|
||||
filterPlaceholder,
|
||||
@ -117,6 +133,11 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const itemIds = useMemo(() => {
|
||||
if (itemIdsProp) return itemIdsProp;
|
||||
return sections.flatMap((section) => section.ids);
|
||||
}, [itemIdsProp, sections]);
|
||||
|
||||
const allowMultiple = optionalProps.allowMultiple;
|
||||
const lockedSelectedIds = allowMultiple ? optionalProps.lockedSelectedIds : undefined;
|
||||
const lockedUnselectedIds = allowMultiple ? optionalProps.lockedUnselectedIds : undefined;
|
||||
@ -344,6 +365,28 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||
);
|
||||
}, [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 (
|
||||
<div className={buildClassName(styles.container, className)}>
|
||||
{isSearchable && (
|
||||
@ -389,7 +432,7 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewportIds?.length ? (
|
||||
{hasContent ? (
|
||||
<InfiniteScroll
|
||||
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
|
||||
items={viewportIds}
|
||||
@ -398,7 +441,7 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportIds.map((id) => renderItem(id))}
|
||||
{renderItems()}
|
||||
</InfiniteScroll>
|
||||
) : !isLoading && viewportIds && !viewportIds.length ? (
|
||||
<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 {
|
||||
max-width: calc(50% - 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
|
||||
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': {
|
||||
const { gift, price } = action;
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ import GiftInfoValueModal from './gift/value/GiftInfoValueModal.async';
|
||||
import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async';
|
||||
import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
||||
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
|
||||
import LeaveGroupModal from './leaveGroup/LeaveGroupModal.async';
|
||||
import LocationAccessModal from './locationAccess/LocationAccessModal.async';
|
||||
import MapModal from './map/MapModal.async';
|
||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||
@ -66,6 +67,7 @@ import StealthModeModal from './storyStealthMode/StealthModeModal.async';
|
||||
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
|
||||
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
|
||||
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
|
||||
import TwoFaCheckModal from './twoFaCheck/TwoFaCheckModal.async';
|
||||
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
|
||||
import WebAppModal from './webApp/WebAppModal.async';
|
||||
|
||||
@ -131,6 +133,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'storyStealthModal' |
|
||||
'isPasskeyModalOpen' |
|
||||
'birthdaySetupModal' |
|
||||
'leaveGroupModal' |
|
||||
'isTwoFaCheckModalOpen' |
|
||||
'isQuickChatPickerOpen' |
|
||||
'isCocoonModalOpen'
|
||||
>;
|
||||
@ -208,6 +212,8 @@ const MODALS: ModalRegistry = {
|
||||
storyStealthModal: StealthModeModal,
|
||||
isPasskeyModalOpen: PasskeyModal,
|
||||
birthdaySetupModal: BirthdaySetupModal,
|
||||
leaveGroupModal: LeaveGroupModal,
|
||||
isTwoFaCheckModalOpen: TwoFaCheckModal,
|
||||
isQuickChatPickerOpen: QuickChatPickerModal,
|
||||
isCocoonModalOpen: CocoonModal,
|
||||
};
|
||||
|
||||
@ -6,18 +6,14 @@ import type { TabState } from '../../../../global/types';
|
||||
|
||||
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import Avatar from '../../../common/Avatar';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import TransferBetweenPeers from '../../../common/TransferBetweenPeers';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
|
||||
import styles from './GiftAuctionChangeRecipientModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['giftAuctionChangeRecipientModal'];
|
||||
};
|
||||
@ -27,8 +23,6 @@ type StateProps = {
|
||||
newPeer?: ApiPeer;
|
||||
};
|
||||
|
||||
const AVATAR_SIZE = 4 * REM;
|
||||
|
||||
const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps & StateProps) => {
|
||||
const { closeGiftAuctionChangeRecipientModal, openGiftAuctionBidModal } = getActions();
|
||||
const lang = useLang();
|
||||
@ -60,11 +54,7 @@ const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps &
|
||||
confirmLabel={lang('Continue')}
|
||||
confirmHandler={handleConfirm}
|
||||
>
|
||||
<div className={styles.preview}>
|
||||
<Avatar peer={renderingOldPeer} size={AVATAR_SIZE} />
|
||||
<Icon name="next" className={styles.arrow} />
|
||||
<Avatar peer={renderingNewPeer} size={AVATAR_SIZE} />
|
||||
</div>
|
||||
<TransferBetweenPeers fromPeer={renderingOldPeer} toPeer={renderingNewPeer} />
|
||||
<p>
|
||||
{lang('GiftAuctionChangeRecipientDescription', {
|
||||
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';
|
||||
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 useFlag from '../../../hooks/useFlag';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import PasswordConfirmModal from '../../common/PasswordConfirmModal';
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
@ -58,12 +60,17 @@ const ManageGroupAdminRights = ({
|
||||
onClose,
|
||||
onScreenSelect,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { updateChatAdmin } = getActions();
|
||||
const {
|
||||
updateChatAdmin, transferChannelOwnership, showNotification,
|
||||
openTwoFaCheckModal, verifyTransferOwnership,
|
||||
} = getActions();
|
||||
|
||||
const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
|
||||
const [isTouched, setIsTouched] = useState(Boolean(isNewAdmin));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag();
|
||||
const [isTransferDialogOpen, openTransferDialog, closeTransferDialog] = useFlag();
|
||||
const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag();
|
||||
const [customTitle, setCustomTitle] = useState('');
|
||||
const lang = useLang();
|
||||
|
||||
@ -196,6 +203,52 @@ const ManageGroupAdminRights = ({
|
||||
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) {
|
||||
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 && (
|
||||
<ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
|
||||
{lang('EditAdminRemoveAdmin')}
|
||||
@ -422,6 +480,24 @@ const ManageGroupAdminRights = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -889,12 +889,28 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
|
||||
});
|
||||
|
||||
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
const { chatId, shouldSkipOwnershipCheck, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
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);
|
||||
setGlobal(global);
|
||||
|
||||
@ -902,15 +918,76 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise<void>
|
||||
actions.openChat({ id: undefined, tabId });
|
||||
}
|
||||
|
||||
const { id: channelId, accessHash } = chat;
|
||||
if (channelId && accessHash) {
|
||||
await callApi('leaveChannel', { channelId, accessHash });
|
||||
global = getGlobal();
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
global = deleteChatMessages(global, chatId, localMessageIds);
|
||||
setGlobal(global);
|
||||
await callApi('leaveChannel', { chat });
|
||||
global = getGlobal();
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
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 => {
|
||||
|
||||
@ -952,6 +952,29 @@ addCallback((global: GlobalState) => {
|
||||
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 => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
|
||||
@ -401,7 +401,10 @@ export interface ActionPayloads {
|
||||
joinChannel: {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
leaveChannel: { chatId: string } & WithTabId;
|
||||
leaveChannel: {
|
||||
chatId: string;
|
||||
shouldSkipOwnershipCheck?: boolean;
|
||||
} & WithTabId;
|
||||
deleteChannel: { chatId: string } & WithTabId;
|
||||
toggleChatPinned: {
|
||||
id: string;
|
||||
@ -2556,6 +2559,29 @@ export interface ActionPayloads {
|
||||
openGiftRecipientPicker: 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;
|
||||
closeQuickChatPicker: WithTabId | undefined;
|
||||
|
||||
|
||||
@ -971,6 +971,13 @@ export type TabState = {
|
||||
|
||||
isPasskeyModalOpen?: boolean;
|
||||
|
||||
leaveGroupModal?: {
|
||||
chatId: string;
|
||||
nextOwnerId?: string;
|
||||
};
|
||||
|
||||
isTwoFaCheckModalOpen?: true;
|
||||
|
||||
isWaitingForStarGiftUpgrade?: true;
|
||||
isWaitingForStarGiftTransfer?: true;
|
||||
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 {
|
||||
public credentialJson: PublicKeyCredentialJSON;
|
||||
|
||||
@ -185,6 +197,7 @@ export const rpcErrorRe = new Map<RegExp, any>([
|
||||
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
|
||||
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
||||
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
|
||||
[/SESSION_TOO_FRESH_(\d+)/, SessionFreshError],
|
||||
[/^Timeout$/, TimedOutError],
|
||||
[/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.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||
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.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;
|
||||
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.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood;
|
||||
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.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
||||
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
||||
|
||||
@ -295,6 +295,7 @@
|
||||
"channels.togglePreHistoryHidden",
|
||||
"channels.getGroupsForDiscussion",
|
||||
"channels.setDiscussionGroup",
|
||||
"channels.editCreator",
|
||||
"channels.getSendAs",
|
||||
"channels.toggleJoinToSend",
|
||||
"channels.toggleJoinRequest",
|
||||
@ -396,6 +397,7 @@
|
||||
"channels.toggleForum",
|
||||
"channels.toggleParticipantsHidden",
|
||||
"channels.toggleViewForumAsMessages",
|
||||
"channels.getFutureCreatorAfterLeave",
|
||||
"photos.uploadContactProfilePhoto",
|
||||
"messages.getMessagesViews",
|
||||
"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;
|
||||
'EditAdminRank': undefined;
|
||||
'EditAdminRemoveAdmin': undefined;
|
||||
'EditAdminTransferChannelOwnership': undefined;
|
||||
'EditAdminTransferGroupOwnership': undefined;
|
||||
'EditAdminTransferChangeOwner': undefined;
|
||||
'ChannelAdminDismiss': undefined;
|
||||
'ChannelPermissionsHeader': undefined;
|
||||
'UserRestrictionsSend': undefined;
|
||||
@ -1917,6 +1920,18 @@ export interface LangPair {
|
||||
'SettingsDataClearMediaCache': undefined;
|
||||
'SettingsDataClearMediaCacheDescription': 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;
|
||||
'CocoonTitle': undefined;
|
||||
'CocoonDescription': undefined;
|
||||
@ -2101,6 +2116,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'ErrorPasswordFresh': {
|
||||
'time': V;
|
||||
};
|
||||
'ErrorSessionFresh': {
|
||||
'time': V;
|
||||
};
|
||||
'ErrorUnexpectedMessage': {
|
||||
'error': V;
|
||||
};
|
||||
@ -2150,6 +2168,16 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'AreYouSureDeleteThisChatWithGroup': {
|
||||
'chat': V;
|
||||
};
|
||||
'EditAdminTransferOwnershipText': {
|
||||
'chat': V;
|
||||
'user': V;
|
||||
};
|
||||
'EditAdminTransferChannelOwnershipSuccess': {
|
||||
'user': V;
|
||||
};
|
||||
'EditAdminTransferGroupOwnershipSuccess': {
|
||||
'user': V;
|
||||
};
|
||||
'LinkExpiresIn': {
|
||||
'time': V;
|
||||
};
|
||||
@ -3274,6 +3302,14 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'ActionStarGiftPrepaidUpgraded': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionNewCreatorPending': {
|
||||
'user': V;
|
||||
'from': V;
|
||||
};
|
||||
'ActionChangeCreator': {
|
||||
'from': V;
|
||||
'user': V;
|
||||
};
|
||||
'FileTransferProgress': {
|
||||
'currentSize': V;
|
||||
'totalSize': V;
|
||||
@ -3375,6 +3411,16 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'status': V;
|
||||
'onlineCount': V;
|
||||
};
|
||||
'LeaveGroupTitle': {
|
||||
'group': V;
|
||||
};
|
||||
'LeaveGroupDescription': {
|
||||
'nextOwner': V;
|
||||
'group': V;
|
||||
};
|
||||
'LeaveGroupJoinedDate': {
|
||||
'date': V;
|
||||
};
|
||||
'TranslateMenuCocoon': {
|
||||
'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_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',
|
||||
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',
|
||||
BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired',
|
||||
PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user