From 9738c342422183980ffeb4f5a2338762d3f4781f Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sat, 22 Nov 2025 12:54:12 +0100 Subject: [PATCH] Profile: Optimize accordion transitions (#6488) --- .../common/profile/BusinessHours.module.scss | 49 ++++++- .../common/profile/BusinessHours.tsx | 122 ++++++++---------- .../common/profile/ChatExtra.module.scss | 69 ++++++++-- src/components/common/profile/ChatExtra.tsx | 104 ++++++++++++--- src/components/right/Profile.tsx | 1 - src/styles/index.scss | 12 ++ src/util/animations/viewTransitionTypes.ts | 10 ++ 7 files changed, 267 insertions(+), 100 deletions(-) diff --git a/src/components/common/profile/BusinessHours.module.scss b/src/components/common/profile/BusinessHours.module.scss index 83f78c0b0..2d7e96ba5 100644 --- a/src/components/common/profile/BusinessHours.module.scss +++ b/src/components/common/profile/BusinessHours.module.scss @@ -1,5 +1,9 @@ +@use '../../../styles/mixins'; + .root { cursor: pointer; + + @include mixins.with-vt-type('profileBusinessHours'); } .top { @@ -32,6 +36,8 @@ margin-inline-end: 0.375rem; font-size: 1.25rem; color: var(--color-text-secondary); + + @include mixins.with-vt-type('profileBusinessHours'); } .offset-trigger { @@ -54,13 +60,13 @@ &:hover { background-color: var(--color-primary-opacity); } + + @include mixins.with-vt-type('profileBusinessHours'); } .transition { height: 0; margin-bottom: 0.5rem; - /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ - transition: height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .timetable { @@ -91,3 +97,42 @@ .current-day { color: var(--color-primary); } + +@include mixins.on-active-vt('profileBusinessHours') { + &::view-transition-image-pair(.businessHours) { + overflow: hidden; + } + + &::view-transition-old(.businessHours), + &::view-transition-new(.businessHours) { + animation-name: none; + } +} + +@include mixins.on-active-vt('profileBusinessHoursExpand') { + &::view-transition-old(.expandArrow) { + animation-name: vt-expand-icon-spin; + } + + &::view-transition-new(.expandArrow) { + display: none; + } + + &::view-transition-old(.businessHours) { + display: none; + } +} + +@include mixins.on-active-vt('profileBusinessHoursCollapse') { + &::view-transition-old(.expandArrow) { + animation-name: vt-collapse-icon-spin; + } + + &::view-transition-new(.expandArrow) { + display: none; + } + + &::view-transition-new(.businessHours) { + display: none; + } +} diff --git a/src/components/common/profile/BusinessHours.tsx b/src/components/common/profile/BusinessHours.tsx index 70c2581e6..aabc94384 100644 --- a/src/components/common/profile/BusinessHours.tsx +++ b/src/components/common/profile/BusinessHours.tsx @@ -1,11 +1,14 @@ -import type React from '../../../lib/teact/teact'; import { - memo, useEffect, useMemo, useRef, + memo, useMemo, } from '../../../lib/teact/teact'; import type { ApiBusinessWorkHours } from '../../../api/types'; -import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom'; +import { + VTT_PROFILE_BUSINESS_HOURS, + VTT_PROFILE_BUSINESS_HOURS_COLLAPSE, + VTT_PROFILE_BUSINESS_HOURS_EXPAND, +} from '../../../util/animations/viewTransitionTypes'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import { formatTime, formatWeekday } from '../../../util/dates/dateFormat'; @@ -13,6 +16,8 @@ import { getUtcOffset, getWeekStart, shiftTimeRanges, splitDays, } from '../../../util/dates/workHours'; +import { useViewTransition } from '../../../hooks/animations/useViewTransition'; +import { useVtn } from '../../../hooks/animations/useVtn'; import useSelectorSignal from '../../../hooks/data/useSelectorSignal'; import useInterval from '../../../hooks/schedulers/useInterval'; import useDerivedState from '../../../hooks/useDerivedState'; @@ -22,7 +27,6 @@ import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import ListItem from '../../ui/ListItem'; -import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition'; import Icon from '../icons/Icon'; import styles from './BusinessHours.module.scss'; @@ -31,17 +35,21 @@ const DAYS = Array.from({ length: 7 }, (_, i) => i); type OwnProps = { businessHours: ApiBusinessWorkHours; + className?: string; }; const BusinessHours = ({ businessHours, + className, }: OwnProps) => { - const transitionRef = useRef(); const [isExpanded, expand, collapse] = useFlag(false); const [isMyTime, showInMyTime, showInLocalTime] = useFlag(false); - const lang = useOldLang(); + const oldLang = useOldLang(); const forceUpdate = useForceUpdate(); + const { startViewTransition } = useViewTransition(); + const { createVtnStyle } = useVtn(); + useInterval(forceUpdate, 60 * 1000); const timezoneSignal = useSelectorSignal((global) => global.timezones?.byId); @@ -62,20 +70,20 @@ const BusinessHours = ({ DAYS.forEach((day) => { const segments = days[day]; if (!segments) { - result[day] = [lang('BusinessHoursDayClosed')]; + result[day] = [oldLang('BusinessHoursDayClosed')]; return; } result[day] = segments.map(({ startMinute, endMinute }) => { - if (endMinute - startMinute === 24 * 60) return lang('BusinessHoursDayFullOpened'); - const start = formatTime(lang, weekStart + startMinute * 60 * 1000); - const end = formatTime(lang, weekStart + endMinute * 60 * 1000); + if (endMinute - startMinute === 24 * 60) return oldLang('BusinessHoursDayFullOpened'); + const start = formatTime(oldLang, weekStart + startMinute * 60 * 1000); + const end = formatTime(oldLang, weekStart + endMinute * 60 * 1000); return `${start} – ${end}`; }); }); return result; - }, [businessHours.workHours, isMyTime, lang, timezoneMinuteDifference]); + }, [businessHours.workHours, isMyTime, oldLang, timezoneMinuteDifference]); const isBusinessOpen = useMemo(() => { const localTimeHours = shiftTimeRanges(businessHours.workHours, timezoneMinuteDifference); @@ -96,41 +104,25 @@ const BusinessHours = ({ const handleClick = useLastCallback(() => { if (isExpanded) { - collapse(); + startViewTransition(VTT_PROFILE_BUSINESS_HOURS_COLLAPSE, () => { + collapse(); + }); } else { - expand(); + startViewTransition(VTT_PROFILE_BUSINESS_HOURS_EXPAND, () => { + expand(); + }); } }); const handleTriggerOffset = useLastCallback((e: React.MouseEvent) => { e.stopPropagation(); - if (isMyTime) { - showInLocalTime(); - } else { - showInMyTime(); - } - }); - - useEffect(() => { - if (!isExpanded) return; - const slide = document.querySelector(`.${ACTIVE_SLIDE_CLASS_NAME} > .${styles.timetable}`); - if (!slide) return; - - const height = slide.offsetHeight; - requestMutation(() => { - transitionRef.current!.style.height = `${height}px`; - }); - }, [isExpanded]); - - const handleAnimationStart = useLastCallback(() => { - const slide = document.querySelector(`.${TO_SLIDE_CLASS_NAME} > .${styles.timetable}`)!; - - requestMeasure(() => { - const height = slide.offsetHeight; - requestMutation(() => { - transitionRef.current!.style.height = `${height}px`; - }); + startViewTransition(VTT_PROFILE_BUSINESS_HOURS, () => { + if (isMyTime) { + showInLocalTime(); + } else { + showInMyTime(); + } }); }); @@ -139,7 +131,8 @@ const BusinessHours = ({ icon="clock" iconClassName={styles.icon} multiline - className={styles.root} + className={buildClassName(styles.root, className)} + style={createVtnStyle('businessHours', true)} isStatic={isExpanded} ripple narrow @@ -148,48 +141,43 @@ const BusinessHours = ({ >
-
{lang('BusinessHoursProfile')}
-
- {isBusinessOpen ? lang('BusinessHoursProfileNowOpen') : lang('BusinessHoursProfileNowClosed')} +
{oldLang('BusinessHoursProfile')}
+
+ {isBusinessOpen ? oldLang('BusinessHoursProfileNowOpen') : oldLang('BusinessHoursProfileNowClosed')}
- +
{isExpanded && (
{Boolean(timezoneMinuteDifference) && (
- {lang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')} + {oldLang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')}
)} - -
- {DAYS.map((day) => ( - <> -
- {formatWeekday(lang, day === 6 ? 0 : day + 1)} -
-
- {workHours[day].map((segment) => ( -
{segment}
- ))} -
- - ))} -
-
+
+ {DAYS.map((day) => ( + <> +
+ {formatWeekday(oldLang, day === 6 ? 0 : day + 1)} +
+
+ {workHours[day].map((segment) => ( +
{segment}
+ ))} +
+ + ))} +
)} diff --git a/src/components/common/profile/ChatExtra.module.scss b/src/components/common/profile/ChatExtra.module.scss index ba25ae906..01e7ef501 100644 --- a/src/components/common/profile/ChatExtra.module.scss +++ b/src/components/common/profile/ChatExtra.module.scss @@ -9,6 +9,8 @@ border-radius: 0.25rem; object-fit: cover; + + @include mixins.with-vt-type('chatExtra'); } .personalChannel { @@ -18,6 +20,8 @@ column-gap: 0.5rem; margin-bottom: 0.5rem; + + @include mixins.with-vt-type('chatExtra'); } .personalChannelTitle { @@ -31,6 +35,11 @@ color: var(--color-text-secondary); } +.phone, .description, .link, .miniapp, .notifications, .location, .note, .savedMessages, .botEmojiStatus, +.botLocation, .subscribers { + @include mixins.with-vt-type('chatExtra'); +} + .botVerificationSection, .sectionInfo { font-size: 0.875rem; @@ -39,6 +48,8 @@ .botVerificationSection { padding-inline: 1.25rem; + + @include mixins.with-vt-type('chatExtra'); } .botVerificationIcon { @@ -71,9 +82,15 @@ margin-bottom: 0.5rem; } +.note { + @include mixins.with-vt-type('profileNote'); +} + .noteSubtitle { display: flex !important; align-items: center; + + @include mixins.with-vt-type('profileNote'); } .noteListItemIcon { @@ -87,13 +104,9 @@ .noteText { position: relative; - overflow: hidden; display: inline-block; - width: 100%; - /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ - transition: max-height 0.3s ease; &::after { pointer-events: none; @@ -111,23 +124,61 @@ transition: opacity 0.3s ease; } + + @include mixins.with-vt-type('profileNote'); } .noteTextCollapsed::after { opacity: 1; } -.noteCollapseIcon { +.noteExpandIcon { margin-inline-start: 0.125rem; font-size: 0.9375rem; line-height: 0.9375rem; - transition: transform 0.3s ease; -} -.expandedIcon { - transform: rotate(-180deg); + @include mixins.with-vt-type('profileNote'); } .clickable { cursor: var(--custom-cursor, pointer); } + +@include mixins.on-active-vt('profileNote') { + &::view-transition-image-pair(.noteText) { + overflow: hidden; + } + + &::view-transition-old(.noteText), + &::view-transition-new(.noteText) { + animation-name: none; + } +} + +@include mixins.on-active-vt('profileNoteExpand') { + &::view-transition-old(.noteExpandIcon) { + animation-name: vt-expand-icon-spin; + } + + &::view-transition-new(.noteExpandIcon) { + display: none; + } + + &::view-transition-old(.noteText) { + display: none; + } +} + +@include mixins.on-active-vt('profileNoteCollapse') { + &::view-transition-old(.noteExpandIcon) { + animation-name: vt-collapse-icon-spin; + } + + &::view-transition-new(.noteExpandIcon) { + display: none; + } + + &::view-transition-new(.noteText) { + display: none; + } +} diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index b9283bdac..2b677f563 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -38,6 +38,7 @@ import { selectUser, selectUserFullInfo, } from '../../../global/selectors'; +import { VTT_PROFILE_NOTE_COLLAPSE, VTT_PROFILE_NOTE_EXPAND } from '../../../util/animations/viewTransitionTypes'; import buildClassName from '../../../util/buildClassName'; import { copyTextToClipboard } from '../../../util/clipboard'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; @@ -48,6 +49,8 @@ import formatUsername from '../helpers/formatUsername'; import renderText from '../helpers/renderText'; import { renderTextWithEntities } from '../helpers/renderTextWithEntities'; +import { useViewTransition } from '../../../hooks/animations/useViewTransition'; +import { useVtn } from '../../../hooks/animations/useVtn'; import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useLang from '../../../hooks/useLang'; @@ -75,7 +78,6 @@ type OwnProps = { isSavedDialog?: boolean; isInSettings?: boolean; className?: string; - style?: string; }; type StateProps = { @@ -105,7 +107,7 @@ const DEFAULT_MAP_CONFIG = { }; const BOT_VERIFICATION_ICON_SIZE = 16; -const MAX_LINES = 3; +const NOTE_MAX_LINES = 3; const ChatExtra = ({ chatOrUserId, @@ -127,7 +129,6 @@ const ChatExtra = ({ botAppPermissions, botVerification, className, - style, isInSettings, canViewSubscribers, }: OwnProps & StateProps) => { @@ -163,6 +164,9 @@ const ChatExtra = ({ const oldLang = useOldLang(); const lang = useLang(); + const { startViewTransition } = useViewTransition(); + const { createVtnStyle } = useVtn(); + const noteTextRef = useRef(); const shouldRenderNote = Boolean(note); @@ -173,7 +177,7 @@ const ChatExtra = ({ setIsCollapsed: setIsNoteCollapsed, } = useCollapsibleLines( noteTextRef, - MAX_LINES, + NOTE_MAX_LINES, undefined, !shouldRenderNote, ); @@ -264,11 +268,16 @@ const ChatExtra = ({ const canExpandNote = isNoteCollapsible && isNoteCollapsed; const handleExpandNote = useLastCallback(() => { - setIsNoteCollapsed(false); + startViewTransition(VTT_PROFILE_NOTE_EXPAND, () => { + setIsNoteCollapsed(false); + }); }); const handleToggleNote = useLastCallback(() => { - setIsNoteCollapsed((prev) => !prev); + const isCollapsed = isNoteCollapsed; + startViewTransition(isCollapsed ? VTT_PROFILE_NOTE_EXPAND : VTT_PROFILE_NOTE_COLLAPSE, () => { + setIsNoteCollapsed(() => !isCollapsed); + }); }); function copy(text: string, entity: string) { @@ -382,9 +391,9 @@ const ChatExtra = ({ } return ( -
+
{personalChannel && ( -
+

{oldLang('ProfileChannel')}

{oldLang('Subscribers', personalChannel.membersCount, 'i')} @@ -400,8 +409,15 @@ const ChatExtra = ({
)} {Boolean(formattedNumber?.length) && ( - - + {formattedNumber} {oldLang('Phone')} @@ -410,10 +426,12 @@ const ChatExtra = ({ {description && Boolean(description.length) && ( { @@ -432,10 +450,11 @@ const ChatExtra = ({ copy(link, oldLang('SetUrlPlaceholder'))} + style={createVtnStyle('link')} >
{link}
{oldLang('SetUrlPlaceholder')} @@ -447,8 +466,10 @@ const ChatExtra = ({ {hasMainMiniApp && (