Profile: Redesign (#6895)
This commit is contained in:
parent
e881ecdb3e
commit
6ceb7b6573
@ -10,11 +10,11 @@
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
top: 0.3125rem;
|
||||
left: 0.3125rem;
|
||||
|
||||
padding: 0 0.3125rem;
|
||||
border-radius: 0.1875rem;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.125rem;
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
padding-top: 0.875rem;
|
||||
border-radius: 0.625rem;
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
background-color: var(--color-background);
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@include mixins.with-vt-type('chatExtra');
|
||||
}
|
||||
@ -40,6 +40,10 @@
|
||||
@include mixins.with-vt-type('chatExtra');
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.botVerificationSection,
|
||||
.sectionInfo {
|
||||
font-size: 0.875rem;
|
||||
@ -71,6 +75,7 @@
|
||||
.personalChannelItem {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
margin-top: 0.375rem;
|
||||
|
||||
:global(.Avatar) {
|
||||
margin-right: 0.9375rem !important;
|
||||
@ -144,6 +149,10 @@
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
.securityRiskIsland {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.unofficialSecurityRisk {
|
||||
margin-bottom: 0.5rem;
|
||||
padding-inline: 0.5rem;
|
||||
|
||||
@ -57,10 +57,11 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Island from '../../gili/layout/Island';
|
||||
import Switch from '../../gili/primitives/Switch';
|
||||
import Chat from '../../left/main/Chat';
|
||||
import Button from '../../ui/Button';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import CompactMapPreview from '../CompactMapPreview';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import Icon from '../icons/Icon';
|
||||
@ -75,7 +76,9 @@ type OwnProps = {
|
||||
isOwnProfile?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
isInSettings?: boolean;
|
||||
withIslands?: boolean;
|
||||
className?: string;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -127,7 +130,9 @@ const ChatExtra = ({
|
||||
botAppPermissions,
|
||||
botVerification,
|
||||
className,
|
||||
style,
|
||||
isInSettings,
|
||||
withIslands,
|
||||
canViewSubscribers,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
@ -387,13 +392,17 @@ const ChatExtra = ({
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = withIslands ? Island : 'div';
|
||||
|
||||
return (
|
||||
<div className={buildClassName('ChatExtra', className)} style={createVtnStyle('chatExtra')}>
|
||||
<div className={buildClassName('ChatExtra', className)} style={style || createVtnStyle('chatExtra')}>
|
||||
{user && userFullInfo?.isUnofficialSecurityRisk && (
|
||||
<div className={styles.unofficialSecurityRisk}>
|
||||
<Icon className={buildClassName(styles.riskIcon, 'in-text-icon')} name="info-filled" />
|
||||
{lang('UnofficialSecurityRisk', { peer: getPeerTitle(lang, user) })}
|
||||
</div>
|
||||
<Wrapper className={withIslands ? styles.securityRiskIsland : undefined}>
|
||||
<div className={styles.unofficialSecurityRisk}>
|
||||
<Icon className={buildClassName(styles.riskIcon, 'in-text-icon')} name="info-filled" />
|
||||
{lang('UnofficialSecurityRisk', { peer: getPeerTitle(lang, user) })}
|
||||
</div>
|
||||
</Wrapper>
|
||||
)}
|
||||
{personalChannel && (
|
||||
<div className={styles.personalChannel} style={createVtnStyle('personalChannel')}>
|
||||
@ -401,244 +410,244 @@ const ChatExtra = ({
|
||||
<span className={styles.personalChannelSubscribers}>
|
||||
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
|
||||
</span>
|
||||
<Chat
|
||||
chatId={personalChannel.id}
|
||||
orderDiff={0}
|
||||
shiftDiff={0}
|
||||
animationType={ChatAnimationTypes.None}
|
||||
isPreview
|
||||
previewMessageId={personalChannelMessageId}
|
||||
className={styles.personalChannelItem}
|
||||
/>
|
||||
<Wrapper className={styles.personalChannelItem}>
|
||||
<Chat
|
||||
chatId={personalChannel.id}
|
||||
orderDiff={0}
|
||||
shiftDiff={0}
|
||||
animationType={ChatAnimationTypes.None}
|
||||
isPreview
|
||||
previewMessageId={personalChannelMessageId}
|
||||
/>
|
||||
</Wrapper>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(formattedNumber?.length) && (
|
||||
<ListItem
|
||||
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="subtitle">{oldLang('Phone')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeUsernames && renderUsernames(activeUsernames)}
|
||||
{description && Boolean(description.length) && (
|
||||
<ListItem
|
||||
icon="info"
|
||||
className={styles.description}
|
||||
multiline
|
||||
narrow
|
||||
isStatic
|
||||
allowSelection
|
||||
style={createVtnStyle('description')}
|
||||
>
|
||||
<span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{
|
||||
renderText(description, [
|
||||
'br',
|
||||
shouldRenderAllLinks ? 'links' : 'tg_links',
|
||||
'emoji',
|
||||
])
|
||||
}
|
||||
</span>
|
||||
<span className="subtitle">{oldLang(userId ? 'UserBio' : 'Info')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeChatUsernames && !isTopicInfo && renderUsernames(activeChatUsernames, true)}
|
||||
{((!activeChatUsernames && canInviteUsers) || isTopicInfo) && link && (
|
||||
<ListItem
|
||||
icon="link"
|
||||
multiline
|
||||
className={styles.link}
|
||||
narrow
|
||||
ripple
|
||||
onClick={() => copy(link, oldLang('SetUrlPlaceholder'))}
|
||||
style={createVtnStyle('link')}
|
||||
>
|
||||
<div className="title">{link}</div>
|
||||
<span className="subtitle">{oldLang('SetUrlPlaceholder')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{birthday && (
|
||||
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
|
||||
)}
|
||||
{hasMainMiniApp && (
|
||||
<ListItem
|
||||
multiline
|
||||
className={styles.miniapp}
|
||||
isStatic
|
||||
narrow
|
||||
style={createVtnStyle('miniapp')}
|
||||
>
|
||||
<Button
|
||||
className={styles.openAppButton}
|
||||
onClick={handleOpenApp}
|
||||
<Wrapper>
|
||||
{Boolean(formattedNumber?.length) && (
|
||||
<ListItem
|
||||
icon="phone"
|
||||
className={styles.phone}
|
||||
multiline
|
||||
narrow
|
||||
ripple
|
||||
onClick={handlePhoneClick}
|
||||
style={createVtnStyle('phone')}
|
||||
>
|
||||
{oldLang('ProfileBotOpenApp')}
|
||||
</Button>
|
||||
<div className={styles.sectionInfo}>
|
||||
{appTermsInfo}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{!isOwnProfile && !isInSettings && (
|
||||
<ListItem
|
||||
icon={isMuted ? 'mute' : 'unmute'}
|
||||
className={styles.notifications}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleToggleNotifications}
|
||||
style={createVtnStyle('notifications')}
|
||||
>
|
||||
<span>{lang('Notifications')}</span>
|
||||
<Switcher
|
||||
id="group-notifications"
|
||||
label={lang(userId ? 'AriaToggleUserNotifications' : 'AriaToggleChatNotifications')}
|
||||
checked={!isMuted}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{businessWorkHours && (
|
||||
<BusinessHours businessHours={businessWorkHours} />
|
||||
)}
|
||||
{businessLocation && (
|
||||
<ListItem
|
||||
icon="location"
|
||||
ripple
|
||||
multiline
|
||||
narrow
|
||||
className={styles.location}
|
||||
style={createVtnStyle('location')}
|
||||
rightElement={locationRightComponent}
|
||||
onClick={handleClickLocation}
|
||||
>
|
||||
<div className="title">{businessLocation.address}</div>
|
||||
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{shouldRenderNote && (
|
||||
<ListItem
|
||||
icon="note"
|
||||
iconClassName={styles.noteListItemIcon}
|
||||
multiline
|
||||
narrow
|
||||
isStatic
|
||||
allowSelection
|
||||
className={styles.note}
|
||||
style={createVtnStyle('note')}
|
||||
>
|
||||
<div
|
||||
ref={noteTextRef}
|
||||
className={buildClassName(
|
||||
'title',
|
||||
'word-break',
|
||||
'allow-selection',
|
||||
styles.noteText,
|
||||
isNoteCollapsed && styles.noteTextCollapsed,
|
||||
)}
|
||||
style={createVtnStyle('noteText', true)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={canExpandNote ? handleExpandNote : undefined}
|
||||
<span className="title" dir={lang.isRtl ? 'rtl' : undefined}>{formattedNumber}</span>
|
||||
<span className="subtitle">{oldLang('Phone')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeUsernames && renderUsernames(activeUsernames)}
|
||||
{description && Boolean(description.length) && (
|
||||
<ListItem
|
||||
icon="info"
|
||||
className={styles.description}
|
||||
multiline
|
||||
narrow
|
||||
isStatic
|
||||
allowSelection
|
||||
style={createVtnStyle('description')}
|
||||
>
|
||||
{renderTextWithEntities({
|
||||
text: note.text,
|
||||
entities: note.entities,
|
||||
})}
|
||||
</div>
|
||||
<div className={buildClassName('subtitle', styles.noteSubtitle)} style={createVtnStyle('noteSubtitle')}>
|
||||
<span>{lang('UserNoteTitle')}</span>
|
||||
<span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{
|
||||
renderText(description, [
|
||||
'br',
|
||||
shouldRenderAllLinks ? 'links' : 'tg_links',
|
||||
'emoji',
|
||||
])
|
||||
}
|
||||
</span>
|
||||
<span className="subtitle">{oldLang(userId ? 'UserBio' : 'Info')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeChatUsernames && !isTopicInfo && renderUsernames(activeChatUsernames, true)}
|
||||
{((!activeChatUsernames && canInviteUsers) || isTopicInfo) && link && (
|
||||
<ListItem
|
||||
icon="link"
|
||||
multiline
|
||||
className={styles.link}
|
||||
narrow
|
||||
ripple
|
||||
onClick={() => copy(link, oldLang('SetUrlPlaceholder'))}
|
||||
style={createVtnStyle('link')}
|
||||
>
|
||||
<div className="title">{link}</div>
|
||||
<span className="subtitle">{oldLang('SetUrlPlaceholder')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{birthday && (
|
||||
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
|
||||
)}
|
||||
{hasMainMiniApp && (
|
||||
<ListItem
|
||||
multiline
|
||||
className={styles.miniapp}
|
||||
isStatic
|
||||
narrow
|
||||
style={createVtnStyle('miniapp')}
|
||||
>
|
||||
<Button
|
||||
className={styles.openAppButton}
|
||||
onClick={handleOpenApp}
|
||||
>
|
||||
{oldLang('ProfileBotOpenApp')}
|
||||
</Button>
|
||||
<div className={styles.sectionInfo}>
|
||||
{appTermsInfo}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{!isOwnProfile && !isInSettings && (
|
||||
<ListItem
|
||||
icon={isMuted ? 'mute' : 'unmute'}
|
||||
className={styles.notifications}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleToggleNotifications}
|
||||
style={createVtnStyle('notifications')}
|
||||
>
|
||||
<span>{lang('Notifications')}</span>
|
||||
<Switch
|
||||
id="group-notifications"
|
||||
checked={!isMuted}
|
||||
className={styles.switch}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{businessWorkHours && (
|
||||
<BusinessHours businessHours={businessWorkHours} />
|
||||
)}
|
||||
{businessLocation && (
|
||||
<ListItem
|
||||
icon="location"
|
||||
ripple
|
||||
multiline
|
||||
narrow
|
||||
className={styles.location}
|
||||
style={createVtnStyle('location')}
|
||||
rightElement={locationRightComponent}
|
||||
onClick={handleClickLocation}
|
||||
>
|
||||
<div className="title">{businessLocation.address}</div>
|
||||
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{shouldRenderNote && (
|
||||
<ListItem
|
||||
icon="note"
|
||||
iconClassName={styles.noteListItemIcon}
|
||||
multiline
|
||||
narrow
|
||||
isStatic
|
||||
allowSelection
|
||||
className={styles.note}
|
||||
style={createVtnStyle('note')}
|
||||
>
|
||||
<div
|
||||
ref={noteTextRef}
|
||||
className={buildClassName(
|
||||
'title',
|
||||
'word-break',
|
||||
'allow-selection',
|
||||
styles.noteText,
|
||||
isNoteCollapsed && styles.noteTextCollapsed,
|
||||
)}
|
||||
style={createVtnStyle('noteText', true)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={canExpandNote ? handleExpandNote : undefined}
|
||||
>
|
||||
{renderTextWithEntities({
|
||||
text: note.text,
|
||||
entities: note.entities,
|
||||
})}
|
||||
</div>
|
||||
<div className={buildClassName('subtitle', styles.noteSubtitle)} style={createVtnStyle('noteSubtitle')}>
|
||||
<span>{lang('UserNoteTitle')}</span>
|
||||
|
||||
<span className={styles.noteHint}>{lang('UserNoteHint')}</span>
|
||||
{isNoteCollapsible && (
|
||||
<Icon
|
||||
className={buildClassName(
|
||||
styles.noteExpandIcon,
|
||||
styles.clickable,
|
||||
)}
|
||||
style={createVtnStyle('noteExpandIcon', true)}
|
||||
onClick={handleToggleNote}
|
||||
name={isNoteCollapsed ? 'down' : 'up'}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.noteHint}>{lang('UserNoteHint')}</span>
|
||||
{isNoteCollapsible && (
|
||||
<Icon
|
||||
className={buildClassName(
|
||||
styles.noteExpandIcon,
|
||||
styles.clickable,
|
||||
)}
|
||||
style={createVtnStyle('noteExpandIcon', true)}
|
||||
onClick={handleToggleNote}
|
||||
name={isNoteCollapsed ? 'down' : 'up'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{hasSavedMessages && !isOwnProfile && !isInSettings && (
|
||||
<ListItem
|
||||
icon="saved-messages"
|
||||
className={styles.savedMessages}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleOpenSavedDialog}
|
||||
style={createVtnStyle('savedMessages')}
|
||||
>
|
||||
<span>{oldLang('SavedMessagesTab')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && (
|
||||
<ListItem
|
||||
icon="user"
|
||||
className={styles.botEmojiStatus}
|
||||
narrow
|
||||
ripple
|
||||
onClick={manageEmojiStatusChange}
|
||||
style={createVtnStyle('botEmojiStatus')}
|
||||
>
|
||||
<span>{oldLang('BotProfilePermissionEmojiStatus')}</span>
|
||||
<Switch
|
||||
checked={Boolean(isBotCanManageEmojiStatus)}
|
||||
className={styles.switch}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{botAppPermissions?.geolocation !== undefined && (
|
||||
<ListItem
|
||||
icon="location"
|
||||
className={styles.botLocation}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleLocationPermissionChange}
|
||||
style={createVtnStyle('botLocation')}
|
||||
>
|
||||
<span>{oldLang('BotProfilePermissionLocation')}</span>
|
||||
<Switch
|
||||
checked={Boolean(botAppPermissions?.geolocation)}
|
||||
className={styles.switch}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{canViewSubscribers && (
|
||||
<ListItem
|
||||
icon="group"
|
||||
narrow
|
||||
multiline
|
||||
ripple
|
||||
className={styles.subscribers}
|
||||
onClick={handleOpenSubscribers}
|
||||
style={createVtnStyle('subscribers')}
|
||||
>
|
||||
<div className="title">{lang('ProfileItemSubscribers')}</div>
|
||||
<span className="subtitle">{lang.number(chat?.membersCount || 0)}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{botVerification && (
|
||||
<div className={styles.botVerificationSection} style={createVtnStyle('botVerification')}>
|
||||
<CustomEmoji
|
||||
className={styles.botVerificationIcon}
|
||||
documentId={botVerification.iconId}
|
||||
size={BOT_VERIFICATION_ICON_SIZE}
|
||||
/>
|
||||
{botVerification.description}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{hasSavedMessages && !isOwnProfile && !isInSettings && (
|
||||
<ListItem
|
||||
icon="saved-messages"
|
||||
className={styles.savedMessages}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleOpenSavedDialog}
|
||||
style={createVtnStyle('savedMessages')}
|
||||
>
|
||||
<span>{oldLang('SavedMessagesTab')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && (
|
||||
<ListItem
|
||||
icon="user"
|
||||
className={styles.botEmojiStatus}
|
||||
narrow
|
||||
ripple
|
||||
onClick={manageEmojiStatusChange}
|
||||
style={createVtnStyle('botEmojiStatus')}
|
||||
>
|
||||
<span>{oldLang('BotProfilePermissionEmojiStatus')}</span>
|
||||
<Switcher
|
||||
label={oldLang('BotProfilePermissionEmojiStatus')}
|
||||
checked={isBotCanManageEmojiStatus}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{botAppPermissions?.geolocation !== undefined && (
|
||||
<ListItem
|
||||
icon="location"
|
||||
className={styles.botLocation}
|
||||
narrow
|
||||
ripple
|
||||
onClick={handleLocationPermissionChange}
|
||||
style={createVtnStyle('botLocation')}
|
||||
>
|
||||
<span>{oldLang('BotProfilePermissionLocation')}</span>
|
||||
<Switcher
|
||||
label={oldLang('BotProfilePermissionLocation')}
|
||||
checked={botAppPermissions?.geolocation}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{canViewSubscribers && (
|
||||
<ListItem
|
||||
icon="group"
|
||||
narrow
|
||||
multiline
|
||||
ripple
|
||||
className={styles.subscribers}
|
||||
onClick={handleOpenSubscribers}
|
||||
style={createVtnStyle('subscribers')}
|
||||
>
|
||||
<div className="title">{lang('ProfileItemSubscribers')}</div>
|
||||
<span className="subtitle">{lang.number(chat?.membersCount || 0)}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{botVerification && (
|
||||
<div className={styles.botVerificationSection} style={createVtnStyle('botVerification')}>
|
||||
<CustomEmoji
|
||||
className={styles.botVerificationIcon}
|
||||
documentId={botVerification.iconId}
|
||||
size={BOT_VERIFICATION_ICON_SIZE}
|
||||
/>
|
||||
{botVerification.description}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
--profile-info-bg: var(--color-background);
|
||||
--rating-outline-color: #000000;
|
||||
--rating-text-color: #000000;
|
||||
|
||||
@ -14,7 +15,7 @@
|
||||
|
||||
color: var(--color-white);
|
||||
|
||||
background-color: var(--color-background);
|
||||
background-color: var(--profile-info-bg);
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
|
||||
@ -33,7 +34,7 @@
|
||||
color: var(--color-text);
|
||||
|
||||
.info {
|
||||
background-image: linear-gradient(0deg, var(--color-background) 50%, transparent);
|
||||
background-image: linear-gradient(0deg, var(--profile-info-bg) 50%, transparent);
|
||||
}
|
||||
|
||||
.userRatingNegativeWrapper,
|
||||
@ -445,11 +446,6 @@
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileAvatar') {
|
||||
&::view-transition-old(.profileInfo),
|
||||
&::view-transition-new(.profileInfo) {
|
||||
animation-name: none;
|
||||
}
|
||||
|
||||
&::view-transition-group(.photoDashes) {
|
||||
z-index: 1;
|
||||
}
|
||||
@ -460,10 +456,22 @@
|
||||
|
||||
&::view-transition-group(.profileInfo) {
|
||||
z-index: -2;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
&::view-transition-old(.profileInfo),
|
||||
&::view-transition-new(.profileInfo) {
|
||||
object-fit: none;
|
||||
object-position: top;
|
||||
animation-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileExpand') {
|
||||
&::view-transition-old(.profileInfo) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::view-transition-old(.avatar) {
|
||||
z-index: -1;
|
||||
animation-name: none;
|
||||
@ -487,6 +495,10 @@
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileCollapse') {
|
||||
&::view-transition-new(.profileInfo) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::view-transition-new(.avatar) {
|
||||
z-index: -1;
|
||||
animation-name: none;
|
||||
|
||||
@ -12,4 +12,8 @@
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(1rem);
|
||||
}
|
||||
|
||||
.noPadding {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import type { ElementRef } from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import styles from './Surface.module.scss';
|
||||
|
||||
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
scrollable?: boolean;
|
||||
noPadding?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Surface = ({
|
||||
ref,
|
||||
scrollable,
|
||||
noPadding,
|
||||
className,
|
||||
children,
|
||||
...otherProps
|
||||
@ -17,10 +23,12 @@ const Surface = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isScrollable && 'custom-scroll',
|
||||
isScrollable && styles.scrollable,
|
||||
noPadding && styles.noPadding,
|
||||
className,
|
||||
)}
|
||||
{...otherProps}
|
||||
|
||||
@ -19,7 +19,7 @@ type OwnProps = {
|
||||
disabled?: boolean;
|
||||
withPermissionColors?: boolean;
|
||||
className?: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||
@ -40,7 +40,7 @@ const Switch = ({
|
||||
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
||||
|
||||
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.currentTarget.checked);
|
||||
onChange?.(e.currentTarget.checked);
|
||||
});
|
||||
|
||||
if (interactive?.isLoading) return undefined;
|
||||
|
||||
@ -413,7 +413,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
ref={ref}
|
||||
className={chatClassName}
|
||||
href={href}
|
||||
style={`top: ${offsetTop}px`}
|
||||
style={offsetTop !== undefined ? `top: ${offsetTop}px` : undefined}
|
||||
ripple={!isForum && !isMobile}
|
||||
contextActions={contextActions}
|
||||
withPortalForMenu
|
||||
|
||||
44
src/components/left/main/ChatFolderTabList.module.scss
Normal file
44
src/components/left/main/ChatFolderTabList.module.scss
Normal file
@ -0,0 +1,44 @@
|
||||
@layer component.chatFolderTabList {
|
||||
.root {
|
||||
--fade-mask-height: 2.625rem;
|
||||
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 0.25rem;
|
||||
inset-inline: 0.5rem;
|
||||
|
||||
transition: opacity var(--layer-transition);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
padding: 0 0.3125rem;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.25rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
|
||||
background: var(--color-gray);
|
||||
}
|
||||
|
||||
.badgeActive {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
:global(html.theme-dark) .tabList {
|
||||
border: 1px solid var(--color-borders);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
51
src/components/left/main/ChatFolderTabList.tsx
Normal file
51
src/components/left/main/ChatFolderTabList.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { TabWithProperties } from '../../ui/TabList';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import TabList from '../../ui/TabList';
|
||||
|
||||
import styles from './ChatFolderTabList.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
tabs: readonly TabWithProperties[];
|
||||
activeTab: number;
|
||||
isHidden?: boolean;
|
||||
className?: string;
|
||||
onSwitchTab: (index: number) => void;
|
||||
};
|
||||
|
||||
const ChatFolderTabList = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
isHidden,
|
||||
className,
|
||||
onSwitchTab,
|
||||
}: OwnProps) => {
|
||||
const renderExtra = useLastCallback((tab: TabWithProperties) => {
|
||||
if (!tab.badgeCount) return undefined;
|
||||
return (
|
||||
<span className={buildClassName(styles.badge, tab.isBadgeActive && styles.badgeActive)}>
|
||||
{tab.badgeCount}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, isHidden && styles.hidden)}>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
withFadeMask
|
||||
renderExtra={renderExtra}
|
||||
className={buildClassName(styles.tabList, className)}
|
||||
onSwitchTab={onSwitchTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatFolderTabList);
|
||||
@ -26,8 +26,8 @@ import useScrolledState from '../../../hooks/useScrolledState';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import StoryRibbon from '../../story/StoryRibbon';
|
||||
import SquareTabList from '../../ui/SquareTabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import ChatFolderTabList from './ChatFolderTabList';
|
||||
import ChatList from './ChatList';
|
||||
|
||||
type OwnProps = {
|
||||
@ -87,7 +87,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const { isAtBeginning: isNotScrolled, handleScroll, updateScrollState } = useScrolledState();
|
||||
const { handleScroll, updateScrollState } = useScrolledState();
|
||||
|
||||
useEffect(() => {
|
||||
loadChatFolders();
|
||||
@ -119,6 +119,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const { displayedFolders, folderTabs } = useFolderTabs({
|
||||
sidebarMode: false,
|
||||
noEmoticons: true,
|
||||
orderedFolderIds,
|
||||
chatFoldersById,
|
||||
maxFolders,
|
||||
@ -252,31 +253,31 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
ref={ref}
|
||||
className={buildClassName(
|
||||
'ChatFolders',
|
||||
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
|
||||
shouldRenderStoryRibbon && 'with-story-ribbon',
|
||||
isFoldersSidebarShown && 'ChatFolders--tabs-sidebar-shown',
|
||||
)}
|
||||
>
|
||||
{shouldRenderStoryRibbon && <StoryRibbon isClosing={isStoryRibbonClosing} />}
|
||||
{shouldRenderFolders ? (
|
||||
<SquareTabList
|
||||
contextRootElementSelector="#LeftColumn"
|
||||
tabs={folderTabs}
|
||||
activeTab={activeChatFolder}
|
||||
onSwitchTab={handleSwitchTab}
|
||||
className={!isNotScrolled ? 'scrolled' : undefined}
|
||||
/>
|
||||
) : shouldRenderPlaceholder ? (
|
||||
<div ref={placeholderRef} className="tabs-placeholder" />
|
||||
) : undefined}
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
|
||||
activeKey={activeChatFolder}
|
||||
renderCount={hasFolders ? folderTabs.length : undefined}
|
||||
>
|
||||
{renderCurrentTab}
|
||||
</Transition>
|
||||
<div className={buildClassName('ChatFolders-content', shouldRenderFolders && 'with-tabs')}>
|
||||
{shouldRenderFolders ? (
|
||||
<ChatFolderTabList
|
||||
tabs={folderTabs}
|
||||
activeTab={activeChatFolder}
|
||||
isHidden={shouldHideFolderTabs}
|
||||
onSwitchTab={handleSwitchTab}
|
||||
/>
|
||||
) : shouldRenderPlaceholder ? (
|
||||
<div ref={placeholderRef} className="tabs-placeholder" />
|
||||
) : undefined}
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
|
||||
activeKey={activeChatFolder}
|
||||
renderCount={hasFolders ? folderTabs.length : undefined}
|
||||
>
|
||||
{renderCurrentTab}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -48,6 +48,11 @@ type OwnProps = {
|
||||
isFoldersSidebarShown?: boolean;
|
||||
isStoryRibbonShown?: boolean;
|
||||
foldersDispatch?: FolderEditDispatch;
|
||||
noAbsolutePositioning?: boolean;
|
||||
noVirtualization?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
noFastList?: boolean;
|
||||
scrollContainerClosest?: string;
|
||||
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
@ -67,6 +72,11 @@ const ChatList = ({
|
||||
isFoldersSidebarShown,
|
||||
isStoryRibbonShown,
|
||||
foldersDispatch,
|
||||
noAbsolutePositioning,
|
||||
noVirtualization,
|
||||
noScrollRestore,
|
||||
noFastList,
|
||||
scrollContainerClosest,
|
||||
onScroll,
|
||||
}: OwnProps) => {
|
||||
const {
|
||||
@ -99,7 +109,10 @@ const ChatList = ({
|
||||
orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd,
|
||||
} = useOrderDiff(orderedIds, panesHeight);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
||||
const chatListSlice = noVirtualization
|
||||
? Math.max(CHAT_LIST_SLICE, orderedIds?.length || 0)
|
||||
: CHAT_LIST_SLICE;
|
||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, chatListSlice);
|
||||
|
||||
// Support <Alt>+<Up/Down> to navigate between chats
|
||||
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
||||
@ -194,7 +207,9 @@ const ChatList = ({
|
||||
|
||||
return viewportIds!.map((id, i) => {
|
||||
const isPinned = viewportOffset + i < pinnedCount;
|
||||
const offsetTop = panesHeight + archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||
const offsetTop = noAbsolutePositioning
|
||||
? undefined
|
||||
: panesHeight + archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||
|
||||
return (
|
||||
<Chat
|
||||
@ -219,6 +234,8 @@ const ChatList = ({
|
||||
});
|
||||
}
|
||||
|
||||
const totalHeight = chatsHeight + archiveHeight + panesHeight;
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open', className)}
|
||||
@ -226,8 +243,11 @@ const ChatList = ({
|
||||
items={viewportIds}
|
||||
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||
preloadBackwards={CHAT_LIST_SLICE}
|
||||
withAbsolutePositioning
|
||||
maxHeight={chatsHeight + archiveHeight + panesHeight}
|
||||
withAbsolutePositioning={!noAbsolutePositioning}
|
||||
maxHeight={!noAbsolutePositioning ? totalHeight : undefined}
|
||||
scrollContainerClosest={scrollContainerClosest}
|
||||
noScrollRestore={noScrollRestore}
|
||||
noFastList={noFastList}
|
||||
onLoadMore={getMore}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
|
||||
@ -30,35 +30,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.SquareTabList {
|
||||
z-index: 1;
|
||||
|
||||
justify-content: flex-start;
|
||||
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
|
||||
transition: opacity var(--layer-transition), box-shadow 0.2s, border-color 0.2s;
|
||||
|
||||
&.scrolled {
|
||||
border-bottom-color: transparent;
|
||||
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&--tabs-hidden .SquareTabList {
|
||||
pointer-events: none;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&--tabs-sidebar-shown .chat-list {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.Tab {
|
||||
flex: 0 0 auto;
|
||||
&-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
> .Transition {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.with-tabs > .Transition {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
&.with-tabs .chat-list {
|
||||
padding-top: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
> .Transition {
|
||||
|
||||
@ -27,26 +27,13 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tabListWrapper {
|
||||
.fadeMask {
|
||||
|
||||
--fade-mask-color: var(--color-background-secondary);
|
||||
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: calc(100% - 2rem);
|
||||
left: 0;
|
||||
|
||||
width: calc(100% - 2rem);
|
||||
height: 3.5rem;
|
||||
margin-inline: 1rem;
|
||||
|
||||
background: linear-gradient(to bottom, var(--color-background-secondary) 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.tabList {
|
||||
|
||||
@ -280,17 +280,17 @@ const AiMessageEditorModal = ({
|
||||
)}
|
||||
isSlim
|
||||
>
|
||||
<div className={styles.tabListWrapper}>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
activeTab={activeTabIndex}
|
||||
onSwitchTab={handleTabChange}
|
||||
className={styles.tabList}
|
||||
tabClassName={styles.tab}
|
||||
stretched
|
||||
itemAlignment="vertical"
|
||||
/>
|
||||
</div>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
activeTab={activeTabIndex}
|
||||
withFadeMask
|
||||
fadeMaskClassName={styles.fadeMask}
|
||||
className={styles.tabList}
|
||||
tabClassName={styles.tab}
|
||||
stretched
|
||||
itemAlignment="vertical"
|
||||
onSwitchTab={handleTabChange}
|
||||
/>
|
||||
|
||||
<div className={styles.transitionWrapper}>
|
||||
<Transition
|
||||
|
||||
326
src/components/right/Profile.module.scss
Normal file
326
src/components/right/Profile.module.scss
Normal file
@ -0,0 +1,326 @@
|
||||
@use '../../styles/mixins';
|
||||
|
||||
.root {
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
:global(.FloatingActionButton) {
|
||||
z-index: 1;
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
&:global(.revealed) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profileInfo {
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
:global(.ProfileInfo) {
|
||||
--profile-info-bg: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
:global(.ChatInfo) {
|
||||
grid-area: chat_info;
|
||||
|
||||
:global(.status.online) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ChatExtra) {
|
||||
|
||||
:global(.narrow) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:global([dir="rtl"]) {
|
||||
:global(.Switcher) {
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatExtraBlock {
|
||||
margin: 1rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
@include mixins.adapt-margin-to-scrollbar(1rem);
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
}
|
||||
|
||||
.sharedMediaTabs {
|
||||
--fade-mask-color: var(--color-background-secondary);
|
||||
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 1rem;
|
||||
|
||||
margin-top: 1rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(1rem);
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: -1rem;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: calc(100% + 1rem);
|
||||
|
||||
background: linear-gradient(to bottom, var(--fade-mask-color, var(--color-background)) 0%, transparent 100%);
|
||||
backdrop-filter: blur(0.5rem);
|
||||
|
||||
mask-image: linear-gradient(to bottom, black 65%, transparent 100%);
|
||||
|
||||
:global(html.theme-dark) & {
|
||||
backdrop-filter: blur(1rem);
|
||||
mask-image: linear-gradient(to bottom, black 85%, transparent 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nothingFoundGifts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding-top: 5rem;
|
||||
|
||||
.description {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
margin-block: 1rem;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.Link) {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-links);
|
||||
transition: opacity 0.15s ease-in;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sharedMedia {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
|
||||
margin-top: 1rem;
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
:global(.info .Transition) {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.sharedMediaIsland {
|
||||
overflow: hidden;
|
||||
margin: 0 1rem 1rem;
|
||||
padding: 0;
|
||||
|
||||
@include mixins.adapt-margin-to-scrollbar(1rem);
|
||||
|
||||
:global(.ListItem-button) {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sharedMediaIslandContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.savedDialogsIsland {
|
||||
overflow: clip;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.savedDialogs {
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
|
||||
:global(.Chat) {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
&.showContentPanel {
|
||||
transform: translateY(3rem);
|
||||
}
|
||||
|
||||
&.noTransition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.emptyList {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
padding-top: 5rem;
|
||||
|
||||
:global(.Spinner) {
|
||||
--spinner-size: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.storiesArchiveList,
|
||||
&.storiesList,
|
||||
&.mediaList,
|
||||
&.gifList,
|
||||
&.previewMediaList,
|
||||
&.giftsList {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.0625rem;
|
||||
}
|
||||
|
||||
&.storiesArchiveList,
|
||||
&.storiesList,
|
||||
&.mediaList,
|
||||
&.gifList,
|
||||
&.previewMediaList {
|
||||
overflow: hidden;
|
||||
margin-inline-start: var(--scrollbar-width);
|
||||
border-radius: var(--border-radius-island);
|
||||
}
|
||||
|
||||
&.giftsList {
|
||||
gap: 0.625rem;
|
||||
margin-inline-start: var(--scrollbar-width);
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
}
|
||||
|
||||
&.documentsList {
|
||||
padding: 1.25rem;
|
||||
|
||||
:global(.File + .File) {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.linksList {
|
||||
padding: 1.25rem;
|
||||
|
||||
:global(.ProgressSpinner),
|
||||
:global(.message-transfer-progress) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.audioList,
|
||||
&.voiceList {
|
||||
padding: 1.25rem;
|
||||
|
||||
:global(.Audio) {
|
||||
:global(.media-loading) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& + :global(.Audio) {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.similarChannelsList,
|
||||
&.similarBotsList,
|
||||
&.commonChatsList,
|
||||
&.membersList,
|
||||
&.giftsList {
|
||||
padding: 0.5rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.similarBotsList,
|
||||
&.similarChannelsList {
|
||||
:global(.ListItem.blured) {
|
||||
filter: opacity(0.8);
|
||||
}
|
||||
|
||||
.showMoreBots,
|
||||
.showMoreChannels {
|
||||
z-index: 1;
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
margin: 0 auto;
|
||||
margin-top: -1.8125rem;
|
||||
border-radius: var(--border-radius-default-small);
|
||||
|
||||
box-shadow: -1rem 0 1rem 1rem var(--color-background), -1rem 0 1rem 0.3125rem var(--color-background);
|
||||
}
|
||||
|
||||
.moreSimilar {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentCategoriesPanel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
transition: transform var(--layer-transition), opacity 0.2s ease;
|
||||
|
||||
&.hiddenPanel {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.noTransition {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.savedGift {
|
||||
@include mixins.with-vt-type('profileGifts');
|
||||
}
|
||||
@ -1,250 +0,0 @@
|
||||
@use '../../styles/mixins';
|
||||
|
||||
.Profile {
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
> .profile-info > .ChatInfo {
|
||||
grid-area: chat_info;
|
||||
|
||||
.status.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
> .profile-info > .ChatExtra {
|
||||
padding: 0.875rem 0.5rem 0.5rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
@include mixins.side-panel-section;
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.narrow {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] {
|
||||
.Switcher {
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.FloatingActionButton {
|
||||
z-index: 1;
|
||||
|
||||
&.revealed {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nothing-found-gifts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding-top: 5rem;
|
||||
|
||||
.description {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
margin-block: 1rem;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Link {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-links);
|
||||
transition: opacity 0.15s ease-in;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shared-media {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.SquareTabList {
|
||||
z-index: 1;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.info .Transition {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.Transition {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.saved-dialogs {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.shared-media-transition {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
transition: transform var(--layer-transition);
|
||||
&.showContentPanel {
|
||||
transform: translateY(3rem);
|
||||
padding-bottom: 3.5rem !important;
|
||||
}
|
||||
&.noTransition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.empty-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
padding-top: 5rem;
|
||||
|
||||
.Spinner {
|
||||
--spinner-size: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.storiesArchive-list,
|
||||
&.stories-list,
|
||||
&.media-list,
|
||||
&.gif-list,
|
||||
&.previewMedia-list,
|
||||
&.gifts-list {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.0625rem;
|
||||
}
|
||||
|
||||
&.gifts-list {
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
&.documents-list {
|
||||
padding: 1.25rem;
|
||||
|
||||
& .File + .File {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.links-list {
|
||||
padding: 1.25rem;
|
||||
|
||||
.ProgressSpinner,
|
||||
.message-transfer-progress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.audio-list,
|
||||
&.voice-list {
|
||||
padding: 1.25rem;
|
||||
|
||||
& .Audio {
|
||||
.media-loading {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& + .Audio {
|
||||
margin-top: 1.6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.similarChannels-list,
|
||||
&.similarBots-list,
|
||||
&.commonChats-list,
|
||||
&.members-list,
|
||||
&.gifts-list {
|
||||
padding: 0.5rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.similarBots-list,
|
||||
&.similarChannels-list {
|
||||
.ListItem.blured {
|
||||
filter: opacity(0.8);
|
||||
}
|
||||
|
||||
.show-more-bots,
|
||||
.show-more-channels {
|
||||
z-index: 1;
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
margin: 0 auto;
|
||||
margin-top: -1.8125rem;
|
||||
border-radius: var(--border-radius-default-small);
|
||||
|
||||
box-shadow: -1rem 0 1rem 1rem var(--color-background), -1rem 0 1rem 0.3125rem var(--color-background);
|
||||
}
|
||||
|
||||
.more-similar {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentCategoriesPanel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
transition: transform var(--layer-transition), opacity 0.2s ease;
|
||||
|
||||
&.hiddenPanel {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.noTransition {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.saved-gift {
|
||||
@include mixins.with-vt-type('profileGifts');
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from '@teact';
|
||||
import type { TeactNode } from '@teact';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
@ -18,15 +19,13 @@ import type {
|
||||
} from '../../api/types';
|
||||
import type { ProfileCollectionKey } from '../../global/selectors/payments';
|
||||
import type { TabState } from '../../global/types';
|
||||
import type { AnimationLevel, ProfileState, ProfileTabType, SharedMediaType, ThemeKey, ThreadId } from '../../types';
|
||||
import type { AnimationLevel, ProfileState, ProfileTabType,
|
||||
SharedMediaType, ThemeKey, ThreadId } from '../../types';
|
||||
import type { RegularLangKey } from '../../types/language';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
|
||||
import { AudioOrigin, LoadMoreDirection, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
|
||||
|
||||
import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config';
|
||||
import { selectActiveGiftsCollectionId } from '../../global/selectors/payments';
|
||||
|
||||
const CONTENT_PANEL_SHOW_DELAY = 300;
|
||||
import {
|
||||
getHasAdminRight,
|
||||
getIsDownloading,
|
||||
@ -63,6 +62,7 @@ import {
|
||||
} from '../../global/selectors';
|
||||
import { selectPremiumLimit } from '../../global/selectors/limits';
|
||||
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
|
||||
import { selectActiveGiftsCollectionId } from '../../global/selectors/payments';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import { selectActiveStoriesCollectionId } from '../../global/selectors/stories';
|
||||
import {
|
||||
@ -85,7 +85,6 @@ import { useViewTransition } from '../../hooks/animations/useViewTransition';
|
||||
import { useVtn } from '../../hooks/animations/useVtn.ts';
|
||||
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
|
||||
import useTopOverscroll from '../../hooks/scroll/useTopOverscroll.tsx';
|
||||
import useCacheBuster from '../../hooks/useCacheBuster';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||
@ -111,6 +110,8 @@ import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ChatExtra from '../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../common/profile/ProfileInfo.tsx';
|
||||
import WebLink from '../common/WebLink';
|
||||
import Island from '../gili/layout/Island';
|
||||
import Surface from '../gili/layout/Surface';
|
||||
import ChatList from '../left/main/ChatList';
|
||||
import MediaStory from '../story/MediaStory';
|
||||
import Button from '../ui/Button';
|
||||
@ -119,13 +120,13 @@ import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import Link from '../ui/Link';
|
||||
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import SquareTabList, { type TabWithProperties } from '../ui/SquareTabList';
|
||||
import TabList, { type TabWithProperties } from '../ui/TabList';
|
||||
import Transition from '../ui/Transition';
|
||||
import DeleteMemberModal from './DeleteMemberModal';
|
||||
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
|
||||
import StoryAlbumList from './stories/StoryAlbumList';
|
||||
|
||||
import './Profile.scss';
|
||||
import styles from './Profile.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -134,6 +135,7 @@ type OwnProps = {
|
||||
isMobile?: boolean;
|
||||
isActive: boolean;
|
||||
onProfileStateChange: (state: ProfileState) => void;
|
||||
onProfileExpandedChange?: (isExpanded: boolean) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -208,6 +210,7 @@ const TABS: LocalTabProps[] = [
|
||||
{ type: 'gif', key: 'ProfileTabGifs' },
|
||||
];
|
||||
|
||||
const CONTENT_PANEL_SHOW_DELAY = 300;
|
||||
const HIDDEN_RENDER_DELAY = 1000;
|
||||
const INTERSECTION_THROTTLE = 500;
|
||||
|
||||
@ -220,6 +223,26 @@ const VALID_USER_MAIN_TAB_TYPES = new Set<StringAutocomplete<ApiProfileTab>>([
|
||||
const SHARED_MEDIA_TYPES = new Set<StringAutocomplete<SharedMediaType>>([
|
||||
'media', 'documents', 'links', 'audio', 'voice', 'gif',
|
||||
]);
|
||||
const NON_ISLAND_TABS = new Set<ProfileTabType>([
|
||||
'media', 'gif', 'stories', 'storiesArchive', 'previewMedia', 'gifts',
|
||||
]);
|
||||
|
||||
const CONTENT_LIST_CLASS: Record<string, string> = {
|
||||
media: styles.mediaList,
|
||||
documents: styles.documentsList,
|
||||
links: styles.linksList,
|
||||
audio: styles.audioList,
|
||||
voice: styles.voiceList,
|
||||
gif: styles.gifList,
|
||||
stories: styles.storiesList,
|
||||
storiesArchive: styles.storiesArchiveList,
|
||||
previewMedia: styles.previewMediaList,
|
||||
gifts: styles.giftsList,
|
||||
members: styles.membersList,
|
||||
commonChats: styles.commonChatsList,
|
||||
similarChannels: styles.similarChannelsList,
|
||||
similarBots: styles.similarBotsList,
|
||||
};
|
||||
|
||||
const Profile = ({
|
||||
chatId,
|
||||
@ -279,6 +302,7 @@ const Profile = ({
|
||||
canUpdateMainTab,
|
||||
canAutoPlayGifs,
|
||||
onProfileStateChange,
|
||||
onProfileExpandedChange,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
setSharedMediaSearchType,
|
||||
@ -324,7 +348,9 @@ const Profile = ({
|
||||
const isGeneralSavedMessages = isSavedMessages && !isSavedDialog;
|
||||
const [isProfileExpanded, expandProfile, collapseProfile] = useFlag();
|
||||
|
||||
const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0);
|
||||
useEffect(() => {
|
||||
onProfileExpandedChange?.(isProfileExpanded);
|
||||
}, [isProfileExpanded, onProfileExpandedChange]);
|
||||
|
||||
const isUser = isUserId(chatId);
|
||||
const validMainTabTypes = isUser ? VALID_USER_MAIN_TAB_TYPES : VALID_CHANNEL_MAIN_TAB_TYPES;
|
||||
@ -438,10 +464,10 @@ const Profile = ({
|
||||
setActiveTab(peerFullInfo.mainTab); // Only focus when loading full info
|
||||
}, [peerFullInfo]);
|
||||
|
||||
const handleSwitchTab = useCallback((index: number) => {
|
||||
const handleSwitchTab = useLastCallback((index: number) => {
|
||||
startAutoScrollToTabsIfNeeded();
|
||||
setActiveTab(tabs[index].type);
|
||||
}, [tabs]);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPreviewMediaTab && !botPreviewMedia) {
|
||||
@ -511,15 +537,15 @@ const Profile = ({
|
||||
if (!isSynced) return;
|
||||
loadCommonChats({ userId: chatId });
|
||||
});
|
||||
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
|
||||
const handleLoadPeerStories = useLastCallback(({ offsetId }: { offsetId: number }) => {
|
||||
loadPeerProfileStories({ peerId: chatId, offsetId });
|
||||
}, [chatId]);
|
||||
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
|
||||
});
|
||||
const handleLoadStoriesArchive = useLastCallback(({ offsetId }: { offsetId: number }) => {
|
||||
loadStoriesArchive({ peerId: chatId, offsetId });
|
||||
}, [chatId]);
|
||||
const handleLoadGifts = useCallback(() => {
|
||||
});
|
||||
const handleLoadGifts = useLastCallback(() => {
|
||||
loadPeerSavedGifts({ peerId: chatId });
|
||||
}, [chatId]);
|
||||
});
|
||||
|
||||
const handleLoadMoreMembers = useLastCallback(() => {
|
||||
if (!isSynced) return;
|
||||
@ -574,6 +600,14 @@ const Profile = ({
|
||||
similarBots,
|
||||
});
|
||||
|
||||
const shouldWrapInIsland = !NON_ISLAND_TABS.has(resultType);
|
||||
|
||||
useEffect(() => {
|
||||
if (getMore && !viewportIds && isSynced) {
|
||||
getMore({ direction: LoadMoreDirection.Backwards });
|
||||
}
|
||||
}, [getMore, viewportIds, resultType, isSynced]);
|
||||
|
||||
const shouldRenderProfileInfo = !noProfileInfo && !isSavedMessages;
|
||||
|
||||
const isFirstTab = tabs[0].type === resultType;
|
||||
@ -589,21 +623,20 @@ const Profile = ({
|
||||
const shouldShowContentPanel = (isGiftsResult && hasGiftsCollections) || (isStoriesResult && hasStoryAlbums);
|
||||
|
||||
useEffect(() => {
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
if (hasGiftsCollections) {
|
||||
setTimeout(() => {
|
||||
markGiftCollectionsShowed();
|
||||
}, CONTENT_PANEL_SHOW_DELAY);
|
||||
timers.push(setTimeout(markGiftCollectionsShowed, CONTENT_PANEL_SHOW_DELAY));
|
||||
} else {
|
||||
unmarkGiftCollectionsShowed();
|
||||
}
|
||||
|
||||
if (hasStoryAlbums) {
|
||||
setTimeout(() => {
|
||||
markStoryAlbumsShowed();
|
||||
}, CONTENT_PANEL_SHOW_DELAY);
|
||||
timers.push(setTimeout(markStoryAlbumsShowed, CONTENT_PANEL_SHOW_DELAY));
|
||||
} else {
|
||||
unmarkStoryAlbums();
|
||||
}
|
||||
|
||||
return () => timers.forEach(clearTimeout);
|
||||
}, [hasGiftsCollections, hasStoryAlbums, markGiftCollectionsShowed, markStoryAlbumsShowed]);
|
||||
|
||||
usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
|
||||
@ -638,8 +671,6 @@ const Profile = ({
|
||||
|
||||
useTransitionFixes(containerRef);
|
||||
|
||||
const [cacheBuster, resetCacheBuster] = useCacheBuster();
|
||||
|
||||
const { observe: observeIntersectionForMedia } = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE,
|
||||
@ -777,14 +808,14 @@ const Profile = ({
|
||||
|
||||
function renderNothingFoundGiftsWithFilter() {
|
||||
return (
|
||||
<div className="nothing-found-gifts">
|
||||
<div className={styles.nothingFoundGifts}>
|
||||
<AnimatedIconWithPreview
|
||||
size={160}
|
||||
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
|
||||
nonInteractive
|
||||
noLoop
|
||||
/>
|
||||
<div className="description">
|
||||
<div className={styles.description}>
|
||||
{lang('GiftSearchEmpty')}
|
||||
</div>
|
||||
<Link
|
||||
@ -797,20 +828,77 @@ const Profile = ({
|
||||
);
|
||||
}
|
||||
|
||||
const shouldWrapInInfiniteScroll = shouldWrapInIsland && resultType !== 'dialogs';
|
||||
|
||||
function wrapInIsland(content: TeactNode, className?: string) {
|
||||
if (!shouldWrapInIsland) return content;
|
||||
|
||||
const inner = shouldWrapInInfiniteScroll ? (
|
||||
<InfiniteScroll
|
||||
items={canRenderContent ? viewportIds : undefined}
|
||||
itemSelector={`.${CONTENT_LIST_CLASS[resultType]} > .scroll-item`}
|
||||
preloadBackwards={canRenderContent
|
||||
? (resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE) : 0}
|
||||
onLoadMore={getMore}
|
||||
scrollContainerClosest=".Profile"
|
||||
sensitiveArea={PROFILE_SENSITIVE_AREA}
|
||||
noScrollRestore
|
||||
noFastList
|
||||
>
|
||||
{content}
|
||||
</InfiniteScroll>
|
||||
) : content;
|
||||
|
||||
return (
|
||||
<div className={styles.sharedMediaIslandContainer}>
|
||||
<Island className={buildClassName(styles.sharedMediaIsland, 'custom-scroll', className)}>
|
||||
{inner}
|
||||
</Island>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if (resultType === 'dialogs') {
|
||||
return (
|
||||
<ChatList className="saved-dialogs" folderType="saved" isActive />
|
||||
return wrapInIsland(
|
||||
<ChatList
|
||||
className={styles.savedDialogs}
|
||||
folderType="saved"
|
||||
isActive
|
||||
noAbsolutePositioning
|
||||
noVirtualization
|
||||
noScrollRestore
|
||||
noFastList
|
||||
scrollContainerClosest=".Profile"
|
||||
/>,
|
||||
styles.savedDialogsIsland,
|
||||
);
|
||||
}
|
||||
|
||||
const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById;
|
||||
const needsMessages = resultType === 'media' || resultType === 'gif' || resultType === 'documents'
|
||||
|| resultType === 'links' || resultType === 'audio' || resultType === 'voice';
|
||||
const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || (needsMessages && !messagesById);
|
||||
const noSpinner = isFirstTab && !canRenderContent;
|
||||
|
||||
if (shouldWrapInIsland) {
|
||||
return renderSpinnerOrContent(noContent, noSpinner);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sharedMediaIslandContainer}>
|
||||
{renderCategories()}
|
||||
{renderSpinnerOrContent(noContent, noSpinner)}
|
||||
<InfiniteScroll
|
||||
itemSelector={`.${CONTENT_LIST_CLASS[resultType]} > .scroll-item`}
|
||||
items={canRenderContent ? viewportIds : undefined}
|
||||
sensitiveArea={PROFILE_SENSITIVE_AREA}
|
||||
preloadBackwards={canRenderContent ? SHARED_MEDIA_SLICE : 0}
|
||||
scrollContainerClosest=".Profile"
|
||||
noScrollRestore
|
||||
onLoadMore={getMore}
|
||||
noFastList
|
||||
>
|
||||
{renderSpinnerOrContent(noContent, noSpinner)}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -820,9 +908,9 @@ const Profile = ({
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
'contentCategoriesPanel',
|
||||
!shouldShowContentPanel && 'hiddenPanel',
|
||||
isGiftCollectionsShowed && 'noTransition',
|
||||
styles.contentCategoriesPanel,
|
||||
!shouldShowContentPanel && styles.hiddenPanel,
|
||||
isGiftCollectionsShowed && styles.noTransition,
|
||||
)}
|
||||
>
|
||||
<StarGiftCollectionList peerId={chatId} />
|
||||
@ -834,9 +922,9 @@ const Profile = ({
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
'contentCategoriesPanel',
|
||||
!shouldShowContentPanel && 'hiddenPanel',
|
||||
isStoryAlbumsShowed && 'noTransition',
|
||||
styles.contentCategoriesPanel,
|
||||
!shouldShowContentPanel && styles.hiddenPanel,
|
||||
isStoryAlbumsShowed && styles.noTransition,
|
||||
)}
|
||||
>
|
||||
<StoryAlbumList peerId={chatId} />
|
||||
@ -853,7 +941,7 @@ const Profile = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="content empty-list"
|
||||
className={buildClassName(styles.content, styles.emptyList)}
|
||||
>
|
||||
{!noSpinner && !forceRenderHiddenMembers && <Spinner />}
|
||||
{forceRenderHiddenMembers && <NothingFound text={lang('ChatMemberListNoAccess')} />}
|
||||
@ -903,7 +991,7 @@ const Profile = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="content empty-list">
|
||||
<div className={buildClassName(styles.content, styles.emptyList)}>
|
||||
<NothingFound text={text} />
|
||||
</div>
|
||||
);
|
||||
@ -916,18 +1004,18 @@ const Profile = ({
|
||||
|
||||
const noTransition = resultType === 'gifts' ? isGiftCollectionsShowed
|
||||
: resultType === 'stories' ? isStoryAlbumsShowed : false;
|
||||
return (
|
||||
const contentEl = (
|
||||
<div
|
||||
className={buildClassName(
|
||||
`content ${resultType}-list`,
|
||||
shouldShowContentPanel && 'showContentPanel',
|
||||
noTransition && 'noTransition',
|
||||
styles.content,
|
||||
CONTENT_LIST_CLASS[resultType],
|
||||
shouldShowContentPanel && styles.showContentPanel,
|
||||
noTransition && styles.noTransition,
|
||||
)}
|
||||
dir={lang.isRtl && (resultType === 'media' || resultType === 'gif') ? 'rtl' : undefined}
|
||||
teactFastList
|
||||
>
|
||||
{resultType === 'media' || resultType === 'gif' ? (
|
||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
||||
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||
<Media
|
||||
key={id}
|
||||
message={messagesById[id]}
|
||||
@ -938,16 +1026,16 @@ const Profile = ({
|
||||
/>
|
||||
))
|
||||
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
|
||||
(viewportIds as number[]).map((id, i) => storyByIds?.[id] && (
|
||||
(viewportIds as number[]).filter((id) => Boolean(storyByIds?.[id])).map((id, i) => (
|
||||
<MediaStory
|
||||
teactOrderKey={i}
|
||||
key={`${resultType}_${id}`}
|
||||
story={storyByIds[id]}
|
||||
story={storyByIds![id]}
|
||||
isArchive={resultType === 'storiesArchive'}
|
||||
/>
|
||||
))
|
||||
) : resultType === 'documents' ? (
|
||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
||||
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||
<Document
|
||||
key={id}
|
||||
id={`shared-media${getMessageHtmlId(id)}`}
|
||||
@ -964,7 +1052,7 @@ const Profile = ({
|
||||
/>
|
||||
))
|
||||
) : resultType === 'links' ? (
|
||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
||||
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||
<WebLink
|
||||
key={id}
|
||||
message={messagesById[id]}
|
||||
@ -974,7 +1062,7 @@ const Profile = ({
|
||||
/>
|
||||
))
|
||||
) : resultType === 'audio' ? (
|
||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
||||
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||
<Audio
|
||||
key={id}
|
||||
theme={theme}
|
||||
@ -989,13 +1077,12 @@ const Profile = ({
|
||||
/>
|
||||
))
|
||||
) : resultType === 'voice' ? (
|
||||
(viewportIds as number[]).map((id) => {
|
||||
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => {
|
||||
const global = getGlobal();
|
||||
const message = messagesById[id];
|
||||
if (!message) return undefined;
|
||||
|
||||
const media = selectMessageDownloadableMedia(global, message)!;
|
||||
return messagesById[id] && (
|
||||
return (
|
||||
<Audio
|
||||
key={id}
|
||||
theme={theme}
|
||||
@ -1073,14 +1160,14 @@ const Profile = ({
|
||||
{!isCurrentUserPremium && (
|
||||
<>
|
||||
<Button
|
||||
className="show-more-channels"
|
||||
className={styles.showMoreChannels}
|
||||
onClick={() => openPremiumModal()}
|
||||
iconName="unlock-badge"
|
||||
iconAlignment="end"
|
||||
>
|
||||
{oldLang('UnlockSimilar')}
|
||||
</Button>
|
||||
<div className="more-similar">
|
||||
<div className={styles.moreSimilar}>
|
||||
{renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])}
|
||||
</div>
|
||||
</>
|
||||
@ -1107,10 +1194,10 @@ const Profile = ({
|
||||
))}
|
||||
{!isCurrentUserPremium && (
|
||||
<>
|
||||
<Button className="show-more-bots" onClick={() => openPremiumModal()} iconName="unlock-badge">
|
||||
<Button className={styles.showMoreBots} onClick={() => openPremiumModal()} iconName="unlock-badge">
|
||||
{lang('UnlockMoreSimilarBots')}
|
||||
</Button>
|
||||
<div className="more-similar">
|
||||
<div className={styles.moreSimilar}>
|
||||
{renderText(lang('MoreSimilarBotsDescription', { count: limitSimilarPeers }, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
@ -1126,7 +1213,7 @@ const Profile = ({
|
||||
<SavedGift
|
||||
peerId={chatId}
|
||||
key={getSavedGiftKey(gift)}
|
||||
className="saved-gift"
|
||||
className={styles.savedGift}
|
||||
style={createVtnStyle(getSavedGiftKey(gift))}
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
@ -1136,6 +1223,8 @@ const Profile = ({
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
|
||||
return wrapInIsland(contentEl);
|
||||
}
|
||||
|
||||
const shouldUseTransitionForContent = resultType === 'stories' || resultType === 'gifts';
|
||||
@ -1149,13 +1238,9 @@ const Profile = ({
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const handleOnStop = useLastCallback(() => {
|
||||
setRestoreContentHeightKey(restoreContentHeightKey + 1);
|
||||
});
|
||||
|
||||
function renderProfileInfo(peerId: string, isReady: boolean) {
|
||||
return (
|
||||
<div className="profile-info">
|
||||
<div className={buildClassName(styles.profileInfo, 'profile-info')}>
|
||||
<ProfileInfo
|
||||
isExpanded={isProfileExpanded}
|
||||
peerId={peerId}
|
||||
@ -1167,6 +1252,9 @@ const Profile = ({
|
||||
chatOrUserId={profileId}
|
||||
isSavedDialog={isSavedDialog}
|
||||
isOwnProfile={isOwnProfile}
|
||||
withIslands
|
||||
className={styles.chatExtraBlock}
|
||||
style={createVtnStyle('chatExtraBlock', true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -1180,22 +1268,15 @@ const Profile = ({
|
||||
if (shouldUseTransitionForContent) {
|
||||
return (
|
||||
<Transition
|
||||
className={`${resultType}-list`}
|
||||
className={CONTENT_LIST_CLASS[resultType]}
|
||||
activeKey={contentTransitionKey}
|
||||
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||
shouldCleanup
|
||||
shouldRestoreHeight
|
||||
restoreHeightKey={restoreContentHeightKey}
|
||||
contentSelector=".Transition > .Transition_slide-active > .content"
|
||||
contentSelector={`.Transition > .Transition_slide-active > .${styles.content}`}
|
||||
>
|
||||
<Transition
|
||||
activeKey={isSpinner ? 0 : 1}
|
||||
name="fade"
|
||||
shouldCleanup
|
||||
shouldRestoreHeight
|
||||
restoreHeightKey={restoreContentHeightKey}
|
||||
contentSelector=".content"
|
||||
onStop={handleOnStop}
|
||||
contentSelector={`.${styles.content}`}
|
||||
>
|
||||
{baseContent}
|
||||
</Transition>
|
||||
@ -1207,34 +1288,18 @@ const Profile = ({
|
||||
<Transition
|
||||
activeKey={isSpinner ? 0 : 1}
|
||||
name="fade"
|
||||
shouldCleanup
|
||||
shouldRestoreHeight
|
||||
>
|
||||
{baseContent}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
const activeListSelector = `.shared-media-transition > .Transition_slide-active`;
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
const nestedSelector = `${activeListSelector} > .Transition > .Transition_slide-active > .Transition > .Transition_slide-active`;
|
||||
const itemSelector = !shouldUseTransitionForContent
|
||||
? `${activeListSelector} .${resultType}-list > .scroll-item`
|
||||
: `${nestedSelector} > .${resultType}-list > .scroll-item`;
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
<Surface
|
||||
ref={containerRef}
|
||||
className="Profile custom-scroll"
|
||||
itemSelector={itemSelector}
|
||||
items={canRenderContent ? viewportIds : undefined}
|
||||
cacheBuster={cacheBuster}
|
||||
sensitiveArea={PROFILE_SENSITIVE_AREA}
|
||||
preloadBackwards={canRenderContent ? (resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE) : 0}
|
||||
// To prevent scroll jumps caused by reordering member list
|
||||
noScrollRestoreOnTop
|
||||
noFastList
|
||||
onLoadMore={getMore}
|
||||
scrollable
|
||||
noPadding
|
||||
className={buildClassName(styles.root, 'Profile', isGeneralSavedMessages && 'is-saved-messages')}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{!noProfileInfo && !isSavedMessages && (
|
||||
@ -1244,33 +1309,42 @@ const Profile = ({
|
||||
)
|
||||
)}
|
||||
{!isRestricted && (
|
||||
<div
|
||||
className="shared-media"
|
||||
style={createVtnStyle('sharedMedia')}
|
||||
>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={shouldSkipTransitionRef.current ? 'none'
|
||||
: resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||
activeKey={activeKey}
|
||||
renderCount={tabs.length}
|
||||
shouldRestoreHeight
|
||||
className="shared-media-transition"
|
||||
onStop={resetCacheBuster}
|
||||
restoreHeightKey={shouldUseTransitionForContent ? restoreContentHeightKey : undefined}
|
||||
contentSelector={shouldUseTransitionForContent
|
||||
? '.Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .content'
|
||||
: undefined}
|
||||
<>
|
||||
<div
|
||||
className={buildClassName(styles.sharedMediaTabs, 'shared-media-tabs')}
|
||||
style={createVtnStyle('sharedMediaTabs')}
|
||||
>
|
||||
{renderContent()}
|
||||
</Transition>
|
||||
<SquareTabList activeTab={activeTabIndex} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
||||
</div>
|
||||
<TabList
|
||||
activeTab={activeTabIndex}
|
||||
tabs={tabs}
|
||||
onSwitchTab={handleSwitchTab}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.sharedMedia}
|
||||
style={createVtnStyle('sharedMedia')}
|
||||
>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={shouldSkipTransitionRef.current ? 'none'
|
||||
: resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||
activeKey={activeKey}
|
||||
renderCount={tabs.length}
|
||||
className="shared-media-transition"
|
||||
contentSelector={shouldUseTransitionForContent
|
||||
? `.Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .${styles.content}`
|
||||
: undefined}
|
||||
>
|
||||
{renderContent()}
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{canAddMembers && (
|
||||
<FloatingActionButton
|
||||
className={buildClassName(!isActive && 'hidden')}
|
||||
className={buildClassName(!isActive && styles.hidden)}
|
||||
style={createVtnStyle('profileFab')}
|
||||
isShown={canRenderContent}
|
||||
onClick={handleNewMemberDialogOpen}
|
||||
ariaLabel={oldLang('lng_channel_add_users')}
|
||||
@ -1284,7 +1358,7 @@ const Profile = ({
|
||||
onClose={handleDeleteMembersModalClose}
|
||||
/>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
</Surface>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
padding-right: env(safe-area-inset-right);
|
||||
border-left: 1px solid var(--color-borders);
|
||||
|
||||
background: var(--color-background);
|
||||
background: var(--color-background-secondary);
|
||||
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
selectAreActiveChatsLoaded,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatWithSelf,
|
||||
selectPeerHasProfileBackground,
|
||||
selectRightColumnContentKey,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
@ -59,6 +60,7 @@ type StateProps = {
|
||||
isSavedMessages?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
isOwnProfile?: boolean;
|
||||
hasProfileBackground?: boolean;
|
||||
};
|
||||
|
||||
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
|
||||
@ -85,6 +87,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
isSavedMessages,
|
||||
isSavedDialog,
|
||||
isOwnProfile,
|
||||
hasProfileBackground,
|
||||
}) => {
|
||||
const {
|
||||
toggleChatInfo,
|
||||
@ -116,6 +119,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial);
|
||||
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
|
||||
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
|
||||
const [isProfileExpanded, setIsProfileExpanded] = useState(false);
|
||||
const [isProfileScrolled, setIsProfileScrolled] = useState(false);
|
||||
const isScrolledDown = profileState !== ProfileState.Profile;
|
||||
|
||||
const isOpen = contentKey !== undefined;
|
||||
@ -134,6 +139,14 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
const isEditingTopic = contentKey === RightColumnContent.EditTopic;
|
||||
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
|
||||
|
||||
const headerBackground: 'regular' | 'secondary' = (() => {
|
||||
if (isSavedMessages) return 'secondary';
|
||||
if (!isProfile) return 'regular';
|
||||
if (isScrolledDown) return 'secondary';
|
||||
if (!isProfileScrolled && !isProfileExpanded && !hasProfileBackground) return 'secondary';
|
||||
return 'regular';
|
||||
})();
|
||||
|
||||
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
|
||||
|
||||
const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1;
|
||||
@ -141,8 +154,27 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
useScrollNotch({
|
||||
containerRef,
|
||||
selector: ':scope .custom-scroll, :scope .panel-content',
|
||||
shouldHideTopNotch: isSavedMessages || (isProfile && isScrolledDown),
|
||||
}, [contentKey, managementScreen, chatId, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProfile || isScrolledDown || isSavedMessages) {
|
||||
setIsProfileScrolled(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollEl = containerRef.current?.querySelector<HTMLElement>('.custom-scroll');
|
||||
if (!scrollEl) return undefined;
|
||||
|
||||
const handleProfileScroll = () => {
|
||||
setIsProfileScrolled(scrollEl.scrollTop > 1);
|
||||
};
|
||||
|
||||
handleProfileScroll();
|
||||
scrollEl.addEventListener('scroll', handleProfileScroll, { passive: true });
|
||||
return () => scrollEl.removeEventListener('scroll', handleProfileScroll);
|
||||
}, [chatId, threadId, isProfile, isScrolledDown, isSavedMessages]);
|
||||
|
||||
const close = useLastCallback((shouldScrollUp = true) => {
|
||||
switch (contentKey) {
|
||||
case RightColumnContent.AddingMembers:
|
||||
@ -320,6 +352,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
isMobile={isMobile}
|
||||
isActive={isOpen && isActive}
|
||||
onProfileStateChange={setProfileState}
|
||||
onProfileExpandedChange={setIsProfileExpanded}
|
||||
/>
|
||||
);
|
||||
case RightColumnContent.Management:
|
||||
@ -376,6 +409,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
threadId={threadId}
|
||||
isColumnOpen={isOpen}
|
||||
isProfile={isProfile}
|
||||
headerBackground={headerBackground}
|
||||
isManagement={isManagement}
|
||||
isStatistics={isStatistics}
|
||||
isBoostStatistics={isBoostStatistics}
|
||||
@ -439,6 +473,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSavedMessages,
|
||||
isSavedDialog,
|
||||
isOwnProfile,
|
||||
hasProfileBackground: chatId ? selectPeerHasProfileBackground(global, chatId) : undefined,
|
||||
};
|
||||
},
|
||||
)(RightColumn));
|
||||
|
||||
@ -9,7 +9,13 @@
|
||||
height: var(--header-height);
|
||||
padding: 0.5rem 0.8125rem;
|
||||
|
||||
background: var(--color-background);
|
||||
background-color: var(--color-background);
|
||||
|
||||
transition: background-color 150ms;
|
||||
|
||||
&.secondary {
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ type OwnProps = {
|
||||
isCreatingTopic?: boolean;
|
||||
isEditingTopic?: boolean;
|
||||
isAddingChatMembers?: boolean;
|
||||
headerBackground?: 'regular' | 'secondary';
|
||||
profileState?: ProfileState;
|
||||
managementScreen?: ManagementScreens;
|
||||
onClose: (shouldScrollUp?: boolean) => void;
|
||||
@ -158,6 +159,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
isCreatingTopic,
|
||||
isEditingTopic,
|
||||
isAddingChatMembers,
|
||||
headerBackground,
|
||||
profileState,
|
||||
managementScreen,
|
||||
canAddContact,
|
||||
@ -266,6 +268,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const isSecondaryBackground = headerBackground === 'secondary';
|
||||
const contentKey = isProfile ? (
|
||||
profileState === ProfileState.Profile ? (
|
||||
HeaderContent.Profile
|
||||
@ -703,7 +707,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="RightHeader"
|
||||
className={buildClassName('RightHeader', isSecondaryBackground && 'secondary')}
|
||||
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
|
||||
style={createVtnStyle('rightHeader', true)}
|
||||
>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
.tabList {
|
||||
margin-top: 0.5rem;
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@ const runThrottledForScroll = throttle((cb) => cb(), 250, false);
|
||||
|
||||
let isScrollingProgrammatically = false;
|
||||
|
||||
function getTabsNaturalTop(container: HTMLElement): number {
|
||||
const profileInfo = container.querySelector<HTMLElement>('.profile-info');
|
||||
return profileInfo ? profileInfo.offsetHeight : 0;
|
||||
}
|
||||
|
||||
export default function useProfileState({
|
||||
containerRef,
|
||||
tabType,
|
||||
@ -37,9 +42,9 @@ export default function useProfileState({
|
||||
useEffectWithPrevDeps(([prevTabType]) => {
|
||||
if ((prevTabType && prevTabType !== tabType && allowAutoScrollToTabs) || (tabType && forceScrollProfileTab)) {
|
||||
const container = containerRef.current!;
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList')!;
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.shared-media-tabs')!;
|
||||
handleStopAutoScrollToTabs();
|
||||
if (container.scrollTop < tabsEl.offsetTop) {
|
||||
if (container.scrollTop < getTabsNaturalTop(container)) {
|
||||
onProfileStateChange(getStateFromTabType(tabType));
|
||||
isScrollingProgrammatically = true;
|
||||
animateScroll({
|
||||
@ -69,8 +74,8 @@ export default function useProfileState({
|
||||
return;
|
||||
}
|
||||
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (!tabListEl || tabListEl.offsetTop > container.scrollTop) {
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.shared-media-tabs');
|
||||
if (!tabsEl || getTabsNaturalTop(container) > container.scrollTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -94,13 +99,12 @@ export default function useProfileState({
|
||||
return;
|
||||
}
|
||||
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (!tabListEl) {
|
||||
if (!container.querySelector('.shared-media-tabs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state: ProfileState = ProfileState.Profile;
|
||||
if (Math.ceil(container.scrollTop) >= tabListEl.offsetTop) {
|
||||
if (Math.ceil(container.scrollTop) >= getTabsNaturalTop(container)) {
|
||||
state = getStateFromTabType(tabType);
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,8 @@ import sortChatIds from '../../common/helpers/sortChatIds';
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
|
||||
const SHARED_MEDIA_TYPES: SharedMediaType[] = ['media', 'documents', 'links', 'audio', 'voice', 'gif'];
|
||||
|
||||
export default function useProfileViewportIds({
|
||||
loadMoreMembers,
|
||||
loadCommonChats,
|
||||
@ -59,7 +61,8 @@ export default function useProfileViewportIds({
|
||||
similarChannels?: string[];
|
||||
similarBots?: string[];
|
||||
}) {
|
||||
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
|
||||
const resultType = mediaSearchType && SHARED_MEDIA_TYPES.includes(tabType as SharedMediaType)
|
||||
? mediaSearchType : tabType;
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
if (!groupChatMembers || !usersById || !userStatusesById) {
|
||||
@ -218,7 +221,7 @@ function useInfiniteScrollForLoadableItems<ListId extends string | number>(
|
||||
handleLoadMore,
|
||||
itemIds,
|
||||
undefined,
|
||||
MEMBERS_SLICE,
|
||||
itemIds?.length || MEMBERS_SLICE,
|
||||
);
|
||||
|
||||
const isOnTop = !viewportIds || !itemIds || viewportIds[0] === itemIds[0];
|
||||
@ -250,11 +253,16 @@ function useInfiniteScrollForSharedMedia(
|
||||
}
|
||||
}, [chatMessages, foundIds, currentResultType, forSharedMediaType]);
|
||||
|
||||
const msgLen = messageIdsRef.current?.length ?? 0;
|
||||
const listSlice = forSharedMediaType === 'media'
|
||||
? Math.max(SHARED_MEDIA_SLICE, msgLen)
|
||||
: Math.max(MESSAGE_SEARCH_SLICE, msgLen);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(
|
||||
handleLoadMore,
|
||||
messageIdsRef.current,
|
||||
undefined,
|
||||
forSharedMediaType === 'media' ? SHARED_MEDIA_SLICE : MESSAGE_SEARCH_SLICE,
|
||||
listSlice,
|
||||
);
|
||||
|
||||
const isOnTop = !viewportIds || !messageIdsRef.current || viewportIds[0] === messageIdsRef.current[0];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ElementRef } from '../../../lib/teact/teact';
|
||||
import { useEffect } from '../../../lib/teact/teact';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
export default function useTransitionFixes(
|
||||
containerRef: ElementRef<HTMLDivElement>,
|
||||
@ -10,16 +10,23 @@ export default function useTransitionFixes(
|
||||
// Set `min-height` for shared media container to prevent jumping when switching tabs
|
||||
useEffect(() => {
|
||||
function setMinHeight() {
|
||||
const container = containerRef.current!;
|
||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (transitionEl && tabsEl) {
|
||||
const newHeight = container.clientHeight - tabsEl.offsetHeight;
|
||||
requestMeasure(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.shared-media-tabs');
|
||||
const sharedMediaEl = transitionEl?.parentElement;
|
||||
if (!transitionEl || !tabsEl) return;
|
||||
|
||||
const sharedMediaMargin = sharedMediaEl
|
||||
? parseInt(getComputedStyle(sharedMediaEl).marginTop, 10) || 0 : 0;
|
||||
const tabsMarginTop = parseInt(getComputedStyle(tabsEl).marginTop, 10) || 0;
|
||||
const newHeight = container.clientHeight - tabsEl.offsetHeight - tabsMarginTop - sharedMediaMargin;
|
||||
|
||||
requestMutation(() => {
|
||||
transitionEl.style.minHeight = `${newHeight}px`;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setMinHeight();
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
.tabList {
|
||||
margin-block: 0.5rem;
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ type OwnProps = {
|
||||
isShown: boolean;
|
||||
iconName: IconName;
|
||||
className?: string;
|
||||
style?: string;
|
||||
color?: ButtonProps['color'];
|
||||
ariaLabel?: ButtonProps['ariaLabel'];
|
||||
disabled?: boolean;
|
||||
@ -25,6 +26,7 @@ const FloatingActionButton = ({
|
||||
isShown,
|
||||
iconName,
|
||||
className,
|
||||
style,
|
||||
color = 'primary',
|
||||
ariaLabel,
|
||||
disabled,
|
||||
@ -42,6 +44,7 @@ const FloatingActionButton = ({
|
||||
return (
|
||||
<Button
|
||||
className={buttonClassName}
|
||||
style={style}
|
||||
color={color}
|
||||
round
|
||||
disabled={disabled}
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
import { memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import useScrollToActiveTab from '../../hooks/useScrollToActiveTab';
|
||||
|
||||
import Tab from './Tab';
|
||||
|
||||
@ -39,10 +38,6 @@ type OwnProps = {
|
||||
onSwitchTab: (index: number) => void;
|
||||
};
|
||||
|
||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||
// Should match duration from `--slide-transition` CSS variable
|
||||
const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
|
||||
|
||||
const SquareTabList = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
@ -59,30 +54,7 @@ const SquareTabList = ({
|
||||
const lang = useLang();
|
||||
|
||||
useHorizontalScroll(containerRef, undefined, true);
|
||||
|
||||
// Scroll container to place active tab in the center
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||
if (scrollWidth <= offsetWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTabElement = container.childNodes[activeTab] as HTMLElement | null;
|
||||
if (!activeTabElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { offsetLeft: activeTabOffsetLeft, offsetWidth: activeTabOffsetWidth } = activeTabElement;
|
||||
const newLeft = activeTabOffsetLeft - (offsetWidth / 2) + (activeTabOffsetWidth / 2);
|
||||
|
||||
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
|
||||
if (Math.abs(newLeft - scrollLeft) < TAB_SCROLL_THRESHOLD_PX) {
|
||||
return;
|
||||
}
|
||||
|
||||
animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
|
||||
}, [activeTab, containerRef]);
|
||||
useScrollToActiveTab(containerRef, activeTab);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
|
||||
opacity: 0;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
transition: opacity 150ms;
|
||||
|
||||
@ -48,11 +48,12 @@
|
||||
}
|
||||
|
||||
.activeIndicator {
|
||||
pointer-events: none;
|
||||
will-change: clip-path;
|
||||
|
||||
isolation: isolate;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@ -117,4 +118,29 @@
|
||||
.lockIcon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.withFadeMask {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.fadeMaskWrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: var(--fade-mask-height, 50%);
|
||||
|
||||
background: linear-gradient(to bottom, var(--fade-mask-color, var(--color-background)) 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef, useState } from '../../lib/teact/teact';
|
||||
|
||||
import type { IAnchorPosition } from '../../types';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
import type { TabWithProperties } from './SquareTabList';
|
||||
|
||||
export type { TabWithProperties };
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||
import useScrollToActiveTab from '../../hooks/useScrollToActiveTab';
|
||||
|
||||
import CustomEmoji from '../common/CustomEmoji';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Menu from './Menu';
|
||||
import MenuItem from './MenuItem';
|
||||
import MenuSeparator from './MenuSeparator';
|
||||
|
||||
import styles from './TabList.module.scss';
|
||||
|
||||
@ -26,7 +35,10 @@ type OwnProps = {
|
||||
centered?: boolean;
|
||||
stretched?: boolean;
|
||||
itemAlignment?: 'vertical' | 'horizontal';
|
||||
withFadeMask?: boolean;
|
||||
fadeMaskClassName?: string;
|
||||
onSwitchTab: (index: number) => void;
|
||||
renderExtra?: (tab: TabWithProperties, index: number) => TeactNode;
|
||||
};
|
||||
|
||||
const TabList = ({
|
||||
@ -38,11 +50,18 @@ const TabList = ({
|
||||
centered,
|
||||
stretched,
|
||||
itemAlignment,
|
||||
withFadeMask,
|
||||
fadeMaskClassName,
|
||||
renderExtra,
|
||||
onSwitchTab,
|
||||
}: OwnProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const clipPathContainerRef = useRef<HTMLDivElement>();
|
||||
const [clipPath, setClipPath] = useState<string>('');
|
||||
const [isMenuOpen, openMenu, closeMenu] = useFlag();
|
||||
const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>();
|
||||
const [menuTabIndex, setMenuTabIndex] = useState<number | undefined>();
|
||||
const menuTargetRef = useRef<HTMLElement>();
|
||||
|
||||
useHorizontalScroll(containerRef, !tabs.length, true);
|
||||
|
||||
@ -68,12 +87,42 @@ const TabList = ({
|
||||
|
||||
useResizeObserver(clipPathContainerRef, updateClipPath);
|
||||
|
||||
useScrollToActiveTab(containerRef, activeTab);
|
||||
|
||||
const handleTabClick = useLastCallback((index: number) => {
|
||||
onSwitchTab(index);
|
||||
});
|
||||
|
||||
const handleContextMenu = useLastCallback((index: number, e: React.MouseEvent) => {
|
||||
const actions = tabs[index]?.contextActions;
|
||||
if (!actions?.length) return;
|
||||
e.preventDefault();
|
||||
menuTargetRef.current = e.currentTarget as HTMLElement;
|
||||
setMenuTabIndex(index);
|
||||
setMenuAnchor({ x: e.clientX, y: e.clientY });
|
||||
openMenu();
|
||||
});
|
||||
|
||||
const handleMenuClose = useLastCallback(() => {
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleMenuHide = useLastCallback(() => {
|
||||
setMenuAnchor(undefined);
|
||||
setMenuTabIndex(undefined);
|
||||
});
|
||||
|
||||
const getTriggerElement = useLastCallback(() => menuTargetRef.current);
|
||||
const getRootElement = useLastCallback(() => containerRef.current);
|
||||
const getMenuElement = useLastCallback(
|
||||
() => containerRef.current?.querySelector<HTMLElement>('.TabList-context-menu .bubble'),
|
||||
);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
if (!tabs.length) return undefined;
|
||||
|
||||
const hasContextActions = tabs.some((tab) => tab.contextActions?.length);
|
||||
|
||||
const renderTab = (tab: TabWithProperties, index: number) => {
|
||||
const stringEmoticon = typeof tab.emoticon === 'string' ? tab.emoticon : undefined;
|
||||
const customEmoji = typeof tab.emoticon === 'object' ? tab.emoticon : undefined;
|
||||
@ -88,6 +137,7 @@ const TabList = ({
|
||||
stretched && styles.stretched,
|
||||
)}
|
||||
onClick={() => handleTabClick(index)}
|
||||
onContextMenu={hasContextActions ? (e) => handleContextMenu(index, e) : undefined}
|
||||
>
|
||||
{stringEmoticon && <span className={styles.tabEmoji}>{stringEmoticon}</span>}
|
||||
{customEmoji && (
|
||||
@ -99,17 +149,22 @@ const TabList = ({
|
||||
/>
|
||||
)}
|
||||
{tab.icon && <Icon name={tab.icon} className={styles.tabIcon} />}
|
||||
{tab.title}
|
||||
{typeof tab.title === 'string' ? renderText(tab.title) : tab.title}
|
||||
{renderExtra?.(tab, index)}
|
||||
{tab.isBlocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
const contextActions = menuTabIndex !== undefined ? tabs[menuTabIndex]?.contextActions : undefined;
|
||||
|
||||
const tabListElement = (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName(
|
||||
'TabList',
|
||||
styles.container,
|
||||
withFadeMask && styles.withFadeMask,
|
||||
centered && styles.centered,
|
||||
itemAlignment === 'vertical' && styles.vertical,
|
||||
className,
|
||||
@ -131,6 +186,56 @@ const TabList = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const menuElement = contextActions && menuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isMenuOpen}
|
||||
anchor={menuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="TabList-context-menu"
|
||||
autoClose
|
||||
onClose={handleMenuClose}
|
||||
onCloseAnimationEnd={handleMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action: MenuItemContextAction) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || `separator-${contextActions.indexOf(action)}`} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{renderText(action.title)}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
if (!withFadeMask) {
|
||||
return (
|
||||
<>
|
||||
{tabListElement}
|
||||
{menuElement}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={buildClassName(styles.fadeMaskWrapper, fadeMaskClassName)}>
|
||||
{tabListElement}
|
||||
</div>
|
||||
{menuElement}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TabList);
|
||||
|
||||
30
src/hooks/useScrollToActiveTab.ts
Normal file
30
src/hooks/useScrollToActiveTab.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { ElementRef } from '../lib/teact/teact';
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
|
||||
import animateHorizontalScroll from '../util/animateHorizontalScroll';
|
||||
|
||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||
const SCROLL_DURATION = 300;
|
||||
|
||||
export default function useScrollToActiveTab(
|
||||
containerRef: ElementRef<HTMLDivElement>,
|
||||
activeTab: number,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||
if (scrollWidth <= offsetWidth) return;
|
||||
|
||||
const activeTabElement = container.childNodes[activeTab] as HTMLElement | undefined;
|
||||
if (!activeTabElement) return;
|
||||
|
||||
const { offsetLeft, offsetWidth: tabWidth } = activeTabElement;
|
||||
const newLeft = offsetLeft - (offsetWidth / 2) + (tabWidth / 2);
|
||||
|
||||
if (Math.abs(newLeft - scrollLeft) < TAB_SCROLL_THRESHOLD_PX) return;
|
||||
|
||||
animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
|
||||
}, [activeTab, containerRef]);
|
||||
}
|
||||
@ -176,8 +176,10 @@
|
||||
|
||||
width: 100%;
|
||||
padding: 0.5625rem;
|
||||
border-radius: var(--border-radius-island);
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
transition: transform var(--chat-transform-transition);
|
||||
|
||||
@ -195,6 +197,11 @@
|
||||
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
:global(html.theme-dark) & {
|
||||
border: 1px solid var(--color-borders);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin side-panel-section {
|
||||
|
||||
@ -232,7 +232,7 @@
|
||||
--border-radius-modal: 2rem;
|
||||
--border-radius-toast: 1rem;
|
||||
--border-radius-island: 1.5rem;
|
||||
--border-radius-default: 0.75rem;
|
||||
--border-radius-default: 1rem;
|
||||
--border-radius-default-small: 0.625rem;
|
||||
--border-radius-default-tiny: 0.375rem;
|
||||
--border-radius-messages: 0.9375rem;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user