Profile: Optimize accordion transitions (#6488)

This commit is contained in:
zubiden 2025-11-22 12:54:12 +01:00 committed by Alexander Zinchuk
parent 6054536ee9
commit 9738c34242
7 changed files with 267 additions and 100 deletions

View File

@ -1,5 +1,9 @@
@use '../../../styles/mixins';
.root { .root {
cursor: pointer; cursor: pointer;
@include mixins.with-vt-type('profileBusinessHours');
} }
.top { .top {
@ -32,6 +36,8 @@
margin-inline-end: 0.375rem; margin-inline-end: 0.375rem;
font-size: 1.25rem; font-size: 1.25rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
@include mixins.with-vt-type('profileBusinessHours');
} }
.offset-trigger { .offset-trigger {
@ -54,13 +60,13 @@
&:hover { &:hover {
background-color: var(--color-primary-opacity); background-color: var(--color-primary-opacity);
} }
@include mixins.with-vt-type('profileBusinessHours');
} }
.transition { .transition {
height: 0; height: 0;
margin-bottom: 0.5rem; 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 { .timetable {
@ -91,3 +97,42 @@
.current-day { .current-day {
color: var(--color-primary); 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;
}
}

View File

@ -1,11 +1,14 @@
import type React from '../../../lib/teact/teact';
import { import {
memo, useEffect, useMemo, useRef, memo, useMemo,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import type { ApiBusinessWorkHours } from '../../../api/types'; 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 { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatTime, formatWeekday } from '../../../util/dates/dateFormat'; import { formatTime, formatWeekday } from '../../../util/dates/dateFormat';
@ -13,6 +16,8 @@ import {
getUtcOffset, getWeekStart, shiftTimeRanges, splitDays, getUtcOffset, getWeekStart, shiftTimeRanges, splitDays,
} from '../../../util/dates/workHours'; } from '../../../util/dates/workHours';
import { useViewTransition } from '../../../hooks/animations/useViewTransition';
import { useVtn } from '../../../hooks/animations/useVtn';
import useSelectorSignal from '../../../hooks/data/useSelectorSignal'; import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
import useInterval from '../../../hooks/schedulers/useInterval'; import useInterval from '../../../hooks/schedulers/useInterval';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
@ -22,7 +27,6 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';
import Icon from '../icons/Icon'; import Icon from '../icons/Icon';
import styles from './BusinessHours.module.scss'; import styles from './BusinessHours.module.scss';
@ -31,17 +35,21 @@ const DAYS = Array.from({ length: 7 }, (_, i) => i);
type OwnProps = { type OwnProps = {
businessHours: ApiBusinessWorkHours; businessHours: ApiBusinessWorkHours;
className?: string;
}; };
const BusinessHours = ({ const BusinessHours = ({
businessHours, businessHours,
className,
}: OwnProps) => { }: OwnProps) => {
const transitionRef = useRef<HTMLDivElement>();
const [isExpanded, expand, collapse] = useFlag(false); const [isExpanded, expand, collapse] = useFlag(false);
const [isMyTime, showInMyTime, showInLocalTime] = useFlag(false); const [isMyTime, showInMyTime, showInLocalTime] = useFlag(false);
const lang = useOldLang(); const oldLang = useOldLang();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const { startViewTransition } = useViewTransition();
const { createVtnStyle } = useVtn();
useInterval(forceUpdate, 60 * 1000); useInterval(forceUpdate, 60 * 1000);
const timezoneSignal = useSelectorSignal((global) => global.timezones?.byId); const timezoneSignal = useSelectorSignal((global) => global.timezones?.byId);
@ -62,20 +70,20 @@ const BusinessHours = ({
DAYS.forEach((day) => { DAYS.forEach((day) => {
const segments = days[day]; const segments = days[day];
if (!segments) { if (!segments) {
result[day] = [lang('BusinessHoursDayClosed')]; result[day] = [oldLang('BusinessHoursDayClosed')];
return; return;
} }
result[day] = segments.map(({ startMinute, endMinute }) => { result[day] = segments.map(({ startMinute, endMinute }) => {
if (endMinute - startMinute === 24 * 60) return lang('BusinessHoursDayFullOpened'); if (endMinute - startMinute === 24 * 60) return oldLang('BusinessHoursDayFullOpened');
const start = formatTime(lang, weekStart + startMinute * 60 * 1000); const start = formatTime(oldLang, weekStart + startMinute * 60 * 1000);
const end = formatTime(lang, weekStart + endMinute * 60 * 1000); const end = formatTime(oldLang, weekStart + endMinute * 60 * 1000);
return `${start} ${end}`; return `${start} ${end}`;
}); });
}); });
return result; return result;
}, [businessHours.workHours, isMyTime, lang, timezoneMinuteDifference]); }, [businessHours.workHours, isMyTime, oldLang, timezoneMinuteDifference]);
const isBusinessOpen = useMemo(() => { const isBusinessOpen = useMemo(() => {
const localTimeHours = shiftTimeRanges(businessHours.workHours, timezoneMinuteDifference); const localTimeHours = shiftTimeRanges(businessHours.workHours, timezoneMinuteDifference);
@ -96,41 +104,25 @@ const BusinessHours = ({
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (isExpanded) { if (isExpanded) {
collapse(); startViewTransition(VTT_PROFILE_BUSINESS_HOURS_COLLAPSE, () => {
collapse();
});
} else { } else {
expand(); startViewTransition(VTT_PROFILE_BUSINESS_HOURS_EXPAND, () => {
expand();
});
} }
}); });
const handleTriggerOffset = useLastCallback((e: React.MouseEvent) => { const handleTriggerOffset = useLastCallback((e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (isMyTime) { startViewTransition(VTT_PROFILE_BUSINESS_HOURS, () => {
showInLocalTime(); if (isMyTime) {
} else { showInLocalTime();
showInMyTime(); } else {
} showInMyTime();
}); }
useEffect(() => {
if (!isExpanded) return;
const slide = document.querySelector<HTMLElement>(`.${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<HTMLElement>(`.${TO_SLIDE_CLASS_NAME} > .${styles.timetable}`)!;
requestMeasure(() => {
const height = slide.offsetHeight;
requestMutation(() => {
transitionRef.current!.style.height = `${height}px`;
});
}); });
}); });
@ -139,7 +131,8 @@ const BusinessHours = ({
icon="clock" icon="clock"
iconClassName={styles.icon} iconClassName={styles.icon}
multiline multiline
className={styles.root} className={buildClassName(styles.root, className)}
style={createVtnStyle('businessHours', true)}
isStatic={isExpanded} isStatic={isExpanded}
ripple ripple
narrow narrow
@ -148,48 +141,43 @@ const BusinessHours = ({
> >
<div className={styles.top}> <div className={styles.top}>
<div className={styles.left}> <div className={styles.left}>
<div>{lang('BusinessHoursProfile')}</div> <div>{oldLang('BusinessHoursProfile')}</div>
<div className={buildClassName(styles.status, isBusinessOpen && styles.statusOpen)}> <div
{isBusinessOpen ? lang('BusinessHoursProfileNowOpen') : lang('BusinessHoursProfileNowClosed')} className={buildClassName(styles.status, isBusinessOpen && styles.statusOpen)}
>
{isBusinessOpen ? oldLang('BusinessHoursProfileNowOpen') : oldLang('BusinessHoursProfileNowClosed')}
</div> </div>
</div> </div>
<Icon className={styles.arrow} name={isExpanded ? 'up' : 'down'} /> <Icon className={styles.arrow} style={createVtnStyle('expandArrow', true)} name={isExpanded ? 'up' : 'down'} />
</div> </div>
{isExpanded && ( {isExpanded && (
<div className={styles.bottom}> <div className={styles.bottom}>
{Boolean(timezoneMinuteDifference) && ( {Boolean(timezoneMinuteDifference) && (
<div <div
className={styles.offsetTrigger} className={styles.offsetTrigger}
style={createVtnStyle('offsetTrigger')}
role="button" role="button"
tabIndex={0} tabIndex={0}
onMouseDown={!IS_TOUCH_ENV ? handleTriggerOffset : undefined} onMouseDown={!IS_TOUCH_ENV ? handleTriggerOffset : undefined}
onClick={IS_TOUCH_ENV ? handleTriggerOffset : undefined} onClick={IS_TOUCH_ENV ? handleTriggerOffset : undefined}
> >
{lang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')} {oldLang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')}
</div> </div>
)} )}
<Transition <dl className={styles.timetable}>
className={styles.transition} {DAYS.map((day) => (
ref={transitionRef} <>
name="fade" <dt className={buildClassName(styles.weekday, day === currentDay && styles.currentDay)}>
activeKey={Number(isMyTime)} {formatWeekday(oldLang, day === 6 ? 0 : day + 1)}
onStart={handleAnimationStart} </dt>
> <dd className={styles.schedule}>
<dl className={styles.timetable}> {workHours[day].map((segment) => (
{DAYS.map((day) => ( <div>{segment}</div>
<> ))}
<dt className={buildClassName(styles.weekday, day === currentDay && styles.currentDay)}> </dd>
{formatWeekday(lang, day === 6 ? 0 : day + 1)} </>
</dt> ))}
<dd className={styles.schedule}> </dl>
{workHours[day].map((segment) => (
<div>{segment}</div>
))}
</dd>
</>
))}
</dl>
</Transition>
</div> </div>
)} )}
</ListItem> </ListItem>

View File

@ -9,6 +9,8 @@
border-radius: 0.25rem; border-radius: 0.25rem;
object-fit: cover; object-fit: cover;
@include mixins.with-vt-type('chatExtra');
} }
.personalChannel { .personalChannel {
@ -18,6 +20,8 @@
column-gap: 0.5rem; column-gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@include mixins.with-vt-type('chatExtra');
} }
.personalChannelTitle { .personalChannelTitle {
@ -31,6 +35,11 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.phone, .description, .link, .miniapp, .notifications, .location, .note, .savedMessages, .botEmojiStatus,
.botLocation, .subscribers {
@include mixins.with-vt-type('chatExtra');
}
.botVerificationSection, .botVerificationSection,
.sectionInfo { .sectionInfo {
font-size: 0.875rem; font-size: 0.875rem;
@ -39,6 +48,8 @@
.botVerificationSection { .botVerificationSection {
padding-inline: 1.25rem; padding-inline: 1.25rem;
@include mixins.with-vt-type('chatExtra');
} }
.botVerificationIcon { .botVerificationIcon {
@ -71,9 +82,15 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.note {
@include mixins.with-vt-type('profileNote');
}
.noteSubtitle { .noteSubtitle {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@include mixins.with-vt-type('profileNote');
} }
.noteListItemIcon { .noteListItemIcon {
@ -87,13 +104,9 @@
.noteText { .noteText {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
width: 100%; width: 100%;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.3s ease;
&::after { &::after {
pointer-events: none; pointer-events: none;
@ -111,23 +124,61 @@
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
@include mixins.with-vt-type('profileNote');
} }
.noteTextCollapsed::after { .noteTextCollapsed::after {
opacity: 1; opacity: 1;
} }
.noteCollapseIcon { .noteExpandIcon {
margin-inline-start: 0.125rem; margin-inline-start: 0.125rem;
font-size: 0.9375rem; font-size: 0.9375rem;
line-height: 0.9375rem; line-height: 0.9375rem;
transition: transform 0.3s ease;
}
.expandedIcon { @include mixins.with-vt-type('profileNote');
transform: rotate(-180deg);
} }
.clickable { .clickable {
cursor: var(--custom-cursor, pointer); 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;
}
}

View File

@ -38,6 +38,7 @@ import {
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { VTT_PROFILE_NOTE_COLLAPSE, VTT_PROFILE_NOTE_EXPAND } from '../../../util/animations/viewTransitionTypes';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
@ -48,6 +49,8 @@ import formatUsername from '../helpers/formatUsername';
import renderText from '../helpers/renderText'; import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import { useViewTransition } from '../../../hooks/animations/useViewTransition';
import { useVtn } from '../../../hooks/animations/useVtn';
import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines'; import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -75,7 +78,6 @@ type OwnProps = {
isSavedDialog?: boolean; isSavedDialog?: boolean;
isInSettings?: boolean; isInSettings?: boolean;
className?: string; className?: string;
style?: string;
}; };
type StateProps = { type StateProps = {
@ -105,7 +107,7 @@ const DEFAULT_MAP_CONFIG = {
}; };
const BOT_VERIFICATION_ICON_SIZE = 16; const BOT_VERIFICATION_ICON_SIZE = 16;
const MAX_LINES = 3; const NOTE_MAX_LINES = 3;
const ChatExtra = ({ const ChatExtra = ({
chatOrUserId, chatOrUserId,
@ -127,7 +129,6 @@ const ChatExtra = ({
botAppPermissions, botAppPermissions,
botVerification, botVerification,
className, className,
style,
isInSettings, isInSettings,
canViewSubscribers, canViewSubscribers,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
@ -163,6 +164,9 @@ const ChatExtra = ({
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const { startViewTransition } = useViewTransition();
const { createVtnStyle } = useVtn();
const noteTextRef = useRef<HTMLDivElement>(); const noteTextRef = useRef<HTMLDivElement>();
const shouldRenderNote = Boolean(note); const shouldRenderNote = Boolean(note);
@ -173,7 +177,7 @@ const ChatExtra = ({
setIsCollapsed: setIsNoteCollapsed, setIsCollapsed: setIsNoteCollapsed,
} = useCollapsibleLines( } = useCollapsibleLines(
noteTextRef, noteTextRef,
MAX_LINES, NOTE_MAX_LINES,
undefined, undefined,
!shouldRenderNote, !shouldRenderNote,
); );
@ -264,11 +268,16 @@ const ChatExtra = ({
const canExpandNote = isNoteCollapsible && isNoteCollapsed; const canExpandNote = isNoteCollapsible && isNoteCollapsed;
const handleExpandNote = useLastCallback(() => { const handleExpandNote = useLastCallback(() => {
setIsNoteCollapsed(false); startViewTransition(VTT_PROFILE_NOTE_EXPAND, () => {
setIsNoteCollapsed(false);
});
}); });
const handleToggleNote = useLastCallback(() => { 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) { function copy(text: string, entity: string) {
@ -382,9 +391,9 @@ const ChatExtra = ({
} }
return ( return (
<div className={buildClassName('ChatExtra', className)} style={style}> <div className={buildClassName('ChatExtra', className)} style={createVtnStyle('chatExtra')}>
{personalChannel && ( {personalChannel && (
<div className={styles.personalChannel}> <div className={styles.personalChannel} style={createVtnStyle('personalChannel')}>
<h3 className={styles.personalChannelTitle}>{oldLang('ProfileChannel')}</h3> <h3 className={styles.personalChannelTitle}>{oldLang('ProfileChannel')}</h3>
<span className={styles.personalChannelSubscribers}> <span className={styles.personalChannelSubscribers}>
{oldLang('Subscribers', personalChannel.membersCount, 'i')} {oldLang('Subscribers', personalChannel.membersCount, 'i')}
@ -400,8 +409,15 @@ const ChatExtra = ({
</div> </div>
)} )}
{Boolean(formattedNumber?.length) && ( {Boolean(formattedNumber?.length) && (
<ListItem
<ListItem icon="phone" multiline narrow ripple onClick={handlePhoneClick}> icon="phone"
className={styles.phone}
multiline
narrow
ripple
onClick={handlePhoneClick}
style={createVtnStyle('phone')}
>
<span className="title" dir={lang.isRtl ? 'rtl' : undefined}>{formattedNumber}</span> <span className="title" dir={lang.isRtl ? 'rtl' : undefined}>{formattedNumber}</span>
<span className="subtitle">{oldLang('Phone')}</span> <span className="subtitle">{oldLang('Phone')}</span>
</ListItem> </ListItem>
@ -410,10 +426,12 @@ const ChatExtra = ({
{description && Boolean(description.length) && ( {description && Boolean(description.length) && (
<ListItem <ListItem
icon="info" icon="info"
className={styles.description}
multiline multiline
narrow narrow
isStatic isStatic
allowSelection allowSelection
style={createVtnStyle('description')}
> >
<span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}> <span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}>
{ {
@ -432,10 +450,11 @@ const ChatExtra = ({
<ListItem <ListItem
icon="link" icon="link"
multiline multiline
className={styles.link}
narrow narrow
ripple ripple
onClick={() => copy(link, oldLang('SetUrlPlaceholder'))} onClick={() => copy(link, oldLang('SetUrlPlaceholder'))}
style={createVtnStyle('link')}
> >
<div className="title">{link}</div> <div className="title">{link}</div>
<span className="subtitle">{oldLang('SetUrlPlaceholder')}</span> <span className="subtitle">{oldLang('SetUrlPlaceholder')}</span>
@ -447,8 +466,10 @@ const ChatExtra = ({
{hasMainMiniApp && ( {hasMainMiniApp && (
<ListItem <ListItem
multiline multiline
className={styles.miniapp}
isStatic isStatic
narrow narrow
style={createVtnStyle('miniapp')}
> >
<Button <Button
className={styles.openAppButton} className={styles.openAppButton}
@ -462,7 +483,14 @@ const ChatExtra = ({
</ListItem> </ListItem>
)} )}
{!isOwnProfile && !isInSettings && ( {!isOwnProfile && !isInSettings && (
<ListItem icon={isMuted ? 'mute' : 'unmute'} narrow ripple onClick={handleToggleNotifications}> <ListItem
icon={isMuted ? 'mute' : 'unmute'}
className={styles.notifications}
narrow
ripple
onClick={handleToggleNotifications}
style={createVtnStyle('notifications')}
>
<span>{lang('Notifications')}</span> <span>{lang('Notifications')}</span>
<Switcher <Switcher
id="group-notifications" id="group-notifications"
@ -481,6 +509,8 @@ const ChatExtra = ({
ripple ripple
multiline multiline
narrow narrow
className={styles.location}
style={createVtnStyle('location')}
rightElement={locationRightComponent} rightElement={locationRightComponent}
onClick={handleClickLocation} onClick={handleClickLocation}
> >
@ -496,6 +526,8 @@ const ChatExtra = ({
narrow narrow
isStatic isStatic
allowSelection allowSelection
className={styles.note}
style={createVtnStyle('note')}
> >
<div <div
ref={noteTextRef} ref={noteTextRef}
@ -506,6 +538,7 @@ const ChatExtra = ({
styles.noteText, styles.noteText,
isNoteCollapsed && styles.noteTextCollapsed, isNoteCollapsed && styles.noteTextCollapsed,
)} )}
style={createVtnStyle('noteText', true)}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
onClick={canExpandNote ? handleExpandNote : undefined} onClick={canExpandNote ? handleExpandNote : undefined}
> >
@ -514,31 +547,45 @@ const ChatExtra = ({
entities: note.entities, entities: note.entities,
})} })}
</div> </div>
<div className={buildClassName('subtitle', styles.noteSubtitle)}> <div className={buildClassName('subtitle', styles.noteSubtitle)} style={createVtnStyle('noteSubtitle')}>
<span>{lang('UserNoteTitle')}</span> <span>{lang('UserNoteTitle')}</span>
<span className={styles.noteHint}>{lang('UserNoteHint')}</span> <span className={styles.noteHint}>{lang('UserNoteHint')}</span>
{isNoteCollapsible && ( {isNoteCollapsible && (
<Icon <Icon
className={buildClassName( className={buildClassName(
styles.noteCollapseIcon, styles.noteExpandIcon,
styles.clickable, styles.clickable,
!isNoteCollapsed && styles.expandedIcon,
)} )}
style={createVtnStyle('noteExpandIcon', true)}
onClick={handleToggleNote} onClick={handleToggleNote}
name="down" name={isNoteCollapsed ? 'down' : 'up'}
/> />
)} )}
</div> </div>
</ListItem> </ListItem>
)} )}
{hasSavedMessages && !isOwnProfile && !isInSettings && ( {hasSavedMessages && !isOwnProfile && !isInSettings && (
<ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}> <ListItem
icon="saved-messages"
className={styles.savedMessages}
narrow
ripple
onClick={handleOpenSavedDialog}
style={createVtnStyle('savedMessages')}
>
<span>{oldLang('SavedMessagesTab')}</span> <span>{oldLang('SavedMessagesTab')}</span>
</ListItem> </ListItem>
)} )}
{userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && ( {userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && (
<ListItem icon="user" narrow ripple onClick={manageEmojiStatusChange}> <ListItem
icon="user"
className={styles.botEmojiStatus}
narrow
ripple
onClick={manageEmojiStatusChange}
style={createVtnStyle('botEmojiStatus')}
>
<span>{oldLang('BotProfilePermissionEmojiStatus')}</span> <span>{oldLang('BotProfilePermissionEmojiStatus')}</span>
<Switcher <Switcher
label={oldLang('BotProfilePermissionEmojiStatus')} label={oldLang('BotProfilePermissionEmojiStatus')}
@ -548,7 +595,14 @@ const ChatExtra = ({
</ListItem> </ListItem>
)} )}
{botAppPermissions?.geolocation !== undefined && ( {botAppPermissions?.geolocation !== undefined && (
<ListItem icon="location" narrow ripple onClick={handleLocationPermissionChange}> <ListItem
icon="location"
className={styles.botLocation}
narrow
ripple
onClick={handleLocationPermissionChange}
style={createVtnStyle('botLocation')}
>
<span>{oldLang('BotProfilePermissionLocation')}</span> <span>{oldLang('BotProfilePermissionLocation')}</span>
<Switcher <Switcher
label={oldLang('BotProfilePermissionLocation')} label={oldLang('BotProfilePermissionLocation')}
@ -558,13 +612,21 @@ const ChatExtra = ({
</ListItem> </ListItem>
)} )}
{canViewSubscribers && ( {canViewSubscribers && (
<ListItem icon="group" narrow multiline ripple onClick={handleOpenSubscribers}> <ListItem
icon="group"
narrow
multiline
ripple
className={styles.subscribers}
onClick={handleOpenSubscribers}
style={createVtnStyle('subscribers')}
>
<div className="title">{lang('ProfileItemSubscribers')}</div> <div className="title">{lang('ProfileItemSubscribers')}</div>
<span className="subtitle">{lang.number(chat?.membersCount || 0)}</span> <span className="subtitle">{lang.number(chat?.membersCount || 0)}</span>
</ListItem> </ListItem>
)} )}
{botVerification && ( {botVerification && (
<div className={styles.botVerificationSection}> <div className={styles.botVerificationSection} style={createVtnStyle('botVerification')}>
<CustomEmoji <CustomEmoji
className={styles.botVerificationIcon} className={styles.botVerificationIcon}
documentId={botVerification.iconId} documentId={botVerification.iconId}

View File

@ -1116,7 +1116,6 @@ const Profile = ({
chatOrUserId={profileId} chatOrUserId={profileId}
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
isOwnProfile={isOwnProfile} isOwnProfile={isOwnProfile}
style={createVtnStyle('chatExtra')}
/> />
</div> </div>
); );

View File

@ -424,6 +424,18 @@ body:not(.is-ios) {
} }
} }
@keyframes vt-expand-icon-spin {
to {
transform: rotate(180deg);
}
}
@keyframes vt-collapse-icon-spin {
to {
transform: rotate(-180deg);
}
}
.component-theme-dark { .component-theme-dark {
--color-background: rgb(33, 33, 33); --color-background: rgb(33, 33, 33);
--color-background-compact-menu: rgb(33, 33, 33, 0.867); --color-background-compact-menu: rgb(33, 33, 33, 0.867);

View File

@ -21,3 +21,13 @@ export const VTT_RIGHT_PROFILE_EXPAND = VTT_RIGHT_PROFILE_AVATAR.with('profileEx
export const VTT_RIGHT_PROFILE_COLLAPSE = VTT_RIGHT_PROFILE_AVATAR.with('profileCollapse'); export const VTT_RIGHT_PROFILE_COLLAPSE = VTT_RIGHT_PROFILE_AVATAR.with('profileCollapse');
export const VTT_PROFILE_GIFTS = VTT_RIGHT_COLUMN.with('profileGifts'); export const VTT_PROFILE_GIFTS = VTT_RIGHT_COLUMN.with('profileGifts');
export const VTT_CHAT_EXTRA = VTT_RIGHT_COLUMN.with('chatExtra');
export const VTT_PROFILE_BUSINESS_HOURS = VTT_CHAT_EXTRA.with('profileBusinessHours');
export const VTT_PROFILE_BUSINESS_HOURS_EXPAND = VTT_PROFILE_BUSINESS_HOURS.with('profileBusinessHoursExpand');
export const VTT_PROFILE_BUSINESS_HOURS_COLLAPSE = VTT_PROFILE_BUSINESS_HOURS.with('profileBusinessHoursCollapse');
export const VTT_PROFILE_NOTE = VTT_CHAT_EXTRA.with('profileNote');
export const VTT_PROFILE_NOTE_EXPAND = VTT_PROFILE_NOTE.with('profileNoteExpand');
export const VTT_PROFILE_NOTE_COLLAPSE = VTT_PROFILE_NOTE.with('profileNoteCollapse');