Settings Privacy: Implement account self destruction (#5939)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-06-04 20:41:29 +02:00
parent f2d14ca78f
commit f1a538ed8e
21 changed files with 380 additions and 21 deletions

View File

@ -99,3 +99,29 @@ export function toggleSponsoredMessages({
shouldReturnTrue: true,
});
}
export function buildApiAccountDays(ttl: GramJs.AccountDaysTTL): { days: number } {
return {
days: ttl.days,
};
}
export function buildApiAccountDaysTTL(days: number): GramJs.AccountDaysTTL {
return new GramJs.AccountDaysTTL({
days,
});
}
export async function fetchAccountTTL() {
const result = await invokeRequest(new GramJs.account.GetAccountTTL());
if (!result) return undefined;
return buildApiAccountDays(result);
}
export function setAccountTTL({ days }: { days: number }) {
return invokeRequest(new GramJs.account.SetAccountTTL({
ttl: buildApiAccountDaysTTL(days),
}), {
shouldReturnTrue: true,
});
}

View File

@ -1552,6 +1552,10 @@
"CloseMiniApps" = "Close Mini Apps";
"DoNotAskAgain" = "Don't ask again";
"PaymentInfoDone" = "Proceed to checkout";
"DeleteMyAccount" = "Delete my account";
"DeleteAccountIfAwayFor" = "If away for";
"SelfDestructTitle" = "Account self-destruction";
"SelfDestructSessionsDescription" = "If you don't come online from a specific session at least once within this period, it will be terminated.";
"EmojiStatusAccessText" = "**{name}** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **{name}**.";
"VideoConversionTitle" = "Improving Video...";
"VideoConversionText" = "The video will be published after it's optimized for the best viewing experience.";

View File

@ -23,6 +23,7 @@ export { default as StatusPickerMenu } from '../components/left/main/StatusPicke
export { default as SuggestedStatusModal } from '../components/modals/suggestedStatus/SuggestedStatusModal';
export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
export { default as DeleteAccountModal } from '../components/modals/deleteAccount/DeleteAccountModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';

View File

@ -1,4 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import { useMemo } from '../../../lib/teact/teact';
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -6,14 +7,17 @@ import type { ApiPrivacySettings } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import { SettingsScreens } from '../../../types';
import { ACCOUNT_TTL_OPTIONS } from '../../../config.ts';
import {
selectCanSetPasscode, selectIsCurrentUserFrozen,
selectIsCurrentUserPremium,
} from '../../../global/selectors';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { getClosestEntry } from '../../../util/getClosestEntry.ts';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback.ts';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
@ -41,8 +45,11 @@ type StateProps = {
canDisplayChatInTitle?: boolean;
isCurrentUserFrozen?: boolean;
privacy: GlobalState['settings']['privacy'];
accountDaysTtl?: number;
};
const DAYS_PER_MONTH = 30;
const SettingsPrivacy: FC<OwnProps & StateProps> = ({
isActive,
isCurrentUserPremium,
@ -61,8 +68,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
privacy,
onReset,
isCurrentUserFrozen,
accountDaysTtl,
}) => {
const {
openDeleteAccountModal,
loadPrivacySettings,
loadBlockedUsers,
loadContentSettings,
@ -72,6 +81,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
loadWebAuthorizations,
setSharedSettingOption,
openSettingsScreen,
loadAccountDaysTtl,
} = getActions();
useEffect(() => {
@ -86,6 +96,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
useEffect(() => {
if (isActive && !isCurrentUserFrozen) {
loadGlobalPrivacySettings();
loadAccountDaysTtl();
}
}, [isActive, isCurrentUserFrozen, loadGlobalPrivacySettings]);
@ -113,6 +124,16 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
updateContentSettings({ isSensitiveEnabled: isChecked });
}, [updateContentSettings]);
const handleOpenDeleteAccountModal = useLastCallback(() => {
if (!accountDaysTtl) return;
openDeleteAccountModal({ days: accountDaysTtl });
});
const dayOption = useMemo(() => {
if (!accountDaysTtl) return undefined;
return getClosestEntry(ACCOUNT_TTL_OPTIONS, accountDaysTtl / DAYS_PER_MONTH).toString();
}, [accountDaysTtl]);
function getVisibilityValue(setting?: ApiPrivacySettings) {
if (!setting) return oldLang('Loading');
@ -402,6 +423,21 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
onCheck={handleChatInTitleChange}
/>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>
{lang('DeleteMyAccount')}
</h4>
<ListItem
narrow
onClick={handleOpenDeleteAccountModal}
>
{lang('DeleteAccountIfAwayFor')}
<span className="settings-item__current-value">
{lang('Months', { count: dayOption }, { pluralValue: 1 })}
</span>
</ListItem>
</div>
</div>
);
};
@ -415,6 +451,7 @@ export default memo(withGlobal<OwnProps>(
shouldNewNonContactPeersRequirePremium, nonContactPeersPaidStars,
},
privacy,
accountDaysTtl,
},
blocked,
passcode: {
@ -443,6 +480,7 @@ export default memo(withGlobal<OwnProps>(
canDisplayChatInTitle,
canSetPasscode: selectCanSetPasscode(global),
isCurrentUserFrozen,
accountDaysTtl,
};
},
)(SettingsPrivacy));

View File

@ -14,6 +14,7 @@ import BoostModal from './boost/BoostModal.async';
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import DeleteAccountModal from './deleteAccount/DeleteAccountModal.async';
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
import FrozenAccountModal from './frozenAccount/FrozenAccountModal.async';
import PremiumGiftModal from './gift/GiftModal.async';
@ -82,7 +83,8 @@ type ModalKey = keyof Pick<TabState,
'giftStatusInfoModal' |
'giftTransferModal' |
'chatRefundModal' |
'isFrozenAccountModalOpen'
'isFrozenAccountModalOpen' |
'deleteAccountModal'
>;
type StateProps = {
@ -135,6 +137,7 @@ const MODALS: ModalRegistry = {
giftTransferModal: GiftTransferModal,
chatRefundModal: ChatRefundModal,
isFrozenAccountModalOpen: FrozenAccountModal,
deleteAccountModal: DeleteAccountModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -0,0 +1,17 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './DeleteAccountModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const DeleteAccountModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const DeleteAccountModal = useModuleLoader(Bundles.Extra, 'DeleteAccountModal', !modal);
return DeleteAccountModal ? <DeleteAccountModal {...props} /> : undefined;
};
export default DeleteAccountModalAsync;

View File

@ -0,0 +1,3 @@
.root :global(.modal-dialog) {
max-width: 23.25rem;
}

View File

@ -0,0 +1,117 @@
import React, { memo, useEffect, useMemo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { TabState } from '../../../global/types';
import { ACCOUNT_TTL_OPTIONS } from '../../../config.ts';
import { getClosestEntry } from '../../../util/getClosestEntry.ts';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import RadioGroup from '../../ui/RadioGroup.tsx';
import styles from './DeleteAccountModal.module.scss';
export type OwnProps = {
modal: TabState['deleteAccountModal'];
};
export type StateProps = {
selfDestructAccountDays?: number;
};
const DAYS_PER_MONTH = 30;
const DeleteAccountModal = ({
modal,
selfDestructAccountDays,
}: OwnProps & StateProps) => {
const {
closeGiftCodeModal, closeDeleteAccountModal, setAccountTTL,
} = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const [selectedOption, setSelectedOption] = useState<string>();
const optionToDays = useLastCallback((value: string): number => {
return Number(value) * DAYS_PER_MONTH;
});
const initialSelectedOption = useMemo(() => {
if (!selfDestructAccountDays) return undefined;
return getClosestEntry(ACCOUNT_TTL_OPTIONS, selfDestructAccountDays / DAYS_PER_MONTH).toString();
}, [selfDestructAccountDays]);
useEffect(() => {
if (initialSelectedOption) {
setSelectedOption(initialSelectedOption);
}
}, [initialSelectedOption]);
const options: { value: string; label: string }[] = useMemo(() => {
return ACCOUNT_TTL_OPTIONS.map((months) => ({
value: String(months),
label: lang('Months', { count: months }, { pluralValue: 1 }),
}));
}, [lang]);
const handleChange = useLastCallback((value: string) => {
setSelectedOption(value);
});
const confirmHandler = useLastCallback(() => {
if (!selectedOption) return;
setAccountTTL({ days: optionToDays(selectedOption) });
});
const onCloseHandler = useLastCallback(() => {
closeDeleteAccountModal();
});
return (
<Modal
isOpen={isOpen}
title={lang('SelfDestructTitle')}
onClose={closeGiftCodeModal}
className={styles.root}
>
<p>{lang('SelfDestructSessionsDescription')}</p>
<RadioGroup
className="dialog-checkbox-group"
name="quick-reaction-settings"
options={options}
selected={selectedOption}
onChange={handleChange}
withIcon
/>
<div
className="dialog-buttons mt-2"
>
<Button
className="confirm-dialog-button"
isText
onClick={confirmHandler}
>
{lang('Save')}
</Button>
<Button color="danger" className="confirm-dialog-button" isText onClick={onCloseHandler}>
{lang('Cancel')}
</Button>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const { selfDestructAccountDays } = modal || {};
return {
selfDestructAccountDays,
};
},
)(DeleteAccountModal));

View File

@ -471,3 +471,5 @@ export const DEFAULT_GIFT_PROFILE_FILTER_OPTIONS: GiftProfileFilterOptions = {
export const DEFAULT_RESALE_GIFTS_FILTER_OPTIONS: ResaleGiftsFilterOptions = {
sortType: 'byDate',
};
export const ACCOUNT_TTL_OPTIONS = [1, 3, 6, 12, 18, 24];

View File

@ -243,3 +243,37 @@ addActionHandler('terminateAllWebAuthorizations', async (global): Promise<void>
};
setGlobal(global);
});
addActionHandler('loadAccountDaysTtl', async (global, actions, payload): Promise<void> => {
const result = await callApi('fetchAccountTTL');
if (!result) return;
global = getGlobal();
global = {
...global,
settings: {
...global.settings,
accountDaysTtl: result.days,
},
};
setGlobal(global);
});
addActionHandler('setAccountTTL', async (global, actions, payload): Promise<void> => {
const { days, tabId = getCurrentTabId() } = payload || {};
if (!days) return;
const result = await callApi('setAccountTTL', { days });
if (!result) return;
global = getGlobal();
global = {
...global,
settings: {
...global.settings,
accountDaysTtl: days,
},
};
setGlobal(global);
actions.closeDeleteAccountModal({ tabId });
});

View File

@ -1,8 +1,9 @@
import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { addActionHandler } from '../..';
import { addActionHandler, setGlobal } from '../..';
import { updateTabState } from '../../reducers/tabs';
import { selectTabState } from '../../selectors';
addActionHandler('openFrozenAccountModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
@ -19,3 +20,24 @@ addActionHandler('closeFrozenAccountModal', (global, actions, payload): ActionRe
isFrozenAccountModalOpen: false,
}, tabId);
});
addActionHandler('openDeleteAccountModal', (global, actions, payload): ActionReturnType => {
const { days, tabId = getCurrentTabId() } = payload || {};
if (!days) return;
global = updateTabState(global, {
...selectTabState(global, tabId),
deleteAccountModal: {
selfDestructAccountDays: days,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeDeleteAccountModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
deleteAccountModal: undefined,
}, tabId);
});

View File

@ -281,6 +281,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.peers = initialState.peers;
}
if (!cached.settings.accountDaysTtl) {
cached.settings.accountDaysTtl = initialState.settings.accountDaysTtl;
}
if (!cached.cacheVersion) {
cached.cacheVersion = initialState.cacheVersion;
// Reset because of the new action message structure
@ -708,7 +712,7 @@ function omitLocalMedia(message: ApiMessage): ApiMessage {
function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] {
const {
byKey, botVerificationShownPeerIds, notifyDefaults, lastPremiumBandwithNotificationDate, themes,
byKey, botVerificationShownPeerIds, notifyDefaults, lastPremiumBandwithNotificationDate, themes, accountDaysTtl,
} = global.settings;
return {
@ -718,6 +722,7 @@ function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings
lastPremiumBandwithNotificationDate,
notifyDefaults,
themes,
accountDaysTtl,
};
}

View File

@ -309,6 +309,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
patternColor: DARK_THEME_PATTERN_COLOR,
},
},
accountDaysTtl: 365,
},
serviceNotifications: [],

View File

@ -1040,6 +1040,14 @@ export interface ActionPayloads {
};
openFrozenAccountModal: WithTabId | undefined;
closeFrozenAccountModal: WithTabId | undefined;
openDeleteAccountModal: {
days: number;
} & WithTabId | undefined;
closeDeleteAccountModal: WithTabId | undefined;
setAccountTTL: {
days: number;
} & WithTabId | undefined;
loadAccountDaysTtl: undefined;
// Chats
loadPeerSettings: {

View File

@ -415,6 +415,7 @@ export type GlobalState = {
paidReactionPrivacy?: ApiPaidReactionPrivacyType;
botVerificationShownPeerIds: string[];
themes: Partial<Record<ThemeKey, IThemeSettings>>;
accountDaysTtl: number;
};
push?: {

View File

@ -741,6 +741,10 @@ export type TabState = {
info: ApiCheckedGiftCode;
};
deleteAccountModal?: {
selfDestructAccountDays: number;
};
paidReactionModal?: {
chatId: string;
messageId: number;

View File

@ -3212,9 +3212,10 @@ namespace Api {
resaleStars?: long;
canTransferAt?: int;
canResellAt?: int;
CONSTRUCTOR_ID: 2900347777;
CONSTRUCTOR_ID: 775611918;
SUBCLASS_OF_ID: 2256589094;
className: 'MessageActionStarGiftUnique';
static fromReader(reader: Reader): MessageActionStarGiftUnique;
}
export class MessageActionPaidMessagesRefunded extends VirtualClass<{
@ -3255,6 +3256,10 @@ namespace Api {
callId: long;
duration?: int;
otherParticipants?: Api.TypePeer[];
CONSTRUCTOR_ID: 805187450;
SUBCLASS_OF_ID: 2256589094;
className: 'MessageActionConferenceCall';
static fromReader(reader: Reader): MessageActionConferenceCall;
}
export class Dialog extends VirtualClass<{
@ -5951,6 +5956,10 @@ namespace Api {
subChainId: int;
blocks: bytes[];
nextOffset: int;
CONSTRUCTOR_ID: 2759272591;
SUBCLASS_OF_ID: 2676568142;
className: 'UpdateGroupCallChainBlocks';
static fromReader(reader: Reader): UpdateGroupCallChainBlocks;
}
export class UpdatesTooLong extends VirtualClass<void> {
@ -9904,6 +9913,10 @@ namespace Api {
slug: string;
}> {
slug: string;
CONSTRUCTOR_ID: 2679894519;
SUBCLASS_OF_ID: 3634081085;
className: 'PhoneCallDiscardReasonMigrateConferenceCall';
static fromReader(reader: Reader): PhoneCallDiscardReasonMigrateConferenceCall;
}
export class DataJSON extends VirtualClass<{
@ -10250,8 +10263,7 @@ namespace Api {
participantId: long;
protocol: Api.TypePhoneCallProtocol;
receiveDate?: int;
conferenceCall?: Api.TypeInputGroupCall;
CONSTRUCTOR_ID: 4006881368;
CONSTRUCTOR_ID: 3307368215;
SUBCLASS_OF_ID: 3296664529;
className: 'PhoneCallWaiting';
@ -10277,8 +10289,7 @@ namespace Api {
participantId: long;
gAHash: bytes;
protocol: Api.TypePhoneCallProtocol;
conferenceCall?: Api.TypeInputGroupCall;
CONSTRUCTOR_ID: 1161174115;
CONSTRUCTOR_ID: 347139340;
SUBCLASS_OF_ID: 3296664529;
className: 'PhoneCallRequested';
@ -10304,8 +10315,7 @@ namespace Api {
participantId: long;
gB: bytes;
protocol: Api.TypePhoneCallProtocol;
conferenceCall?: Api.TypeInputGroupCall;
CONSTRUCTOR_ID: 587035009;
CONSTRUCTOR_ID: 912311057;
SUBCLASS_OF_ID: 3296664529;
className: 'PhoneCallAccepted';
@ -10343,8 +10353,7 @@ namespace Api {
connections: Api.TypePhoneConnection[];
startDate: int;
customParameters?: Api.TypeDataJSON;
conferenceCall?: Api.TypeInputGroupCall;
CONSTRUCTOR_ID: 1000707084;
CONSTRUCTOR_ID: 810769141;
SUBCLASS_OF_ID: 3296664529;
className: 'PhoneCall';
@ -10366,8 +10375,7 @@ namespace Api {
id: long;
reason?: Api.TypePhoneCallDiscardReason;
duration?: int;
conferenceCall?: Api.TypeInputGroupCall;
CONSTRUCTOR_ID: 4191311107;
CONSTRUCTOR_ID: 1355435489;
SUBCLASS_OF_ID: 3296664529;
className: 'PhoneCallDiscarded';
@ -11111,6 +11119,10 @@ namespace Api {
newValue: Bool;
}> {
newValue: Bool;
CONSTRUCTOR_ID: 3306682238;
SUBCLASS_OF_ID: 2998503411;
className: 'ChannelAdminLogEventActionToggleAutotranslation';
static fromReader(reader: Reader): ChannelAdminLogEventActionToggleAutotranslation;
}
export class ChannelAdminLogEvent extends VirtualClass<{
@ -13281,11 +13293,11 @@ namespace Api {
unmutedVideoCount?: int;
unmutedVideoLimit: int;
version: int;
conferenceFromCall?: long;
inviteLink?: string;
CONSTRUCTOR_ID: 3455636451;
CONSTRUCTOR_ID: 1429932961;
SUBCLASS_OF_ID: 548729632;
className: 'GroupCall';
static fromReader(reader: Reader): GroupCall;
}
export class InputGroupCall extends VirtualClass<{
@ -13304,12 +13316,20 @@ namespace Api {
slug: string;
}> {
slug: string;
CONSTRUCTOR_ID: 4261839423;
SUBCLASS_OF_ID: 1482758833;
className: 'InputGroupCallSlug';
static fromReader(reader: Reader): InputGroupCallSlug;
}
export class InputGroupCallInviteMessage extends VirtualClass<{
msgId: int;
}> {
msgId: int;
CONSTRUCTOR_ID: 2349883455;
SUBCLASS_OF_ID: 1482758833;
className: 'InputGroupCallInviteMessage';
static fromReader(reader: Reader): InputGroupCallInviteMessage;
}
export class GroupCallParticipant extends VirtualClass<{
@ -14052,6 +14072,10 @@ namespace Api {
}> {
slug: string;
toId: Api.TypeInputPeer;
CONSTRUCTOR_ID: 1674298252;
SUBCLASS_OF_ID: 1919851518;
className: 'InputInvoiceStarGiftResale';
static fromReader(reader: Reader): InputInvoiceStarGiftResale;
}
export class InputStorePaymentPremiumSubscription extends VirtualClass<{
@ -16601,9 +16625,10 @@ namespace Api {
upgradeStars?: long;
resellMinStars?: long;
title?: string;
CONSTRUCTOR_ID: 46953416;
CONSTRUCTOR_ID: 3324693032;
SUBCLASS_OF_ID: 3273414923;
className: 'StarGift';
static fromReader(reader: Reader): StarGift;
}
export class StarGiftUnique extends VirtualClass<{
@ -16634,9 +16659,10 @@ namespace Api {
availabilityTotal: int;
giftAddress?: string;
resellStars?: long;
CONSTRUCTOR_ID: 1549979985;
CONSTRUCTOR_ID: 1678891913;
SUBCLASS_OF_ID: 3273414923;
className: 'StarGiftUnique';
static fromReader(reader: Reader): StarGiftUnique;
}
export class MessageReportOption extends VirtualClass<{
@ -16838,7 +16864,7 @@ namespace Api {
patternColor: int;
textColor: int;
rarityPermille: int;
CONSTRUCTOR_ID: 2485589858;
CONSTRUCTOR_ID: 3644687772;
SUBCLASS_OF_ID: 2276819400;
className: 'StarGiftAttributeBackdrop';
@ -16900,9 +16926,10 @@ namespace Api {
transferStars?: long;
canTransferAt?: int;
canResellAt?: int;
CONSTRUCTOR_ID: 1616305061;
CONSTRUCTOR_ID: 3755607193;
SUBCLASS_OF_ID: 2385198100;
className: 'SavedStarGift';
static fromReader(reader: Reader): SavedStarGift;
}
export class InputSavedStarGiftUser extends VirtualClass<{
@ -16931,6 +16958,10 @@ namespace Api {
slug: string;
}> {
slug: string;
CONSTRUCTOR_ID: 545636920;
SUBCLASS_OF_ID: 2406848942;
className: 'InputSavedStarGiftSlug';
static fromReader(reader: Reader): InputSavedStarGiftSlug;
}
export class PaidReactionPrivacyDefault extends VirtualClass<void> {
@ -17059,18 +17090,30 @@ namespace Api {
documentId: long;
}> {
documentId: long;
CONSTRUCTOR_ID: 1219145276;
SUBCLASS_OF_ID: 3005295287;
className: 'StarGiftAttributeIdModel';
static fromReader(reader: Reader): StarGiftAttributeIdModel;
}
export class StarGiftAttributeIdPattern extends VirtualClass<{
documentId: long;
}> {
documentId: long;
CONSTRUCTOR_ID: 1242965043;
SUBCLASS_OF_ID: 3005295287;
className: 'StarGiftAttributeIdPattern';
static fromReader(reader: Reader): StarGiftAttributeIdPattern;
}
export class StarGiftAttributeIdBackdrop extends VirtualClass<{
backdropId: int;
}> {
backdropId: int;
CONSTRUCTOR_ID: 520210263;
SUBCLASS_OF_ID: 3005295287;
className: 'StarGiftAttributeIdBackdrop';
static fromReader(reader: Reader): StarGiftAttributeIdBackdrop;
}
export class StarGiftAttributeCounter extends VirtualClass<{
@ -17079,6 +17122,10 @@ namespace Api {
}> {
attribute: Api.TypeStarGiftAttributeId;
count: int;
CONSTRUCTOR_ID: 783398488;
SUBCLASS_OF_ID: 2351477395;
className: 'StarGiftAttributeCounter';
static fromReader(reader: Reader): StarGiftAttributeCounter;
}
export class PendingSuggestion extends VirtualClass<{
@ -17091,6 +17138,10 @@ namespace Api {
title: Api.TypeTextWithEntities;
description: Api.TypeTextWithEntities;
url: string;
CONSTRUCTOR_ID: 3890753042;
SUBCLASS_OF_ID: 3126949031;
className: 'PendingSuggestion';
static fromReader(reader: Reader): PendingSuggestion;
}
export class ResPQ extends VirtualClass<{
@ -19899,9 +19950,10 @@ namespace Api {
customPendingSuggestion?: Api.TypePendingSuggestion;
chats: Api.TypeChat[];
users: Api.TypeUser[];
CONSTRUCTOR_ID: 2352576831;
CONSTRUCTOR_ID: 145021050;
SUBCLASS_OF_ID: 2639877442;
className: 'PromoData';
static fromReader(reader: Reader): PromoData;
}
export class CountryCode extends VirtualClass<{
@ -21081,6 +21133,10 @@ namespace Api {
chats: Api.TypeChat[];
counters?: Api.TypeStarGiftAttributeCounter[];
users: Api.TypeUser[];
CONSTRUCTOR_ID: 2491028191;
SUBCLASS_OF_ID: 3000743907;
className: 'ResaleStarGifts';
static fromReader(reader: Reader): ResaleStarGifts;
}
}
@ -21696,6 +21752,10 @@ namespace Api {
countRemains: int;
}> {
countRemains: int;
CONSTRUCTOR_ID: 3280453710;
SUBCLASS_OF_ID: 3411255960;
className: 'CanSendStoryCount';
static fromReader(reader: Reader): CanSendStoryCount;
}
}

View File

@ -1466,6 +1466,8 @@ account.checkUsername#2714d86c username:string = Bool;
account.updateUsername#3e0bdd7c username:string = User;
account.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules;
account.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector<InputPrivacyRule> = account.PrivacyRules;
account.getAccountTTL#8fc711d = AccountDaysTTL;
account.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool;
account.getAuthorizations#e320c158 = account.Authorizations;
account.resetAuthorization#df77f3bc hash:long = Bool;
account.getPassword#548a30f5 = account.Password;

View File

@ -63,6 +63,8 @@
"account.getCollectibleEmojiStatuses",
"account.addNoPaidMessagesException",
"account.getPaidMessagesRevenue",
"account.getAccountTTL",
"account.setAccountTTL",
"users.getUsers",
"users.getFullUser",
"contacts.getContacts",

View File

@ -1272,6 +1272,10 @@ export interface LangPair {
'CloseMiniApps': undefined;
'DoNotAskAgain': undefined;
'PaymentInfoDone': undefined;
'DeleteMyAccount': undefined;
'DeleteAccountIfAwayFor': undefined;
'SelfDestructTitle': undefined;
'SelfDestructSessionsDescription': undefined;
'VideoConversionTitle': undefined;
'VideoConversionText': undefined;
'VideoConversionDone': undefined;

View File

@ -0,0 +1,5 @@
export function getClosestEntry(arr: number[], value: number): number {
return arr.reduce((prev, curr) => {
return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
});
}