Support note for contact (#6413)

This commit is contained in:
Alexander Zinchuk 2025-11-06 11:36:35 +01:00
parent 28a41a6764
commit f71b451051
25 changed files with 616 additions and 335 deletions

View File

@ -118,6 +118,7 @@ export interface GramJsAppConfig extends LimitsConfig {
verify_age_bot_username?: string; verify_age_bot_username?: string;
verify_age_country?: string; verify_age_country?: string;
verify_age_min?: number; verify_age_min?: number;
contact_note_length_limit?: number;
} }
function buildEmojiSounds(appConfig: GramJsAppConfig) { function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -187,6 +188,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
maxReactions: getLimit(appConfig, 'reactions_user_max', 'maxReactions'), maxReactions: getLimit(appConfig, 'reactions_user_max', 'maxReactions'),
moreAccounts: DEFAULT_LIMITS.moreAccounts, moreAccounts: DEFAULT_LIMITS.moreAccounts,
}, },
contactNoteLimit: appConfig.contact_note_length_limit,
hash, hash,
storyViewersExpirePeriod: appConfig.story_viewers_expire_period, storyViewersExpirePeriod: appConfig.story_viewers_expire_period,
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString(), storyChangelogUserId: appConfig.stories_changelog_user_id?.toString(),

View File

@ -14,7 +14,7 @@ import { toJSNumber } from '../../../util/numbers';
import { buildApiBotInfo } from './bots'; import { buildApiBotInfo } from './bots';
import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business';
import { import {
buildApiPhoto, buildApiUsernames, buildApiFormattedText, buildApiPhoto, buildApiUsernames,
} from './common'; } from './common';
import { buildApiDisallowedGiftsSettings } from './gifts'; import { buildApiDisallowedGiftsSettings } from './gifts';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
@ -36,7 +36,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
contactRequirePremium, businessWorkHours, businessLocation, businessIntro, contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification, birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
botCanManageEmojiStatus, settings, sendPaidMessagesStars, displayGiftsButton, disallowedGifts, botCanManageEmojiStatus, settings, sendPaidMessagesStars, displayGiftsButton, disallowedGifts,
starsRating, starsMyPendingRating, starsMyPendingRatingDate, mainTab, starsRating, starsMyPendingRating, starsMyPendingRatingDate, mainTab, note,
}, },
users, users,
} = mtpUserFull; } = mtpUserFull;
@ -76,6 +76,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
paidMessagesStars: toJSNumber(sendPaidMessagesStars), paidMessagesStars: toJSNumber(sendPaidMessagesStars),
settings: buildApiPeerSettings(settings), settings: buildApiPeerSettings(settings),
mainTab: mainTab && buildApiProfileTab(mainTab), mainTab: mainTab && buildApiProfileTab(mainTab),
note: note && buildApiFormattedText(note),
}; };
} }

View File

@ -1,6 +1,7 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiEmojiStatusType, ApiPeer, ApiUser, import type {
ApiEmojiStatusType, ApiFormattedText, ApiPeer, ApiUser,
} from '../../types'; } from '../../types';
import { toJSNumber } from '../../../util/numbers'; import { toJSNumber } from '../../../util/numbers';
@ -12,6 +13,7 @@ import {
buildInputContact, buildInputContact,
buildInputEmojiStatus, buildInputEmojiStatus,
buildInputPeer, buildInputPeer,
buildInputTextWithEntities,
buildInputUser, buildInputUser,
buildMtpPeerId, buildMtpPeerId,
DEFAULT_PRIMITIVES, DEFAULT_PRIMITIVES,
@ -212,6 +214,7 @@ export function updateContact({
firstName = DEFAULT_PRIMITIVES.STRING, firstName = DEFAULT_PRIMITIVES.STRING,
lastName = DEFAULT_PRIMITIVES.STRING, lastName = DEFAULT_PRIMITIVES.STRING,
shouldSharePhoneNumber = false, shouldSharePhoneNumber = false,
note,
}: { }: {
id: string; id: string;
accessHash?: string; accessHash?: string;
@ -219,6 +222,7 @@ export function updateContact({
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
shouldSharePhoneNumber?: boolean; shouldSharePhoneNumber?: boolean;
note?: ApiFormattedText;
}) { }) {
return invokeRequest(new GramJs.contacts.AddContact({ return invokeRequest(new GramJs.contacts.AddContact({
id: buildInputUser(id, accessHash), id: buildInputUser(id, accessHash),
@ -226,6 +230,7 @@ export function updateContact({
lastName, lastName,
phone: phoneNumber, phone: phoneNumber,
addPhonePrivacyException: shouldSharePhoneNumber || undefined, addPhonePrivacyException: shouldSharePhoneNumber || undefined,
note: note ? buildInputTextWithEntities(note) : undefined,
}), { }), {
shouldReturnTrue: true, shouldReturnTrue: true,
}); });
@ -364,3 +369,14 @@ export function saveCloseFriends(userIds: string[]) {
shouldReturnTrue: true, shouldReturnTrue: true,
}); });
} }
export function updateContactNote(user: ApiUser, note: ApiFormattedText) {
const { id, accessHash } = user;
return invokeRequest(new GramJs.contacts.UpdateContactNote({
id: buildInputUser(id, accessHash),
note: buildInputTextWithEntities(note),
}), {
shouldReturnTrue: true,
});
}

View File

@ -274,6 +274,7 @@ export interface ApiAppConfig {
verifyAgeBotUsername?: string; verifyAgeBotUsername?: string;
verifyAgeCountry?: string; verifyAgeCountry?: string;
verifyAgeMin?: number; verifyAgeMin?: number;
contactNoteLimit?: number;
} }
export interface ApiConfig { export interface ApiConfig {

View File

@ -1,7 +1,7 @@
import type { API_CHAT_TYPES } from '../../config'; import type { API_CHAT_TYPES } from '../../config';
import type { ApiBotInfo } from './bots'; import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business'; import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiDocument, ApiFormattedText, ApiPhoto } from './messages';
import type { import type {
ApiBotVerification, ApiBotVerification,
ApiEmojiStatusType, ApiEmojiStatusType,
@ -82,6 +82,7 @@ export interface ApiUserFullInfo {
paidMessagesStars?: number; paidMessagesStars?: number;
settings?: ApiPeerSettings; settings?: ApiPeerSettings;
mainTab?: ApiProfileTab; mainTab?: ApiProfileTab;
note?: ApiFormattedText;
} }
export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown'; export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown';

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M20 0H8.6C5.5 0 2.9 2.6 2.9 5.7v20.5c0 3.2 2.6 5.7 5.7 5.7h14.7c3.2 0 5.7-2.6 5.7-5.7V9.1q0-.9-.6-1.5L21.6.7c-.4-.4-1-.6-1.5-.6Zm-.1 2.5 6.7 6.7h-3.5c-1.8 0-3.2-1.4-3.2-3.2zm3.5 27H8.7c-1.8 0-3.2-1.4-3.2-3.2V5.7c0-1.8 1.4-3.2 3.2-3.2h8.7V6c0 3.2 2.6 5.7 5.7 5.7h3.5v14.5c0 1.8-1.4 3.2-3.2 3.2Z" class="st0"/><path d="M8.9 18.7H23v2.5H8.9zM8.9 23.6H21v2.5H8.9zM8.9 13.7H23v2.5H8.9z" class="st0"/></svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -2293,3 +2293,7 @@
"TitleGiftLocked" = "Gift Locked"; "TitleGiftLocked" = "Gift Locked";
"GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**."; "GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**.";
"QuickPreview" = "Quick Preview"; "QuickPreview" = "Quick Preview";
"UserNoteTitle" = "Notes";
"UserNoteHint" = "only visible to you";
"EditUserNoteHint" = "Notes are only visible to you.";

View File

@ -1,3 +1,5 @@
@use '../../../styles/mixins';
.businessLocation { .businessLocation {
flex-shrink: 0; flex-shrink: 0;
@ -68,3 +70,44 @@
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.noteSubtitle {
display: flex !important;
align-items: center;
}
.noteListItemIcon {
align-self: flex-start;
padding-top: 1rem;
}
.noteHint {
margin-left: auto;
}
.noteText {
overflow: hidden;
display: inline-block;
width: 100%;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.3s ease;
}
.noteTextCollapsed {
@include mixins.gradient-border-bottom(1rem);
}
.noteCollapseIcon {
margin-inline-start: 0.125rem;
font-size: 0.9375rem;
line-height: 0.9375rem;
transition: transform 0.3s ease;
}
.expandedIcon {
transform: rotate(-180deg);
}
.clickable {
cursor: var(--custom-cursor, pointer);
}

View File

@ -1,5 +1,5 @@
import { import {
memo, useMemo, memo, useMemo, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
@ -46,7 +46,9 @@ import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { ChatAnimationTypes } from '../../left/main/hooks'; import { ChatAnimationTypes } from '../../left/main/hooks';
import formatUsername from '../helpers/formatUsername'; import formatUsername from '../helpers/formatUsername';
import renderText from '../helpers/renderText'; import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -60,6 +62,7 @@ import ListItem from '../../ui/ListItem';
import Skeleton from '../../ui/placeholder/Skeleton'; import Skeleton from '../../ui/placeholder/Skeleton';
import Switcher from '../../ui/Switcher'; import Switcher from '../../ui/Switcher';
import CustomEmoji from '../CustomEmoji'; import CustomEmoji from '../CustomEmoji';
import Icon from '../icons/Icon';
import SafeLink from '../SafeLink'; import SafeLink from '../SafeLink';
import BusinessHours from './BusinessHours'; import BusinessHours from './BusinessHours';
import UserBirthday from './UserBirthday'; import UserBirthday from './UserBirthday';
@ -102,6 +105,7 @@ const DEFAULT_MAP_CONFIG = {
}; };
const BOT_VERIFICATION_ICON_SIZE = 16; const BOT_VERIFICATION_ICON_SIZE = 16;
const MAX_LINES = 3;
const ChatExtra = ({ const ChatExtra = ({
chatOrUserId, chatOrUserId,
@ -154,10 +158,19 @@ const ChatExtra = ({
businessWorkHours, businessWorkHours,
personalChannelMessageId, personalChannelMessageId,
birthday, birthday,
note,
} = userFullInfo || {}; } = userFullInfo || {};
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const noteTextRef = useRef<HTMLDivElement>();
const {
isCollapsed: isNoteCollapsed,
isCollapsible: isNoteCollapsible,
setIsCollapsed: setIsNoteCollapsed,
} = useCollapsibleLines(noteTextRef, MAX_LINES, undefined);
useEffectWithPrevDeps(([prevPeerId]) => { useEffectWithPrevDeps(([prevPeerId]) => {
if (!peerId || prevPeerId === peerId) return; if (!peerId || prevPeerId === peerId) return;
if (user || (chat && isChatChannel(chat))) { if (user || (chat && isChatChannel(chat))) {
@ -241,6 +254,16 @@ const ChatExtra = ({
openSavedDialog({ chatId: chatOrUserId }); openSavedDialog({ chatId: chatOrUserId });
}); });
const canExpandNote = isNoteCollapsible && isNoteCollapsed;
const handleExpandNote = useLastCallback(() => {
setIsNoteCollapsed(false);
});
const handleToggleNote = useLastCallback(() => {
setIsNoteCollapsed((prev) => !prev);
});
function copy(text: string, entity: string) { function copy(text: string, entity: string) {
copyTextToClipboard(text); copyTextToClipboard(text);
showNotification({ message: `${entity} was copied` }); showNotification({ message: `${entity} was copied` });
@ -458,6 +481,50 @@ const ChatExtra = ({
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span> <span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
</ListItem> </ListItem>
)} )}
{note && (
<ListItem
icon="note"
iconClassName={styles.noteListItemIcon}
multiline
narrow
isStatic
allowSelection
>
<div
ref={noteTextRef}
className={buildClassName(
'title',
'word-break',
'allow-selection',
styles.noteText,
isNoteCollapsed && styles.noteTextCollapsed,
)}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={canExpandNote ? handleExpandNote : undefined}
>
{renderTextWithEntities({
text: note.text,
entities: note.entities,
})}
</div>
<div className={buildClassName('subtitle', styles.noteSubtitle)}>
<span>{lang('UserNoteTitle')}</span>
<span className={styles.noteHint}>{lang('UserNoteHint')}</span>
{isNoteCollapsible && (
<Icon
className={buildClassName(
styles.noteCollapseIcon,
styles.clickable,
!isNoteCollapsed && styles.expandedIcon,
)}
onClick={handleToggleNote}
name="down"
/>
)}
</div>
</ListItem>
)}
{hasSavedMessages && !isOwnProfile && !isInSettings && ( {hasSavedMessages && !isOwnProfile && !isInSettings && (
<ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}> <ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}>
<span>{oldLang('SavedMessagesTab')}</span> <span>{oldLang('SavedMessagesTab')}</span>

View File

@ -40,6 +40,7 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
&__note-description,
&__help-text { &__help-text {
font-size: 0.9375rem; font-size: 0.9375rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
@ -47,5 +48,9 @@
&__negative { &__negative {
margin-top: -1rem; margin-top: -1rem;
} }
&__edit {
margin-top: -0.875rem;
}
} }
} }

View File

@ -8,13 +8,15 @@ import { getActions, withGlobal } from '../../global';
import type { ApiCountryCode, ApiUser, ApiUserStatus } from '../../api/types'; import type { ApiCountryCode, ApiUser, ApiUserStatus } from '../../api/types';
import { getUserStatus } from '../../global/helpers'; import { getUserStatus } from '../../global/helpers';
import { selectUser, selectUserStatus } from '../../global/selectors'; import { selectUser, selectUserFullInfo, selectUserStatus } from '../../global/selectors';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import { formatPhoneNumberWithCode } from '../../util/phoneNumber'; import { formatPhoneNumberWithCode } from '../../util/phoneNumber';
import { DEFAULT_MAX_NOTE_LENGTH } from '../../limits';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Avatar from '../common/Avatar'; import Avatar from '../common/Avatar';
@ -22,6 +24,7 @@ import Button from '../ui/Button';
import Checkbox from '../ui/Checkbox'; import Checkbox from '../ui/Checkbox';
import InputText from '../ui/InputText'; import InputText from '../ui/InputText';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
import TextArea from '../ui/TextArea';
import './NewContactModal.scss'; import './NewContactModal.scss';
@ -37,6 +40,8 @@ type StateProps = {
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
phoneCodeList: ApiCountryCode[]; phoneCodeList: ApiCountryCode[];
contactNoteLimit: number;
noteText?: string;
}; };
const NewContactModal: FC<OwnProps & StateProps> = ({ const NewContactModal: FC<OwnProps & StateProps> = ({
@ -46,10 +51,13 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
user, user,
userStatus, userStatus,
phoneCodeList, phoneCodeList,
contactNoteLimit,
noteText,
}) => { }) => {
const { updateContact, importContact, closeNewContactDialog } = getActions(); const { updateContact, importContact, closeNewContactDialog } = getActions();
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
const renderingUser = useCurrentOrPrev(user); const renderingUser = useCurrentOrPrev(user);
const renderingIsByPhoneNumber = useCurrentOrPrev(isByPhoneNumber); const renderingIsByPhoneNumber = useCurrentOrPrev(isByPhoneNumber);
const inputRef = useRef<HTMLInputElement>(); const inputRef = useRef<HTMLInputElement>();
@ -58,18 +66,23 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
const [firstName, setFirstName] = useState<string>(renderingUser?.firstName ?? ''); const [firstName, setFirstName] = useState<string>(renderingUser?.firstName ?? '');
const [lastName, setLastName] = useState<string>(renderingUser?.lastName ?? ''); const [lastName, setLastName] = useState<string>(renderingUser?.lastName ?? '');
const [phone, setPhone] = useState<string>(renderingUser?.phoneNumber ?? ''); const [phone, setPhone] = useState<string>(renderingUser?.phoneNumber ?? '');
const [note, setNote] = useState<string>('');
const [shouldSharePhoneNumber, setShouldSharePhoneNumber] = useState<boolean>(true); const [shouldSharePhoneNumber, setShouldSharePhoneNumber] = useState<boolean>(true);
const canBeSubmitted = Boolean(firstName && (!isByPhoneNumber || phone)); const canBeSubmitted = Boolean(firstName && (!isByPhoneNumber || phone));
const noteSymbolsLeft = contactNoteLimit - note.length;
const noteRef = useRef<HTMLTextAreaElement>();
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
markIsShown(); markIsShown();
setFirstName(renderingUser?.firstName ?? ''); setFirstName(renderingUser?.firstName ?? '');
setLastName(renderingUser?.lastName ?? ''); setLastName(renderingUser?.lastName ?? '');
setPhone(renderingUser?.phoneNumber ?? ''); setPhone(renderingUser?.phoneNumber ?? '');
setNote(noteText ?? '');
setShouldSharePhoneNumber(true); setShouldSharePhoneNumber(true);
} }
}, [isOpen, markIsShown, renderingUser?.firstName, renderingUser?.lastName, renderingUser?.phoneNumber]); }, [isOpen, markIsShown, noteText, renderingUser?.firstName, renderingUser?.lastName, renderingUser?.phoneNumber]);
useEffect(() => { useEffect(() => {
if (!IS_TOUCH_ENV && isShown) { if (!IS_TOUCH_ENV && isShown) {
@ -91,14 +104,21 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
setLastName(e.target.value); setLastName(e.target.value);
}, []); }, []);
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setNote(e.target.value);
}, []);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeNewContactDialog(); closeNewContactDialog();
setFirstName(''); setFirstName('');
setLastName(''); setLastName('');
setPhone(''); setPhone('');
setNote('');
}, [closeNewContactDialog]); }, [closeNewContactDialog]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
const noteToSend = note.trim() ? { text: note, entities: [] } : undefined;
if (isByPhoneNumber || !userId) { if (isByPhoneNumber || !userId) {
importContact({ importContact({
firstName, firstName,
@ -111,9 +131,10 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
firstName, firstName,
lastName, lastName,
shouldSharePhoneNumber, shouldSharePhoneNumber,
note: noteToSend,
}); });
} }
}, [firstName, importContact, isByPhoneNumber, lastName, phone, shouldSharePhoneNumber, updateContact, userId]); }, [firstName, importContact, isByPhoneNumber, lastName, note, phone, shouldSharePhoneNumber, updateContact, userId]);
if (!isOpen && !isShown) { if (!isOpen && !isShown) {
return undefined; return undefined;
@ -122,7 +143,7 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
function renderAddContact() { function renderAddContact() {
return ( return (
<> <>
<div className="NewContactModal__profile" dir={lang.isRtl ? 'rtl' : undefined}> <div className="NewContactModal__profile" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Avatar <Avatar
size="jumbo" size="jumbo"
peer={renderingUser} peer={renderingUser}
@ -132,29 +153,42 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
<p className="NewContactModal__phone-number"> <p className="NewContactModal__phone-number">
{renderingUser?.phoneNumber {renderingUser?.phoneNumber
? formatPhoneNumberWithCode(phoneCodeList, renderingUser.phoneNumber) ? formatPhoneNumberWithCode(phoneCodeList, renderingUser.phoneNumber)
: lang('MobileHidden')} : oldLang('MobileHidden')}
</p> </p>
<span className="NewContactModal__user-status" dir="auto"> <span className="NewContactModal__user-status" dir="auto">
{getUserStatus(lang, renderingUser!, userStatus)} {getUserStatus(oldLang, renderingUser!, userStatus)}
</span> </span>
</div> </div>
</div> </div>
<InputText <InputText
ref={inputRef} ref={inputRef}
value={firstName} value={firstName}
label={lang('FirstName')} label={oldLang('FirstName')}
tabIndex={0} tabIndex={0}
onChange={handleFirstNameChange} onChange={handleFirstNameChange}
/> />
<InputText <InputText
value={lastName} value={lastName}
label={lang('LastName')} label={oldLang('LastName')}
tabIndex={0} tabIndex={0}
onChange={handleLastNameChange} onChange={handleLastNameChange}
/> />
<TextArea
ref={noteRef}
id="user-note"
label={lang('UserNoteTitle')}
onChange={handleNoteChange}
value={note}
maxLength={contactNoteLimit}
maxLengthIndicator={noteSymbolsLeft.toString()}
noReplaceNewlines
/>
<p className="NewContactModal__help-text NewContactModal__help-text__edit">
{lang('EditUserNoteHint')}
</p>
<p className="NewContactModal__help-text"> <p className="NewContactModal__help-text">
{renderText( {renderText(
lang('NewContact.Phone.Hidden.Text', renderingUser?.firstName || ''), oldLang('NewContact.Phone.Hidden.Text', renderingUser?.firstName || ''),
['emoji', 'simple_markdown'], ['emoji', 'simple_markdown'],
)} )}
</p> </p>
@ -163,10 +197,10 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
checked={shouldSharePhoneNumber} checked={shouldSharePhoneNumber}
tabIndex={0} tabIndex={0}
onCheck={setShouldSharePhoneNumber} onCheck={setShouldSharePhoneNumber}
label={lang('lng_new_contact_share')} label={oldLang('lng_new_contact_share')}
/> />
<p className="NewContactModal__help-text NewContactModal__help-text__negative"> <p className="NewContactModal__help-text NewContactModal__help-text__negative">
{renderText(lang('AddContact.SharedContactExceptionInfo', renderingUser?.firstName))} {renderText(oldLang('AddContact.SharedContactExceptionInfo', renderingUser?.firstName))}
</p> </p>
</> </>
); );
@ -181,19 +215,19 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
ref={inputRef} ref={inputRef}
value={phone} value={phone}
inputMode="tel" inputMode="tel"
label={lang('lng_contact_phone')} label={oldLang('lng_contact_phone')}
tabIndex={0} tabIndex={0}
onChange={handlePhoneChange} onChange={handlePhoneChange}
/> />
<InputText <InputText
value={firstName} value={firstName}
label={lang('FirstName')} label={oldLang('FirstName')}
tabIndex={0} tabIndex={0}
onChange={handleFirstNameChange} onChange={handleFirstNameChange}
/> />
<InputText <InputText
value={lastName} value={lastName}
label={lang('LastName')} label={oldLang('LastName')}
tabIndex={0} tabIndex={0}
onChange={handleLastNameChange} onChange={handleLastNameChange}
/> />
@ -205,7 +239,7 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
return ( return (
<Modal <Modal
className="NewContactModal" className="NewContactModal"
title={lang('NewContact')} title={oldLang('NewContact')}
isOpen={isOpen} isOpen={isOpen}
onClose={handleClose} onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown} onCloseAnimationEnd={unmarkIsShown}
@ -219,14 +253,14 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
disabled={!canBeSubmitted} disabled={!canBeSubmitted}
onClick={handleSubmit} onClick={handleSubmit}
> >
{lang('Done')} {oldLang('Done')}
</Button> </Button>
<Button <Button
isText isText
className="confirm-dialog-button" className="confirm-dialog-button"
onClick={handleClose} onClick={handleClose}
> >
{lang('Cancel')} {oldLang('Cancel')}
</Button> </Button>
</div> </div>
</Modal> </Modal>
@ -236,10 +270,15 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId }): Complete<StateProps> => { (global, { userId }): Complete<StateProps> => {
const user = userId ? selectUser(global, userId) : undefined; const user = userId ? selectUser(global, userId) : undefined;
const userFullInfo = userId ? selectUserFullInfo(global, userId) : undefined;
const contactNoteLimit = global.appConfig?.contactNoteLimit || DEFAULT_MAX_NOTE_LENGTH;
return { return {
user, user,
userStatus: userId ? selectUserStatus(global, userId) : undefined, userStatus: userId ? selectUserStatus(global, userId) : undefined,
phoneCodeList: global.countryList.phoneCodes, phoneCodeList: global.countryList.phoneCodes,
contactNoteLimit,
noteText: userFullInfo?.note?.text,
}; };
}, },
)(NewContactModal)); )(NewContactModal));

View File

@ -19,9 +19,11 @@ import {
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { DEFAULT_MAX_NOTE_LENGTH } from '../../../limits';
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 useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -34,6 +36,7 @@ import InputText from '../../ui/InputText';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import SelectAvatar from '../../ui/SelectAvatar'; import SelectAvatar from '../../ui/SelectAvatar';
import Spinner from '../../ui/Spinner'; import Spinner from '../../ui/Spinner';
import TextArea from '../../ui/TextArea';
import './Management.scss'; import './Management.scss';
@ -49,6 +52,8 @@ type StateProps = {
isMuted?: boolean; isMuted?: boolean;
personalPhoto?: ApiPhoto; personalPhoto?: ApiPhoto;
notPersonalPhoto?: ApiPhoto; notPersonalPhoto?: ApiPhoto;
noteText?: string;
contactNoteLimit: number;
}; };
const ERROR_FIRST_NAME_MISSING = 'Please provide first name'; const ERROR_FIRST_NAME_MISSING = 'Please provide first name';
@ -62,9 +67,12 @@ const ManageUser: FC<OwnProps & StateProps> = ({
isActive, isActive,
personalPhoto, personalPhoto,
notPersonalPhoto, notPersonalPhoto,
noteText,
contactNoteLimit,
}) => { }) => {
const { const {
updateContact, updateContact,
updateContactNote,
deleteContact, deleteContact,
closeManagement, closeManagement,
uploadContactProfilePhoto, uploadContactProfilePhoto,
@ -76,7 +84,8 @@ const ManageUser: FC<OwnProps & StateProps> = ({
const [isProfileFieldsTouched, markProfileFieldsTouched, unmarkProfileFieldsTouched] = useFlag(); const [isProfileFieldsTouched, markProfileFieldsTouched, unmarkProfileFieldsTouched] = useFlag();
const [error, setError] = useState<string | undefined>(); const [error, setError] = useState<string | undefined>();
const [isNotificationsTouched, markNotificationsTouched, unmarkNotificationsTouched] = useFlag(); const [isNotificationsTouched, markNotificationsTouched, unmarkNotificationsTouched] = useFlag();
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
useHistoryBack({ useHistoryBack({
isActive, isActive,
@ -85,9 +94,11 @@ const ManageUser: FC<OwnProps & StateProps> = ({
const currentFirstName = user ? (user.firstName || '') : ''; const currentFirstName = user ? (user.firstName || '') : '';
const currentLastName = user ? (user.lastName || '') : ''; const currentLastName = user ? (user.lastName || '') : '';
const currentNote = noteText || '';
const [firstName, setFirstName] = useState(currentFirstName); const [firstName, setFirstName] = useState(currentFirstName);
const [lastName, setLastName] = useState(currentLastName); const [lastName, setLastName] = useState(currentLastName);
const [note, setNote] = useState(currentNote);
const [isNotificationsEnabled, setIsNotificationsEnabled] = useState(!isMuted); const [isNotificationsEnabled, setIsNotificationsEnabled] = useState(!isMuted);
useEffect(() => { useEffect(() => {
@ -103,7 +114,8 @@ const ManageUser: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
setFirstName(currentFirstName); setFirstName(currentFirstName);
setLastName(currentLastName); setLastName(currentLastName);
}, [currentFirstName, currentLastName, user]); setNote(currentNote);
}, [currentFirstName, currentLastName, currentNote]);
useEffect(() => { useEffect(() => {
if (progress === ManagementProgress.Complete) { if (progress === ManagementProgress.Complete) {
@ -127,6 +139,11 @@ const ManageUser: FC<OwnProps & StateProps> = ({
markProfileFieldsTouched(); markProfileFieldsTouched();
}, []); }, []);
const handleNoteChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setNote(e.target.value);
markProfileFieldsTouched();
}, []);
const handleNotificationChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleNotificationChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setIsNotificationsEnabled(e.target.checked); setIsNotificationsEnabled(e.target.checked);
markNotificationsTouched(); markNotificationsTouched();
@ -136,24 +153,36 @@ const ManageUser: FC<OwnProps & StateProps> = ({
const handleProfileSave = useCallback(() => { const handleProfileSave = useCallback(() => {
const trimmedFirstName = firstName.trim(); const trimmedFirstName = firstName.trim();
const trimmedLastName = lastName.trim(); const trimmedLastName = lastName.trim();
const trimmedNote = note.trim();
if (!trimmedFirstName.length) { if (!trimmedFirstName.length) {
setError(ERROR_FIRST_NAME_MISSING); setError(ERROR_FIRST_NAME_MISSING);
return; return;
} }
firstNameRef.current?.blur();
lastNameRef.current?.blur();
noteRef.current?.blur();
updateContact({ updateContact({
userId, userId,
firstName: trimmedFirstName, firstName: trimmedFirstName,
lastName: trimmedLastName, lastName: trimmedLastName,
}); });
if (trimmedNote !== currentNote) {
updateContactNote({
userId,
note: { text: trimmedNote, entities: [] },
});
}
if (isNotificationsTouched) { if (isNotificationsTouched) {
updateChatMutedState({ updateChatMutedState({
chatId: userId, mutedUntil: isNotificationsEnabled ? UNMUTE_TIMESTAMP : MUTE_INDEFINITE_TIMESTAMP, chatId: userId, mutedUntil: isNotificationsEnabled ? UNMUTE_TIMESTAMP : MUTE_INDEFINITE_TIMESTAMP,
}); });
} }
}, [firstName, isNotificationsEnabled, isNotificationsTouched, lastName, userId]); }, [currentNote, firstName, isNotificationsEnabled, isNotificationsTouched, lastName, note, userId]);
const handleDeleteContact = useCallback(() => { const handleDeleteContact = useCallback(() => {
deleteContact({ userId }); deleteContact({ userId });
@ -161,6 +190,10 @@ const ManageUser: FC<OwnProps & StateProps> = ({
closeManagement(); closeManagement();
}, [closeDeleteDialog, closeManagement, deleteContact, userId]); }, [closeDeleteDialog, closeManagement, deleteContact, userId]);
const firstNameRef = useRef<HTMLInputElement>();
const lastNameRef = useRef<HTMLInputElement>();
const noteRef = useRef<HTMLTextAreaElement>();
const inputRef = useRef<HTMLInputElement>(); const inputRef = useRef<HTMLInputElement>();
const isSuggestRef = useRef(false); const isSuggestRef = useRef(false);
@ -191,6 +224,7 @@ const ManageUser: FC<OwnProps & StateProps> = ({
const canSetPersonalPhoto = !isUserBot(user) && user.id !== SERVICE_NOTIFICATIONS_USER_ID; const canSetPersonalPhoto = !isUserBot(user) && user.id !== SERVICE_NOTIFICATIONS_USER_ID;
const isLoading = progress === ManagementProgress.InProgress; const isLoading = progress === ManagementProgress.InProgress;
const noteSymbolsLeft = contactNoteLimit - note.length;
return ( return (
<div className="Management"> <div className="Management">
@ -205,24 +239,37 @@ const ManageUser: FC<OwnProps & StateProps> = ({
/> />
<div className="settings-edit"> <div className="settings-edit">
<InputText <InputText
ref={firstNameRef}
id="user-first-name" id="user-first-name"
label={lang('UserInfo.FirstNamePlaceholder')} label={oldLang('UserInfo.FirstNamePlaceholder')}
onChange={handleFirstNameChange} onChange={handleFirstNameChange}
value={firstName} value={firstName}
error={error === ERROR_FIRST_NAME_MISSING ? error : undefined} error={error === ERROR_FIRST_NAME_MISSING ? error : undefined}
/> />
<InputText <InputText
ref={lastNameRef}
id="user-last-name" id="user-last-name"
label={lang('UserInfo.LastNamePlaceholder')} label={oldLang('UserInfo.LastNamePlaceholder')}
onChange={handleLastNameChange} onChange={handleLastNameChange}
value={lastName} value={lastName}
/> />
<TextArea
ref={noteRef}
id="user-note"
label={lang('UserNoteTitle')}
onChange={handleNoteChange}
value={note}
maxLength={contactNoteLimit}
maxLengthIndicator={noteSymbolsLeft.toString()}
noReplaceNewlines
/>
</div> </div>
<p className="section-edit-info" dir="auto">{lang('EditUserNoteHint')}</p>
<div className="ListItem narrow"> <div className="ListItem narrow">
<Checkbox <Checkbox
checked={isNotificationsEnabled} checked={isNotificationsEnabled}
label={lang('Notifications')} label={oldLang('Notifications')}
subLabel={lang(isNotificationsEnabled subLabel={oldLang(isNotificationsEnabled
? 'UserInfo.NotificationsEnabled' ? 'UserInfo.NotificationsEnabled'
: 'UserInfo.NotificationsDisabled')} : 'UserInfo.NotificationsDisabled')}
onChange={handleNotificationChange} onChange={handleNotificationChange}
@ -232,10 +279,10 @@ const ManageUser: FC<OwnProps & StateProps> = ({
{canSetPersonalPhoto && ( {canSetPersonalPhoto && (
<div className="section"> <div className="section">
<ListItem icon="camera-add" ripple onClick={handleSuggestPhoto}> <ListItem icon="camera-add" ripple onClick={handleSuggestPhoto}>
<span className="list-item-ellipsis">{lang('UserInfo.SuggestPhoto', user.firstName)}</span> <span className="list-item-ellipsis">{oldLang('UserInfo.SuggestPhoto', user.firstName)}</span>
</ListItem> </ListItem>
<ListItem icon="camera-add" ripple onClick={handleSetPersonalPhoto}> <ListItem icon="camera-add" ripple onClick={handleSetPersonalPhoto}>
<span className="list-item-ellipsis">{lang('UserInfo.SetCustomPhoto', user.firstName)}</span> <span className="list-item-ellipsis">{oldLang('UserInfo.SetCustomPhoto', user.firstName)}</span>
</ListItem> </ListItem>
{personalPhoto && ( {personalPhoto && (
<ListItem <ListItem
@ -251,15 +298,15 @@ const ManageUser: FC<OwnProps & StateProps> = ({
ripple ripple
onClick={openResetPersonalPhotoDialog} onClick={openResetPersonalPhotoDialog}
> >
{lang('UserInfo.ResetCustomPhoto')} {oldLang('UserInfo.ResetCustomPhoto')}
</ListItem> </ListItem>
)} )}
<p className="section-help" dir="auto">{lang('UserInfo.CustomPhotoInfo', user.firstName)}</p> <p className="section-help" dir="auto">{oldLang('UserInfo.CustomPhotoInfo', user.firstName)}</p>
</div> </div>
)} )}
<div className="section"> <div className="section">
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}> <ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
{lang('DeleteContact')} {oldLang('DeleteContact')}
</ListItem> </ListItem>
</div> </div>
</div> </div>
@ -267,7 +314,7 @@ const ManageUser: FC<OwnProps & StateProps> = ({
isShown={isProfileFieldsTouched} isShown={isProfileFieldsTouched}
onClick={handleProfileSave} onClick={handleProfileSave}
disabled={isLoading} disabled={isLoading}
ariaLabel={lang('Save')} ariaLabel={oldLang('Save')}
> >
{isLoading ? ( {isLoading ? (
<Spinner color="white" /> <Spinner color="white" />
@ -278,16 +325,16 @@ const ManageUser: FC<OwnProps & StateProps> = ({
<ConfirmDialog <ConfirmDialog
isOpen={isDeleteDialogOpen} isOpen={isDeleteDialogOpen}
onClose={closeDeleteDialog} onClose={closeDeleteDialog}
text={lang('AreYouSureDeleteContact')} text={oldLang('AreYouSureDeleteContact')}
confirmLabel={lang('DeleteContact')} confirmLabel={oldLang('DeleteContact')}
confirmHandler={handleDeleteContact} confirmHandler={handleDeleteContact}
confirmIsDestructive confirmIsDestructive
/> />
<ConfirmDialog <ConfirmDialog
isOpen={isResetPersonalPhotoDialogOpen} isOpen={isResetPersonalPhotoDialogOpen}
onClose={closeResetPersonalPhotoDialog} onClose={closeResetPersonalPhotoDialog}
text={lang('UserInfo.ResetToOriginalAlertText', user.firstName)} text={oldLang('UserInfo.ResetToOriginalAlertText', user.firstName)}
confirmLabel={lang('Reset')} confirmLabel={oldLang('Reset')}
confirmHandler={handleResetPersonalAvatar} confirmHandler={handleResetPersonalAvatar}
confirmIsDestructive confirmIsDestructive
/> />
@ -308,9 +355,11 @@ export default memo(withGlobal<OwnProps>(
const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)); const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id));
const personalPhoto = userFullInfo?.personalPhoto; const personalPhoto = userFullInfo?.personalPhoto;
const notPersonalPhoto = userFullInfo?.profilePhoto || userFullInfo?.fallbackPhoto; const notPersonalPhoto = userFullInfo?.profilePhoto || userFullInfo?.fallbackPhoto;
const noteText = userFullInfo?.note?.text;
const contactNoteLimit = global.appConfig?.contactNoteLimit || DEFAULT_MAX_NOTE_LENGTH;
return { return {
user, progress, isMuted, personalPhoto, notPersonalPhoto, user, progress, isMuted, personalPhoto, notPersonalPhoto, noteText, contactNoteLimit,
}; };
}, },
)(ManageUser)); )(ManageUser));

View File

@ -97,12 +97,17 @@
} }
} }
.section-edit-info,
.section-info { .section-info {
padding: 0 1rem; padding: 0 1rem;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.section-edit-info {
margin-top: -0.875rem;
}
.invite-link { .invite-link {
padding: 0 1rem; padding: 0 1rem;
} }

View File

@ -239,7 +239,7 @@ addActionHandler('openChatRefundModal', async (global, actions, payload): Promis
addActionHandler('updateContact', async (global, actions, payload): Promise<void> => { addActionHandler('updateContact', async (global, actions, payload): Promise<void> => {
const { const {
userId, firstName, lastName, shouldSharePhoneNumber, userId, firstName, lastName, shouldSharePhoneNumber, note,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
@ -253,7 +253,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise<void
setGlobal(global); setGlobal(global);
let result; let result;
if (!user.isContact && user.phoneNumber) { if (!user.isContact && user.phoneNumber && !note) {
result = await callApi('importContact', { phone: user.phoneNumber, firstName, lastName }); result = await callApi('importContact', { phone: user.phoneNumber, firstName, lastName });
} else { } else {
const { id, accessHash } = user; const { id, accessHash } = user;
@ -264,6 +264,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise<void
firstName, firstName,
lastName, lastName,
shouldSharePhoneNumber, shouldSharePhoneNumber,
note,
}); });
} }
@ -289,6 +290,29 @@ addActionHandler('updateContact', async (global, actions, payload): Promise<void
setGlobal(global); setGlobal(global);
}); });
addActionHandler('updateContactNote', async (global, actions, payload): Promise<void> => {
const {
userId, note,
tabId = getCurrentTabId(),
} = payload;
const user = selectUser(global, userId);
if (!user) {
return;
}
global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.InProgress, tabId);
setGlobal(global);
const result = await callApi('updateContactNote', user, note);
global = getGlobal();
if (result) global = updateUserFullInfo(global, userId, { note });
global = updateManagementProgress(global, ManagementProgress.Complete, tabId);
setGlobal(global);
});
addActionHandler('deleteContact', async (global, actions, payload): Promise<void> => { addActionHandler('deleteContact', async (global, actions, payload): Promise<void> => {
const { userId } = payload; const { userId } = payload;

View File

@ -1867,6 +1867,11 @@ export interface ActionPayloads {
firstName: string; firstName: string;
lastName?: string; lastName?: string;
shouldSharePhoneNumber?: boolean; shouldSharePhoneNumber?: boolean;
note?: ApiFormattedText;
} & WithTabId;
updateContactNote: {
userId: string;
note: ApiFormattedText;
} & WithTabId; } & WithTabId;
toggleNoPaidMessagesException: { toggleNoPaidMessagesException: {
userId: string; userId: string;

View File

@ -20,15 +20,19 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
) { ) {
const isFirstRenderRef = useRef(true); const isFirstRenderRef = useRef(true);
const cutoutHeightRef = useRef<number | undefined>(); const cutoutHeightRef = useRef<number | undefined>();
const fullHeightRef = useRef<number | undefined>();
const [isCollapsible, setIsCollapsible] = useState(!isDisabled); const [isCollapsible, setIsCollapsible] = useState(!isDisabled);
const [isCollapsed, setIsCollapsed] = useState(isCollapsible); const [isCollapsed, setIsCollapsed] = useState(isCollapsible);
useLayoutEffect(() => { useLayoutEffect(() => {
const element = (cutoutRef || ref).current; const element = (cutoutRef || ref).current;
const shouldUseStyleInExpand = !cutoutRef;
if (isDisabled || !element || isFirstRenderRef.current) return; if (isDisabled || !element || isFirstRenderRef.current) return;
requestMutation(() => { requestMutation(() => {
element.style.maxHeight = isCollapsed ? `${cutoutHeightRef.current}px` : ''; element.style.maxHeight = isCollapsed ? `${cutoutHeightRef.current}px` :
shouldUseStyleInExpand ? `${fullHeightRef.current}px` : ``;
}); });
}, [cutoutRef, isCollapsed, isDisabled, ref]); }, [cutoutRef, isCollapsed, isDisabled, ref]);
@ -39,6 +43,7 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
const element = ref.current; const element = ref.current;
const { lineHeight, totalLines } = calcTextLineHeightAndCount(element); const { lineHeight, totalLines } = calcTextLineHeightAndCount(element);
fullHeightRef.current = element.scrollHeight;
if (totalLines > maxLinesBeforeCollapse) { if (totalLines > maxLinesBeforeCollapse) {
cutoutHeightRef.current = lineHeight * maxLinesBeforeCollapse; cutoutHeightRef.current = lineHeight * maxLinesBeforeCollapse;
setIsCollapsible(true); setIsCollapsible(true);
@ -63,7 +68,9 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
isFirstRenderRef.current = false; isFirstRenderRef.current = false;
const element = (cutoutRef || ref).current; const element = (cutoutRef || ref).current;
if (!element) return; if (!element) return;
element.style.maxHeight = cutoutHeightRef.current ? `${cutoutHeightRef.current}px` : ''; element.style.maxHeight = cutoutHeightRef.current ?
`${cutoutHeightRef.current}px` :
`${fullHeightRef.current}px`;
}; };
}); });
} }

View File

@ -1569,6 +1569,7 @@ contacts.addContact#d9ba2e54 flags:# add_phone_privacy_exception:flags.0?true id
contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer;
contacts.editCloseFriends#ba6705f0 id:Vector<long> = Bool; contacts.editCloseFriends#ba6705f0 id:Vector<long> = Bool;
contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers; contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers;
contacts.updateContactNote#139f63fb id:InputUser note:TextWithEntities = Bool;
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages; messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs;
messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;

View File

@ -79,6 +79,7 @@
"contacts.resolveUsername", "contacts.resolveUsername",
"contacts.getTopPeers", "contacts.getTopPeers",
"contacts.addContact", "contacts.addContact",
"contacts.updateContactNote",
"contacts.resolvePhone", "contacts.resolvePhone",
"contacts.editCloseFriends", "contacts.editCloseFriends",
"contacts.getSponsoredPeers", "contacts.getSponsoredPeers",

View File

@ -37,6 +37,7 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
}; };
export const DEFAULT_MAX_MESSAGE_LENGTH = 4096; export const DEFAULT_MAX_MESSAGE_LENGTH = 4096;
export const DEFAULT_MAX_NOTE_LENGTH = 128;
export const DEFAULT_APP_CONFIG: ApiAppConfig = { export const DEFAULT_APP_CONFIG: ApiAppConfig = {
hash: 0, hash: 0,

View File

@ -3,8 +3,8 @@
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
src: url("./icons.woff2?c4762f96fd2b9a4edb1c0206e3cf5af6") format("woff2"), src: url("./icons.woff2?ef047748945945a49b46209abb54795c") format("woff2"),
url("./icons.woff?c4762f96fd2b9a4edb1c0206e3cf5af6") format("woff"); url("./icons.woff?ef047748945945a49b46209abb54795c") format("woff");
} }
.icon-char::before { .icon-char::before {
@ -471,435 +471,438 @@ url("./icons.woff?c4762f96fd2b9a4edb1c0206e3cf5af6") format("woff");
.icon-non-contacts::before { .icon-non-contacts::before {
content: "\f196"; content: "\f196";
} }
.icon-one-filled::before { .icon-note::before {
content: "\f197"; content: "\f197";
} }
.icon-open-in-new-tab::before { .icon-one-filled::before {
content: "\f198"; content: "\f198";
} }
.icon-password-off::before { .icon-open-in-new-tab::before {
content: "\f199"; content: "\f199";
} }
.icon-pause::before { .icon-password-off::before {
content: "\f19a"; content: "\f19a";
} }
.icon-permissions::before { .icon-pause::before {
content: "\f19b"; content: "\f19b";
} }
.icon-phone-discard-outline::before { .icon-permissions::before {
content: "\f19c"; content: "\f19c";
} }
.icon-phone-discard::before { .icon-phone-discard-outline::before {
content: "\f19d"; content: "\f19d";
} }
.icon-phone::before { .icon-phone-discard::before {
content: "\f19e"; content: "\f19e";
} }
.icon-photo::before { .icon-phone::before {
content: "\f19f"; content: "\f19f";
} }
.icon-pin-badge::before { .icon-photo::before {
content: "\f1a0"; content: "\f1a0";
} }
.icon-pin-list::before { .icon-pin-badge::before {
content: "\f1a1"; content: "\f1a1";
} }
.icon-pin::before { .icon-pin-list::before {
content: "\f1a2"; content: "\f1a2";
} }
.icon-pinned-chat::before { .icon-pin::before {
content: "\f1a3"; content: "\f1a3";
} }
.icon-pinned-message::before { .icon-pinned-chat::before {
content: "\f1a4"; content: "\f1a4";
} }
.icon-pip::before { .icon-pinned-message::before {
content: "\f1a5"; content: "\f1a5";
} }
.icon-play-story::before { .icon-pip::before {
content: "\f1a6"; content: "\f1a6";
} }
.icon-play::before { .icon-play-story::before {
content: "\f1a7"; content: "\f1a7";
} }
.icon-poll::before { .icon-play::before {
content: "\f1a8"; content: "\f1a8";
} }
.icon-previous::before { .icon-poll::before {
content: "\f1a9"; content: "\f1a9";
} }
.icon-privacy-policy::before { .icon-previous::before {
content: "\f1aa"; content: "\f1aa";
} }
.icon-proof-of-ownership::before { .icon-privacy-policy::before {
content: "\f1ab"; content: "\f1ab";
} }
.icon-quote-text::before { .icon-proof-of-ownership::before {
content: "\f1ac"; content: "\f1ac";
} }
.icon-quote::before { .icon-quote-text::before {
content: "\f1ad"; content: "\f1ad";
} }
.icon-radial-badge::before { .icon-quote::before {
content: "\f1ae"; content: "\f1ae";
} }
.icon-rating-icons-level1::before { .icon-radial-badge::before {
content: "\f1af"; content: "\f1af";
} }
.icon-rating-icons-level10::before { .icon-rating-icons-level1::before {
content: "\f1b0"; content: "\f1b0";
} }
.icon-rating-icons-level2::before { .icon-rating-icons-level10::before {
content: "\f1b1"; content: "\f1b1";
} }
.icon-rating-icons-level20::before { .icon-rating-icons-level2::before {
content: "\f1b2"; content: "\f1b2";
} }
.icon-rating-icons-level3::before { .icon-rating-icons-level20::before {
content: "\f1b3"; content: "\f1b3";
} }
.icon-rating-icons-level30::before { .icon-rating-icons-level3::before {
content: "\f1b4"; content: "\f1b4";
} }
.icon-rating-icons-level4::before { .icon-rating-icons-level30::before {
content: "\f1b5"; content: "\f1b5";
} }
.icon-rating-icons-level40::before { .icon-rating-icons-level4::before {
content: "\f1b6"; content: "\f1b6";
} }
.icon-rating-icons-level5::before { .icon-rating-icons-level40::before {
content: "\f1b7"; content: "\f1b7";
} }
.icon-rating-icons-level50::before { .icon-rating-icons-level5::before {
content: "\f1b8"; content: "\f1b8";
} }
.icon-rating-icons-level6::before { .icon-rating-icons-level50::before {
content: "\f1b9"; content: "\f1b9";
} }
.icon-rating-icons-level60::before { .icon-rating-icons-level6::before {
content: "\f1ba"; content: "\f1ba";
} }
.icon-rating-icons-level7::before { .icon-rating-icons-level60::before {
content: "\f1bb"; content: "\f1bb";
} }
.icon-rating-icons-level70::before { .icon-rating-icons-level7::before {
content: "\f1bc"; content: "\f1bc";
} }
.icon-rating-icons-level8::before { .icon-rating-icons-level70::before {
content: "\f1bd"; content: "\f1bd";
} }
.icon-rating-icons-level80::before { .icon-rating-icons-level8::before {
content: "\f1be"; content: "\f1be";
} }
.icon-rating-icons-level9::before { .icon-rating-icons-level80::before {
content: "\f1bf"; content: "\f1bf";
} }
.icon-rating-icons-level90::before { .icon-rating-icons-level9::before {
content: "\f1c0"; content: "\f1c0";
} }
.icon-rating-icons-negative::before { .icon-rating-icons-level90::before {
content: "\f1c1"; content: "\f1c1";
} }
.icon-readchats::before { .icon-rating-icons-negative::before {
content: "\f1c2"; content: "\f1c2";
} }
.icon-recent::before { .icon-readchats::before {
content: "\f1c3"; content: "\f1c3";
} }
.icon-refund::before { .icon-recent::before {
content: "\f1c4"; content: "\f1c4";
} }
.icon-reload::before { .icon-refund::before {
content: "\f1c5"; content: "\f1c5";
} }
.icon-remove-quote::before { .icon-reload::before {
content: "\f1c6"; content: "\f1c6";
} }
.icon-remove::before { .icon-remove-quote::before {
content: "\f1c7"; content: "\f1c7";
} }
.icon-reopen-topic::before { .icon-remove::before {
content: "\f1c8"; content: "\f1c8";
} }
.icon-replace::before { .icon-reopen-topic::before {
content: "\f1c9"; content: "\f1c9";
} }
.icon-replies::before { .icon-replace::before {
content: "\f1ca"; content: "\f1ca";
} }
.icon-reply-filled::before { .icon-replies::before {
content: "\f1cb"; content: "\f1cb";
} }
.icon-reply::before { .icon-reply-filled::before {
content: "\f1cc"; content: "\f1cc";
} }
.icon-revenue-split::before { .icon-reply::before {
content: "\f1cd"; content: "\f1cd";
} }
.icon-revote::before { .icon-revenue-split::before {
content: "\f1ce"; content: "\f1ce";
} }
.icon-save-story::before { .icon-revote::before {
content: "\f1cf"; content: "\f1cf";
} }
.icon-saved-messages::before { .icon-save-story::before {
content: "\f1d0"; content: "\f1d0";
} }
.icon-schedule::before { .icon-saved-messages::before {
content: "\f1d1"; content: "\f1d1";
} }
.icon-scheduled::before { .icon-schedule::before {
content: "\f1d2"; content: "\f1d2";
} }
.icon-sd-photo::before { .icon-scheduled::before {
content: "\f1d3"; content: "\f1d3";
} }
.icon-search::before { .icon-sd-photo::before {
content: "\f1d4"; content: "\f1d4";
} }
.icon-select::before { .icon-search::before {
content: "\f1d5"; content: "\f1d5";
} }
.icon-sell-outline::before { .icon-select::before {
content: "\f1d6"; content: "\f1d6";
} }
.icon-sell::before { .icon-sell-outline::before {
content: "\f1d7"; content: "\f1d7";
} }
.icon-send-outline::before { .icon-sell::before {
content: "\f1d8"; content: "\f1d8";
} }
.icon-send::before { .icon-send-outline::before {
content: "\f1d9"; content: "\f1d9";
} }
.icon-settings-filled::before { .icon-send::before {
content: "\f1da"; content: "\f1da";
} }
.icon-settings::before { .icon-settings-filled::before {
content: "\f1db"; content: "\f1db";
} }
.icon-share-filled::before { .icon-settings::before {
content: "\f1dc"; content: "\f1dc";
} }
.icon-share-screen-outlined::before { .icon-share-filled::before {
content: "\f1dd"; content: "\f1dd";
} }
.icon-share-screen-stop::before { .icon-share-screen-outlined::before {
content: "\f1de"; content: "\f1de";
} }
.icon-share-screen::before { .icon-share-screen-stop::before {
content: "\f1df"; content: "\f1df";
} }
.icon-show-message::before { .icon-share-screen::before {
content: "\f1e0"; content: "\f1e0";
} }
.icon-sidebar::before { .icon-show-message::before {
content: "\f1e1"; content: "\f1e1";
} }
.icon-skip-next::before { .icon-sidebar::before {
content: "\f1e2"; content: "\f1e2";
} }
.icon-skip-previous::before { .icon-skip-next::before {
content: "\f1e3"; content: "\f1e3";
} }
.icon-smallscreen::before { .icon-skip-previous::before {
content: "\f1e4"; content: "\f1e4";
} }
.icon-smile::before { .icon-smallscreen::before {
content: "\f1e5"; content: "\f1e5";
} }
.icon-sort-by-date::before { .icon-smile::before {
content: "\f1e6"; content: "\f1e6";
} }
.icon-sort-by-number::before { .icon-sort-by-date::before {
content: "\f1e7"; content: "\f1e7";
} }
.icon-sort-by-price::before { .icon-sort-by-number::before {
content: "\f1e8"; content: "\f1e8";
} }
.icon-sort::before { .icon-sort-by-price::before {
content: "\f1e9"; content: "\f1e9";
} }
.icon-speaker-muted-story::before { .icon-sort::before {
content: "\f1ea"; content: "\f1ea";
} }
.icon-speaker-outline::before { .icon-speaker-muted-story::before {
content: "\f1eb"; content: "\f1eb";
} }
.icon-speaker-story::before { .icon-speaker-outline::before {
content: "\f1ec"; content: "\f1ec";
} }
.icon-speaker::before { .icon-speaker-story::before {
content: "\f1ed"; content: "\f1ed";
} }
.icon-spoiler-disable::before { .icon-speaker::before {
content: "\f1ee"; content: "\f1ee";
} }
.icon-spoiler::before { .icon-spoiler-disable::before {
content: "\f1ef"; content: "\f1ef";
} }
.icon-sport::before { .icon-spoiler::before {
content: "\f1f0"; content: "\f1f0";
} }
.icon-star::before { .icon-sport::before {
content: "\f1f1"; content: "\f1f1";
} }
.icon-stars-lock::before { .icon-star::before {
content: "\f1f2"; content: "\f1f2";
} }
.icon-stars-refund::before { .icon-stars-lock::before {
content: "\f1f3"; content: "\f1f3";
} }
.icon-stats::before { .icon-stars-refund::before {
content: "\f1f4"; content: "\f1f4";
} }
.icon-stealth-future::before { .icon-stats::before {
content: "\f1f5"; content: "\f1f5";
} }
.icon-stealth-past::before { .icon-stealth-future::before {
content: "\f1f6"; content: "\f1f6";
} }
.icon-stickers::before { .icon-stealth-past::before {
content: "\f1f7"; content: "\f1f7";
} }
.icon-stop-raising-hand::before { .icon-stickers::before {
content: "\f1f8"; content: "\f1f8";
} }
.icon-stop::before { .icon-stop-raising-hand::before {
content: "\f1f9"; content: "\f1f9";
} }
.icon-story-caption::before { .icon-stop::before {
content: "\f1fa"; content: "\f1fa";
} }
.icon-story-expired::before { .icon-story-caption::before {
content: "\f1fb"; content: "\f1fb";
} }
.icon-story-priority::before { .icon-story-expired::before {
content: "\f1fc"; content: "\f1fc";
} }
.icon-story-reply::before { .icon-story-priority::before {
content: "\f1fd"; content: "\f1fd";
} }
.icon-strikethrough::before { .icon-story-reply::before {
content: "\f1fe"; content: "\f1fe";
} }
.icon-tag-add::before { .icon-strikethrough::before {
content: "\f1ff"; content: "\f1ff";
} }
.icon-tag-crossed::before { .icon-tag-add::before {
content: "\f200"; content: "\f200";
} }
.icon-tag-filter::before { .icon-tag-crossed::before {
content: "\f201"; content: "\f201";
} }
.icon-tag-name::before { .icon-tag-filter::before {
content: "\f202"; content: "\f202";
} }
.icon-tag::before { .icon-tag-name::before {
content: "\f203"; content: "\f203";
} }
.icon-timer::before { .icon-tag::before {
content: "\f204"; content: "\f204";
} }
.icon-toncoin::before { .icon-timer::before {
content: "\f205"; content: "\f205";
} }
.icon-trade::before { .icon-toncoin::before {
content: "\f206"; content: "\f206";
} }
.icon-transcribe::before { .icon-trade::before {
content: "\f207"; content: "\f207";
} }
.icon-truck::before { .icon-transcribe::before {
content: "\f208"; content: "\f208";
} }
.icon-unarchive::before { .icon-truck::before {
content: "\f209"; content: "\f209";
} }
.icon-underlined::before { .icon-unarchive::before {
content: "\f20a"; content: "\f20a";
} }
.icon-understood::before { .icon-underlined::before {
content: "\f20b"; content: "\f20b";
} }
.icon-unique-profile::before { .icon-understood::before {
content: "\f20c"; content: "\f20c";
} }
.icon-unlist-outline::before { .icon-unique-profile::before {
content: "\f20d"; content: "\f20d";
} }
.icon-unlist::before { .icon-unlist-outline::before {
content: "\f20e"; content: "\f20e";
} }
.icon-unlock-badge::before { .icon-unlist::before {
content: "\f20f"; content: "\f20f";
} }
.icon-unlock::before { .icon-unlock-badge::before {
content: "\f210"; content: "\f210";
} }
.icon-unmute::before { .icon-unlock::before {
content: "\f211"; content: "\f211";
} }
.icon-unpin::before { .icon-unmute::before {
content: "\f212"; content: "\f212";
} }
.icon-unread::before { .icon-unpin::before {
content: "\f213"; content: "\f213";
} }
.icon-up::before { .icon-unread::before {
content: "\f214"; content: "\f214";
} }
.icon-user-filled::before { .icon-up::before {
content: "\f215"; content: "\f215";
} }
.icon-user-online::before { .icon-user-filled::before {
content: "\f216"; content: "\f216";
} }
.icon-user-stars::before { .icon-user-online::before {
content: "\f217"; content: "\f217";
} }
.icon-user::before { .icon-user-stars::before {
content: "\f218"; content: "\f218";
} }
.icon-video-outlined::before { .icon-user::before {
content: "\f219"; content: "\f219";
} }
.icon-video-stop::before { .icon-video-outlined::before {
content: "\f21a"; content: "\f21a";
} }
.icon-video::before { .icon-video-stop::before {
content: "\f21b"; content: "\f21b";
} }
.icon-view-once::before { .icon-video::before {
content: "\f21c"; content: "\f21c";
} }
.icon-voice-chat::before { .icon-view-once::before {
content: "\f21d"; content: "\f21d";
} }
.icon-volume-1::before { .icon-voice-chat::before {
content: "\f21e"; content: "\f21e";
} }
.icon-volume-2::before { .icon-volume-1::before {
content: "\f21f"; content: "\f21f";
} }
.icon-volume-3::before { .icon-volume-2::before {
content: "\f220"; content: "\f220";
} }
.icon-warning::before { .icon-volume-3::before {
content: "\f221"; content: "\f221";
} }
.icon-web::before { .icon-warning::before {
content: "\f222"; content: "\f222";
} }
.icon-webapp::before { .icon-web::before {
content: "\f223"; content: "\f223";
} }
.icon-word-wrap::before { .icon-webapp::before {
content: "\f224"; content: "\f224";
} }
.icon-zoom-in::before { .icon-word-wrap::before {
content: "\f225"; content: "\f225";
} }
.icon-zoom-out::before { .icon-zoom-in::before {
content: "\f226"; content: "\f226";
} }
.icon-zoom-out::before {
content: "\f227";
}

View File

@ -166,148 +166,149 @@ $icons-map: (
"nochannel": "\f194", "nochannel": "\f194",
"noise-suppression": "\f195", "noise-suppression": "\f195",
"non-contacts": "\f196", "non-contacts": "\f196",
"one-filled": "\f197", "note": "\f197",
"open-in-new-tab": "\f198", "one-filled": "\f198",
"password-off": "\f199", "open-in-new-tab": "\f199",
"pause": "\f19a", "password-off": "\f19a",
"permissions": "\f19b", "pause": "\f19b",
"phone-discard-outline": "\f19c", "permissions": "\f19c",
"phone-discard": "\f19d", "phone-discard-outline": "\f19d",
"phone": "\f19e", "phone-discard": "\f19e",
"photo": "\f19f", "phone": "\f19f",
"pin-badge": "\f1a0", "photo": "\f1a0",
"pin-list": "\f1a1", "pin-badge": "\f1a1",
"pin": "\f1a2", "pin-list": "\f1a2",
"pinned-chat": "\f1a3", "pin": "\f1a3",
"pinned-message": "\f1a4", "pinned-chat": "\f1a4",
"pip": "\f1a5", "pinned-message": "\f1a5",
"play-story": "\f1a6", "pip": "\f1a6",
"play": "\f1a7", "play-story": "\f1a7",
"poll": "\f1a8", "play": "\f1a8",
"previous": "\f1a9", "poll": "\f1a9",
"privacy-policy": "\f1aa", "previous": "\f1aa",
"proof-of-ownership": "\f1ab", "privacy-policy": "\f1ab",
"quote-text": "\f1ac", "proof-of-ownership": "\f1ac",
"quote": "\f1ad", "quote-text": "\f1ad",
"radial-badge": "\f1ae", "quote": "\f1ae",
"rating-icons-level1": "\f1af", "radial-badge": "\f1af",
"rating-icons-level10": "\f1b0", "rating-icons-level1": "\f1b0",
"rating-icons-level2": "\f1b1", "rating-icons-level10": "\f1b1",
"rating-icons-level20": "\f1b2", "rating-icons-level2": "\f1b2",
"rating-icons-level3": "\f1b3", "rating-icons-level20": "\f1b3",
"rating-icons-level30": "\f1b4", "rating-icons-level3": "\f1b4",
"rating-icons-level4": "\f1b5", "rating-icons-level30": "\f1b5",
"rating-icons-level40": "\f1b6", "rating-icons-level4": "\f1b6",
"rating-icons-level5": "\f1b7", "rating-icons-level40": "\f1b7",
"rating-icons-level50": "\f1b8", "rating-icons-level5": "\f1b8",
"rating-icons-level6": "\f1b9", "rating-icons-level50": "\f1b9",
"rating-icons-level60": "\f1ba", "rating-icons-level6": "\f1ba",
"rating-icons-level7": "\f1bb", "rating-icons-level60": "\f1bb",
"rating-icons-level70": "\f1bc", "rating-icons-level7": "\f1bc",
"rating-icons-level8": "\f1bd", "rating-icons-level70": "\f1bd",
"rating-icons-level80": "\f1be", "rating-icons-level8": "\f1be",
"rating-icons-level9": "\f1bf", "rating-icons-level80": "\f1bf",
"rating-icons-level90": "\f1c0", "rating-icons-level9": "\f1c0",
"rating-icons-negative": "\f1c1", "rating-icons-level90": "\f1c1",
"readchats": "\f1c2", "rating-icons-negative": "\f1c2",
"recent": "\f1c3", "readchats": "\f1c3",
"refund": "\f1c4", "recent": "\f1c4",
"reload": "\f1c5", "refund": "\f1c5",
"remove-quote": "\f1c6", "reload": "\f1c6",
"remove": "\f1c7", "remove-quote": "\f1c7",
"reopen-topic": "\f1c8", "remove": "\f1c8",
"replace": "\f1c9", "reopen-topic": "\f1c9",
"replies": "\f1ca", "replace": "\f1ca",
"reply-filled": "\f1cb", "replies": "\f1cb",
"reply": "\f1cc", "reply-filled": "\f1cc",
"revenue-split": "\f1cd", "reply": "\f1cd",
"revote": "\f1ce", "revenue-split": "\f1ce",
"save-story": "\f1cf", "revote": "\f1cf",
"saved-messages": "\f1d0", "save-story": "\f1d0",
"schedule": "\f1d1", "saved-messages": "\f1d1",
"scheduled": "\f1d2", "schedule": "\f1d2",
"sd-photo": "\f1d3", "scheduled": "\f1d3",
"search": "\f1d4", "sd-photo": "\f1d4",
"select": "\f1d5", "search": "\f1d5",
"sell-outline": "\f1d6", "select": "\f1d6",
"sell": "\f1d7", "sell-outline": "\f1d7",
"send-outline": "\f1d8", "sell": "\f1d8",
"send": "\f1d9", "send-outline": "\f1d9",
"settings-filled": "\f1da", "send": "\f1da",
"settings": "\f1db", "settings-filled": "\f1db",
"share-filled": "\f1dc", "settings": "\f1dc",
"share-screen-outlined": "\f1dd", "share-filled": "\f1dd",
"share-screen-stop": "\f1de", "share-screen-outlined": "\f1de",
"share-screen": "\f1df", "share-screen-stop": "\f1df",
"show-message": "\f1e0", "share-screen": "\f1e0",
"sidebar": "\f1e1", "show-message": "\f1e1",
"skip-next": "\f1e2", "sidebar": "\f1e2",
"skip-previous": "\f1e3", "skip-next": "\f1e3",
"smallscreen": "\f1e4", "skip-previous": "\f1e4",
"smile": "\f1e5", "smallscreen": "\f1e5",
"sort-by-date": "\f1e6", "smile": "\f1e6",
"sort-by-number": "\f1e7", "sort-by-date": "\f1e7",
"sort-by-price": "\f1e8", "sort-by-number": "\f1e8",
"sort": "\f1e9", "sort-by-price": "\f1e9",
"speaker-muted-story": "\f1ea", "sort": "\f1ea",
"speaker-outline": "\f1eb", "speaker-muted-story": "\f1eb",
"speaker-story": "\f1ec", "speaker-outline": "\f1ec",
"speaker": "\f1ed", "speaker-story": "\f1ed",
"spoiler-disable": "\f1ee", "speaker": "\f1ee",
"spoiler": "\f1ef", "spoiler-disable": "\f1ef",
"sport": "\f1f0", "spoiler": "\f1f0",
"star": "\f1f1", "sport": "\f1f1",
"stars-lock": "\f1f2", "star": "\f1f2",
"stars-refund": "\f1f3", "stars-lock": "\f1f3",
"stats": "\f1f4", "stars-refund": "\f1f4",
"stealth-future": "\f1f5", "stats": "\f1f5",
"stealth-past": "\f1f6", "stealth-future": "\f1f6",
"stickers": "\f1f7", "stealth-past": "\f1f7",
"stop-raising-hand": "\f1f8", "stickers": "\f1f8",
"stop": "\f1f9", "stop-raising-hand": "\f1f9",
"story-caption": "\f1fa", "stop": "\f1fa",
"story-expired": "\f1fb", "story-caption": "\f1fb",
"story-priority": "\f1fc", "story-expired": "\f1fc",
"story-reply": "\f1fd", "story-priority": "\f1fd",
"strikethrough": "\f1fe", "story-reply": "\f1fe",
"tag-add": "\f1ff", "strikethrough": "\f1ff",
"tag-crossed": "\f200", "tag-add": "\f200",
"tag-filter": "\f201", "tag-crossed": "\f201",
"tag-name": "\f202", "tag-filter": "\f202",
"tag": "\f203", "tag-name": "\f203",
"timer": "\f204", "tag": "\f204",
"toncoin": "\f205", "timer": "\f205",
"trade": "\f206", "toncoin": "\f206",
"transcribe": "\f207", "trade": "\f207",
"truck": "\f208", "transcribe": "\f208",
"unarchive": "\f209", "truck": "\f209",
"underlined": "\f20a", "unarchive": "\f20a",
"understood": "\f20b", "underlined": "\f20b",
"unique-profile": "\f20c", "understood": "\f20c",
"unlist-outline": "\f20d", "unique-profile": "\f20d",
"unlist": "\f20e", "unlist-outline": "\f20e",
"unlock-badge": "\f20f", "unlist": "\f20f",
"unlock": "\f210", "unlock-badge": "\f210",
"unmute": "\f211", "unlock": "\f211",
"unpin": "\f212", "unmute": "\f212",
"unread": "\f213", "unpin": "\f213",
"up": "\f214", "unread": "\f214",
"user-filled": "\f215", "up": "\f215",
"user-online": "\f216", "user-filled": "\f216",
"user-stars": "\f217", "user-online": "\f217",
"user": "\f218", "user-stars": "\f218",
"video-outlined": "\f219", "user": "\f219",
"video-stop": "\f21a", "video-outlined": "\f21a",
"video": "\f21b", "video-stop": "\f21b",
"view-once": "\f21c", "video": "\f21c",
"voice-chat": "\f21d", "view-once": "\f21d",
"volume-1": "\f21e", "voice-chat": "\f21e",
"volume-2": "\f21f", "volume-1": "\f21f",
"volume-3": "\f220", "volume-2": "\f220",
"warning": "\f221", "volume-3": "\f221",
"web": "\f222", "warning": "\f222",
"webapp": "\f223", "web": "\f223",
"word-wrap": "\f224", "webapp": "\f224",
"zoom-in": "\f225", "word-wrap": "\f225",
"zoom-out": "\f226", "zoom-in": "\f226",
"zoom-out": "\f227",
); );

Binary file not shown.

Binary file not shown.

View File

@ -149,6 +149,7 @@ export type FontIconName =
| 'nochannel' | 'nochannel'
| 'noise-suppression' | 'noise-suppression'
| 'non-contacts' | 'non-contacts'
| 'note'
| 'one-filled' | 'one-filled'
| 'open-in-new-tab' | 'open-in-new-tab'
| 'password-off' | 'password-off'

View File

@ -1716,6 +1716,9 @@ export interface LangPair {
'ConfirmBuyGiftForTonDescription': undefined; 'ConfirmBuyGiftForTonDescription': undefined;
'TitleGiftLocked': undefined; 'TitleGiftLocked': undefined;
'QuickPreview': undefined; 'QuickPreview': undefined;
'UserNoteTitle': undefined;
'UserNoteHint': undefined;
'EditUserNoteHint': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {