diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 32d30f738..375b59493 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiBirthday, ApiPeerSettings, + ApiStarsRating, ApiUser, ApiUserFullInfo, ApiUserStatus, @@ -27,6 +28,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, }, users, } = mtpUserFull; @@ -57,6 +59,9 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse botVerification: botVerification && buildApiBotVerification(botVerification), areAdsEnabled: sponsoredEnabled, starGiftCount: stargiftsCount, + starsRating: starsRating && buildApiStarsRating(starsRating), + starsMyPendingRating: starsMyPendingRating && buildApiStarsRating(starsMyPendingRating), + starsMyPendingRatingDate, isBotCanManageEmojiStatus: botCanManageEmojiStatus, hasScheduledMessages: hasScheduled, paidMessagesStars: sendPaidMessagesStars?.toJSNumber(), @@ -182,3 +187,12 @@ export function buildApiUserStatuses(mtpUsers: GramJs.TypeUser[]) { export function buildApiBirthday(birthday: GramJs.TypeBirthday): ApiBirthday { 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(), + }; +} diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index 07299692a..c7ceb13e1 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -281,3 +281,10 @@ export interface ApiDisallowedGiftsSettings { shouldDisallowUniqueStarGifts?: true; shouldDisallowPremiumGifts?: true; } + +export interface ApiStarsRating { + level: number; + currentLevelStars: number; + stars: number; + nextLevelStars?: number; +} diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 4605adc5e..5e2092421 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -4,7 +4,7 @@ import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from import type { ApiPeerColor, ApiPeerSettings } from './chats'; import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiBotVerification } from './misc'; -import type { ApiSavedStarGift } from './stars'; +import type { ApiSavedStarGift, ApiStarsRating } from './stars'; export interface ApiUser { id: string; @@ -65,6 +65,9 @@ export interface ApiUserFullInfo { businessWorkHours?: ApiBusinessWorkHours; businessIntro?: ApiBusinessIntro; starGiftCount?: number; + starsRating?: ApiStarsRating; + starsMyPendingRating?: ApiStarsRating; + starsMyPendingRatingDate?: number; isBotCanManageEmojiStatus?: boolean; isBotAccessEmojiGranted?: boolean; hasScheduledMessages?: boolean; diff --git a/src/assets/font-icons/rating-icons/level1.svg b/src/assets/font-icons/rating-icons/level1.svg new file mode 100644 index 000000000..85a8470eb --- /dev/null +++ b/src/assets/font-icons/rating-icons/level1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level10.svg b/src/assets/font-icons/rating-icons/level10.svg new file mode 100644 index 000000000..dceed1a9d --- /dev/null +++ b/src/assets/font-icons/rating-icons/level10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level2.svg b/src/assets/font-icons/rating-icons/level2.svg new file mode 100644 index 000000000..0c541d181 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level20.svg b/src/assets/font-icons/rating-icons/level20.svg new file mode 100644 index 000000000..879e2bbb5 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level3.svg b/src/assets/font-icons/rating-icons/level3.svg new file mode 100644 index 000000000..b958afe13 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level30.svg b/src/assets/font-icons/rating-icons/level30.svg new file mode 100644 index 000000000..d2df9be81 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level30.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level4.svg b/src/assets/font-icons/rating-icons/level4.svg new file mode 100644 index 000000000..321951d31 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level40.svg b/src/assets/font-icons/rating-icons/level40.svg new file mode 100644 index 000000000..7822cefd5 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level40.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level5.svg b/src/assets/font-icons/rating-icons/level5.svg new file mode 100644 index 000000000..0d5ca78c2 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level50.svg b/src/assets/font-icons/rating-icons/level50.svg new file mode 100644 index 000000000..e3465be14 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level50.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level6.svg b/src/assets/font-icons/rating-icons/level6.svg new file mode 100644 index 000000000..dd093bacb --- /dev/null +++ b/src/assets/font-icons/rating-icons/level6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level60.svg b/src/assets/font-icons/rating-icons/level60.svg new file mode 100644 index 000000000..f7b4edcd1 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level60.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level7.svg b/src/assets/font-icons/rating-icons/level7.svg new file mode 100644 index 000000000..f841c774a --- /dev/null +++ b/src/assets/font-icons/rating-icons/level7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level70.svg b/src/assets/font-icons/rating-icons/level70.svg new file mode 100644 index 000000000..0f4093341 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level70.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level8.svg b/src/assets/font-icons/rating-icons/level8.svg new file mode 100644 index 000000000..cdb36e980 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level80.svg b/src/assets/font-icons/rating-icons/level80.svg new file mode 100644 index 000000000..cb688b638 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level80.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level9.svg b/src/assets/font-icons/rating-icons/level9.svg new file mode 100644 index 000000000..c2be303a3 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/level90.svg b/src/assets/font-icons/rating-icons/level90.svg new file mode 100644 index 000000000..dcb7afb72 --- /dev/null +++ b/src/assets/font-icons/rating-icons/level90.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/rating-icons/negative.svg b/src/assets/font-icons/rating-icons/negative.svg new file mode 100644 index 000000000..9ee1b764e --- /dev/null +++ b/src/assets/font-icons/rating-icons/negative.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/refund.svg b/src/assets/font-icons/refund.svg new file mode 100644 index 000000000..5caf76f2a --- /dev/null +++ b/src/assets/font-icons/refund.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/understood.svg b/src/assets/font-icons/understood.svg new file mode 100644 index 000000000..4b30bc3ab --- /dev/null +++ b/src/assets/font-icons/understood.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/user-stars.svg b/src/assets/font-icons/user-stars.svg new file mode 100644 index 000000000..0954f398d --- /dev/null +++ b/src/assets/font-icons/user-stars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/warning.svg b/src/assets/font-icons/warning.svg new file mode 100644 index 000000000..aff72046a --- /dev/null +++ b/src/assets/font-icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index f054cee41..d30141abb 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -2200,3 +2200,22 @@ "PublicPostsSubscribeToPremium" = "Subscribe to Premium"; "NotificationPaidExtraSearch" = "{stars} spent on extra 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 >"; \ No newline at end of file diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 238b82768..098f4dfe7 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -103,3 +103,4 @@ export { default as InviteViaLinkModal } from '../components/modals/inviteViaLin export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal'; export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal'; export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; +export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal'; diff --git a/src/components/common/PremiumProgress.module.scss b/src/components/common/PremiumProgress.module.scss index 2babeac81..9db1c5b58 100644 --- a/src/components/common/PremiumProgress.module.scss +++ b/src/components/common/PremiumProgress.module.scss @@ -1,5 +1,8 @@ +/* stylelint-disable plugin/no-low-performance-animation-properties */ + .root { --percent: calc(var(--progress, 0.5) * 100%); + --color-negative-progress: #E05356; position: relative; @@ -73,21 +76,44 @@ align-items: center; justify-content: center; + height: 2rem; padding: 0.25rem 0.75rem; border-radius: 1rem; color: #ffffff; 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 { position: absolute; bottom: -4px; - left: calc(var(--tail-position, 0.5) * 100%); - transform: translateX(-50%); display: inline-block; + + color: #7E85FF; + + transition: left 0.3s ease; } .floating-badge-icon { @@ -109,6 +135,10 @@ align-items: center; font-weight: var(--font-weight-medium); + + opacity: 1; + + transition: opacity 0.15s ease; } .left { @@ -119,8 +149,21 @@ right: 0.75rem; } -.progress { - --multiplier: calc(1 / var(--progress) - 1); +.progressWrapper { + 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; top: 0; @@ -129,16 +172,18 @@ overflow: hidden; - width: max(var(--percent), 0.625rem); - border-top-left-radius: 0.625rem; - border-bottom-left-radius: 0.625rem; + width: calc(var(--positive-progress) * 100%); 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 { color: white; white-space: nowrap; + opacity: 1; + transition: opacity 0.15s ease; } .right { @@ -146,12 +191,44 @@ } } -.fullProgress { - border-radius: 0.625rem; +.negativeLayer, +.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 { - .progress { + .positiveLayer, + .positiveProgress { background-color: var(--color-primary); background-image: none; } @@ -159,4 +236,69 @@ .floating-badge { 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; + } } diff --git a/src/components/common/PremiumProgress.tsx b/src/components/common/PremiumProgress.tsx index f2a899768..b44812d8e 100644 --- a/src/components/common/PremiumProgress.tsx +++ b/src/components/common/PremiumProgress.tsx @@ -7,14 +7,22 @@ import type { IconName } from '../../types/icons'; import buildClassName from '../../util/buildClassName'; 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 usePrevious from '../../hooks/usePrevious'; import useResizeObserver from '../../hooks/useResizeObserver'; +import useSyncEffect from '../../hooks/useSyncEffect'; +import Transition from '../ui/Transition'; import Icon from './icons/Icon'; import styles from './PremiumProgress.module.scss'; +export type AnimationDirection = 'forward' | 'backward' | 'none'; + type OwnProps = { leftText?: string; rightText?: string; @@ -22,6 +30,8 @@ type OwnProps = { floatingBadgeText?: string; progress?: number; isPrimary?: boolean; + isNegative?: boolean; + animationDirection?: AnimationDirection; className?: string; }; @@ -30,47 +40,216 @@ const PremiumProgress: FC = ({ rightText, floatingBadgeText, floatingBadgeIcon, - progress, + progress = 0, isPrimary, + isNegative, + animationDirection = 'none', className, }) => { const lang = useOldLang(); - const floatingBadgeRef = useRef(); + const floatingBadgeContentRef = useRef(); const parentContainerRef = useRef(); 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 = () => { - if (floatingBadgeRef.current && parentContainerRef.current && progress !== undefined) { - const badgeWidth = floatingBadgeRef.current.offsetWidth; + if (floatingBadgeContentRef.current && parentContainerRef.current) { const parentWidth = parentContainerRef.current.offsetWidth; - const minShift = badgeWidth / 2; - const maxShift = parentWidth - badgeWidth / 2; - const currentShift = progress * parentWidth; - const safeShift = Math.max(minShift, Math.min(currentShift, maxShift)); + const halfBadgeWidth = badgeWidth / 2; + const minBadgeShift = halfBadgeWidth; + const maxBadgeShift = parentWidth - halfBadgeWidth; + 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); - - 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); + setBeakPosition(newBeakPositionPx); } }; - useEffect(updateBadgePosition, [progress]); + useEffect(updateBadgePosition, [badgeProgress, badgeWidth, isNegative, CORNER_BEAK_THRESHOLD]); 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 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 ( +
+
+ {displayLeftText} +
+
+ {displayRightText} +
+
+ ); + }; return (
= ({ styles.root, hasFloatingBadge && styles.withBadge, isPrimary && styles.primary, + isNegative && styles.negative, + shouldAnimateCaptions && styles.transitioning, + isCycling && styles.cycling, className, )} style={buildStyle( - progress !== undefined && `--progress: ${progress}`, - tailPosition !== undefined && `--tail-position: ${tailPosition}`, + `--positive-progress: ${positiveProgress}`, + `--negative-progress: ${negativeProgress}`, + `--layer-progress: ${layerProgress}`, `--shift-x: ${shiftX}`, + `--cycling-animation-badge-position: ${FULL_CYCLE_TRANSITION_MS}ms`, + `--cycling-animation-progress: ${LAYER_PROGRESS_TRANSITION_MS}ms`, )} > {hasFloatingBadge && (
-
- {floatingBadgeIcon && } - {floatingBadgeText && ( -
- {floatingBadgeText} +
+ +
+ {floatingBadgeIcon && } + {floatingBadgeText && ( +
+ {floatingBadgeText} +
+ )}
- )} +
-
+
- +
)}
- {leftText} + {displayLeftText}
- {rightText} + {displayRightText}
-
-
- {leftText} -
-
- {rightText} -
+ +
+ {renderProgressLayer( + true, + positiveProgress, + buildClassName(hideMainLayer && styles.hidden), + disableMainProgressTransition, + )} + + {renderProgressLayer( + false, + negativeProgress, + buildClassName(hideMainLayer && styles.hidden), + disableMainProgressTransition, + )} + + {renderProgressLayer( + !isNegative, + layerProgress, + buildClassName( + isNegative ? styles.negativeLayer : styles.positiveLayer, + showLayer && styles.show, + ), + disableLayerProgressTransition, + )}
); diff --git a/src/components/common/ProfileInfo.module.scss b/src/components/common/ProfileInfo.module.scss index e05271164..6a5be5096 100644 --- a/src/components/common/ProfileInfo.module.scss +++ b/src/components/common/ProfileInfo.module.scss @@ -191,11 +191,64 @@ 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; } -.get-status { +.getStatus { --blured-background-color: #c4c9cc42; pointer-events: all; diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index e740047f0..7eccc6d7c 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -3,9 +3,10 @@ import { memo, useEffect, useState } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { - ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserStatus, + ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserFullInfo, ApiUserStatus, } from '../../api/types'; import type { AnimationLevel } from '../../types'; +import type { IconName } from '../../types/icons'; import { MediaViewerOrigin } from '../../types'; import { @@ -20,6 +21,7 @@ import { selectThreadMessagesCount, selectTopic, selectUser, + selectUserFullInfo, selectUserStatus, } from '../../global/selectors'; import { selectSharedSettings } from '../../global/selectors/sharedState.ts'; @@ -40,12 +42,15 @@ import usePhotosPreload from './hooks/usePhotosPreload'; import Transition from '../ui/Transition'; import Avatar from './Avatar'; import FullNameTitle from './FullNameTitle'; +import Icon from './icons/Icon'; import ProfilePhoto from './ProfilePhoto'; import TopicIcon from './TopicIcon'; import './ProfileInfo.scss'; import styles from './ProfileInfo.module.scss'; +const MAX_LEVEL_ICON = 90; + type OwnProps = { peerId: string; forceShowSelf?: boolean; @@ -56,6 +61,7 @@ type OwnProps = { type StateProps = { user?: ApiUser; + userFullInfo?: ApiUserFullInfo; userStatus?: ApiUserStatus; chat?: ApiChat; mediaIndex?: number; @@ -78,6 +84,7 @@ const ProfileInfo: FC = ({ forceShowSelf, canPlayVideo, user, + userFullInfo, userStatus, chat, mediaIndex, @@ -98,6 +105,7 @@ const ProfileInfo: FC = ({ openPrivacySettingsNoticeModal, loadMoreProfilePhotos, openUniqueGiftBySlug, + openProfileRatingModal, } = getActions(); const oldLang = useOldLang(); @@ -182,6 +190,12 @@ const ProfileInfo: FC = ({ openPrivacySettingsNoticeModal({ chatId: chat!.id, isReadDate: false }); }); + const handleRatingClick = useLastCallback((level: number) => { + if (user) { + openProfileRatingModal({ userId: user.id, level }); + } + }); + function handleSelectFallbackPhoto() { if (!isFirst) return; setHasSlideAnimation(true); @@ -268,6 +282,43 @@ const ProfileInfo: FC = ({ ); } + function renderUserRating() { + if (!userFullInfo?.starsRating) return undefined; + + const level = userFullInfo.starsRating.level; + const isNegative = level < 0; + + const onRatingClick = () => handleRatingClick(level); + + if (isNegative) { + return ( + + + ! + + ); + } + + 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 ( + + + {level} + + ); + } + function renderStatus() { const isAnonymousForwards = isAnonymousForwardsChat(peerId); const isSystemBotChat = isSystemBot(peerId); @@ -290,6 +341,7 @@ const ProfileInfo: FC = ({ isUserOnline(user, userStatus) && 'online', )} > + {renderUserRating()} {getUserStatus(oldLang, user, userStatus)} @@ -400,6 +452,7 @@ const ProfileInfo: FC = ({ export default memo(withGlobal( (global, { peerId }): StateProps => { const user = selectUser(global, peerId); + const userFullInfo = user ? selectUserFullInfo(global, peerId) : undefined; const userStatus = selectUserStatus(global, peerId); const chat = selectChat(global, peerId); const profilePhotos = selectPeerPhotos(global, peerId); @@ -415,6 +468,7 @@ export default memo(withGlobal( return { user, + userFullInfo, userStatus, chat, mediaIndex, diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index ba383a60a..c632c7994 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -35,6 +35,7 @@ import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async'; import PaidReactionModal from './paidReaction/PaidReactionModal.async'; import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async'; import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async'; +import ProfileRatingModal from './profileRating/ProfileRatingModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async'; import ReportModal from './reportModal/ReportModal.async'; import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async'; @@ -93,7 +94,8 @@ type ModalKey = keyof Pick; type StateProps = { @@ -151,6 +153,7 @@ const MODALS: ModalRegistry = { isFrozenAccountModalOpen: FrozenAccountModal, deleteAccountModal: DeleteAccountModal, isAgeVerificationModalOpen: AgeVerificationModal, + profileRatingModal: ProfileRatingModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/profileRating/ProfileRatingModal.async.tsx b/src/components/modals/profileRating/ProfileRatingModal.async.tsx new file mode 100644 index 000000000..c57cd57a6 --- /dev/null +++ b/src/components/modals/profileRating/ProfileRatingModal.async.tsx @@ -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 = (props) => { + const { modal } = props; + const ProfileRatingModal = useModuleLoader(Bundles.Extra, 'ProfileRatingModal', !modal); + + return ProfileRatingModal ? : undefined; +}; + +export default ProfileRatingModalAsync; diff --git a/src/components/modals/profileRating/ProfileRatingModal.module.scss b/src/components/modals/profileRating/ProfileRatingModal.module.scss new file mode 100644 index 000000000..3fa583751 --- /dev/null +++ b/src/components/modals/profileRating/ProfileRatingModal.module.scss @@ -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); +} diff --git a/src/components/modals/profileRating/ProfileRatingModal.tsx b/src/components/modals/profileRating/ProfileRatingModal.tsx new file mode 100644 index 000000000..c9334b552 --- /dev/null +++ b/src/components/modals/profileRating/ProfileRatingModal.tsx @@ -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 ( + + {badgeText} + + ); + }; + + 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 ( + + {showFutureRating ? ( +

+ {lang('DescriptionFutureRating', { + time: formatShortDuration(lang, waitTime), + points: Math.abs(pendingPoints), + link: ( + + {lang('LinkDescriptionRatingBack')} + + ), + }, { + pluralValue: Math.abs(pendingPoints), + withNodes: true, + })} +

+ ) : ( +

+ {lang('DescriptionPendingRating', { + time: formatShortDuration(lang, waitTime), + points: Math.abs(pendingPoints), + link: ( + + {lang('LinkDescriptionRatingPreview')} + + ), + }, { + pluralValue: Math.abs(pendingPoints), + withNodes: true, + })} +

+ )} +
+ ); + }; + + 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 ( +
+
+ {lang('TitleRating')} +
+ = 0} + isNegative={currentLevel < 0} + animationDirection={animationDirection} + className={buildClassName(styles.ratingProgress, shouldShowPreview && styles.withPreview)} + /> + {renderPreviewDescription()} +

+ {user?.id === currentUserId + ? lang('RatingYourReflectsActivity') + : lang('RatingReflectsActivity', { name: getPeerTitle(lang, user) || userFallbackText })} +

+
+ ); + }, [modal, user, currentUserId, starsRating, + pendingRating, pendingRatingDate, showFutureRating, + lang, handleShowFuture, handleShowCurrent, isOpen]); + + const listItemData = [ + ['gift', lang('RatingGiftsFromTelegram'), ( + + {renderBadge('added')} + {lang('RatingGiftsFromTelegramDesc')} + + )], + ['user-stars', lang('RatingGiftsAndPostsFromUsers'), ( + + {renderBadge('added')} + {lang('RatingGiftsAndPostsFromUsersDesc')} + + )], + ['refund', lang('RatingRefundsAndConversions'), ( + + {renderBadge('deducted')} + {lang('RatingRefundsAndConversionsDesc')} + + )], + ] satisfies TableAboutData; + + const footer = useMemo(() => { + if (!isOpen) return undefined; + return ( +
+ +
+ ); + }, [lang, isOpen, handleClose]); + + return ( + + ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/global/actions/ui/users.ts b/src/global/actions/ui/users.ts index da6006991..2c94d7495 100644 --- a/src/global/actions/ui/users.ts +++ b/src/global/actions/ui/users.ts @@ -64,3 +64,16 @@ addActionHandler('closeSuggestedStatusModal', (global, actions, payload): Action }); 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'); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index db404b905..7c1ba9adb 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1850,6 +1850,11 @@ export interface ActionPayloads { userId: string; } & WithTabId; closeChatRefundModal: WithTabId | undefined; + openProfileRatingModal: { + userId: string; + level: number; + } & WithTabId; + closeProfileRatingModal: WithTabId | undefined; loadMoreProfilePhotos: { peerId: string; isPreload?: boolean; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 57362af1d..f10a1caab 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -852,6 +852,11 @@ export type TabState = { duration?: number; }; + profileRatingModal?: { + userId: string; + level: number; + }; + monetizationVerificationModal?: { chatId: string; isLoading?: boolean; diff --git a/src/styles/icons.scss b/src/styles/icons.scss index d828c8197..b89d13721 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -207,101 +207,124 @@ $icons-map: ( "quote-text": "\f1aa", "quote": "\f1ab", "radial-badge": "\f1ac", - "readchats": "\f1ad", - "recent": "\f1ae", - "reload": "\f1af", - "remove-quote": "\f1b0", - "remove": "\f1b1", - "reopen-topic": "\f1b2", - "replace": "\f1b3", - "replies": "\f1b4", - "reply-filled": "\f1b5", - "reply": "\f1b6", - "revenue-split": "\f1b7", - "revote": "\f1b8", - "save-story": "\f1b9", - "saved-messages": "\f1ba", - "schedule": "\f1bb", - "sd-photo": "\f1bc", - "search": "\f1bd", - "select": "\f1be", - "sell-outline": "\f1bf", - "sell": "\f1c0", - "send-outline": "\f1c1", - "send": "\f1c2", - "settings-filled": "\f1c3", - "settings": "\f1c4", - "share-filled": "\f1c5", - "share-screen-outlined": "\f1c6", - "share-screen-stop": "\f1c7", - "share-screen": "\f1c8", - "show-message": "\f1c9", - "sidebar": "\f1ca", - "skip-next": "\f1cb", - "skip-previous": "\f1cc", - "smallscreen": "\f1cd", - "smile": "\f1ce", - "sort-by-date": "\f1cf", - "sort-by-number": "\f1d0", - "sort-by-price": "\f1d1", - "sort": "\f1d2", - "speaker-muted-story": "\f1d3", - "speaker-outline": "\f1d4", - "speaker-story": "\f1d5", - "speaker": "\f1d6", - "spoiler-disable": "\f1d7", - "spoiler": "\f1d8", - "sport": "\f1d9", - "star": "\f1da", - "stars-lock": "\f1db", - "stats": "\f1dc", - "stealth-future": "\f1dd", - "stealth-past": "\f1de", - "stickers": "\f1df", - "stop-raising-hand": "\f1e0", - "stop": "\f1e1", - "story-caption": "\f1e2", - "story-expired": "\f1e3", - "story-priority": "\f1e4", - "story-reply": "\f1e5", - "strikethrough": "\f1e6", - "tag-add": "\f1e7", - "tag-crossed": "\f1e8", - "tag-filter": "\f1e9", - "tag-name": "\f1ea", - "tag": "\f1eb", - "timer": "\f1ec", - "toncoin": "\f1ed", - "trade": "\f1ee", - "transcribe": "\f1ef", - "truck": "\f1f0", - "unarchive": "\f1f1", - "underlined": "\f1f2", - "unique-profile": "\f1f3", - "unlist-outline": "\f1f4", - "unlist": "\f1f5", - "unlock-badge": "\f1f6", - "unlock": "\f1f7", - "unmute": "\f1f8", - "unpin": "\f1f9", - "unread": "\f1fa", - "up": "\f1fb", - "user-filled": "\f1fc", - "user-online": "\f1fd", - "user": "\f1fe", - "video-outlined": "\f1ff", - "video-stop": "\f200", - "video": "\f201", - "view-once": "\f202", - "voice-chat": "\f203", - "volume-1": "\f204", - "volume-2": "\f205", - "volume-3": "\f206", - "web": "\f207", - "webapp": "\f208", - "word-wrap": "\f209", - "zoom-in": "\f20a", - "zoom-out": "\f20b", + "rating-icons-level1": "\f1ad", + "rating-icons-level10": "\f1ae", + "rating-icons-level2": "\f1af", + "rating-icons-level20": "\f1b0", + "rating-icons-level3": "\f1b1", + "rating-icons-level30": "\f1b2", + "rating-icons-level4": "\f1b3", + "rating-icons-level40": "\f1b4", + "rating-icons-level5": "\f1b5", + "rating-icons-level50": "\f1b6", + "rating-icons-level6": "\f1b7", + "rating-icons-level60": "\f1b8", + "rating-icons-level7": "\f1b9", + "rating-icons-level70": "\f1ba", + "rating-icons-level8": "\f1bb", + "rating-icons-level80": "\f1bc", + "rating-icons-level9": "\f1bd", + "rating-icons-level90": "\f1be", + "rating-icons-negative": "\f1bf", + "readchats": "\f1c0", + "recent": "\f1c1", + "refund": "\f1c2", + "reload": "\f1c3", + "remove-quote": "\f1c4", + "remove": "\f1c5", + "reopen-topic": "\f1c6", + "replace": "\f1c7", + "replies": "\f1c8", + "reply-filled": "\f1c9", + "reply": "\f1ca", + "revenue-split": "\f1cb", + "revote": "\f1cc", + "save-story": "\f1cd", + "saved-messages": "\f1ce", + "schedule": "\f1cf", + "sd-photo": "\f1d0", + "search": "\f1d1", + "select": "\f1d2", + "sell-outline": "\f1d3", + "sell": "\f1d4", + "send-outline": "\f1d5", + "send": "\f1d6", + "settings-filled": "\f1d7", + "settings": "\f1d8", + "share-filled": "\f1d9", + "share-screen-outlined": "\f1da", + "share-screen-stop": "\f1db", + "share-screen": "\f1dc", + "show-message": "\f1dd", + "sidebar": "\f1de", + "skip-next": "\f1df", + "skip-previous": "\f1e0", + "smallscreen": "\f1e1", + "smile": "\f1e2", + "sort-by-date": "\f1e3", + "sort-by-number": "\f1e4", + "sort-by-price": "\f1e5", + "sort": "\f1e6", + "speaker-muted-story": "\f1e7", + "speaker-outline": "\f1e8", + "speaker-story": "\f1e9", + "speaker": "\f1ea", + "spoiler-disable": "\f1eb", + "spoiler": "\f1ec", + "sport": "\f1ed", + "star": "\f1ee", + "stars-lock": "\f1ef", + "stats": "\f1f0", + "stealth-future": "\f1f1", + "stealth-past": "\f1f2", + "stickers": "\f1f3", + "stop-raising-hand": "\f1f4", + "stop": "\f1f5", + "story-caption": "\f1f6", + "story-expired": "\f1f7", + "story-priority": "\f1f8", + "story-reply": "\f1f9", + "strikethrough": "\f1fa", + "tag-add": "\f1fb", + "tag-crossed": "\f1fc", + "tag-filter": "\f1fd", + "tag-name": "\f1fe", + "tag": "\f1ff", + "timer": "\f200", + "toncoin": "\f201", + "trade": "\f202", + "transcribe": "\f203", + "truck": "\f204", + "unarchive": "\f205", + "underlined": "\f206", + "understood": "\f207", + "unique-profile": "\f208", + "unlist-outline": "\f209", + "unlist": "\f20a", + "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 { @@ -820,12 +843,72 @@ $icons-map: ( .icon-radial-badge::before { 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 { content: map.get($icons-map, "readchats"); } .icon-recent::before { content: map.get($icons-map, "recent"); } +.icon-refund::before { + content: map.get($icons-map, "refund"); +} .icon-reload::before { content: map.get($icons-map, "reload"); } @@ -1030,6 +1113,9 @@ $icons-map: ( .icon-underlined::before { content: map.get($icons-map, "underlined"); } +.icon-understood::before { + content: map.get($icons-map, "understood"); +} .icon-unique-profile::before { content: map.get($icons-map, "unique-profile"); } @@ -1063,6 +1149,9 @@ $icons-map: ( .icon-user-online::before { content: map.get($icons-map, "user-online"); } +.icon-user-stars::before { + content: map.get($icons-map, "user-stars"); +} .icon-user::before { content: map.get($icons-map, "user"); } @@ -1090,6 +1179,9 @@ $icons-map: ( .icon-volume-3::before { content: map.get($icons-map, "volume-3"); } +.icon-warning::before { + content: map.get($icons-map, "warning"); +} .icon-web::before { content: map.get($icons-map, "web"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 4844820ba..76c161972 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 8bd0da8ac..bc3e457fb 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index fbd04f0d3..5e4364c6e 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -171,8 +171,28 @@ export type FontIconName = | 'quote-text' | 'quote' | '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' | 'recent' + | 'refund' | 'reload' | 'remove-quote' | 'remove' @@ -241,6 +261,7 @@ export type FontIconName = | 'truck' | 'unarchive' | 'underlined' + | 'understood' | 'unique-profile' | 'unlist-outline' | 'unlist' @@ -252,6 +273,7 @@ export type FontIconName = | 'up' | 'user-filled' | 'user-online' + | 'user-stars' | 'user' | 'video-outlined' | 'video-stop' @@ -261,6 +283,7 @@ export type FontIconName = | 'volume-1' | 'volume-2' | 'volume-3' + | 'warning' | 'web' | 'webapp' | 'word-wrap' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index c82dcea22..7b93c7834 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1642,6 +1642,19 @@ export interface LangPair { 'PublicPostsPremiumFeatureSubtitle': undefined; 'PublicPostsSubscribeToPremium': 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 { @@ -2850,6 +2863,12 @@ export interface LangPairWithVariables { 'NotificationPaidExtraSearch': { 'stars': V; }; + 'RatingReflectsActivity': { + 'name': V; + }; + 'RatingLevel': { + 'level': V; + }; } export interface LangPairPlural { @@ -3177,6 +3196,16 @@ export interface LangPairPluralWithVariables { 'HintPublicPostsSearchQuota': { 'count': V; }; + 'DescriptionPendingRating': { + 'time': V; + 'points': V; + 'link': V; + }; + 'DescriptionFutureRating': { + 'time': V; + 'points': V; + 'link': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;