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_country?: string;
verify_age_min?: number;
contact_note_length_limit?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -187,6 +188,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
maxReactions: getLimit(appConfig, 'reactions_user_max', 'maxReactions'),
moreAccounts: DEFAULT_LIMITS.moreAccounts,
},
contactNoteLimit: appConfig.contact_note_length_limit,
hash,
storyViewersExpirePeriod: appConfig.story_viewers_expire_period,
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString(),

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import type { API_CHAT_TYPES } from '../../config';
import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiDocument, ApiFormattedText, ApiPhoto } from './messages';
import type {
ApiBotVerification,
ApiEmojiStatusType,
@ -82,6 +82,7 @@ export interface ApiUserFullInfo {
paidMessagesStars?: number;
settings?: ApiPeerSettings;
mainTab?: ApiProfileTab;
note?: ApiFormattedText;
}
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";
"GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**.";
"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 {
flex-shrink: 0;
@ -68,3 +70,44 @@
margin-top: 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 {
memo, useMemo,
memo, useMemo, useRef,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
@ -46,7 +46,9 @@ import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { ChatAnimationTypes } from '../../left/main/hooks';
import formatUsername from '../helpers/formatUsername';
import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
@ -60,6 +62,7 @@ import ListItem from '../../ui/ListItem';
import Skeleton from '../../ui/placeholder/Skeleton';
import Switcher from '../../ui/Switcher';
import CustomEmoji from '../CustomEmoji';
import Icon from '../icons/Icon';
import SafeLink from '../SafeLink';
import BusinessHours from './BusinessHours';
import UserBirthday from './UserBirthday';
@ -102,6 +105,7 @@ const DEFAULT_MAP_CONFIG = {
};
const BOT_VERIFICATION_ICON_SIZE = 16;
const MAX_LINES = 3;
const ChatExtra = ({
chatOrUserId,
@ -154,10 +158,19 @@ const ChatExtra = ({
businessWorkHours,
personalChannelMessageId,
birthday,
note,
} = userFullInfo || {};
const oldLang = useOldLang();
const lang = useLang();
const noteTextRef = useRef<HTMLDivElement>();
const {
isCollapsed: isNoteCollapsed,
isCollapsible: isNoteCollapsible,
setIsCollapsed: setIsNoteCollapsed,
} = useCollapsibleLines(noteTextRef, MAX_LINES, undefined);
useEffectWithPrevDeps(([prevPeerId]) => {
if (!peerId || prevPeerId === peerId) return;
if (user || (chat && isChatChannel(chat))) {
@ -241,6 +254,16 @@ const ChatExtra = ({
openSavedDialog({ chatId: chatOrUserId });
});
const canExpandNote = isNoteCollapsible && isNoteCollapsed;
const handleExpandNote = useLastCallback(() => {
setIsNoteCollapsed(false);
});
const handleToggleNote = useLastCallback(() => {
setIsNoteCollapsed((prev) => !prev);
});
function copy(text: string, entity: string) {
copyTextToClipboard(text);
showNotification({ message: `${entity} was copied` });
@ -458,6 +481,50 @@ const ChatExtra = ({
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
</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 && (
<ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}>
<span>{oldLang('SavedMessagesTab')}</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,15 +20,19 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
) {
const isFirstRenderRef = useRef(true);
const cutoutHeightRef = useRef<number | undefined>();
const fullHeightRef = useRef<number | undefined>();
const [isCollapsible, setIsCollapsible] = useState(!isDisabled);
const [isCollapsed, setIsCollapsed] = useState(isCollapsible);
useLayoutEffect(() => {
const element = (cutoutRef || ref).current;
const shouldUseStyleInExpand = !cutoutRef;
if (isDisabled || !element || isFirstRenderRef.current) return;
requestMutation(() => {
element.style.maxHeight = isCollapsed ? `${cutoutHeightRef.current}px` : '';
element.style.maxHeight = isCollapsed ? `${cutoutHeightRef.current}px` :
shouldUseStyleInExpand ? `${fullHeightRef.current}px` : ``;
});
}, [cutoutRef, isCollapsed, isDisabled, ref]);
@ -39,6 +43,7 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
const element = ref.current;
const { lineHeight, totalLines } = calcTextLineHeightAndCount(element);
fullHeightRef.current = element.scrollHeight;
if (totalLines > maxLinesBeforeCollapse) {
cutoutHeightRef.current = lineHeight * maxLinesBeforeCollapse;
setIsCollapsible(true);
@ -63,7 +68,9 @@ export default function useCollapsibleLines<T extends HTMLElement, C extends HTM
isFirstRenderRef.current = false;
const element = (cutoutRef || ref).current;
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.editCloseFriends#ba6705f0 id:Vector<long> = Bool;
contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers;
contacts.updateContactNote#139f63fb id:InputUser note:TextWithEntities = Bool;
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.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.getTopPeers",
"contacts.addContact",
"contacts.updateContactNote",
"contacts.resolvePhone",
"contacts.editCloseFriends",
"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_NOTE_LENGTH = 128;
export const DEFAULT_APP_CONFIG: ApiAppConfig = {
hash: 0,

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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