Profile: Redesign (#6895)
This commit is contained in:
parent
e881ecdb3e
commit
6ceb7b6573
@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
.video-duration {
|
.video-duration {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.25rem;
|
top: 0.3125rem;
|
||||||
left: 0.25rem;
|
left: 0.3125rem;
|
||||||
|
|
||||||
padding: 0 0.3125rem;
|
padding: 0 0.3125rem;
|
||||||
border-radius: 0.1875rem;
|
border-radius: 0.375rem;
|
||||||
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.125rem;
|
line-height: 1.125rem;
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
padding-top: 0.875rem;
|
padding-top: 0.875rem;
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
|
|
||||||
background-color: var(--color-background-secondary);
|
background-color: var(--color-background);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover::before {
|
&:hover::before {
|
||||||
opacity: 1;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
@include mixins.with-vt-type('chatExtra');
|
@include mixins.with-vt-type('chatExtra');
|
||||||
}
|
}
|
||||||
@ -40,6 +40,10 @@
|
|||||||
@include mixins.with-vt-type('chatExtra');
|
@include mixins.with-vt-type('chatExtra');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.botVerificationSection,
|
.botVerificationSection,
|
||||||
.sectionInfo {
|
.sectionInfo {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@ -71,6 +75,7 @@
|
|||||||
.personalChannelItem {
|
.personalChannelItem {
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
|
||||||
:global(.Avatar) {
|
:global(.Avatar) {
|
||||||
margin-right: 0.9375rem !important;
|
margin-right: 0.9375rem !important;
|
||||||
@ -144,6 +149,10 @@
|
|||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.securityRiskIsland {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.unofficialSecurityRisk {
|
.unofficialSecurityRisk {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
padding-inline: 0.5rem;
|
padding-inline: 0.5rem;
|
||||||
|
|||||||
@ -57,10 +57,11 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Island from '../../gili/layout/Island';
|
||||||
|
import Switch from '../../gili/primitives/Switch';
|
||||||
import Chat from '../../left/main/Chat';
|
import Chat from '../../left/main/Chat';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import Switcher from '../../ui/Switcher';
|
|
||||||
import CompactMapPreview from '../CompactMapPreview';
|
import CompactMapPreview from '../CompactMapPreview';
|
||||||
import CustomEmoji from '../CustomEmoji';
|
import CustomEmoji from '../CustomEmoji';
|
||||||
import Icon from '../icons/Icon';
|
import Icon from '../icons/Icon';
|
||||||
@ -75,7 +76,9 @@ type OwnProps = {
|
|||||||
isOwnProfile?: boolean;
|
isOwnProfile?: boolean;
|
||||||
isSavedDialog?: boolean;
|
isSavedDialog?: boolean;
|
||||||
isInSettings?: boolean;
|
isInSettings?: boolean;
|
||||||
|
withIslands?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -127,7 +130,9 @@ const ChatExtra = ({
|
|||||||
botAppPermissions,
|
botAppPermissions,
|
||||||
botVerification,
|
botVerification,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
isInSettings,
|
isInSettings,
|
||||||
|
withIslands,
|
||||||
canViewSubscribers,
|
canViewSubscribers,
|
||||||
}: OwnProps & StateProps) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
@ -387,13 +392,17 @@ const ChatExtra = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Wrapper = withIslands ? Island : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('ChatExtra', className)} style={createVtnStyle('chatExtra')}>
|
<div className={buildClassName('ChatExtra', className)} style={style || createVtnStyle('chatExtra')}>
|
||||||
{user && userFullInfo?.isUnofficialSecurityRisk && (
|
{user && userFullInfo?.isUnofficialSecurityRisk && (
|
||||||
<div className={styles.unofficialSecurityRisk}>
|
<Wrapper className={withIslands ? styles.securityRiskIsland : undefined}>
|
||||||
<Icon className={buildClassName(styles.riskIcon, 'in-text-icon')} name="info-filled" />
|
<div className={styles.unofficialSecurityRisk}>
|
||||||
{lang('UnofficialSecurityRisk', { peer: getPeerTitle(lang, user) })}
|
<Icon className={buildClassName(styles.riskIcon, 'in-text-icon')} name="info-filled" />
|
||||||
</div>
|
{lang('UnofficialSecurityRisk', { peer: getPeerTitle(lang, user) })}
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
)}
|
)}
|
||||||
{personalChannel && (
|
{personalChannel && (
|
||||||
<div className={styles.personalChannel} style={createVtnStyle('personalChannel')}>
|
<div className={styles.personalChannel} style={createVtnStyle('personalChannel')}>
|
||||||
@ -401,244 +410,244 @@ const ChatExtra = ({
|
|||||||
<span className={styles.personalChannelSubscribers}>
|
<span className={styles.personalChannelSubscribers}>
|
||||||
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
|
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
|
||||||
</span>
|
</span>
|
||||||
<Chat
|
<Wrapper className={styles.personalChannelItem}>
|
||||||
chatId={personalChannel.id}
|
<Chat
|
||||||
orderDiff={0}
|
chatId={personalChannel.id}
|
||||||
shiftDiff={0}
|
orderDiff={0}
|
||||||
animationType={ChatAnimationTypes.None}
|
shiftDiff={0}
|
||||||
isPreview
|
animationType={ChatAnimationTypes.None}
|
||||||
previewMessageId={personalChannelMessageId}
|
isPreview
|
||||||
className={styles.personalChannelItem}
|
previewMessageId={personalChannelMessageId}
|
||||||
/>
|
/>
|
||||||
|
</Wrapper>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{Boolean(formattedNumber?.length) && (
|
<Wrapper>
|
||||||
<ListItem
|
{Boolean(formattedNumber?.length) && (
|
||||||
icon="phone"
|
<ListItem
|
||||||
className={styles.phone}
|
icon="phone"
|
||||||
multiline
|
className={styles.phone}
|
||||||
narrow
|
multiline
|
||||||
ripple
|
narrow
|
||||||
onClick={handlePhoneClick}
|
ripple
|
||||||
style={createVtnStyle('phone')}
|
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}
|
|
||||||
>
|
>
|
||||||
{oldLang('ProfileBotOpenApp')}
|
<span className="title" dir={lang.isRtl ? 'rtl' : undefined}>{formattedNumber}</span>
|
||||||
</Button>
|
<span className="subtitle">{oldLang('Phone')}</span>
|
||||||
<div className={styles.sectionInfo}>
|
</ListItem>
|
||||||
{appTermsInfo}
|
)}
|
||||||
</div>
|
{activeUsernames && renderUsernames(activeUsernames)}
|
||||||
</ListItem>
|
{description && Boolean(description.length) && (
|
||||||
)}
|
<ListItem
|
||||||
{!isOwnProfile && !isInSettings && (
|
icon="info"
|
||||||
<ListItem
|
className={styles.description}
|
||||||
icon={isMuted ? 'mute' : 'unmute'}
|
multiline
|
||||||
className={styles.notifications}
|
narrow
|
||||||
narrow
|
isStatic
|
||||||
ripple
|
allowSelection
|
||||||
onClick={handleToggleNotifications}
|
style={createVtnStyle('description')}
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
{renderTextWithEntities({
|
<span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
text: note.text,
|
{
|
||||||
entities: note.entities,
|
renderText(description, [
|
||||||
})}
|
'br',
|
||||||
</div>
|
shouldRenderAllLinks ? 'links' : 'tg_links',
|
||||||
<div className={buildClassName('subtitle', styles.noteSubtitle)} style={createVtnStyle('noteSubtitle')}>
|
'emoji',
|
||||||
<span>{lang('UserNoteTitle')}</span>
|
])
|
||||||
|
}
|
||||||
|
</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>
|
<span className={styles.noteHint}>{lang('UserNoteHint')}</span>
|
||||||
{isNoteCollapsible && (
|
{isNoteCollapsible && (
|
||||||
<Icon
|
<Icon
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.noteExpandIcon,
|
styles.noteExpandIcon,
|
||||||
styles.clickable,
|
styles.clickable,
|
||||||
)}
|
)}
|
||||||
style={createVtnStyle('noteExpandIcon', true)}
|
style={createVtnStyle('noteExpandIcon', true)}
|
||||||
onClick={handleToggleNote}
|
onClick={handleToggleNote}
|
||||||
name={isNoteCollapsed ? 'down' : 'up'}
|
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>
|
</div>
|
||||||
</ListItem>
|
)}
|
||||||
)}
|
</Wrapper>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
--profile-info-bg: var(--color-background);
|
||||||
--rating-outline-color: #000000;
|
--rating-outline-color: #000000;
|
||||||
--rating-text-color: #000000;
|
--rating-text-color: #000000;
|
||||||
|
|
||||||
@ -14,7 +15,7 @@
|
|||||||
|
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
|
||||||
background-color: var(--color-background);
|
background-color: var(--profile-info-bg);
|
||||||
|
|
||||||
@include mixins.with-vt-type('profileAvatar');
|
@include mixins.with-vt-type('profileAvatar');
|
||||||
|
|
||||||
@ -33,7 +34,7 @@
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
background-image: linear-gradient(0deg, var(--color-background) 50%, transparent);
|
background-image: linear-gradient(0deg, var(--profile-info-bg) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.userRatingNegativeWrapper,
|
.userRatingNegativeWrapper,
|
||||||
@ -445,11 +446,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.on-active-vt('profileAvatar') {
|
@include mixins.on-active-vt('profileAvatar') {
|
||||||
&::view-transition-old(.profileInfo),
|
|
||||||
&::view-transition-new(.profileInfo) {
|
|
||||||
animation-name: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-group(.photoDashes) {
|
&::view-transition-group(.photoDashes) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@ -460,10 +456,22 @@
|
|||||||
|
|
||||||
&::view-transition-group(.profileInfo) {
|
&::view-transition-group(.profileInfo) {
|
||||||
z-index: -2;
|
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') {
|
@include mixins.on-active-vt('profileExpand') {
|
||||||
|
&::view-transition-old(.profileInfo) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&::view-transition-old(.avatar) {
|
&::view-transition-old(.avatar) {
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
animation-name: none;
|
animation-name: none;
|
||||||
@ -487,6 +495,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.on-active-vt('profileCollapse') {
|
@include mixins.on-active-vt('profileCollapse') {
|
||||||
|
&::view-transition-new(.profileInfo) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&::view-transition-new(.avatar) {
|
&::view-transition-new(.avatar) {
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
animation-name: none;
|
animation-name: none;
|
||||||
|
|||||||
@ -12,4 +12,8 @@
|
|||||||
|
|
||||||
@include mixins.adapt-padding-to-scrollbar(1rem);
|
@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 buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import styles from './Surface.module.scss';
|
import styles from './Surface.module.scss';
|
||||||
|
|
||||||
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
|
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
ref?: ElementRef<HTMLDivElement>;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
|
noPadding?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Surface = ({
|
const Surface = ({
|
||||||
|
ref,
|
||||||
scrollable,
|
scrollable,
|
||||||
|
noPadding,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...otherProps
|
...otherProps
|
||||||
@ -17,10 +23,12 @@ const Surface = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
isScrollable && 'custom-scroll',
|
isScrollable && 'custom-scroll',
|
||||||
isScrollable && styles.scrollable,
|
isScrollable && styles.scrollable,
|
||||||
|
noPadding && styles.noPadding,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ type OwnProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
withPermissionColors?: boolean;
|
withPermissionColors?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||||
@ -40,7 +40,7 @@ const Switch = ({
|
|||||||
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
||||||
|
|
||||||
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(e.currentTarget.checked);
|
onChange?.(e.currentTarget.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interactive?.isLoading) return undefined;
|
if (interactive?.isLoading) return undefined;
|
||||||
|
|||||||
@ -413,7 +413,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={chatClassName}
|
className={chatClassName}
|
||||||
href={href}
|
href={href}
|
||||||
style={`top: ${offsetTop}px`}
|
style={offsetTop !== undefined ? `top: ${offsetTop}px` : undefined}
|
||||||
ripple={!isForum && !isMobile}
|
ripple={!isForum && !isMobile}
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
withPortalForMenu
|
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 useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
|
||||||
import StoryRibbon from '../../story/StoryRibbon';
|
import StoryRibbon from '../../story/StoryRibbon';
|
||||||
import SquareTabList from '../../ui/SquareTabList';
|
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
|
import ChatFolderTabList from './ChatFolderTabList';
|
||||||
import ChatList from './ChatList';
|
import ChatList from './ChatList';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -87,7 +87,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const { isAtBeginning: isNotScrolled, handleScroll, updateScrollState } = useScrolledState();
|
const { handleScroll, updateScrollState } = useScrolledState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadChatFolders();
|
loadChatFolders();
|
||||||
@ -119,6 +119,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const { displayedFolders, folderTabs } = useFolderTabs({
|
const { displayedFolders, folderTabs } = useFolderTabs({
|
||||||
sidebarMode: false,
|
sidebarMode: false,
|
||||||
|
noEmoticons: true,
|
||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
@ -252,31 +253,31 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'ChatFolders',
|
'ChatFolders',
|
||||||
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
|
|
||||||
shouldRenderStoryRibbon && 'with-story-ribbon',
|
shouldRenderStoryRibbon && 'with-story-ribbon',
|
||||||
isFoldersSidebarShown && 'ChatFolders--tabs-sidebar-shown',
|
isFoldersSidebarShown && 'ChatFolders--tabs-sidebar-shown',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{shouldRenderStoryRibbon && <StoryRibbon isClosing={isStoryRibbonClosing} />}
|
{shouldRenderStoryRibbon && <StoryRibbon isClosing={isStoryRibbonClosing} />}
|
||||||
{shouldRenderFolders ? (
|
<div className={buildClassName('ChatFolders-content', shouldRenderFolders && 'with-tabs')}>
|
||||||
<SquareTabList
|
{shouldRenderFolders ? (
|
||||||
contextRootElementSelector="#LeftColumn"
|
<ChatFolderTabList
|
||||||
tabs={folderTabs}
|
tabs={folderTabs}
|
||||||
activeTab={activeChatFolder}
|
activeTab={activeChatFolder}
|
||||||
onSwitchTab={handleSwitchTab}
|
isHidden={shouldHideFolderTabs}
|
||||||
className={!isNotScrolled ? 'scrolled' : undefined}
|
onSwitchTab={handleSwitchTab}
|
||||||
/>
|
/>
|
||||||
) : shouldRenderPlaceholder ? (
|
) : shouldRenderPlaceholder ? (
|
||||||
<div ref={placeholderRef} className="tabs-placeholder" />
|
<div ref={placeholderRef} className="tabs-placeholder" />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Transition
|
<Transition
|
||||||
ref={transitionRef}
|
ref={transitionRef}
|
||||||
name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
|
name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
|
||||||
activeKey={activeChatFolder}
|
activeKey={activeChatFolder}
|
||||||
renderCount={hasFolders ? folderTabs.length : undefined}
|
renderCount={hasFolders ? folderTabs.length : undefined}
|
||||||
>
|
>
|
||||||
{renderCurrentTab}
|
{renderCurrentTab}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -48,6 +48,11 @@ type OwnProps = {
|
|||||||
isFoldersSidebarShown?: boolean;
|
isFoldersSidebarShown?: boolean;
|
||||||
isStoryRibbonShown?: boolean;
|
isStoryRibbonShown?: boolean;
|
||||||
foldersDispatch?: FolderEditDispatch;
|
foldersDispatch?: FolderEditDispatch;
|
||||||
|
noAbsolutePositioning?: boolean;
|
||||||
|
noVirtualization?: boolean;
|
||||||
|
noScrollRestore?: boolean;
|
||||||
|
noFastList?: boolean;
|
||||||
|
scrollContainerClosest?: string;
|
||||||
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
|
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,6 +72,11 @@ const ChatList = ({
|
|||||||
isFoldersSidebarShown,
|
isFoldersSidebarShown,
|
||||||
isStoryRibbonShown,
|
isStoryRibbonShown,
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
|
noAbsolutePositioning,
|
||||||
|
noVirtualization,
|
||||||
|
noScrollRestore,
|
||||||
|
noFastList,
|
||||||
|
scrollContainerClosest,
|
||||||
onScroll,
|
onScroll,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const {
|
const {
|
||||||
@ -99,7 +109,10 @@ const ChatList = ({
|
|||||||
orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd,
|
orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd,
|
||||||
} = useOrderDiff(orderedIds, panesHeight);
|
} = 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
|
// Support <Alt>+<Up/Down> to navigate between chats
|
||||||
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
||||||
@ -194,7 +207,9 @@ const ChatList = ({
|
|||||||
|
|
||||||
return viewportIds!.map((id, i) => {
|
return viewportIds!.map((id, i) => {
|
||||||
const isPinned = viewportOffset + i < pinnedCount;
|
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 (
|
return (
|
||||||
<Chat
|
<Chat
|
||||||
@ -219,6 +234,8 @@ const ChatList = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalHeight = chatsHeight + archiveHeight + panesHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open', className)}
|
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open', className)}
|
||||||
@ -226,8 +243,11 @@ const ChatList = ({
|
|||||||
items={viewportIds}
|
items={viewportIds}
|
||||||
itemSelector=".ListItem:not(.chat-item-archive)"
|
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||||
preloadBackwards={CHAT_LIST_SLICE}
|
preloadBackwards={CHAT_LIST_SLICE}
|
||||||
withAbsolutePositioning
|
withAbsolutePositioning={!noAbsolutePositioning}
|
||||||
maxHeight={chatsHeight + archiveHeight + panesHeight}
|
maxHeight={!noAbsolutePositioning ? totalHeight : undefined}
|
||||||
|
scrollContainerClosest={scrollContainerClosest}
|
||||||
|
noScrollRestore={noScrollRestore}
|
||||||
|
noFastList={noFastList}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
onScroll={onScroll}
|
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 {
|
&--tabs-sidebar-shown .chat-list {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tab {
|
&-content {
|
||||||
flex: 0 0 auto;
|
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 {
|
> .Transition {
|
||||||
|
|||||||
@ -27,26 +27,13 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabListWrapper {
|
.fadeMask {
|
||||||
|
|
||||||
|
--fade-mask-color: var(--color-background-secondary);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: 100%;
|
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 {
|
.tabList {
|
||||||
|
|||||||
@ -280,17 +280,17 @@ const AiMessageEditorModal = ({
|
|||||||
)}
|
)}
|
||||||
isSlim
|
isSlim
|
||||||
>
|
>
|
||||||
<div className={styles.tabListWrapper}>
|
<TabList
|
||||||
<TabList
|
tabs={tabs}
|
||||||
tabs={tabs}
|
activeTab={activeTabIndex}
|
||||||
activeTab={activeTabIndex}
|
withFadeMask
|
||||||
onSwitchTab={handleTabChange}
|
fadeMaskClassName={styles.fadeMask}
|
||||||
className={styles.tabList}
|
className={styles.tabList}
|
||||||
tabClassName={styles.tab}
|
tabClassName={styles.tab}
|
||||||
stretched
|
stretched
|
||||||
itemAlignment="vertical"
|
itemAlignment="vertical"
|
||||||
/>
|
onSwitchTab={handleTabChange}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className={styles.transitionWrapper}>
|
<div className={styles.transitionWrapper}>
|
||||||
<Transition
|
<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 { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -18,15 +19,13 @@ import type {
|
|||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ProfileCollectionKey } from '../../global/selectors/payments';
|
import type { ProfileCollectionKey } from '../../global/selectors/payments';
|
||||||
import type { TabState } from '../../global/types';
|
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 type { RegularLangKey } from '../../types/language';
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
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 { 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 {
|
import {
|
||||||
getHasAdminRight,
|
getHasAdminRight,
|
||||||
getIsDownloading,
|
getIsDownloading,
|
||||||
@ -63,6 +62,7 @@ import {
|
|||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { selectPremiumLimit } from '../../global/selectors/limits';
|
import { selectPremiumLimit } from '../../global/selectors/limits';
|
||||||
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
|
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
|
||||||
|
import { selectActiveGiftsCollectionId } from '../../global/selectors/payments';
|
||||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||||
import { selectActiveStoriesCollectionId } from '../../global/selectors/stories';
|
import { selectActiveStoriesCollectionId } from '../../global/selectors/stories';
|
||||||
import {
|
import {
|
||||||
@ -85,7 +85,6 @@ import { useViewTransition } from '../../hooks/animations/useViewTransition';
|
|||||||
import { useVtn } from '../../hooks/animations/useVtn.ts';
|
import { useVtn } from '../../hooks/animations/useVtn.ts';
|
||||||
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
|
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
|
||||||
import useTopOverscroll from '../../hooks/scroll/useTopOverscroll.tsx';
|
import useTopOverscroll from '../../hooks/scroll/useTopOverscroll.tsx';
|
||||||
import useCacheBuster from '../../hooks/useCacheBuster';
|
|
||||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
@ -111,6 +110,8 @@ import PrivateChatInfo from '../common/PrivateChatInfo';
|
|||||||
import ChatExtra from '../common/profile/ChatExtra';
|
import ChatExtra from '../common/profile/ChatExtra';
|
||||||
import ProfileInfo from '../common/profile/ProfileInfo.tsx';
|
import ProfileInfo from '../common/profile/ProfileInfo.tsx';
|
||||||
import WebLink from '../common/WebLink';
|
import WebLink from '../common/WebLink';
|
||||||
|
import Island from '../gili/layout/Island';
|
||||||
|
import Surface from '../gili/layout/Surface';
|
||||||
import ChatList from '../left/main/ChatList';
|
import ChatList from '../left/main/ChatList';
|
||||||
import MediaStory from '../story/MediaStory';
|
import MediaStory from '../story/MediaStory';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@ -119,13 +120,13 @@ import InfiniteScroll from '../ui/InfiniteScroll';
|
|||||||
import Link from '../ui/Link';
|
import Link from '../ui/Link';
|
||||||
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
||||||
import Spinner from '../ui/Spinner';
|
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 Transition from '../ui/Transition';
|
||||||
import DeleteMemberModal from './DeleteMemberModal';
|
import DeleteMemberModal from './DeleteMemberModal';
|
||||||
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
|
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
|
||||||
import StoryAlbumList from './stories/StoryAlbumList';
|
import StoryAlbumList from './stories/StoryAlbumList';
|
||||||
|
|
||||||
import './Profile.scss';
|
import styles from './Profile.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -134,6 +135,7 @@ type OwnProps = {
|
|||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onProfileStateChange: (state: ProfileState) => void;
|
onProfileStateChange: (state: ProfileState) => void;
|
||||||
|
onProfileExpandedChange?: (isExpanded: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -208,6 +210,7 @@ const TABS: LocalTabProps[] = [
|
|||||||
{ type: 'gif', key: 'ProfileTabGifs' },
|
{ type: 'gif', key: 'ProfileTabGifs' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CONTENT_PANEL_SHOW_DELAY = 300;
|
||||||
const HIDDEN_RENDER_DELAY = 1000;
|
const HIDDEN_RENDER_DELAY = 1000;
|
||||||
const INTERSECTION_THROTTLE = 500;
|
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>>([
|
const SHARED_MEDIA_TYPES = new Set<StringAutocomplete<SharedMediaType>>([
|
||||||
'media', 'documents', 'links', 'audio', 'voice', 'gif',
|
'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 = ({
|
const Profile = ({
|
||||||
chatId,
|
chatId,
|
||||||
@ -279,6 +302,7 @@ const Profile = ({
|
|||||||
canUpdateMainTab,
|
canUpdateMainTab,
|
||||||
canAutoPlayGifs,
|
canAutoPlayGifs,
|
||||||
onProfileStateChange,
|
onProfileStateChange,
|
||||||
|
onProfileExpandedChange,
|
||||||
}: OwnProps & StateProps) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
setSharedMediaSearchType,
|
setSharedMediaSearchType,
|
||||||
@ -324,7 +348,9 @@ const Profile = ({
|
|||||||
const isGeneralSavedMessages = isSavedMessages && !isSavedDialog;
|
const isGeneralSavedMessages = isSavedMessages && !isSavedDialog;
|
||||||
const [isProfileExpanded, expandProfile, collapseProfile] = useFlag();
|
const [isProfileExpanded, expandProfile, collapseProfile] = useFlag();
|
||||||
|
|
||||||
const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0);
|
useEffect(() => {
|
||||||
|
onProfileExpandedChange?.(isProfileExpanded);
|
||||||
|
}, [isProfileExpanded, onProfileExpandedChange]);
|
||||||
|
|
||||||
const isUser = isUserId(chatId);
|
const isUser = isUserId(chatId);
|
||||||
const validMainTabTypes = isUser ? VALID_USER_MAIN_TAB_TYPES : VALID_CHANNEL_MAIN_TAB_TYPES;
|
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
|
setActiveTab(peerFullInfo.mainTab); // Only focus when loading full info
|
||||||
}, [peerFullInfo]);
|
}, [peerFullInfo]);
|
||||||
|
|
||||||
const handleSwitchTab = useCallback((index: number) => {
|
const handleSwitchTab = useLastCallback((index: number) => {
|
||||||
startAutoScrollToTabsIfNeeded();
|
startAutoScrollToTabsIfNeeded();
|
||||||
setActiveTab(tabs[index].type);
|
setActiveTab(tabs[index].type);
|
||||||
}, [tabs]);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasPreviewMediaTab && !botPreviewMedia) {
|
if (hasPreviewMediaTab && !botPreviewMedia) {
|
||||||
@ -511,15 +537,15 @@ const Profile = ({
|
|||||||
if (!isSynced) return;
|
if (!isSynced) return;
|
||||||
loadCommonChats({ userId: chatId });
|
loadCommonChats({ userId: chatId });
|
||||||
});
|
});
|
||||||
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
|
const handleLoadPeerStories = useLastCallback(({ offsetId }: { offsetId: number }) => {
|
||||||
loadPeerProfileStories({ peerId: chatId, offsetId });
|
loadPeerProfileStories({ peerId: chatId, offsetId });
|
||||||
}, [chatId]);
|
});
|
||||||
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
|
const handleLoadStoriesArchive = useLastCallback(({ offsetId }: { offsetId: number }) => {
|
||||||
loadStoriesArchive({ peerId: chatId, offsetId });
|
loadStoriesArchive({ peerId: chatId, offsetId });
|
||||||
}, [chatId]);
|
});
|
||||||
const handleLoadGifts = useCallback(() => {
|
const handleLoadGifts = useLastCallback(() => {
|
||||||
loadPeerSavedGifts({ peerId: chatId });
|
loadPeerSavedGifts({ peerId: chatId });
|
||||||
}, [chatId]);
|
});
|
||||||
|
|
||||||
const handleLoadMoreMembers = useLastCallback(() => {
|
const handleLoadMoreMembers = useLastCallback(() => {
|
||||||
if (!isSynced) return;
|
if (!isSynced) return;
|
||||||
@ -574,6 +600,14 @@ const Profile = ({
|
|||||||
similarBots,
|
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 shouldRenderProfileInfo = !noProfileInfo && !isSavedMessages;
|
||||||
|
|
||||||
const isFirstTab = tabs[0].type === resultType;
|
const isFirstTab = tabs[0].type === resultType;
|
||||||
@ -589,21 +623,20 @@ const Profile = ({
|
|||||||
const shouldShowContentPanel = (isGiftsResult && hasGiftsCollections) || (isStoriesResult && hasStoryAlbums);
|
const shouldShowContentPanel = (isGiftsResult && hasGiftsCollections) || (isStoriesResult && hasStoryAlbums);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||||
if (hasGiftsCollections) {
|
if (hasGiftsCollections) {
|
||||||
setTimeout(() => {
|
timers.push(setTimeout(markGiftCollectionsShowed, CONTENT_PANEL_SHOW_DELAY));
|
||||||
markGiftCollectionsShowed();
|
|
||||||
}, CONTENT_PANEL_SHOW_DELAY);
|
|
||||||
} else {
|
} else {
|
||||||
unmarkGiftCollectionsShowed();
|
unmarkGiftCollectionsShowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasStoryAlbums) {
|
if (hasStoryAlbums) {
|
||||||
setTimeout(() => {
|
timers.push(setTimeout(markStoryAlbumsShowed, CONTENT_PANEL_SHOW_DELAY));
|
||||||
markStoryAlbumsShowed();
|
|
||||||
}, CONTENT_PANEL_SHOW_DELAY);
|
|
||||||
} else {
|
} else {
|
||||||
unmarkStoryAlbums();
|
unmarkStoryAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => timers.forEach(clearTimeout);
|
||||||
}, [hasGiftsCollections, hasStoryAlbums, markGiftCollectionsShowed, markStoryAlbumsShowed]);
|
}, [hasGiftsCollections, hasStoryAlbums, markGiftCollectionsShowed, markStoryAlbumsShowed]);
|
||||||
|
|
||||||
usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
|
usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
|
||||||
@ -638,8 +671,6 @@ const Profile = ({
|
|||||||
|
|
||||||
useTransitionFixes(containerRef);
|
useTransitionFixes(containerRef);
|
||||||
|
|
||||||
const [cacheBuster, resetCacheBuster] = useCacheBuster();
|
|
||||||
|
|
||||||
const { observe: observeIntersectionForMedia } = useIntersectionObserver({
|
const { observe: observeIntersectionForMedia } = useIntersectionObserver({
|
||||||
rootRef: containerRef,
|
rootRef: containerRef,
|
||||||
throttleMs: INTERSECTION_THROTTLE,
|
throttleMs: INTERSECTION_THROTTLE,
|
||||||
@ -777,14 +808,14 @@ const Profile = ({
|
|||||||
|
|
||||||
function renderNothingFoundGiftsWithFilter() {
|
function renderNothingFoundGiftsWithFilter() {
|
||||||
return (
|
return (
|
||||||
<div className="nothing-found-gifts">
|
<div className={styles.nothingFoundGifts}>
|
||||||
<AnimatedIconWithPreview
|
<AnimatedIconWithPreview
|
||||||
size={160}
|
size={160}
|
||||||
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
|
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
|
||||||
nonInteractive
|
nonInteractive
|
||||||
noLoop
|
noLoop
|
||||||
/>
|
/>
|
||||||
<div className="description">
|
<div className={styles.description}>
|
||||||
{lang('GiftSearchEmpty')}
|
{lang('GiftSearchEmpty')}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<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() {
|
function renderContent() {
|
||||||
if (resultType === 'dialogs') {
|
if (resultType === 'dialogs') {
|
||||||
return (
|
return wrapInIsland(
|
||||||
<ChatList className="saved-dialogs" folderType="saved" isActive />
|
<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;
|
const noSpinner = isFirstTab && !canRenderContent;
|
||||||
|
|
||||||
|
if (shouldWrapInIsland) {
|
||||||
|
return renderSpinnerOrContent(noContent, noSpinner);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.sharedMediaIslandContainer}>
|
||||||
{renderCategories()}
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -820,9 +908,9 @@ const Profile = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'contentCategoriesPanel',
|
styles.contentCategoriesPanel,
|
||||||
!shouldShowContentPanel && 'hiddenPanel',
|
!shouldShowContentPanel && styles.hiddenPanel,
|
||||||
isGiftCollectionsShowed && 'noTransition',
|
isGiftCollectionsShowed && styles.noTransition,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<StarGiftCollectionList peerId={chatId} />
|
<StarGiftCollectionList peerId={chatId} />
|
||||||
@ -834,9 +922,9 @@ const Profile = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'contentCategoriesPanel',
|
styles.contentCategoriesPanel,
|
||||||
!shouldShowContentPanel && 'hiddenPanel',
|
!shouldShowContentPanel && styles.hiddenPanel,
|
||||||
isStoryAlbumsShowed && 'noTransition',
|
isStoryAlbumsShowed && styles.noTransition,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<StoryAlbumList peerId={chatId} />
|
<StoryAlbumList peerId={chatId} />
|
||||||
@ -853,7 +941,7 @@ const Profile = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="content empty-list"
|
className={buildClassName(styles.content, styles.emptyList)}
|
||||||
>
|
>
|
||||||
{!noSpinner && !forceRenderHiddenMembers && <Spinner />}
|
{!noSpinner && !forceRenderHiddenMembers && <Spinner />}
|
||||||
{forceRenderHiddenMembers && <NothingFound text={lang('ChatMemberListNoAccess')} />}
|
{forceRenderHiddenMembers && <NothingFound text={lang('ChatMemberListNoAccess')} />}
|
||||||
@ -903,7 +991,7 @@ const Profile = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content empty-list">
|
<div className={buildClassName(styles.content, styles.emptyList)}>
|
||||||
<NothingFound text={text} />
|
<NothingFound text={text} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -916,18 +1004,18 @@ const Profile = ({
|
|||||||
|
|
||||||
const noTransition = resultType === 'gifts' ? isGiftCollectionsShowed
|
const noTransition = resultType === 'gifts' ? isGiftCollectionsShowed
|
||||||
: resultType === 'stories' ? isStoryAlbumsShowed : false;
|
: resultType === 'stories' ? isStoryAlbumsShowed : false;
|
||||||
return (
|
const contentEl = (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
`content ${resultType}-list`,
|
styles.content,
|
||||||
shouldShowContentPanel && 'showContentPanel',
|
CONTENT_LIST_CLASS[resultType],
|
||||||
noTransition && 'noTransition',
|
shouldShowContentPanel && styles.showContentPanel,
|
||||||
|
noTransition && styles.noTransition,
|
||||||
)}
|
)}
|
||||||
dir={lang.isRtl && (resultType === 'media' || resultType === 'gif') ? 'rtl' : undefined}
|
dir={lang.isRtl && (resultType === 'media' || resultType === 'gif') ? 'rtl' : undefined}
|
||||||
teactFastList
|
|
||||||
>
|
>
|
||||||
{resultType === 'media' || resultType === 'gif' ? (
|
{resultType === 'media' || resultType === 'gif' ? (
|
||||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||||
<Media
|
<Media
|
||||||
key={id}
|
key={id}
|
||||||
message={messagesById[id]}
|
message={messagesById[id]}
|
||||||
@ -938,16 +1026,16 @@ const Profile = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
|
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
|
||||||
(viewportIds as number[]).map((id, i) => storyByIds?.[id] && (
|
(viewportIds as number[]).filter((id) => Boolean(storyByIds?.[id])).map((id, i) => (
|
||||||
<MediaStory
|
<MediaStory
|
||||||
teactOrderKey={i}
|
teactOrderKey={i}
|
||||||
key={`${resultType}_${id}`}
|
key={`${resultType}_${id}`}
|
||||||
story={storyByIds[id]}
|
story={storyByIds![id]}
|
||||||
isArchive={resultType === 'storiesArchive'}
|
isArchive={resultType === 'storiesArchive'}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'documents' ? (
|
) : resultType === 'documents' ? (
|
||||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||||
<Document
|
<Document
|
||||||
key={id}
|
key={id}
|
||||||
id={`shared-media${getMessageHtmlId(id)}`}
|
id={`shared-media${getMessageHtmlId(id)}`}
|
||||||
@ -964,7 +1052,7 @@ const Profile = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'links' ? (
|
) : resultType === 'links' ? (
|
||||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||||
<WebLink
|
<WebLink
|
||||||
key={id}
|
key={id}
|
||||||
message={messagesById[id]}
|
message={messagesById[id]}
|
||||||
@ -974,7 +1062,7 @@ const Profile = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'audio' ? (
|
) : resultType === 'audio' ? (
|
||||||
(viewportIds as number[]).map((id) => messagesById[id] && (
|
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => (
|
||||||
<Audio
|
<Audio
|
||||||
key={id}
|
key={id}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@ -989,13 +1077,12 @@ const Profile = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'voice' ? (
|
) : resultType === 'voice' ? (
|
||||||
(viewportIds as number[]).map((id) => {
|
(viewportIds as number[]).filter((id) => Boolean(messagesById[id])).map((id) => {
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
const message = messagesById[id];
|
const message = messagesById[id];
|
||||||
if (!message) return undefined;
|
|
||||||
|
|
||||||
const media = selectMessageDownloadableMedia(global, message)!;
|
const media = selectMessageDownloadableMedia(global, message)!;
|
||||||
return messagesById[id] && (
|
return (
|
||||||
<Audio
|
<Audio
|
||||||
key={id}
|
key={id}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@ -1073,14 +1160,14 @@ const Profile = ({
|
|||||||
{!isCurrentUserPremium && (
|
{!isCurrentUserPremium && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
className="show-more-channels"
|
className={styles.showMoreChannels}
|
||||||
onClick={() => openPremiumModal()}
|
onClick={() => openPremiumModal()}
|
||||||
iconName="unlock-badge"
|
iconName="unlock-badge"
|
||||||
iconAlignment="end"
|
iconAlignment="end"
|
||||||
>
|
>
|
||||||
{oldLang('UnlockSimilar')}
|
{oldLang('UnlockSimilar')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="more-similar">
|
<div className={styles.moreSimilar}>
|
||||||
{renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])}
|
{renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -1107,10 +1194,10 @@ const Profile = ({
|
|||||||
))}
|
))}
|
||||||
{!isCurrentUserPremium && (
|
{!isCurrentUserPremium && (
|
||||||
<>
|
<>
|
||||||
<Button className="show-more-bots" onClick={() => openPremiumModal()} iconName="unlock-badge">
|
<Button className={styles.showMoreBots} onClick={() => openPremiumModal()} iconName="unlock-badge">
|
||||||
{lang('UnlockMoreSimilarBots')}
|
{lang('UnlockMoreSimilarBots')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="more-similar">
|
<div className={styles.moreSimilar}>
|
||||||
{renderText(lang('MoreSimilarBotsDescription', { count: limitSimilarPeers }, {
|
{renderText(lang('MoreSimilarBotsDescription', { count: limitSimilarPeers }, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
withMarkdown: true,
|
withMarkdown: true,
|
||||||
@ -1126,7 +1213,7 @@ const Profile = ({
|
|||||||
<SavedGift
|
<SavedGift
|
||||||
peerId={chatId}
|
peerId={chatId}
|
||||||
key={getSavedGiftKey(gift)}
|
key={getSavedGiftKey(gift)}
|
||||||
className="saved-gift"
|
className={styles.savedGift}
|
||||||
style={createVtnStyle(getSavedGiftKey(gift))}
|
style={createVtnStyle(getSavedGiftKey(gift))}
|
||||||
gift={gift}
|
gift={gift}
|
||||||
observeIntersection={observeIntersectionForMedia}
|
observeIntersection={observeIntersectionForMedia}
|
||||||
@ -1136,6 +1223,8 @@ const Profile = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return wrapInIsland(contentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldUseTransitionForContent = resultType === 'stories' || resultType === 'gifts';
|
const shouldUseTransitionForContent = resultType === 'stories' || resultType === 'gifts';
|
||||||
@ -1149,13 +1238,9 @@ const Profile = ({
|
|||||||
return 0;
|
return 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const handleOnStop = useLastCallback(() => {
|
|
||||||
setRestoreContentHeightKey(restoreContentHeightKey + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderProfileInfo(peerId: string, isReady: boolean) {
|
function renderProfileInfo(peerId: string, isReady: boolean) {
|
||||||
return (
|
return (
|
||||||
<div className="profile-info">
|
<div className={buildClassName(styles.profileInfo, 'profile-info')}>
|
||||||
<ProfileInfo
|
<ProfileInfo
|
||||||
isExpanded={isProfileExpanded}
|
isExpanded={isProfileExpanded}
|
||||||
peerId={peerId}
|
peerId={peerId}
|
||||||
@ -1167,6 +1252,9 @@ const Profile = ({
|
|||||||
chatOrUserId={profileId}
|
chatOrUserId={profileId}
|
||||||
isSavedDialog={isSavedDialog}
|
isSavedDialog={isSavedDialog}
|
||||||
isOwnProfile={isOwnProfile}
|
isOwnProfile={isOwnProfile}
|
||||||
|
withIslands
|
||||||
|
className={styles.chatExtraBlock}
|
||||||
|
style={createVtnStyle('chatExtraBlock', true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1180,22 +1268,15 @@ const Profile = ({
|
|||||||
if (shouldUseTransitionForContent) {
|
if (shouldUseTransitionForContent) {
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
className={`${resultType}-list`}
|
className={CONTENT_LIST_CLASS[resultType]}
|
||||||
activeKey={contentTransitionKey}
|
activeKey={contentTransitionKey}
|
||||||
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||||
shouldCleanup
|
contentSelector={`.Transition > .Transition_slide-active > .${styles.content}`}
|
||||||
shouldRestoreHeight
|
|
||||||
restoreHeightKey={restoreContentHeightKey}
|
|
||||||
contentSelector=".Transition > .Transition_slide-active > .content"
|
|
||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
activeKey={isSpinner ? 0 : 1}
|
activeKey={isSpinner ? 0 : 1}
|
||||||
name="fade"
|
name="fade"
|
||||||
shouldCleanup
|
contentSelector={`.${styles.content}`}
|
||||||
shouldRestoreHeight
|
|
||||||
restoreHeightKey={restoreContentHeightKey}
|
|
||||||
contentSelector=".content"
|
|
||||||
onStop={handleOnStop}
|
|
||||||
>
|
>
|
||||||
{baseContent}
|
{baseContent}
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -1207,34 +1288,18 @@ const Profile = ({
|
|||||||
<Transition
|
<Transition
|
||||||
activeKey={isSpinner ? 0 : 1}
|
activeKey={isSpinner ? 0 : 1}
|
||||||
name="fade"
|
name="fade"
|
||||||
shouldCleanup
|
|
||||||
shouldRestoreHeight
|
|
||||||
>
|
>
|
||||||
{baseContent}
|
{baseContent}
|
||||||
</Transition>
|
</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 (
|
return (
|
||||||
<InfiniteScroll
|
<Surface
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="Profile custom-scroll"
|
scrollable
|
||||||
itemSelector={itemSelector}
|
noPadding
|
||||||
items={canRenderContent ? viewportIds : undefined}
|
className={buildClassName(styles.root, 'Profile', isGeneralSavedMessages && 'is-saved-messages')}
|
||||||
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}
|
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
{!noProfileInfo && !isSavedMessages && (
|
{!noProfileInfo && !isSavedMessages && (
|
||||||
@ -1244,33 +1309,42 @@ const Profile = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{!isRestricted && (
|
{!isRestricted && (
|
||||||
<div
|
<>
|
||||||
className="shared-media"
|
<div
|
||||||
style={createVtnStyle('sharedMedia')}
|
className={buildClassName(styles.sharedMediaTabs, 'shared-media-tabs')}
|
||||||
>
|
style={createVtnStyle('sharedMediaTabs')}
|
||||||
<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}
|
|
||||||
>
|
>
|
||||||
{renderContent()}
|
<TabList
|
||||||
</Transition>
|
activeTab={activeTabIndex}
|
||||||
<SquareTabList activeTab={activeTabIndex} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
tabs={tabs}
|
||||||
</div>
|
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 && (
|
{canAddMembers && (
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
className={buildClassName(!isActive && 'hidden')}
|
className={buildClassName(!isActive && styles.hidden)}
|
||||||
|
style={createVtnStyle('profileFab')}
|
||||||
isShown={canRenderContent}
|
isShown={canRenderContent}
|
||||||
onClick={handleNewMemberDialogOpen}
|
onClick={handleNewMemberDialogOpen}
|
||||||
ariaLabel={oldLang('lng_channel_add_users')}
|
ariaLabel={oldLang('lng_channel_add_users')}
|
||||||
@ -1284,7 +1358,7 @@ const Profile = ({
|
|||||||
onClose={handleDeleteMembersModalClose}
|
onClose={handleDeleteMembersModalClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfiniteScroll>
|
</Surface>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
padding-right: env(safe-area-inset-right);
|
padding-right: env(safe-area-inset-right);
|
||||||
border-left: 1px solid var(--color-borders);
|
border-left: 1px solid var(--color-borders);
|
||||||
|
|
||||||
background: var(--color-background);
|
background: var(--color-background-secondary);
|
||||||
|
|
||||||
transition: transform var(--layer-transition);
|
transition: transform var(--layer-transition);
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
selectAreActiveChatsLoaded,
|
selectAreActiveChatsLoaded,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
|
selectPeerHasProfileBackground,
|
||||||
selectRightColumnContentKey,
|
selectRightColumnContentKey,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
@ -59,6 +60,7 @@ type StateProps = {
|
|||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
isSavedDialog?: boolean;
|
isSavedDialog?: boolean;
|
||||||
isOwnProfile?: boolean;
|
isOwnProfile?: boolean;
|
||||||
|
hasProfileBackground?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
|
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
|
||||||
@ -85,6 +87,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
isOwnProfile,
|
isOwnProfile,
|
||||||
|
hasProfileBackground,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleChatInfo,
|
toggleChatInfo,
|
||||||
@ -116,6 +119,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial);
|
const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial);
|
||||||
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
|
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
|
||||||
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
|
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
|
||||||
|
const [isProfileExpanded, setIsProfileExpanded] = useState(false);
|
||||||
|
const [isProfileScrolled, setIsProfileScrolled] = useState(false);
|
||||||
const isScrolledDown = profileState !== ProfileState.Profile;
|
const isScrolledDown = profileState !== ProfileState.Profile;
|
||||||
|
|
||||||
const isOpen = contentKey !== undefined;
|
const isOpen = contentKey !== undefined;
|
||||||
@ -134,6 +139,14 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
const isEditingTopic = contentKey === RightColumnContent.EditTopic;
|
const isEditingTopic = contentKey === RightColumnContent.EditTopic;
|
||||||
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
|
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 [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
|
||||||
|
|
||||||
const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1;
|
const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1;
|
||||||
@ -141,8 +154,27 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
useScrollNotch({
|
useScrollNotch({
|
||||||
containerRef,
|
containerRef,
|
||||||
selector: ':scope .custom-scroll, :scope .panel-content',
|
selector: ':scope .custom-scroll, :scope .panel-content',
|
||||||
|
shouldHideTopNotch: isSavedMessages || (isProfile && isScrolledDown),
|
||||||
}, [contentKey, managementScreen, chatId, threadId]);
|
}, [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) => {
|
const close = useLastCallback((shouldScrollUp = true) => {
|
||||||
switch (contentKey) {
|
switch (contentKey) {
|
||||||
case RightColumnContent.AddingMembers:
|
case RightColumnContent.AddingMembers:
|
||||||
@ -320,6 +352,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isActive={isOpen && isActive}
|
isActive={isOpen && isActive}
|
||||||
onProfileStateChange={setProfileState}
|
onProfileStateChange={setProfileState}
|
||||||
|
onProfileExpandedChange={setIsProfileExpanded}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case RightColumnContent.Management:
|
case RightColumnContent.Management:
|
||||||
@ -376,6 +409,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
isColumnOpen={isOpen}
|
isColumnOpen={isOpen}
|
||||||
isProfile={isProfile}
|
isProfile={isProfile}
|
||||||
|
headerBackground={headerBackground}
|
||||||
isManagement={isManagement}
|
isManagement={isManagement}
|
||||||
isStatistics={isStatistics}
|
isStatistics={isStatistics}
|
||||||
isBoostStatistics={isBoostStatistics}
|
isBoostStatistics={isBoostStatistics}
|
||||||
@ -439,6 +473,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
isOwnProfile,
|
isOwnProfile,
|
||||||
|
hasProfileBackground: chatId ? selectPeerHasProfileBackground(global, chatId) : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(RightColumn));
|
)(RightColumn));
|
||||||
|
|||||||
@ -9,7 +9,13 @@
|
|||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
padding: 0.5rem 0.8125rem;
|
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');
|
@include mixins.with-vt-type('rightColumn');
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,7 @@ type OwnProps = {
|
|||||||
isCreatingTopic?: boolean;
|
isCreatingTopic?: boolean;
|
||||||
isEditingTopic?: boolean;
|
isEditingTopic?: boolean;
|
||||||
isAddingChatMembers?: boolean;
|
isAddingChatMembers?: boolean;
|
||||||
|
headerBackground?: 'regular' | 'secondary';
|
||||||
profileState?: ProfileState;
|
profileState?: ProfileState;
|
||||||
managementScreen?: ManagementScreens;
|
managementScreen?: ManagementScreens;
|
||||||
onClose: (shouldScrollUp?: boolean) => void;
|
onClose: (shouldScrollUp?: boolean) => void;
|
||||||
@ -158,6 +159,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
isCreatingTopic,
|
isCreatingTopic,
|
||||||
isEditingTopic,
|
isEditingTopic,
|
||||||
isAddingChatMembers,
|
isAddingChatMembers,
|
||||||
|
headerBackground,
|
||||||
profileState,
|
profileState,
|
||||||
managementScreen,
|
managementScreen,
|
||||||
canAddContact,
|
canAddContact,
|
||||||
@ -266,6 +268,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const isSecondaryBackground = headerBackground === 'secondary';
|
||||||
const contentKey = isProfile ? (
|
const contentKey = isProfile ? (
|
||||||
profileState === ProfileState.Profile ? (
|
profileState === ProfileState.Profile ? (
|
||||||
HeaderContent.Profile
|
HeaderContent.Profile
|
||||||
@ -703,7 +707,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="RightHeader"
|
className={buildClassName('RightHeader', isSecondaryBackground && 'secondary')}
|
||||||
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
|
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
|
||||||
style={createVtnStyle('rightHeader', true)}
|
style={createVtnStyle('rightHeader', true)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
.tabList {
|
.tabList {
|
||||||
margin-top: 0.5rem;
|
margin-inline: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,11 @@ const runThrottledForScroll = throttle((cb) => cb(), 250, false);
|
|||||||
|
|
||||||
let isScrollingProgrammatically = 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({
|
export default function useProfileState({
|
||||||
containerRef,
|
containerRef,
|
||||||
tabType,
|
tabType,
|
||||||
@ -37,9 +42,9 @@ export default function useProfileState({
|
|||||||
useEffectWithPrevDeps(([prevTabType]) => {
|
useEffectWithPrevDeps(([prevTabType]) => {
|
||||||
if ((prevTabType && prevTabType !== tabType && allowAutoScrollToTabs) || (tabType && forceScrollProfileTab)) {
|
if ((prevTabType && prevTabType !== tabType && allowAutoScrollToTabs) || (tabType && forceScrollProfileTab)) {
|
||||||
const container = containerRef.current!;
|
const container = containerRef.current!;
|
||||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList')!;
|
const tabsEl = container.querySelector<HTMLDivElement>('.shared-media-tabs')!;
|
||||||
handleStopAutoScrollToTabs();
|
handleStopAutoScrollToTabs();
|
||||||
if (container.scrollTop < tabsEl.offsetTop) {
|
if (container.scrollTop < getTabsNaturalTop(container)) {
|
||||||
onProfileStateChange(getStateFromTabType(tabType));
|
onProfileStateChange(getStateFromTabType(tabType));
|
||||||
isScrollingProgrammatically = true;
|
isScrollingProgrammatically = true;
|
||||||
animateScroll({
|
animateScroll({
|
||||||
@ -69,8 +74,8 @@ export default function useProfileState({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
const tabsEl = container.querySelector<HTMLDivElement>('.shared-media-tabs');
|
||||||
if (!tabListEl || tabListEl.offsetTop > container.scrollTop) {
|
if (!tabsEl || getTabsNaturalTop(container) > container.scrollTop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,13 +99,12 @@ export default function useProfileState({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
if (!container.querySelector('.shared-media-tabs')) {
|
||||||
if (!tabListEl) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state: ProfileState = ProfileState.Profile;
|
let state: ProfileState = ProfileState.Profile;
|
||||||
if (Math.ceil(container.scrollTop) >= tabListEl.offsetTop) {
|
if (Math.ceil(container.scrollTop) >= getTabsNaturalTop(container)) {
|
||||||
state = getStateFromTabType(tabType);
|
state = getStateFromTabType(tabType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import sortChatIds from '../../common/helpers/sortChatIds';
|
|||||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||||
|
|
||||||
|
const SHARED_MEDIA_TYPES: SharedMediaType[] = ['media', 'documents', 'links', 'audio', 'voice', 'gif'];
|
||||||
|
|
||||||
export default function useProfileViewportIds({
|
export default function useProfileViewportIds({
|
||||||
loadMoreMembers,
|
loadMoreMembers,
|
||||||
loadCommonChats,
|
loadCommonChats,
|
||||||
@ -59,7 +61,8 @@ export default function useProfileViewportIds({
|
|||||||
similarChannels?: string[];
|
similarChannels?: string[];
|
||||||
similarBots?: 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(() => {
|
const memberIds = useMemo(() => {
|
||||||
if (!groupChatMembers || !usersById || !userStatusesById) {
|
if (!groupChatMembers || !usersById || !userStatusesById) {
|
||||||
@ -218,7 +221,7 @@ function useInfiniteScrollForLoadableItems<ListId extends string | number>(
|
|||||||
handleLoadMore,
|
handleLoadMore,
|
||||||
itemIds,
|
itemIds,
|
||||||
undefined,
|
undefined,
|
||||||
MEMBERS_SLICE,
|
itemIds?.length || MEMBERS_SLICE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOnTop = !viewportIds || !itemIds || viewportIds[0] === itemIds[0];
|
const isOnTop = !viewportIds || !itemIds || viewportIds[0] === itemIds[0];
|
||||||
@ -250,11 +253,16 @@ function useInfiniteScrollForSharedMedia(
|
|||||||
}
|
}
|
||||||
}, [chatMessages, foundIds, currentResultType, forSharedMediaType]);
|
}, [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(
|
const [viewportIds, getMore] = useInfiniteScroll(
|
||||||
handleLoadMore,
|
handleLoadMore,
|
||||||
messageIdsRef.current,
|
messageIdsRef.current,
|
||||||
undefined,
|
undefined,
|
||||||
forSharedMediaType === 'media' ? SHARED_MEDIA_SLICE : MESSAGE_SEARCH_SLICE,
|
listSlice,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOnTop = !viewportIds || !messageIdsRef.current || viewportIds[0] === messageIdsRef.current[0];
|
const isOnTop = !viewportIds || !messageIdsRef.current || viewportIds[0] === messageIdsRef.current[0];
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ElementRef } from '../../../lib/teact/teact';
|
import type { ElementRef } from '../../../lib/teact/teact';
|
||||||
import { useEffect } 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(
|
export default function useTransitionFixes(
|
||||||
containerRef: ElementRef<HTMLDivElement>,
|
containerRef: ElementRef<HTMLDivElement>,
|
||||||
@ -10,16 +10,23 @@ export default function useTransitionFixes(
|
|||||||
// Set `min-height` for shared media container to prevent jumping when switching tabs
|
// Set `min-height` for shared media container to prevent jumping when switching tabs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function setMinHeight() {
|
function setMinHeight() {
|
||||||
const container = containerRef.current!;
|
requestMeasure(() => {
|
||||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
const container = containerRef.current;
|
||||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
if (!container) return;
|
||||||
if (transitionEl && tabsEl) {
|
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||||
const newHeight = container.clientHeight - tabsEl.offsetHeight;
|
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(() => {
|
requestMutation(() => {
|
||||||
transitionEl.style.minHeight = `${newHeight}px`;
|
transitionEl.style.minHeight = `${newHeight}px`;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setMinHeight();
|
setMinHeight();
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
.tabList {
|
.tabList {
|
||||||
margin-block: 0.5rem;
|
margin-inline: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type OwnProps = {
|
|||||||
isShown: boolean;
|
isShown: boolean;
|
||||||
iconName: IconName;
|
iconName: IconName;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: string;
|
||||||
color?: ButtonProps['color'];
|
color?: ButtonProps['color'];
|
||||||
ariaLabel?: ButtonProps['ariaLabel'];
|
ariaLabel?: ButtonProps['ariaLabel'];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -25,6 +26,7 @@ const FloatingActionButton = ({
|
|||||||
isShown,
|
isShown,
|
||||||
iconName,
|
iconName,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
color = 'primary',
|
color = 'primary',
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
disabled,
|
disabled,
|
||||||
@ -42,6 +44,7 @@ const FloatingActionButton = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={buttonClassName}
|
className={buttonClassName}
|
||||||
|
style={style}
|
||||||
color={color}
|
color={color}
|
||||||
round
|
round
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
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 { ApiMessageEntityCustomEmoji } from '../../api/types';
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
import type { MenuItemContextAction } from './ListItem';
|
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 buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||||
|
import useScrollToActiveTab from '../../hooks/useScrollToActiveTab';
|
||||||
|
|
||||||
import Tab from './Tab';
|
import Tab from './Tab';
|
||||||
|
|
||||||
@ -39,10 +38,6 @@ type OwnProps = {
|
|||||||
onSwitchTab: (index: number) => void;
|
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 = ({
|
const SquareTabList = ({
|
||||||
tabs,
|
tabs,
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -59,30 +54,7 @@ const SquareTabList = ({
|
|||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useHorizontalScroll(containerRef, undefined, true);
|
useHorizontalScroll(containerRef, undefined, true);
|
||||||
|
useScrollToActiveTab(containerRef, activeTab);
|
||||||
// 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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background-color: var(--color-background);
|
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;
|
transition: opacity 150ms;
|
||||||
|
|
||||||
@ -48,11 +48,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.activeIndicator {
|
.activeIndicator {
|
||||||
|
pointer-events: none;
|
||||||
will-change: clip-path;
|
will-change: clip-path;
|
||||||
|
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 4;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -117,4 +118,29 @@
|
|||||||
.lockIcon {
|
.lockIcon {
|
||||||
font-size: 0.875rem;
|
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 { memo, useEffect, useRef, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { IAnchorPosition } from '../../types';
|
||||||
|
import type { MenuItemContextAction } from './ListItem';
|
||||||
import type { TabWithProperties } from './SquareTabList';
|
import type { TabWithProperties } from './SquareTabList';
|
||||||
|
|
||||||
export type { TabWithProperties };
|
export type { TabWithProperties };
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useResizeObserver from '../../hooks/useResizeObserver';
|
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||||
|
import useScrollToActiveTab from '../../hooks/useScrollToActiveTab';
|
||||||
|
|
||||||
import CustomEmoji from '../common/CustomEmoji';
|
import CustomEmoji from '../common/CustomEmoji';
|
||||||
import Icon from '../common/icons/Icon';
|
import Icon from '../common/icons/Icon';
|
||||||
|
import Menu from './Menu';
|
||||||
|
import MenuItem from './MenuItem';
|
||||||
|
import MenuSeparator from './MenuSeparator';
|
||||||
|
|
||||||
import styles from './TabList.module.scss';
|
import styles from './TabList.module.scss';
|
||||||
|
|
||||||
@ -26,7 +35,10 @@ type OwnProps = {
|
|||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
stretched?: boolean;
|
stretched?: boolean;
|
||||||
itemAlignment?: 'vertical' | 'horizontal';
|
itemAlignment?: 'vertical' | 'horizontal';
|
||||||
|
withFadeMask?: boolean;
|
||||||
|
fadeMaskClassName?: string;
|
||||||
onSwitchTab: (index: number) => void;
|
onSwitchTab: (index: number) => void;
|
||||||
|
renderExtra?: (tab: TabWithProperties, index: number) => TeactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TabList = ({
|
const TabList = ({
|
||||||
@ -38,11 +50,18 @@ const TabList = ({
|
|||||||
centered,
|
centered,
|
||||||
stretched,
|
stretched,
|
||||||
itemAlignment,
|
itemAlignment,
|
||||||
|
withFadeMask,
|
||||||
|
fadeMaskClassName,
|
||||||
|
renderExtra,
|
||||||
onSwitchTab,
|
onSwitchTab,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
const clipPathContainerRef = useRef<HTMLDivElement>();
|
const clipPathContainerRef = useRef<HTMLDivElement>();
|
||||||
const [clipPath, setClipPath] = useState<string>('');
|
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);
|
useHorizontalScroll(containerRef, !tabs.length, true);
|
||||||
|
|
||||||
@ -68,12 +87,42 @@ const TabList = ({
|
|||||||
|
|
||||||
useResizeObserver(clipPathContainerRef, updateClipPath);
|
useResizeObserver(clipPathContainerRef, updateClipPath);
|
||||||
|
|
||||||
|
useScrollToActiveTab(containerRef, activeTab);
|
||||||
|
|
||||||
const handleTabClick = useLastCallback((index: number) => {
|
const handleTabClick = useLastCallback((index: number) => {
|
||||||
onSwitchTab(index);
|
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;
|
if (!tabs.length) return undefined;
|
||||||
|
|
||||||
|
const hasContextActions = tabs.some((tab) => tab.contextActions?.length);
|
||||||
|
|
||||||
const renderTab = (tab: TabWithProperties, index: number) => {
|
const renderTab = (tab: TabWithProperties, index: number) => {
|
||||||
const stringEmoticon = typeof tab.emoticon === 'string' ? tab.emoticon : undefined;
|
const stringEmoticon = typeof tab.emoticon === 'string' ? tab.emoticon : undefined;
|
||||||
const customEmoji = typeof tab.emoticon === 'object' ? tab.emoticon : undefined;
|
const customEmoji = typeof tab.emoticon === 'object' ? tab.emoticon : undefined;
|
||||||
@ -88,6 +137,7 @@ const TabList = ({
|
|||||||
stretched && styles.stretched,
|
stretched && styles.stretched,
|
||||||
)}
|
)}
|
||||||
onClick={() => handleTabClick(index)}
|
onClick={() => handleTabClick(index)}
|
||||||
|
onContextMenu={hasContextActions ? (e) => handleContextMenu(index, e) : undefined}
|
||||||
>
|
>
|
||||||
{stringEmoticon && <span className={styles.tabEmoji}>{stringEmoticon}</span>}
|
{stringEmoticon && <span className={styles.tabEmoji}>{stringEmoticon}</span>}
|
||||||
{customEmoji && (
|
{customEmoji && (
|
||||||
@ -99,17 +149,22 @@ const TabList = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab.icon && <Icon name={tab.icon} className={styles.tabIcon} />}
|
{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} />}
|
{tab.isBlocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const contextActions = menuTabIndex !== undefined ? tabs[menuTabIndex]?.contextActions : undefined;
|
||||||
|
|
||||||
|
const tabListElement = (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
|
'TabList',
|
||||||
styles.container,
|
styles.container,
|
||||||
|
withFadeMask && styles.withFadeMask,
|
||||||
centered && styles.centered,
|
centered && styles.centered,
|
||||||
itemAlignment === 'vertical' && styles.vertical,
|
itemAlignment === 'vertical' && styles.vertical,
|
||||||
className,
|
className,
|
||||||
@ -131,6 +186,56 @@ const TabList = ({
|
|||||||
</div>
|
</div>
|
||||||
</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);
|
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%;
|
width: 100%;
|
||||||
padding: 0.5625rem;
|
padding: 0.5625rem;
|
||||||
|
border-radius: var(--border-radius-island);
|
||||||
|
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
|
box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
transition: transform var(--chat-transform-transition);
|
transition: transform var(--chat-transform-transition);
|
||||||
|
|
||||||
@ -195,6 +197,11 @@
|
|||||||
|
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) & {
|
||||||
|
border: 1px solid var(--color-borders);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin side-panel-section {
|
@mixin side-panel-section {
|
||||||
|
|||||||
@ -232,7 +232,7 @@
|
|||||||
--border-radius-modal: 2rem;
|
--border-radius-modal: 2rem;
|
||||||
--border-radius-toast: 1rem;
|
--border-radius-toast: 1rem;
|
||||||
--border-radius-island: 1.5rem;
|
--border-radius-island: 1.5rem;
|
||||||
--border-radius-default: 0.75rem;
|
--border-radius-default: 1rem;
|
||||||
--border-radius-default-small: 0.625rem;
|
--border-radius-default-small: 0.625rem;
|
||||||
--border-radius-default-tiny: 0.375rem;
|
--border-radius-default-tiny: 0.375rem;
|
||||||
--border-radius-messages: 0.9375rem;
|
--border-radius-messages: 0.9375rem;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user