Profile Info: Support rating (#6140)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-09-09 20:26:05 +02:00
parent 201c510b11
commit 794e442d8b
44 changed files with 1226 additions and 152 deletions

View File

@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBirthday, ApiBirthday,
ApiPeerSettings, ApiPeerSettings,
ApiStarsRating,
ApiUser, ApiUser,
ApiUserFullInfo, ApiUserFullInfo,
ApiUserStatus, ApiUserStatus,
@ -27,6 +28,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,
}, },
users, users,
} = mtpUserFull; } = mtpUserFull;
@ -57,6 +59,9 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
botVerification: botVerification && buildApiBotVerification(botVerification), botVerification: botVerification && buildApiBotVerification(botVerification),
areAdsEnabled: sponsoredEnabled, areAdsEnabled: sponsoredEnabled,
starGiftCount: stargiftsCount, starGiftCount: stargiftsCount,
starsRating: starsRating && buildApiStarsRating(starsRating),
starsMyPendingRating: starsMyPendingRating && buildApiStarsRating(starsMyPendingRating),
starsMyPendingRatingDate,
isBotCanManageEmojiStatus: botCanManageEmojiStatus, isBotCanManageEmojiStatus: botCanManageEmojiStatus,
hasScheduledMessages: hasScheduled, hasScheduledMessages: hasScheduled,
paidMessagesStars: sendPaidMessagesStars?.toJSNumber(), paidMessagesStars: sendPaidMessagesStars?.toJSNumber(),
@ -182,3 +187,12 @@ export function buildApiUserStatuses(mtpUsers: GramJs.TypeUser[]) {
export function buildApiBirthday(birthday: GramJs.TypeBirthday): ApiBirthday { export function buildApiBirthday(birthday: GramJs.TypeBirthday): ApiBirthday {
return omitVirtualClassFields(birthday); return omitVirtualClassFields(birthday);
} }
export function buildApiStarsRating(starsRating: GramJs.StarsRating): ApiStarsRating {
return {
level: starsRating.level,
currentLevelStars: starsRating.currentLevelStars.toJSNumber(),
stars: starsRating.stars.toJSNumber(),
nextLevelStars: starsRating.nextLevelStars?.toJSNumber(),
};
}

View File

@ -281,3 +281,10 @@ export interface ApiDisallowedGiftsSettings {
shouldDisallowUniqueStarGifts?: true; shouldDisallowUniqueStarGifts?: true;
shouldDisallowPremiumGifts?: true; shouldDisallowPremiumGifts?: true;
} }
export interface ApiStarsRating {
level: number;
currentLevelStars: number;
stars: number;
nextLevelStars?: number;
}

View File

@ -4,7 +4,7 @@ import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from
import type { ApiPeerColor, ApiPeerSettings } from './chats'; import type { ApiPeerColor, ApiPeerSettings } from './chats';
import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiBotVerification } from './misc'; import type { ApiBotVerification } from './misc';
import type { ApiSavedStarGift } from './stars'; import type { ApiSavedStarGift, ApiStarsRating } from './stars';
export interface ApiUser { export interface ApiUser {
id: string; id: string;
@ -65,6 +65,9 @@ export interface ApiUserFullInfo {
businessWorkHours?: ApiBusinessWorkHours; businessWorkHours?: ApiBusinessWorkHours;
businessIntro?: ApiBusinessIntro; businessIntro?: ApiBusinessIntro;
starGiftCount?: number; starGiftCount?: number;
starsRating?: ApiStarsRating;
starsMyPendingRating?: ApiStarsRating;
starsMyPendingRatingDate?: number;
isBotCanManageEmojiStatus?: boolean; isBotCanManageEmojiStatus?: boolean;
isBotAccessEmojiGranted?: boolean; isBotAccessEmojiGranted?: boolean;
hasScheduledMessages?: boolean; hasScheduledMessages?: boolean;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M48.512 13a10 10 0 0 1 9.973 9.292c.731 10.345.201 18.298-1.69 23.973-2.526 7.577-14.793 15.84-20.846 15.734-5.774-.1-18.458-8.877-20.744-15.734-1.891-5.675-2.421-13.628-1.69-23.975A10 10 0 0 1 23.49 13z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><g ><path d="M49 11c6.627 0 12 5.373 12 12v23.843a9 9 0 0 1-5.13 8.126l-16.86 8.028a7 7 0 0 1-6.02 0L16.13 54.97A9 9 0 0 1 11 46.843V23c0-6.627 5.373-12 12-12z"/><path d="M23 15h26a8 8 0 0 1 8 8v23.843a5 5 0 0 1-2.85 4.514l-16.86 8.029a3 3 0 0 1-2.58 0l-16.86-8.029A5 5 0 0 1 15 46.843V23a8 8 0 0 1 8-8"/></g></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M44 11c9.389 0 17 7.611 17 17v16c0 9.389-7.611 17-17 17H28c-9.389 0-17-7.611-17-17V28c0-9.389 7.611-17 17-17z"/></svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M60.804 13.571a6 6 0 0 1 .196 1.52V50c0 6.075-4.925 11-11 11H22c-6.075 0-11-4.925-11-11V15.091a6 6 0 0 1 7.52-5.804L36 13.864l17.48-4.577a6 6 0 0 1 7.324 4.284"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M41.006 61.833Q49.502 56.412 55.5 46q5.314-9.225 7.343-19.241l.157-.803a9.45 9.45 0 0 0-6.862-10.882Q46.425 12.5 35.854 12.5q-10.236 0-19.531 2.413l-.883.236a9.45 9.45 0 0 0-6.868 10.389Q10.075 36.583 15.5 46q5.902 10.245 15.616 15.997a9.44 9.44 0 0 0 9.615.006z"/></svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M60.804 13.571a6 6 0 0 1 .196 1.52v32.183a9 9 0 0 1-5.29 8.2l-16.825 7.611a7 7 0 0 1-5.77 0l-16.825-7.61a9 9 0 0 1-5.29-8.2V15.09a6 6 0 0 1 7.52-5.804L36 13.865l17.48-4.578a6 6 0 0 1 7.324 4.284"/></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M47.199 13.639 58.361 24.8c6.185 6.185 6.185 16.213 0 22.398L47.2 58.361c-6.185 6.185-16.213 6.185-22.398 0L13.639 47.2c-6.185-6.185-6.185-16.213 0-22.398L24.8 13.639c6.185-6.185 16.213-6.185 22.398 0"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m39.8 7.94 8.307 6.798 9.369-1.672a6 6 0 0 1 6.918 4.631l.043.22a6 6 0 0 1 .002 2.103l-5.33 30.074A12 12 0 0 1 47.293 60H24.707a12 12 0 0 1-11.816-9.906L7.561 20.02a6 6 0 0 1 6.963-6.954l9.368 1.672 8.309-6.798a6 6 0 0 1 7.598 0"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M46.391 10.5a9.03 9.03 0 0 1 8.45 5.752L58.747 26.3a9 9 0 0 1 .16.445l3.64 11.046A9 9 0 0 1 59.5 47.73l-9.307 7.199-8.345 6.615a9.046 9.046 0 0 1-10.971.202l-8.579-6.301-9.548-6.867a9 9 0 0 1-3.392-9.827l3.29-11.291.06-.19 3.568-10.3a9.03 9.03 0 0 1 8.228-6.063l10.88-.368.122-.003z"/></svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m39.8 7.94 8.307 6.798 9.344-1.668a6 6 0 0 1 6.918 4.632l.043.22a6 6 0 0 1-.007 2.15L59.19 48.154a12 12 0 0 1-6.788 8.713L39.34 62.867a8 8 0 0 1-6.68 0l-13.062-6a12 12 0 0 1-6.788-8.714l-5.215-28.08a6 6 0 0 1 6.954-7.003l9.343 1.668 8.309-6.798a6 6 0 0 1 7.598 0"/></svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m40.7 10.268 15.122 8.792a9.42 9.42 0 0 1 4.678 8.148v17.584a9.42 9.42 0 0 1-4.678 8.148l-15.123 8.792a9.35 9.35 0 0 1-9.398 0L16.178 52.94a9.42 9.42 0 0 1-4.678-8.148V27.208a9.42 9.42 0 0 1 4.678-8.148l15.123-8.792a9.35 9.35 0 0 1 9.398 0"/></svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m42.967 9.658 5.377 5.327c.145.143.326.247.526.3l7.335 1.96c4.884 1.306 7.792 6.307 6.482 11.182l-1.958 7.288c-.05.187-.05.383 0 .57l1.958 7.288c1.31 4.875-1.598 9.876-6.482 11.181l-7.335 1.96c-.2.054-.381.158-.526.301l-5.377 5.327c-3.577 3.544-9.357 3.544-12.934 0l-5.377-5.327a1.2 1.2 0 0 0-.526-.3l-7.335-1.96c-4.884-1.306-7.792-6.307-6.482-11.182l1.958-7.288c.05-.187.05-.383 0-.57l-1.958-7.288c-1.31-4.875 1.598-9.876 6.482-11.181l7.335-1.96c.2-.054.381-.158.526-.301l5.377-5.327c3.577-3.544 9.357-3.544 12.934 0"/></svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m28.489 10 15.23.078a8.86 8.86 0 0 1 6.955 3.445l9.437 12.182a9.06 9.06 0 0 1 1.66 7.562L58.308 48.38a8.96 8.96 0 0 1-4.817 6.073l-13.755 6.664a8.8 8.8 0 0 1-7.766-.04l-13.69-6.803a8.97 8.97 0 0 1-4.755-6.12L10.21 33.007a9.05 9.05 0 0 1 1.734-7.546L21.5 13.375A8.86 8.86 0 0 1 28.49 10"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m53.1 13.65 1.873 5.163c.106.292.3.534.547.69l4.601 2.89c4.31 2.708 5.66 8.401 3.055 12.785l-2.798 4.709c-.16.27-.229.591-.191.91l.648 5.464c.603 5.086-2.952 9.74-8.01 10.41l-5.361.709c-.273.036-.53.166-.732.375l-3.793 3.922a9.215 9.215 0 0 1-13.113.165l-3.888-3.825a1.24 1.24 0 0 0-.739-.355l-5.377-.573c-5.074-.54-8.744-5.106-8.266-10.206l.514-5.479a1.45 1.45 0 0 0-.212-.904l-2.913-4.636c-2.712-4.317-1.501-10.042 2.738-12.857L16.212 20c.244-.162.433-.41.532-.705l1.746-5.208C20.124 9.21 25.357 6.563 30.215 8.2l5.134 1.729c.258.087.536.083.793-.01l5.09-1.86c4.816-1.758 10.115.757 11.868 5.592"/></svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M44.015 11a10 10 0 0 1 7.07 2.929l6.986 6.985A10 10 0 0 1 61 27.985v16.03a10 10 0 0 1-2.929 7.07l-6.985 6.986A10 10 0 0 1 44.015 61h-16.03a10 10 0 0 1-7.07-2.929l-6.986-6.985A10 10 0 0 1 11 44.015v-16.03a10 10 0 0 1 2.929-7.07l6.985-6.986A10 10 0 0 1 27.985 11z"/></svg>

After

Width:  |  Height:  |  Size: 363 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m42.992 9.395 3.545 3.277c.155.144.35.235.56.262l4.79.609a9.014 9.014 0 0 1 7.87 8.588l.189 4.824c.008.211.082.415.211.582l2.956 3.817a9.014 9.014 0 0 1-.508 11.638l-3.277 3.545a1 1 0 0 0-.262.56l-.609 4.79a9.014 9.014 0 0 1-8.588 7.87l-4.824.189a1 1 0 0 0-.582.211l-3.817 2.956a9.014 9.014 0 0 1-11.638-.508l-3.545-3.277a1 1 0 0 0-.56-.262l-4.79-.609a9.014 9.014 0 0 1-7.87-8.588l-.189-4.824a1 1 0 0 0-.211-.582l-2.956-3.817a9.014 9.014 0 0 1 .508-11.638l3.277-3.545c.144-.155.235-.35.262-.56l.609-4.79a9.014 9.014 0 0 1 8.588-7.87l4.824-.189c.211-.008.415-.082.582-.211l3.817-2.956a9.014 9.014 0 0 1 11.638.508"/></svg>

After

Width:  |  Height:  |  Size: 714 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="M36 11c13.807 0 25 11.193 25 25S49.807 61 36 61 11 49.807 11 36s11.193-25 25-25"/></svg>

After

Width:  |  Height:  |  Size: 181 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 72 72"><path d="m39.395 9.26 17.933 8.438a9.025 9.025 0 0 1 5.105 9.264l-2.336 19.111a9.03 9.03 0 0 1-3.503 6.098L41.421 63.676a8.97 8.97 0 0 1-10.842 0L15.406 52.171a9.03 9.03 0 0 1-3.503-6.098L9.567 26.962a9.025 9.025 0 0 1 5.105-9.264L32.605 9.26a7.97 7.97 0 0 1 6.79 0"/></svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M31 25.4 18.5 3.7c-1.1-1.9-3.8-1.9-4.9 0L1 25.4c-1.1 1.9.3 4.3 2.5 4.3h25.1c2.2 0 3.5-2.4 2.4-4.3"/></svg>

After

Width:  |  Height:  |  Size: 196 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M19.7 1.4C13.8 0 8 2.1 4.4 6.4V3.9c0-.6-.5-1.2-1.2-1.2-.6 0-1.2.5-1.2 1.2v5.4c0 .6.5 1.2 1.2 1.2h5.4c.6 0 1.2-.5 1.2-1.2 0-.6-.5-1.2-1.2-1.2H6.1c3.1-4 8.4-5.8 13.6-4.3 4.7 1.4 8.3 5.5 8.9 10.4 1 7.8-5 14.4-12.6 14.4-6.6 0-12.1-5.1-12.6-11.6 0-.6-.6-1.1-1.2-1.1-.7.1-1.2.7-1.2 1.4C1.7 25 8.1 31 16 31c8.9 0 16.1-7.8 14.9-17-.8-6-5.3-11.1-11.2-12.6"/><path d="M21.2 19c0-2.1-1-3.6-4.5-4.6-2.3-.7-3-1.1-3-2.2 0-.8.4-1.7 2-1.7 1.4 0 2 .6 2.2 1.2.1.3.4.5.7.5h1.1c.6 0 1-.6.8-1.1-.4-1-1.3-2-3-2.4-.3-.1-.5-.4-.5-.8s-.4-.8-.8-.8h-.6c-.4 0-.8.4-.8.8s-.2.7-.6.8c-1.8.5-3.1 1.7-3.1 3.7 0 2.1 1.2 3.4 4.6 4.4 2.2.6 2.8 1.3 2.8 2.5s-.8 2-2.5 2c-1.3 0-2.2-.5-2.6-1.6-.1-.3-.4-.5-.7-.5h-1c-.6 0-.9.6-.8 1.1q.75 2.1 3.3 2.7c.4.1.6.4.6.8v.3c0 .4.4.8.8.8h.6c.4 0 .8-.4.8-.8v-.3c0-.4.2-.7.6-.8 2.3-.5 3.6-1.9 3.6-4"/></svg>

After

Width:  |  Height:  |  Size: 895 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M8 18c2.4.4 2.4 3.4 4.3 4.5 1.8 1.1 4.7 1 5.8-1.1 1.5-2.9-.7-5.8-3.7-6.1-2.1-.2-3.9 1.4-6 1.4-.8 0-1.5-.6-1.6-1.4-.1-.7.1-1.2.6-1.7C9 12 11 11.1 12.5 10.9c1.9-.2 5 .1 7.5 1.1 1.1.5 2.9 1.7 3.6 2.6 1-.3-2.8-2.9-2.8-2.9s-.9-1.7-1.5-3c-.9-1.7-1.8-3-2.6-4.7-.4-.8-.4-2.1.4-2.7.3-.2.7-.3 1.1-.3 1.8.2 2.7 1.7 3.6 3C23 5.8 24 7.7 24.9 9.6c1 2.2 2.4 4.4 2.5 6.9.2 2.6-1.1 5.5-1.9 8-.5 1.6-1.3 3.4-2.7 4.5s-3.4 1.5-5.1 1.6c-2.3.1-5 .1-7.1-.8S7.6 27 6.5 25c-.9-1.6-2.1-3.9-1.4-5.7.6-1 1.7-1.5 2.9-1.3"/><path d="m13.3 3.7-.6.3c-.7.6-.3 1.7 0 2.4.3 1 .6 1.6 1.2 2.4.1.1.5.9.6.9 2.4.2 4.7 1.1 4.7 1.1s-1-1.9-1.3-2.3c-.3-.6-.6-1.1-1-1.6-.5-.8-.9-1.6-1.5-2.3-.5-.7-1.1-1.1-2-.9z"/></svg>

After

Width:  |  Height:  |  Size: 764 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M19.5 26.4c-.5 0-.9-.4-1-.8-.4-2.1-2.2-3.6-4.4-3.6H7.6c-2.2 0-4 1.6-4.4 3.6-.1.5-.5.8-1 .8-.6 0-1.1-.6-1-1.2.5-3 3.2-5.3 6.4-5.3h6.6c3.2 0 5.8 2.3 6.4 5.3 0 .7-.5 1.2-1.1 1.2m10.3 0c-.5 0-.9-.4-1-.8-.4-2.1-2.2-3.6-4.4-3.6h-2.8l-1.5-2h4.3c3.2 0 5.8 2.3 6.4 5.3.1.6-.4 1.1-1 1.1M15.9 9.3h-2.3c-.2 0-.4-.1-.4-.3l-.7-2.2c-.2-.7-.8-1.1-1.6-1.1s-1.4.3-1.6 1l-.7 2.2c-.1.2-.2.4-.4.4H5.8c-.7 0-1.3.5-1.6 1.1s0 1.4.6 1.8l1.9 1.4c.2.1.2.3.2.5l-.7 2.2c-.2.7 0 1.4.6 1.8s1.3.4 1.9 0l1.9-1.4c.2-.1.4-.1.5 0l1.9 1.4c.3.2.6.3 1 .3s.7-.1 1-.3c.6-.4.8-1.1.6-1.8l-.7-2.2c-.1-.2 0-.4.2-.5l1.9-1.4c.6-.4.8-1.1.6-1.8s-1-1.1-1.7-1.1m-2.3 3.1c-.6.4-.8 1.2-.6 1.8l.7 2.2-1.9-1.4c-.3-.2-.6-.3-1-.3s-.7.1-1 .3L8 16.3l.6-1.7c.1-.2.2-.4.3-.5l1.9-1.6-2.3.1-.4-.2-1.8-1.3h2.3c.7 0 1.3-.5 1.6-1.1l.7-2.2.7 2.2c.2.7.8 1.1 1.6 1.1h2.3zm16-.6c-.2-.6-.8-1-1.4-1h-1.4c-.3 0-.6-.2-.6-.5L25.7 9c-.2-.6-.8-1-1.4-1s-1.2.4-1.4 1l-.4 1.3c-.1.3-.4.5-.6.5h-1.4c-.6 0-1.2.4-1.4 1s0 1.3.5 1.6l1.1.8c.2.2.3.5.2.8l-.4 1.3c-.2.6 0 1.3.5 1.6q.45.3.9.3c.45 0 .6-.1.9-.3L24 17c.2-.2.5-.2.7 0l1.2.9c.5.4 1.2.4 1.7 0s.7-1 .5-1.6l-.4-1.3c-.1-.3 0-.6.2-.8l1.1-.8c.5-.3.7-1 .6-1.6"/><path d="M25.8 13.6c-.3.2-.4.6-.3.9L26 16l-1.3-.9c-.3-.2-.7-.2-1 0l-1.2.9.4-1.4c0-.1.1-.2.2-.3l1.2-1-1.4.1c-.1 0-.3 0-.4-.1l-1.1-.8H23c.4 0 .7-.2.8-.6l.5-1.5.5 1.5c.1.3.4.6.8.6h1.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M31 25.4 18.5 3.7c-1.1-1.9-3.8-1.9-4.9 0L1 25.4c-1.1 1.9.3 4.3 2.5 4.3h25.1c2.2 0 3.5-2.4 2.4-4.3m-15 .2c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2m2-7c0 1.2-1.1 2.2-2.3 2-1-.1-1.7-1.1-1.7-2.1v-3.7c0-1 .7-2 1.7-2.1 1.2-.2 2.3.8 2.3 2z"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -2200,3 +2200,22 @@
"PublicPostsSubscribeToPremium" = "Subscribe to Premium"; "PublicPostsSubscribeToPremium" = "Subscribe to Premium";
"NotificationPaidExtraSearch" = "{stars} spent on extra search."; "NotificationPaidExtraSearch" = "{stars} spent on extra search.";
"PostsSearchTransaction" = "Posts Search"; "PostsSearchTransaction" = "Posts Search";
"TitleRating" = "Rating";
"RatingReflectsActivity" = "This rating reflects {name}'s activity on Telegram. It is based on:";
"RatingYourReflectsActivity" = "This rating reflects your activity on Telegram. It is based on:";
"RatingGiftsFromTelegram" = "Gifts from Telegram";
"RatingGiftsFromTelegramDesc" = "100% of the Stars spent on gifts purchased from Telegram.";
"RatingGiftsAndPostsFromUsers" = "Gifts and Posts from Users";
"RatingGiftsAndPostsFromUsersDesc" = "20% of the Stars spent on gifts or posts from users and channels.";
"RatingRefundsAndConversions" = "Refunds and Conversions";
"RatingRefundsAndConversionsDesc" = "10x of refunded Stars and 85% of bought gifts converted to Stars.";
"RatingBadgeAdded" = "Added";
"RatingBadgeDeducted" = "Deducted";
"RatingLevel" = "Level {level}";
"RatingNegativeLevel" = "Negative Rating";
"DescriptionPendingRating_one" = "Rating updates in {time} from purchases. {points} point is pending. {link}";
"DescriptionPendingRating_other" = "Rating updates in {time} from purchases. {points} points are pending. {link}";
"DescriptionFutureRating_one" = "This will be your rating in {time}, after {points} point is added. {link}";
"DescriptionFutureRating_other" = "This will be your rating in {time}, after {points} points are added. {link}";
"LinkDescriptionRatingBack" = "Back >";
"LinkDescriptionRatingPreview" = "Preview >";

View File

@ -103,3 +103,4 @@ export { default as InviteViaLinkModal } from '../components/modals/inviteViaLin
export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal'; export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal';
export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal'; export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal';
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';

View File

@ -1,5 +1,8 @@
/* stylelint-disable plugin/no-low-performance-animation-properties */
.root { .root {
--percent: calc(var(--progress, 0.5) * 100%); --percent: calc(var(--progress, 0.5) * 100%);
--color-negative-progress: #E05356;
position: relative; position: relative;
@ -73,21 +76,44 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 2rem;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border-radius: 1rem; border-radius: 1rem;
color: #ffffff; color: #ffffff;
background-color: #7E85FF; background-color: #7E85FF;
transition: width 0.25s ease-in-out;
&.noTransition {
transition: none;
}
}
.floatingBadgeContent {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
max-width: 20rem;
margin-inline: auto;
text-overflow: ellipsis;
white-space: nowrap;
} }
.floating-badge-triangle { .floating-badge-triangle {
position: absolute; position: absolute;
bottom: -4px; bottom: -4px;
left: calc(var(--tail-position, 0.5) * 100%);
transform: translateX(-50%);
display: inline-block; display: inline-block;
color: #7E85FF;
transition: left 0.3s ease;
} }
.floating-badge-icon { .floating-badge-icon {
@ -109,6 +135,10 @@
align-items: center; align-items: center;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
opacity: 1;
transition: opacity 0.15s ease;
} }
.left { .left {
@ -119,8 +149,21 @@
right: 0.75rem; right: 0.75rem;
} }
.progress { .progressWrapper {
--multiplier: calc(1 / var(--progress) - 1); position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
border-radius: 0.625rem;
}
.positiveLayer,
.positiveProgress {
--multiplier: calc(1 / var(--positive-progress) - 1);
position: absolute; position: absolute;
top: 0; top: 0;
@ -129,16 +172,18 @@
overflow: hidden; overflow: hidden;
width: max(var(--percent), 0.625rem); width: calc(var(--positive-progress) * 100%);
border-top-left-radius: 0.625rem;
border-bottom-left-radius: 0.625rem;
background-image: var(--premium-gradient); background-image: var(--premium-gradient);
background-size: calc(1 / var(--progress) * 100%) 100%; background-size: calc(1 / var(--positive-progress) * 100%) 100%;
transition: opacity 0.15s ease, width 0.2s, background-size 0.3s ease;
.left, .right { .left, .right {
color: white; color: white;
white-space: nowrap; white-space: nowrap;
opacity: 1;
transition: opacity 0.15s ease;
} }
.right { .right {
@ -146,12 +191,44 @@
} }
} }
.fullProgress { .negativeLayer,
border-radius: 0.625rem; .negativeProgress {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: auto;
overflow: hidden;
width: calc(var(--negative-progress) * 100%);
background-color: var(--color-negative-progress);
background-image: none;
transition: opacity 0.15s, width 0.2s;
.left, .right {
color: white;
white-space: nowrap;
opacity: 1;
transition: opacity 0.15s ease;
}
.right {
right: 0.75rem;
}
.left {
right: calc(100% - 0.75rem);
left: auto;
transition: right 0.3s ease;
}
} }
.primary { .primary {
.progress { .positiveLayer,
.positiveProgress {
background-color: var(--color-primary); background-color: var(--color-primary);
background-image: none; background-image: none;
} }
@ -159,4 +236,69 @@
.floating-badge { .floating-badge {
background-color: var(--color-primary); background-color: var(--color-primary);
} }
.floating-badge-triangle {
color: var(--color-primary);
}
}
.negative {
.floating-badge {
background-color: var(--color-negative-progress);
}
.floating-badge-triangle {
color: var(--color-negative-progress);
}
}
.transitioning {
.left,
.right {
opacity: 0;
}
}
.noTransition {
&.positiveProgress,
&.negativeProgress {
transition: opacity 0.15s !important;
}
}
.hidden {
opacity: 0 !important;
}
.cycling {
.badgeContainer {
transition-duration: var(--cycling-animation-badge-position);
}
.positiveProgress,
.negativeProgress {
transition: opacity 0.15s, width var(--cycling-animation-progress), background-size 0.3s;
}
}
.positiveLayer {
--positive-progress: var(--layer-progress);
z-index: 2;
opacity: 0;
&.show {
opacity: 1;
}
}
.negativeLayer {
--negative-progress: var(--layer-progress);
z-index: 2;
opacity: 0;
&.show {
opacity: 1;
}
} }

View File

@ -7,14 +7,22 @@ import type { IconName } from '../../types/icons';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
import { REM } from './helpers/mediaDimensions';
import { useTransitionActiveKey } from '../../hooks/animations/useTransitionActiveKey';
import useForceUpdate from '../../hooks/useForceUpdate';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePrevious from '../../hooks/usePrevious';
import useResizeObserver from '../../hooks/useResizeObserver'; import useResizeObserver from '../../hooks/useResizeObserver';
import useSyncEffect from '../../hooks/useSyncEffect';
import Transition from '../ui/Transition';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import styles from './PremiumProgress.module.scss'; import styles from './PremiumProgress.module.scss';
export type AnimationDirection = 'forward' | 'backward' | 'none';
type OwnProps = { type OwnProps = {
leftText?: string; leftText?: string;
rightText?: string; rightText?: string;
@ -22,6 +30,8 @@ type OwnProps = {
floatingBadgeText?: string; floatingBadgeText?: string;
progress?: number; progress?: number;
isPrimary?: boolean; isPrimary?: boolean;
isNegative?: boolean;
animationDirection?: AnimationDirection;
className?: string; className?: string;
}; };
@ -30,47 +40,216 @@ const PremiumProgress: FC<OwnProps> = ({
rightText, rightText,
floatingBadgeText, floatingBadgeText,
floatingBadgeIcon, floatingBadgeIcon,
progress, progress = 0,
isPrimary, isPrimary,
isNegative,
animationDirection = 'none',
className, className,
}) => { }) => {
const lang = useOldLang(); const lang = useOldLang();
const floatingBadgeRef = useRef<HTMLDivElement>(); const floatingBadgeContentRef = useRef<HTMLDivElement>();
const parentContainerRef = useRef<HTMLDivElement>(); const parentContainerRef = useRef<HTMLDivElement>();
const [shiftX, setShiftX] = useState(0); const [shiftX, setShiftX] = useState(0);
const [tailPosition, setTailPosition] = useState(0); const [beakPosition, setBeakPosition] = useState(0);
const [badgeWidth, setBadgeWidth] = useState(0);
const prevBadgeWidth = usePrevious(badgeWidth);
const [positiveProgress, setPositiveProgress] = useState(isNegative ? 0 : progress);
const [negativeProgress, setNegativeProgress] = useState(isNegative ? progress : 0);
const [badgeProgress, setBadgeProgress] = useState(progress);
const [layerProgress, setLayerProgress] = useState(0);
const [showLayer, setShowLayer] = useState(false);
const [disableMainProgressTransition, setDisableMainProgressTransition] = useState(false);
const [disableLayerProgressTransition, setDisableLayerProgressTransition] = useState(false);
const [hideMainLayer, setHideMainLayer] = useState(false);
const [isCycling, setIsCycling] = useState(false);
const badgeActiveKey = useTransitionActiveKey([floatingBadgeText, floatingBadgeIcon]);
const shouldAnimateCaptionsRef = useRef(false);
const prevLeftText = usePrevious(leftText);
const prevRightText = usePrevious(rightText);
const prevIsNegative = usePrevious(isNegative);
const BEAK_WIDTH_PX = 28;
const PROGRESS_BORDER_RADIUS_PX = REM;
const CORNER_BEAK_THRESHOLD = BEAK_WIDTH_PX / 2 + PROGRESS_BORDER_RADIUS_PX;
const BADGE_HORIZONTAL_PADDING_PX = 0.75 * 2 * REM;
const LAYER_PROGRESS_TRANSITION_MS = 400;
const FULL_CYCLE_TRANSITION_MS = LAYER_PROGRESS_TRANSITION_MS * 2;
const APPLY_TRANSITION_DELAY_MS = 50;
const updateBadgePosition = () => { const updateBadgePosition = () => {
if (floatingBadgeRef.current && parentContainerRef.current && progress !== undefined) { if (floatingBadgeContentRef.current && parentContainerRef.current) {
const badgeWidth = floatingBadgeRef.current.offsetWidth;
const parentWidth = parentContainerRef.current.offsetWidth; const parentWidth = parentContainerRef.current.offsetWidth;
const minShift = badgeWidth / 2; const halfBadgeWidth = badgeWidth / 2;
const maxShift = parentWidth - badgeWidth / 2; const minBadgeShift = halfBadgeWidth;
const currentShift = progress * parentWidth; const maxBadgeShift = parentWidth - halfBadgeWidth;
const safeShift = Math.max(minShift, Math.min(currentShift, maxShift)); const halfBeakWidth = BEAK_WIDTH_PX / 2;
const currentShift = isNegative ? (1 - badgeProgress) * parentWidth : badgeProgress * parentWidth;
let safeShift = Math.max(minBadgeShift, Math.min(currentShift, maxBadgeShift));
if (currentShift < CORNER_BEAK_THRESHOLD) {
safeShift = currentShift + halfBadgeWidth;
}
if (currentShift > parentWidth - CORNER_BEAK_THRESHOLD) {
safeShift = currentShift - halfBadgeWidth;
}
const beakOffsetFromCenter = currentShift - safeShift;
const newBeakPositionPx = halfBadgeWidth + beakOffsetFromCenter - halfBeakWidth;
setShiftX(safeShift / parentWidth); setShiftX(safeShift / parentWidth);
setBeakPosition(newBeakPositionPx);
let newTailPosition;
if (currentShift < minShift) {
newTailPosition = (progress * parentWidth) / (minShift * 2);
} else if (currentShift > maxShift) {
const progressMapped = (progress - (maxShift / parentWidth)) / (1 - maxShift / parentWidth);
newTailPosition = 0.5 + (progressMapped * 0.4);
} else {
newTailPosition = 0.5;
}
setTailPosition(newTailPosition);
} }
}; };
useEffect(updateBadgePosition, [progress]); useEffect(updateBadgePosition, [badgeProgress, badgeWidth, isNegative, CORNER_BEAK_THRESHOLD]);
useResizeObserver(parentContainerRef, updateBadgePosition); useResizeObserver(parentContainerRef, updateBadgePosition);
useEffect(() => {
const width = floatingBadgeContentRef?.current?.clientWidth || 0;
setBadgeWidth(width + BADGE_HORIZONTAL_PADDING_PX);
}, [floatingBadgeText, floatingBadgeIcon, BADGE_HORIZONTAL_PADDING_PX]);
const forceUpdate = useForceUpdate();
useSyncEffect(() => {
let timeoutId: number | undefined;
const isNegativeTransition = prevIsNegative !== undefined && prevIsNegative !== isNegative;
const shouldAnimateCaptions = (prevLeftText || prevRightText) && (isNegativeTransition || isCycling);
if (shouldAnimateCaptions && !shouldAnimateCaptionsRef.current) {
shouldAnimateCaptionsRef.current = true;
const timeoutMs = isCycling ? LAYER_PROGRESS_TRANSITION_MS * 2 : LAYER_PROGRESS_TRANSITION_MS;
timeoutId = window.setTimeout(() => {
shouldAnimateCaptionsRef.current = false;
forceUpdate();
}, timeoutMs);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
shouldAnimateCaptionsRef.current = false;
}
};
}, [
leftText, prevLeftText, rightText, prevRightText,
prevIsNegative, isNegative, animationDirection, isCycling,
]);
const shouldAnimateCaptions = shouldAnimateCaptionsRef.current;
useEffect(() => {
if (isNegative) {
setPositiveProgress(0);
setNegativeProgress(progress);
} else {
setNegativeProgress(0);
setPositiveProgress(progress);
}
setBadgeProgress(progress);
}, [progress, isNegative]);
const hasFloatingBadge = Boolean(floatingBadgeIcon || floatingBadgeText); const hasFloatingBadge = Boolean(floatingBadgeIcon || floatingBadgeText);
const isProgressFull = Boolean(progress) && progress > 0.99;
const displayLeftText = shouldAnimateCaptions ? prevLeftText : leftText;
const displayRightText = shouldAnimateCaptions ? prevRightText : rightText;
const prevProgress = usePrevious(progress);
useEffect(() => {
const timers: number[] = [];
if (animationDirection === 'none' || prevProgress === undefined) {
return;
}
const targetProgress = progress;
const setMainProgress = (value: number) => {
if (isNegative) {
setNegativeProgress(value);
} else {
setPositiveProgress(value);
}
};
if (animationDirection === 'forward' || animationDirection === 'backward') {
const isForward = animationDirection === 'forward';
setIsCycling(true);
setMainProgress(isForward ? 1 : 0);
setDisableLayerProgressTransition(true);
setLayerProgress(isForward ? 0 : 1);
timers.push(window.setTimeout(() => {
setDisableLayerProgressTransition(false);
setShowLayer(true);
setLayerProgress(targetProgress);
if (isForward) {
setDisableMainProgressTransition(true);
setMainProgress(0);
}
}, LAYER_PROGRESS_TRANSITION_MS));
timers.push(window.setTimeout(() => {
setDisableMainProgressTransition(true);
setDisableLayerProgressTransition(true);
setHideMainLayer(false);
setMainProgress(targetProgress);
setShowLayer(false);
timers.push(window.setTimeout(() => {
setDisableMainProgressTransition(false);
setDisableLayerProgressTransition(false);
setIsCycling(false);
}, APPLY_TRANSITION_DELAY_MS));
}, FULL_CYCLE_TRANSITION_MS));
}
return () => {
timers.forEach(clearTimeout);
};
}, [
progress, animationDirection, isNegative,
prevProgress, FULL_CYCLE_TRANSITION_MS,
]);
const renderProgressLayer = (
isPositive: boolean,
layerProgress: number,
layerClassName?: string,
disableTransition?: boolean,
) => {
const className = isPositive ? styles.positiveProgress : styles.negativeProgress;
const progressVar = '--layer-progress';
return (
<div
className={buildClassName(
className,
layerClassName,
disableTransition && styles.noTransition,
)}
style={`${progressVar}: ${layerProgress}`}
>
<div className={styles.left}>
<span>{displayLeftText}</span>
</div>
<div className={styles.right}>
<span>{displayRightText}</span>
</div>
</div>
);
};
return ( return (
<div <div
@ -79,46 +258,88 @@ const PremiumProgress: FC<OwnProps> = ({
styles.root, styles.root,
hasFloatingBadge && styles.withBadge, hasFloatingBadge && styles.withBadge,
isPrimary && styles.primary, isPrimary && styles.primary,
isNegative && styles.negative,
shouldAnimateCaptions && styles.transitioning,
isCycling && styles.cycling,
className, className,
)} )}
style={buildStyle( style={buildStyle(
progress !== undefined && `--progress: ${progress}`, `--positive-progress: ${positiveProgress}`,
tailPosition !== undefined && `--tail-position: ${tailPosition}`, `--negative-progress: ${negativeProgress}`,
`--layer-progress: ${layerProgress}`,
`--shift-x: ${shiftX}`, `--shift-x: ${shiftX}`,
`--cycling-animation-badge-position: ${FULL_CYCLE_TRANSITION_MS}ms`,
`--cycling-animation-progress: ${LAYER_PROGRESS_TRANSITION_MS}ms`,
)} )}
> >
{hasFloatingBadge && ( {hasFloatingBadge && (
<div className={styles.badgeContainer}> <div className={styles.badgeContainer}>
<div className={styles.floatingBadgeWrapper}> <div className={styles.floatingBadgeWrapper}>
<div className={styles.floatingBadge} ref={floatingBadgeRef}> <div
{floatingBadgeIcon && <Icon name={floatingBadgeIcon} className={styles.floatingBadgeIcon} />} className={
{floatingBadgeText && ( buildClassName(styles.floatingBadge,
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}> (!prevBadgeWidth || prevBadgeWidth === 0)
{floatingBadgeText} && styles.noTransition)
}
style={`width: ${badgeWidth}px;`}
>
<Transition
activeKey={badgeActiveKey}
name="fade"
shouldCleanup
>
<div
ref={floatingBadgeContentRef}
className={styles.floatingBadgeContent}
>
{floatingBadgeIcon && <Icon name={floatingBadgeIcon} className={styles.floatingBadgeIcon} />}
{floatingBadgeText && (
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
{floatingBadgeText}
</div>
)}
</div> </div>
)} </Transition>
</div> </div>
<div className={styles.floatingBadgeTriangle}> <div className={styles.floatingBadgeTriangle} style={`left: ${beakPosition}px`}>
<svg width="28" height="28" viewBox="0 0 28 28" fill="none"> <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<path d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z" fill="#7E85FF" /> <path d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z" fill="currentColor" />
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>
)} )}
<div className={styles.left}> <div className={styles.left}>
<span>{leftText}</span> <span>{displayLeftText}</span>
</div> </div>
<div className={styles.right}> <div className={styles.right}>
<span>{rightText}</span> <span>{displayRightText}</span>
</div> </div>
<div className={buildClassName(styles.progress, isProgressFull && styles.fullProgress)}>
<div className={styles.left}> <div className={styles.progressWrapper}>
<span>{leftText}</span> {renderProgressLayer(
</div> true,
<div className={styles.right}> positiveProgress,
<span>{rightText}</span> buildClassName(hideMainLayer && styles.hidden),
</div> disableMainProgressTransition,
)}
{renderProgressLayer(
false,
negativeProgress,
buildClassName(hideMainLayer && styles.hidden),
disableMainProgressTransition,
)}
{renderProgressLayer(
!isNegative,
layerProgress,
buildClassName(
isNegative ? styles.negativeLayer : styles.positiveLayer,
showLayer && styles.show,
),
disableLayerProgressTransition,
)}
</div> </div>
</div> </div>
); );

View File

@ -191,11 +191,64 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
.user-status { .userRatingNegativeWrapper,
.userRatingWrapper {
pointer-events: all;
cursor: pointer;
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 0.25rem;
font-size: 1rem;
color: #000000;
}
.userRatingWrapper {
width: 1rem;
font-size: 1.5rem;
}
.ratingNegativeIcon {
pointer-events: none;
font-size: 1rem;
color: var(--color-white);
-webkit-text-stroke: 1px #000000;
}
.ratingIcon {
pointer-events: none;
color: var(--color-white);
-webkit-text-stroke: 1px #000000;
}
.ratingLevel {
pointer-events: none;
position: absolute;
z-index: 1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.625rem;
font-weight: var(--font-weight-bold);
line-height: 1;
color: #000000;
}
.userStatus {
opacity: 0.5; opacity: 0.5;
} }
.get-status { .getStatus {
--blured-background-color: #c4c9cc42; --blured-background-color: #c4c9cc42;
pointer-events: all; pointer-events: all;

View File

@ -3,9 +3,10 @@ import { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserStatus, ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserFullInfo, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { AnimationLevel } from '../../types'; import type { AnimationLevel } from '../../types';
import type { IconName } from '../../types/icons';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { import {
@ -20,6 +21,7 @@ import {
selectThreadMessagesCount, selectThreadMessagesCount,
selectTopic, selectTopic,
selectUser, selectUser,
selectUserFullInfo,
selectUserStatus, selectUserStatus,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState.ts'; import { selectSharedSettings } from '../../global/selectors/sharedState.ts';
@ -40,12 +42,15 @@ import usePhotosPreload from './hooks/usePhotosPreload';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import Avatar from './Avatar'; import Avatar from './Avatar';
import FullNameTitle from './FullNameTitle'; import FullNameTitle from './FullNameTitle';
import Icon from './icons/Icon';
import ProfilePhoto from './ProfilePhoto'; import ProfilePhoto from './ProfilePhoto';
import TopicIcon from './TopicIcon'; import TopicIcon from './TopicIcon';
import './ProfileInfo.scss'; import './ProfileInfo.scss';
import styles from './ProfileInfo.module.scss'; import styles from './ProfileInfo.module.scss';
const MAX_LEVEL_ICON = 90;
type OwnProps = { type OwnProps = {
peerId: string; peerId: string;
forceShowSelf?: boolean; forceShowSelf?: boolean;
@ -56,6 +61,7 @@ type OwnProps = {
type StateProps = type StateProps =
{ {
user?: ApiUser; user?: ApiUser;
userFullInfo?: ApiUserFullInfo;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
chat?: ApiChat; chat?: ApiChat;
mediaIndex?: number; mediaIndex?: number;
@ -78,6 +84,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
forceShowSelf, forceShowSelf,
canPlayVideo, canPlayVideo,
user, user,
userFullInfo,
userStatus, userStatus,
chat, chat,
mediaIndex, mediaIndex,
@ -98,6 +105,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
openPrivacySettingsNoticeModal, openPrivacySettingsNoticeModal,
loadMoreProfilePhotos, loadMoreProfilePhotos,
openUniqueGiftBySlug, openUniqueGiftBySlug,
openProfileRatingModal,
} = getActions(); } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
@ -182,6 +190,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
openPrivacySettingsNoticeModal({ chatId: chat!.id, isReadDate: false }); openPrivacySettingsNoticeModal({ chatId: chat!.id, isReadDate: false });
}); });
const handleRatingClick = useLastCallback((level: number) => {
if (user) {
openProfileRatingModal({ userId: user.id, level });
}
});
function handleSelectFallbackPhoto() { function handleSelectFallbackPhoto() {
if (!isFirst) return; if (!isFirst) return;
setHasSlideAnimation(true); setHasSlideAnimation(true);
@ -268,6 +282,43 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
); );
} }
function renderUserRating() {
if (!userFullInfo?.starsRating) return undefined;
const level = userFullInfo.starsRating.level;
const isNegative = level < 0;
const onRatingClick = () => handleRatingClick(level);
if (isNegative) {
return (
<span className={styles.userRatingNegativeWrapper} onClick={onRatingClick}>
<Icon
name="rating-icons-negative"
className={styles.ratingNegativeIcon}
/>
<span className={styles.ratingLevel}>!</span>
</span>
);
}
const safeLevel = Math.max(level, 1);
const iconLevel = Math.min(safeLevel, MAX_LEVEL_ICON);
const iconName = (iconLevel < 10
? `rating-icons-level${iconLevel}`
: `rating-icons-level${Math.floor(iconLevel / 10) * 10}`) as IconName;
return (
<span className={styles.userRatingWrapper} onClick={onRatingClick}>
<Icon
name={iconName}
className={styles.ratingIcon}
/>
<span className={styles.ratingLevel}>{level}</span>
</span>
);
}
function renderStatus() { function renderStatus() {
const isAnonymousForwards = isAnonymousForwardsChat(peerId); const isAnonymousForwards = isAnonymousForwardsChat(peerId);
const isSystemBotChat = isSystemBot(peerId); const isSystemBotChat = isSystemBot(peerId);
@ -290,6 +341,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
isUserOnline(user, userStatus) && 'online', isUserOnline(user, userStatus) && 'online',
)} )}
> >
{renderUserRating()}
<span className={styles.userStatus} dir="auto"> <span className={styles.userStatus} dir="auto">
{getUserStatus(oldLang, user, userStatus)} {getUserStatus(oldLang, user, userStatus)}
</span> </span>
@ -400,6 +452,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { peerId }): StateProps => { (global, { peerId }): StateProps => {
const user = selectUser(global, peerId); const user = selectUser(global, peerId);
const userFullInfo = user ? selectUserFullInfo(global, peerId) : undefined;
const userStatus = selectUserStatus(global, peerId); const userStatus = selectUserStatus(global, peerId);
const chat = selectChat(global, peerId); const chat = selectChat(global, peerId);
const profilePhotos = selectPeerPhotos(global, peerId); const profilePhotos = selectPeerPhotos(global, peerId);
@ -415,6 +468,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
user, user,
userFullInfo,
userStatus, userStatus,
chat, chat,
mediaIndex, mediaIndex,

View File

@ -35,6 +35,7 @@ import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import PaidReactionModal from './paidReaction/PaidReactionModal.async'; import PaidReactionModal from './paidReaction/PaidReactionModal.async';
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async'; import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async'; import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async';
import ReportModal from './reportModal/ReportModal.async'; import ReportModal from './reportModal/ReportModal.async';
import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async'; import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async';
@ -93,7 +94,8 @@ type ModalKey = keyof Pick<TabState,
'priceConfirmModal' | 'priceConfirmModal' |
'isFrozenAccountModalOpen' | 'isFrozenAccountModalOpen' |
'deleteAccountModal' | 'deleteAccountModal' |
'isAgeVerificationModalOpen' 'isAgeVerificationModalOpen' |
'profileRatingModal'
>; >;
type StateProps = { type StateProps = {
@ -151,6 +153,7 @@ const MODALS: ModalRegistry = {
isFrozenAccountModalOpen: FrozenAccountModal, isFrozenAccountModalOpen: FrozenAccountModal,
deleteAccountModal: DeleteAccountModal, deleteAccountModal: DeleteAccountModal,
isAgeVerificationModalOpen: AgeVerificationModal, isAgeVerificationModalOpen: AgeVerificationModal,
profileRatingModal: ProfileRatingModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>; const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

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

View File

@ -0,0 +1,91 @@
.header {
width: 100%;
padding-top: 0;
padding-bottom: 1rem;
text-align: center;
}
.description {
margin: 0;
padding-inline: 1rem;
line-height: 1.25;
}
.descriptionPreview {
margin-bottom: 1rem;
font-size: 0.875rem;
line-height: 1.25;
color: var(--color-text-secondary);
p {
margin-bottom: 0;
margin-inline: 1rem;
}
}
.previewLink,
.backLink {
cursor: pointer;
color: var(--color-primary);
opacity: 1;
transition: opacity 0.15s;
&:hover {
opacity: 0.75;
}
}
.title {
margin-bottom: 4.75rem;
font-size: 1.5rem;
font-weight: var(--font-weight-medium);
line-height: 1.5rem;
}
.ratingProgress {
margin: 0 auto;
margin-bottom: 0.75rem;
&.withPreview {
margin-bottom: 0.5rem;
}
}
.subtitle {
line-height: 1.25;
}
.footer {
display: flex;
align-self: stretch;
margin-top: 1rem;
}
.badge {
transform: translateY(-1px);
display: inline-block;
margin-right: 0.25rem;
padding: 0.125rem 0.375rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
font-weight: var(--font-weight-medium);
line-height: 0.875rem;
color: var(--color-white);
text-transform: uppercase;
}
.understoodIcon {
margin-right: 0.25rem;
}
.badgeAdded {
background-color: var(--color-primary);
}
.badgeDeducted {
color: var(--color-background);
background-color: var(--color-text-secondary);
}

View File

@ -0,0 +1,260 @@
import { memo, useEffect, useMemo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiStarsRating, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { getPeerTitle } from '../../../global/helpers/peers';
import { selectUser, selectUserFullInfo } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatShortDuration } from '../../../util/dates/dateFormat';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import PremiumProgress, { type AnimationDirection } from '../../common/PremiumProgress';
import Button from '../../ui/Button';
import Transition from '../../ui/Transition';
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
import styles from './ProfileRatingModal.module.scss';
export type OwnProps = {
modal: TabState['profileRatingModal'];
};
type StateProps = {
user?: ApiUser;
currentUserId?: string;
starsRating?: ApiStarsRating;
pendingRating?: ApiStarsRating;
pendingRatingDate?: number;
};
const ProfileRatingModal = ({
modal,
user,
currentUserId,
starsRating,
pendingRating,
pendingRatingDate,
}: OwnProps & StateProps) => {
const {
closeProfileRatingModal,
} = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const [showFutureRating, setShowFutureRating] = useState(false);
const handleClose = useLastCallback(() => {
closeProfileRatingModal();
});
useEffect(() => {
if (!isOpen) {
setShowFutureRating(false);
}
}, [isOpen]);
const handleShowFuture = useLastCallback(() => {
setShowFutureRating(true);
});
const handleShowCurrent = useLastCallback(() => {
setShowFutureRating(false);
});
const renderBadge = (type: 'added' | 'deducted') => {
const isAdded = type === 'added';
const badgeText = isAdded ? lang('RatingBadgeAdded') : lang('RatingBadgeDeducted');
const badgeClass = isAdded ? styles.badgeAdded : styles.badgeDeducted;
return (
<span className={buildClassName(styles.badge, badgeClass)}>
{badgeText}
</span>
);
};
const header = useMemo(() => {
if (!modal || !user || !starsRating || !isOpen) return undefined;
const rating = showFutureRating && pendingRating ? pendingRating : starsRating;
const currentStars = rating.stars;
const currentLevelStars = rating.currentLevelStars;
const nextLevelStars = rating.nextLevelStars;
const currentLevel = rating.level;
const nextLevel = currentLevel + 1;
const isNegative = currentLevel < 0;
const pendingLevel = !showFutureRating && pendingRating ? pendingRating.level : starsRating.level;
let levelProgress = 0;
if (!nextLevelStars) {
levelProgress = 1;
} else if (nextLevelStars > currentLevelStars) {
levelProgress = Math.max(0.03, (currentStars - currentLevelStars) / (nextLevelStars - currentLevelStars));
} else {
levelProgress = 1;
}
const progress = isNegative ? 0.5 : Math.max(0, Math.min(1, levelProgress));
const waitTime = pendingRatingDate ? pendingRatingDate - Math.floor(Date.now() / 1000) : 0;
const pendingPoints = pendingRating ? pendingRating.stars - starsRating.stars : 0;
const shouldShowPreview = pendingRating && pendingRatingDate;
const renderPreviewDescription = () => {
if (!shouldShowPreview) return undefined;
return (
<Transition
name="fade"
className={styles.descriptionPreview}
activeKey={showFutureRating ? 1 : 0}
shouldCleanup
shouldRestoreHeight
>
{showFutureRating ? (
<p>
{lang('DescriptionFutureRating', {
time: formatShortDuration(lang, waitTime),
points: Math.abs(pendingPoints),
link: (
<span className={styles.backLink} onClick={handleShowCurrent}>
{lang('LinkDescriptionRatingBack')}
</span>
),
}, {
pluralValue: Math.abs(pendingPoints),
withNodes: true,
})}
</p>
) : (
<p>
{lang('DescriptionPendingRating', {
time: formatShortDuration(lang, waitTime),
points: Math.abs(pendingPoints),
link: (
<span className={styles.previewLink} onClick={handleShowFuture}>
{lang('LinkDescriptionRatingPreview')}
</span>
),
}, {
pluralValue: Math.abs(pendingPoints),
withNodes: true,
})}
</p>
)}
</Transition>
);
};
let animationDirection: AnimationDirection = 'none';
if (currentLevel >= 0 && pendingLevel >= 0 && currentLevel !== pendingLevel) {
animationDirection = currentLevel > pendingLevel ? 'forward' : 'backward';
}
if (currentLevel < 0 && pendingLevel < 0 && currentLevel !== pendingLevel) {
animationDirection = currentLevel < pendingLevel ? 'backward' : 'forward';
}
const userFallbackText = lang('ActionFallbackUser');
return (
<div className={styles.header}>
<div className={styles.title}>
{lang('TitleRating')}
</div>
<PremiumProgress
leftText={isNegative ? undefined : lang('RatingLevel', { level: currentLevel })}
rightText={isNegative ? lang('RatingNegativeLevel') : lang('RatingLevel', { level: nextLevel })}
floatingBadgeIcon={isNegative ? 'warning' : 'crown-wear'}
floatingBadgeText={isNegative ? currentLevel.toString()
: `${lang.number(currentStars)} / ${lang.number(nextLevelStars || currentStars)}`}
progress={progress}
isPrimary={currentLevel >= 0}
isNegative={currentLevel < 0}
animationDirection={animationDirection}
className={buildClassName(styles.ratingProgress, shouldShowPreview && styles.withPreview)}
/>
{renderPreviewDescription()}
<p className={styles.description}>
{user?.id === currentUserId
? lang('RatingYourReflectsActivity')
: lang('RatingReflectsActivity', { name: getPeerTitle(lang, user) || userFallbackText })}
</p>
</div>
);
}, [modal, user, currentUserId, starsRating,
pendingRating, pendingRatingDate, showFutureRating,
lang, handleShowFuture, handleShowCurrent, isOpen]);
const listItemData = [
['gift', lang('RatingGiftsFromTelegram'), (
<span className={styles.subtitle}>
{renderBadge('added')}
{lang('RatingGiftsFromTelegramDesc')}
</span>
)],
['user-stars', lang('RatingGiftsAndPostsFromUsers'), (
<span className={styles.subtitle}>
{renderBadge('added')}
{lang('RatingGiftsAndPostsFromUsersDesc')}
</span>
)],
['refund', lang('RatingRefundsAndConversions'), (
<span className={styles.subtitle}>
{renderBadge('deducted')}
{lang('RatingRefundsAndConversionsDesc')}
</span>
)],
] satisfies TableAboutData;
const footer = useMemo(() => {
if (!isOpen) return undefined;
return (
<div className={styles.footer}>
<Button
size="smaller"
onClick={handleClose}
>
<Icon name="understood" className={styles.understoodIcon} />
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang, isOpen, handleClose]);
return (
<TableAboutModal
isOpen={isOpen}
header={header}
listItemData={listItemData}
footer={footer}
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const currentUserId = global.currentUserId;
const user = modal?.userId ? selectUser(global, modal.userId) : undefined;
const userFullInfo = modal?.userId
? selectUserFullInfo(global, modal.userId) : undefined;
const starsRating = userFullInfo?.starsRating;
const pendingRating = userFullInfo?.starsMyPendingRating;
const pendingRatingDate = userFullInfo?.starsMyPendingRatingDate;
return {
user,
currentUserId,
starsRating,
pendingRating,
pendingRatingDate,
};
},
)(ProfileRatingModal));

View File

@ -64,3 +64,16 @@ addActionHandler('closeSuggestedStatusModal', (global, actions, payload): Action
}); });
addTabStateResetterAction('closeChatRefundModal', 'chatRefundModal'); addTabStateResetterAction('closeChatRefundModal', 'chatRefundModal');
addActionHandler('openProfileRatingModal', (global, actions, payload): ActionReturnType => {
const { userId, level, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
profileRatingModal: {
userId,
level,
},
}, tabId);
});
addTabStateResetterAction('closeProfileRatingModal', 'profileRatingModal');

View File

@ -1850,6 +1850,11 @@ export interface ActionPayloads {
userId: string; userId: string;
} & WithTabId; } & WithTabId;
closeChatRefundModal: WithTabId | undefined; closeChatRefundModal: WithTabId | undefined;
openProfileRatingModal: {
userId: string;
level: number;
} & WithTabId;
closeProfileRatingModal: WithTabId | undefined;
loadMoreProfilePhotos: { loadMoreProfilePhotos: {
peerId: string; peerId: string;
isPreload?: boolean; isPreload?: boolean;

View File

@ -852,6 +852,11 @@ export type TabState = {
duration?: number; duration?: number;
}; };
profileRatingModal?: {
userId: string;
level: number;
};
monetizationVerificationModal?: { monetizationVerificationModal?: {
chatId: string; chatId: string;
isLoading?: boolean; isLoading?: boolean;

View File

@ -207,101 +207,124 @@ $icons-map: (
"quote-text": "\f1aa", "quote-text": "\f1aa",
"quote": "\f1ab", "quote": "\f1ab",
"radial-badge": "\f1ac", "radial-badge": "\f1ac",
"readchats": "\f1ad", "rating-icons-level1": "\f1ad",
"recent": "\f1ae", "rating-icons-level10": "\f1ae",
"reload": "\f1af", "rating-icons-level2": "\f1af",
"remove-quote": "\f1b0", "rating-icons-level20": "\f1b0",
"remove": "\f1b1", "rating-icons-level3": "\f1b1",
"reopen-topic": "\f1b2", "rating-icons-level30": "\f1b2",
"replace": "\f1b3", "rating-icons-level4": "\f1b3",
"replies": "\f1b4", "rating-icons-level40": "\f1b4",
"reply-filled": "\f1b5", "rating-icons-level5": "\f1b5",
"reply": "\f1b6", "rating-icons-level50": "\f1b6",
"revenue-split": "\f1b7", "rating-icons-level6": "\f1b7",
"revote": "\f1b8", "rating-icons-level60": "\f1b8",
"save-story": "\f1b9", "rating-icons-level7": "\f1b9",
"saved-messages": "\f1ba", "rating-icons-level70": "\f1ba",
"schedule": "\f1bb", "rating-icons-level8": "\f1bb",
"sd-photo": "\f1bc", "rating-icons-level80": "\f1bc",
"search": "\f1bd", "rating-icons-level9": "\f1bd",
"select": "\f1be", "rating-icons-level90": "\f1be",
"sell-outline": "\f1bf", "rating-icons-negative": "\f1bf",
"sell": "\f1c0", "readchats": "\f1c0",
"send-outline": "\f1c1", "recent": "\f1c1",
"send": "\f1c2", "refund": "\f1c2",
"settings-filled": "\f1c3", "reload": "\f1c3",
"settings": "\f1c4", "remove-quote": "\f1c4",
"share-filled": "\f1c5", "remove": "\f1c5",
"share-screen-outlined": "\f1c6", "reopen-topic": "\f1c6",
"share-screen-stop": "\f1c7", "replace": "\f1c7",
"share-screen": "\f1c8", "replies": "\f1c8",
"show-message": "\f1c9", "reply-filled": "\f1c9",
"sidebar": "\f1ca", "reply": "\f1ca",
"skip-next": "\f1cb", "revenue-split": "\f1cb",
"skip-previous": "\f1cc", "revote": "\f1cc",
"smallscreen": "\f1cd", "save-story": "\f1cd",
"smile": "\f1ce", "saved-messages": "\f1ce",
"sort-by-date": "\f1cf", "schedule": "\f1cf",
"sort-by-number": "\f1d0", "sd-photo": "\f1d0",
"sort-by-price": "\f1d1", "search": "\f1d1",
"sort": "\f1d2", "select": "\f1d2",
"speaker-muted-story": "\f1d3", "sell-outline": "\f1d3",
"speaker-outline": "\f1d4", "sell": "\f1d4",
"speaker-story": "\f1d5", "send-outline": "\f1d5",
"speaker": "\f1d6", "send": "\f1d6",
"spoiler-disable": "\f1d7", "settings-filled": "\f1d7",
"spoiler": "\f1d8", "settings": "\f1d8",
"sport": "\f1d9", "share-filled": "\f1d9",
"star": "\f1da", "share-screen-outlined": "\f1da",
"stars-lock": "\f1db", "share-screen-stop": "\f1db",
"stats": "\f1dc", "share-screen": "\f1dc",
"stealth-future": "\f1dd", "show-message": "\f1dd",
"stealth-past": "\f1de", "sidebar": "\f1de",
"stickers": "\f1df", "skip-next": "\f1df",
"stop-raising-hand": "\f1e0", "skip-previous": "\f1e0",
"stop": "\f1e1", "smallscreen": "\f1e1",
"story-caption": "\f1e2", "smile": "\f1e2",
"story-expired": "\f1e3", "sort-by-date": "\f1e3",
"story-priority": "\f1e4", "sort-by-number": "\f1e4",
"story-reply": "\f1e5", "sort-by-price": "\f1e5",
"strikethrough": "\f1e6", "sort": "\f1e6",
"tag-add": "\f1e7", "speaker-muted-story": "\f1e7",
"tag-crossed": "\f1e8", "speaker-outline": "\f1e8",
"tag-filter": "\f1e9", "speaker-story": "\f1e9",
"tag-name": "\f1ea", "speaker": "\f1ea",
"tag": "\f1eb", "spoiler-disable": "\f1eb",
"timer": "\f1ec", "spoiler": "\f1ec",
"toncoin": "\f1ed", "sport": "\f1ed",
"trade": "\f1ee", "star": "\f1ee",
"transcribe": "\f1ef", "stars-lock": "\f1ef",
"truck": "\f1f0", "stats": "\f1f0",
"unarchive": "\f1f1", "stealth-future": "\f1f1",
"underlined": "\f1f2", "stealth-past": "\f1f2",
"unique-profile": "\f1f3", "stickers": "\f1f3",
"unlist-outline": "\f1f4", "stop-raising-hand": "\f1f4",
"unlist": "\f1f5", "stop": "\f1f5",
"unlock-badge": "\f1f6", "story-caption": "\f1f6",
"unlock": "\f1f7", "story-expired": "\f1f7",
"unmute": "\f1f8", "story-priority": "\f1f8",
"unpin": "\f1f9", "story-reply": "\f1f9",
"unread": "\f1fa", "strikethrough": "\f1fa",
"up": "\f1fb", "tag-add": "\f1fb",
"user-filled": "\f1fc", "tag-crossed": "\f1fc",
"user-online": "\f1fd", "tag-filter": "\f1fd",
"user": "\f1fe", "tag-name": "\f1fe",
"video-outlined": "\f1ff", "tag": "\f1ff",
"video-stop": "\f200", "timer": "\f200",
"video": "\f201", "toncoin": "\f201",
"view-once": "\f202", "trade": "\f202",
"voice-chat": "\f203", "transcribe": "\f203",
"volume-1": "\f204", "truck": "\f204",
"volume-2": "\f205", "unarchive": "\f205",
"volume-3": "\f206", "underlined": "\f206",
"web": "\f207", "understood": "\f207",
"webapp": "\f208", "unique-profile": "\f208",
"word-wrap": "\f209", "unlist-outline": "\f209",
"zoom-in": "\f20a", "unlist": "\f20a",
"zoom-out": "\f20b", "unlock-badge": "\f20b",
"unlock": "\f20c",
"unmute": "\f20d",
"unpin": "\f20e",
"unread": "\f20f",
"up": "\f210",
"user-filled": "\f211",
"user-online": "\f212",
"user-stars": "\f213",
"user": "\f214",
"video-outlined": "\f215",
"video-stop": "\f216",
"video": "\f217",
"view-once": "\f218",
"voice-chat": "\f219",
"volume-1": "\f21a",
"volume-2": "\f21b",
"volume-3": "\f21c",
"warning": "\f21d",
"web": "\f21e",
"webapp": "\f21f",
"word-wrap": "\f220",
"zoom-in": "\f221",
"zoom-out": "\f222",
); );
.icon-active-sessions::before { .icon-active-sessions::before {
@ -820,12 +843,72 @@ $icons-map: (
.icon-radial-badge::before { .icon-radial-badge::before {
content: map.get($icons-map, "radial-badge"); content: map.get($icons-map, "radial-badge");
} }
.icon-rating-icons-level1::before {
content: map.get($icons-map, "rating-icons-level1");
}
.icon-rating-icons-level10::before {
content: map.get($icons-map, "rating-icons-level10");
}
.icon-rating-icons-level2::before {
content: map.get($icons-map, "rating-icons-level2");
}
.icon-rating-icons-level20::before {
content: map.get($icons-map, "rating-icons-level20");
}
.icon-rating-icons-level3::before {
content: map.get($icons-map, "rating-icons-level3");
}
.icon-rating-icons-level30::before {
content: map.get($icons-map, "rating-icons-level30");
}
.icon-rating-icons-level4::before {
content: map.get($icons-map, "rating-icons-level4");
}
.icon-rating-icons-level40::before {
content: map.get($icons-map, "rating-icons-level40");
}
.icon-rating-icons-level5::before {
content: map.get($icons-map, "rating-icons-level5");
}
.icon-rating-icons-level50::before {
content: map.get($icons-map, "rating-icons-level50");
}
.icon-rating-icons-level6::before {
content: map.get($icons-map, "rating-icons-level6");
}
.icon-rating-icons-level60::before {
content: map.get($icons-map, "rating-icons-level60");
}
.icon-rating-icons-level7::before {
content: map.get($icons-map, "rating-icons-level7");
}
.icon-rating-icons-level70::before {
content: map.get($icons-map, "rating-icons-level70");
}
.icon-rating-icons-level8::before {
content: map.get($icons-map, "rating-icons-level8");
}
.icon-rating-icons-level80::before {
content: map.get($icons-map, "rating-icons-level80");
}
.icon-rating-icons-level9::before {
content: map.get($icons-map, "rating-icons-level9");
}
.icon-rating-icons-level90::before {
content: map.get($icons-map, "rating-icons-level90");
}
.icon-rating-icons-negative::before {
content: map.get($icons-map, "rating-icons-negative");
}
.icon-readchats::before { .icon-readchats::before {
content: map.get($icons-map, "readchats"); content: map.get($icons-map, "readchats");
} }
.icon-recent::before { .icon-recent::before {
content: map.get($icons-map, "recent"); content: map.get($icons-map, "recent");
} }
.icon-refund::before {
content: map.get($icons-map, "refund");
}
.icon-reload::before { .icon-reload::before {
content: map.get($icons-map, "reload"); content: map.get($icons-map, "reload");
} }
@ -1030,6 +1113,9 @@ $icons-map: (
.icon-underlined::before { .icon-underlined::before {
content: map.get($icons-map, "underlined"); content: map.get($icons-map, "underlined");
} }
.icon-understood::before {
content: map.get($icons-map, "understood");
}
.icon-unique-profile::before { .icon-unique-profile::before {
content: map.get($icons-map, "unique-profile"); content: map.get($icons-map, "unique-profile");
} }
@ -1063,6 +1149,9 @@ $icons-map: (
.icon-user-online::before { .icon-user-online::before {
content: map.get($icons-map, "user-online"); content: map.get($icons-map, "user-online");
} }
.icon-user-stars::before {
content: map.get($icons-map, "user-stars");
}
.icon-user::before { .icon-user::before {
content: map.get($icons-map, "user"); content: map.get($icons-map, "user");
} }
@ -1090,6 +1179,9 @@ $icons-map: (
.icon-volume-3::before { .icon-volume-3::before {
content: map.get($icons-map, "volume-3"); content: map.get($icons-map, "volume-3");
} }
.icon-warning::before {
content: map.get($icons-map, "warning");
}
.icon-web::before { .icon-web::before {
content: map.get($icons-map, "web"); content: map.get($icons-map, "web");
} }

Binary file not shown.

Binary file not shown.

View File

@ -171,8 +171,28 @@ export type FontIconName =
| 'quote-text' | 'quote-text'
| 'quote' | 'quote'
| 'radial-badge' | 'radial-badge'
| 'rating-icons-level1'
| 'rating-icons-level10'
| 'rating-icons-level2'
| 'rating-icons-level20'
| 'rating-icons-level3'
| 'rating-icons-level30'
| 'rating-icons-level4'
| 'rating-icons-level40'
| 'rating-icons-level5'
| 'rating-icons-level50'
| 'rating-icons-level6'
| 'rating-icons-level60'
| 'rating-icons-level7'
| 'rating-icons-level70'
| 'rating-icons-level8'
| 'rating-icons-level80'
| 'rating-icons-level9'
| 'rating-icons-level90'
| 'rating-icons-negative'
| 'readchats' | 'readchats'
| 'recent' | 'recent'
| 'refund'
| 'reload' | 'reload'
| 'remove-quote' | 'remove-quote'
| 'remove' | 'remove'
@ -241,6 +261,7 @@ export type FontIconName =
| 'truck' | 'truck'
| 'unarchive' | 'unarchive'
| 'underlined' | 'underlined'
| 'understood'
| 'unique-profile' | 'unique-profile'
| 'unlist-outline' | 'unlist-outline'
| 'unlist' | 'unlist'
@ -252,6 +273,7 @@ export type FontIconName =
| 'up' | 'up'
| 'user-filled' | 'user-filled'
| 'user-online' | 'user-online'
| 'user-stars'
| 'user' | 'user'
| 'video-outlined' | 'video-outlined'
| 'video-stop' | 'video-stop'
@ -261,6 +283,7 @@ export type FontIconName =
| 'volume-1' | 'volume-1'
| 'volume-2' | 'volume-2'
| 'volume-3' | 'volume-3'
| 'warning'
| 'web' | 'web'
| 'webapp' | 'webapp'
| 'word-wrap' | 'word-wrap'

View File

@ -1642,6 +1642,19 @@ export interface LangPair {
'PublicPostsPremiumFeatureSubtitle': undefined; 'PublicPostsPremiumFeatureSubtitle': undefined;
'PublicPostsSubscribeToPremium': undefined; 'PublicPostsSubscribeToPremium': undefined;
'PostsSearchTransaction': undefined; 'PostsSearchTransaction': undefined;
'TitleRating': undefined;
'RatingYourReflectsActivity': undefined;
'RatingGiftsFromTelegram': undefined;
'RatingGiftsFromTelegramDesc': undefined;
'RatingGiftsAndPostsFromUsers': undefined;
'RatingGiftsAndPostsFromUsersDesc': undefined;
'RatingRefundsAndConversions': undefined;
'RatingRefundsAndConversionsDesc': undefined;
'RatingBadgeAdded': undefined;
'RatingBadgeDeducted': undefined;
'RatingNegativeLevel': undefined;
'LinkDescriptionRatingBack': undefined;
'LinkDescriptionRatingPreview': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -2850,6 +2863,12 @@ export interface LangPairWithVariables<V = LangVariable> {
'NotificationPaidExtraSearch': { 'NotificationPaidExtraSearch': {
'stars': V; 'stars': V;
}; };
'RatingReflectsActivity': {
'name': V;
};
'RatingLevel': {
'level': V;
};
} }
export interface LangPairPlural { export interface LangPairPlural {
@ -3177,6 +3196,16 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'HintPublicPostsSearchQuota': { 'HintPublicPostsSearchQuota': {
'count': V; 'count': V;
}; };
'DescriptionPendingRating': {
'time': V;
'points': V;
'link': V;
};
'DescriptionFutureRating': {
'time': V;
'points': V;
'link': V;
};
} }
export type RegularLangKey = keyof LangPair; export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;