Message List, Shared Media, Left Search, Symbol Menu: Add transition to loading spinner

This commit is contained in:
Alexander Zinchuk 2025-08-21 12:05:12 +02:00
parent c0dbdc35a0
commit b66048aad2
16 changed files with 384 additions and 316 deletions

View File

@ -1,7 +1,7 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '@teact';
import { import {
memo, useEffect, useMemo, useRef, memo, useEffect, useMemo, useRef,
} from '../../lib/teact/teact'; } from '@teact';
import { getGlobal, withGlobal } from '../../global'; import { getGlobal, withGlobal } from '../../global';
import type { import type {
@ -48,6 +48,7 @@ import { useStickerPickerObservers } from './hooks/useStickerPickerObservers';
import StickerSetCover from '../middle/composer/StickerSetCover'; import StickerSetCover from '../middle/composer/StickerSetCover';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import Transition from '../ui/Transition.tsx';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import StickerButton from './StickerButton'; import StickerButton from './StickerButton';
import StickerSet from './StickerSet'; import StickerSet from './StickerSet';
@ -397,19 +398,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
} }
const fullClassName = buildClassName('StickerPicker', styles.root, className); const fullClassName = buildClassName('StickerPicker', styles.root, className);
if (!shouldRenderContent) {
return (
<div className={fullClassName}>
{noPopulatedSets ? (
<div className={pickerStyles.pickerDisabled}>{oldLang('NoStickers')}</div>
) : (
<Loading />
)}
</div>
);
}
const headerClassName = buildClassName( const headerClassName = buildClassName(
pickerStyles.header, pickerStyles.header,
'no-scrollbar', 'no-scrollbar',
@ -423,62 +411,72 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
pickerStyles.hasHeader, pickerStyles.hasHeader,
); );
return ( const isLoading = !shouldRenderContent && !noPopulatedSets;
<div className={fullClassName}>
<div
ref={headerRef}
className={headerClassName}
>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
<div
ref={containerRef}
onScroll={handleContentScroll}
className={listClassName}
>
{allSets.map((stickerSet, i) => {
const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID
|| (stickerSet.id === RECENT_SYMBOL_SET_ID && (withDefaultTopicIcons || isStatusPicker));
const isChatEmojiSet = stickerSet.id === chatEmojiSetId;
return ( return (
<StickerSet <Transition className={fullClassName} name="fade" activeKey={isLoading ? 0 : 1} shouldCleanup>
key={stickerSet.id} {!shouldRenderContent && !noPopulatedSets ? (
stickerSet={stickerSet} <Loading />
loadAndPlay={Boolean(canAnimate && canLoadAndPlay)} ) : !shouldRenderContent && noPopulatedSets ? (
index={i} <div className={pickerStyles.pickerDisabled}>{oldLang('NoStickers')}</div>
idPrefix={prefix} ) : (
observeIntersection={observeIntersectionForSet} <>
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems} <div
observeIntersectionForShowingItems={observeIntersectionForShowingItems} ref={headerRef}
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1} className={headerClassName}
isSavedMessages={isSavedMessages} >
isStatusPicker={isStatusPicker} <div className="shared-canvas-container">
isReactionPicker={isReactionPicker} <canvas ref={sharedCanvasRef} className="shared-canvas" />
shouldHideHeader={shouldHideHeader} <canvas ref={sharedCanvasHqRef} className="shared-canvas" />
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID} {allSets.map(renderCover)}
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID} </div>
isChatEmojiSet={isChatEmojiSet} </div>
isCurrentUserPremium={isCurrentUserPremium} <div
selectedReactionIds={selectedReactionIds} ref={containerRef}
availableReactions={availableReactions} onScroll={handleContentScroll}
isTranslucent={isTranslucent} className={listClassName}
onReactionSelect={onReactionSelect} >
onReactionContext={onReactionContext} {allSets.map((stickerSet, i) => {
onStickerSelect={handleEmojiSelect} const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID
onContextMenuOpen={onContextMenuOpen} || (stickerSet.id === RECENT_SYMBOL_SET_ID && (withDefaultTopicIcons || isStatusPicker));
onContextMenuClose={onContextMenuClose} const isChatEmojiSet = stickerSet.id === chatEmojiSetId;
onContextMenuClick={onContextMenuClick}
forcePlayback return (
/> <StickerSet
); key={stickerSet.id}
})} stickerSet={stickerSet}
</div> loadAndPlay={Boolean(canAnimate && canLoadAndPlay)}
</div> index={i}
idPrefix={prefix}
observeIntersection={observeIntersectionForSet}
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
isSavedMessages={isSavedMessages}
isStatusPicker={isStatusPicker}
isReactionPicker={isReactionPicker}
shouldHideHeader={shouldHideHeader}
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
isChatEmojiSet={isChatEmojiSet}
isCurrentUserPremium={isCurrentUserPremium}
selectedReactionIds={selectedReactionIds}
availableReactions={availableReactions}
isTranslucent={isTranslucent}
onReactionSelect={onReactionSelect}
onReactionContext={onReactionContext}
onStickerSelect={handleEmojiSelect}
onContextMenuOpen={onContextMenuOpen}
onContextMenuClose={onContextMenuClose}
onContextMenuClick={onContextMenuClick}
forcePlayback
/>
);
})}
</div>
</>
)}
</Transition>
); );
}; };

View File

@ -23,6 +23,7 @@ import Audio from '../../common/Audio';
import NothingFound from '../../common/NothingFound'; import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
export type OwnProps = { export type OwnProps = {
isVoice?: boolean; isVoice?: boolean;
@ -126,7 +127,12 @@ const AudioResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading; const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return ( return (
<div className="LeftSearch--content"> <Transition
slideClassName="LeftSearch--content"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll <InfiniteScroll
className="search-content documents-list custom-scroll" className="search-content documents-list custom-scroll"
items={canRenderContents ? foundMessages : undefined} items={canRenderContents ? foundMessages : undefined}
@ -143,7 +149,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
)} )}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </Transition>
); );
}; };

View File

@ -20,6 +20,7 @@ import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Link from '../../ui/Link'; import Link from '../../ui/Link';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import LeftSearchResultChat from './LeftSearchResultChat'; import LeftSearchResultChat from './LeftSearchResultChat';
export type OwnProps = { export type OwnProps = {
@ -79,7 +80,13 @@ const BotAppResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading; const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return ( return (
<div ref={containerRef} className="LeftSearch--content"> <Transition
ref={containerRef}
slideClassName="LeftSearch--content"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll <InfiniteScroll
className="search-content custom-scroll" className="search-content custom-scroll"
items={canRenderContents ? filteredFoundIds : undefined} items={canRenderContents ? filteredFoundIds : undefined}
@ -134,7 +141,7 @@ const BotAppResults: FC<OwnProps & StateProps> = ({
</div> </div>
)} )}
</InfiniteScroll> </InfiniteScroll>
</div> </Transition>
); );
}; };

View File

@ -25,6 +25,7 @@ import Document from '../../common/Document';
import NothingFound from '../../common/NothingFound'; import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
export type OwnProps = { export type OwnProps = {
searchQuery?: string; searchQuery?: string;
@ -129,7 +130,13 @@ const FileResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading; const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return ( return (
<div ref={containerRef} className="LeftSearch--content"> <Transition
ref={containerRef}
slideClassName="LeftSearch--content"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll <InfiniteScroll
className="search-content documents-list custom-scroll" className="search-content documents-list custom-scroll"
items={canRenderContents ? foundMessages : undefined} items={canRenderContents ? foundMessages : undefined}
@ -146,7 +153,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
)} )}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </Transition>
); );
}; };

View File

@ -24,6 +24,7 @@ import NothingFound from '../../common/NothingFound';
import WebLink from '../../common/WebLink'; import WebLink from '../../common/WebLink';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
export type OwnProps = { export type OwnProps = {
searchQuery?: string; searchQuery?: string;
@ -122,7 +123,13 @@ const LinkResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading; const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return ( return (
<div ref={containerRef} className="LeftSearch--content"> <Transition
ref={containerRef}
slideClassName="LeftSearch--content"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll <InfiniteScroll
className="search-content documents-list custom-scroll" className="search-content documents-list custom-scroll"
items={canRenderContents ? foundMessages : undefined} items={canRenderContents ? foundMessages : undefined}
@ -139,7 +146,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
)} )}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </Transition>
); );
}; };

View File

@ -22,6 +22,7 @@ import Media from '../../common/Media';
import NothingFound from '../../common/NothingFound'; import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import ChatMessage from './ChatMessage'; import ChatMessage from './ChatMessage';
export type OwnProps = { export type OwnProps = {
@ -122,7 +123,13 @@ const MediaResults: FC<OwnProps & StateProps> = ({
); );
return ( return (
<div ref={containerRef} className="LeftSearch--content LeftSearch--media"> <Transition
ref={containerRef}
slideClassName="LeftSearch--content LeftSearch--media"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll <InfiniteScroll
className={classNames} className={classNames}
items={canRenderContents ? foundMessages : undefined} items={canRenderContents ? foundMessages : undefined}
@ -141,7 +148,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
{isMediaGrid && renderGallery()} {isMediaGrid && renderGallery()}
{isMessageList && renderSearchResult()} {isMessageList && renderSearchResult()}
</InfiniteScroll> </InfiniteScroll>
</div> </Transition>
); );
}; };

View File

@ -1,16 +1,9 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '@teact';
import { import { beginHeavyAnimation, memo, useEffect, useMemo, useRef } from '@teact';
beginHeavyAnimation, memo, useEffect, useMemo, useRef, import { addExtraClass, removeExtraClass } from '@teact/teact-dom.ts';
} from '../../lib/teact/teact';
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type { ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic } from '../../api/types';
ApiChatFullInfo,
ApiMessage,
ApiRestrictionReason,
ApiTopic,
} from '../../api/types';
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { LoadMoreDirection, type MessageListType, type ThreadId } from '../../types'; import { LoadMoreDirection, type MessageListType, type ThreadId } from '../../types';
@ -83,6 +76,7 @@ import useContainerHeight from './hooks/useContainerHeight';
import useStickyDates from './hooks/useStickyDates'; import useStickyDates from './hooks/useStickyDates';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import Transition from '../ui/Transition.tsx';
import ContactGreeting from './ContactGreeting'; import ContactGreeting from './ContactGreeting';
import MessageListAccountInfo from './MessageListAccountInfo'; import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListContent from './MessageListContent'; import MessageListContent from './MessageListContent';
@ -150,6 +144,17 @@ type StateProps = {
shouldAutoTranslate?: boolean; shouldAutoTranslate?: boolean;
}; };
enum Content {
Loading,
Restricted,
StarsRequired,
PremiumRequired,
AccountInfo,
ContactGreeting,
NoMessages,
MessageList,
}
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000; const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
@ -711,73 +716,98 @@ const MessageList: FC<OwnProps & StateProps> = ({
onScrollDownToggle(false); onScrollDownToggle(false);
}, [hasMessages, onScrollDownToggle]); }, [hasMessages, onScrollDownToggle]);
const activeKey = isRestricted ? (
Content.Restricted
) : paidMessagesStars && !hasMessages && !hasCustomGreeting ? (
Content.StarsRequired
) : isContactRequirePremium && !hasMessages ? (
Content.PremiumRequired
) : (isBot || isNonContact) && !hasMessages ? (
Content.AccountInfo
) : shouldRenderGreeting ? (
Content.ContactGreeting
) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? (
Content.NoMessages
) : hasMessages ? (
Content.MessageList
) : (
Content.Loading
);
function renderContent() {
return activeKey === Content.Restricted ? (
<div className="empty">
<span>
{restrictionReasons?.[0]?.text || `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
</span>
</div>
) : activeKey === Content.StarsRequired ? (
<RequirementToContactMessage paidMessagesStars={paidMessagesStars} peerId={monoforumChannelId || chatId} />
) : activeKey === Content.PremiumRequired ? (
<RequirementToContactMessage peerId={chatId} />
) : activeKey === Content.AccountInfo ? (
<MessageListAccountInfo chatId={chatId} hasMessages={hasMessages} />
) : activeKey === Content.ContactGreeting ? (
<ContactGreeting key={chatId} userId={chatId} />
) : activeKey === Content.NoMessages ? (
<NoMessages
chatId={chatId}
topic={topic}
type={type}
isChatWithSelf={isChatWithSelf}
isGroupChatJustCreated={isGroupChatJustCreated}
/>
) : activeKey === Content.MessageList ? (
<MessageListContent
canShowAds={areAdsEnabled && isChannelChat}
chatId={chatId}
isComments={isComments}
isChannelChat={isChannelChat}
isChatMonoforum={isChatMonoforum}
isSavedDialog={isSavedDialog}
messageIds={messageIds || [lastMessage!.id]}
messageGroups={messageGroups || groupMessages([lastMessage!])}
getContainerHeight={getContainerHeight}
isViewportNewest={Boolean(isViewportNewest)}
isUnread={Boolean(firstUnreadId)}
isEmptyThread={isEmptyThread}
withUsers={withUsers}
noAvatars={noAvatars}
containerRef={containerRef}
anchorIdRef={anchorIdRef}
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
threadId={threadId}
type={type}
isReady={isReady}
hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderAccountInfo={isBot || isNonContact}
nameChangeDate={nameChangeDate}
photoChangeDate={photoChangeDate}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
onIntersectPinnedMessage={onIntersectPinnedMessage}
canPost={canPost}
/>
) : (
<Loading color="white" backgroundColor="dark" />
);
}
return ( return (
<div <Transition
ref={containerRef} ref={containerRef}
className={className} className={className}
name="fade"
activeKey={activeKey}
shouldCleanup
onScroll={handleScroll} onScroll={handleScroll}
onMouseDown={preventMessageInputBlur} onMouseDown={preventMessageInputBlur}
> >
{isRestricted ? ( {renderContent()}
<div className="empty"> </Transition>
<span>
{restrictionReasons?.[0]?.text || `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
</span>
</div>
) : paidMessagesStars && !hasMessages && !hasCustomGreeting ? (
<RequirementToContactMessage paidMessagesStars={paidMessagesStars} peerId={monoforumChannelId || chatId} />
) : isContactRequirePremium && !hasMessages ? (
<RequirementToContactMessage peerId={chatId} />
) : (isBot || isNonContact) && !hasMessages ? (
<MessageListAccountInfo chatId={chatId} hasMessages={hasMessages} />
) : shouldRenderGreeting ? (
<ContactGreeting key={chatId} userId={chatId} />
) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? (
<NoMessages
chatId={chatId}
topic={topic}
type={type}
isChatWithSelf={isChatWithSelf}
isGroupChatJustCreated={isGroupChatJustCreated}
/>
) : hasMessages ? (
<MessageListContent
canShowAds={areAdsEnabled && isChannelChat}
chatId={chatId}
isComments={isComments}
isChannelChat={isChannelChat}
isChatMonoforum={isChatMonoforum}
isSavedDialog={isSavedDialog}
messageIds={messageIds || [lastMessage!.id]}
messageGroups={messageGroups || groupMessages([lastMessage!])}
getContainerHeight={getContainerHeight}
isViewportNewest={Boolean(isViewportNewest)}
isUnread={Boolean(firstUnreadId)}
isEmptyThread={isEmptyThread}
withUsers={withUsers}
noAvatars={noAvatars}
containerRef={containerRef}
anchorIdRef={anchorIdRef}
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
threadId={threadId}
type={type}
isReady={isReady}
hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderAccountInfo={isBot || isNonContact}
nameChangeDate={nameChangeDate}
photoChangeDate={photoChangeDate}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
onIntersectPinnedMessage={onIntersectPinnedMessage}
canPost={canPost}
/>
) : (
<Loading color="white" backgroundColor="dark" />
)}
</div>
); );
}; };

View File

@ -1,17 +1,10 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { import { memo, useEffect, useMemo, useRef, useState } from '../../../lib/teact/teact';
memo, useEffect, useMemo,
useRef, useState,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { IconName } from '../../../types/icons'; import type { IconName } from '../../../types/icons';
import type { import type { EmojiData, EmojiModule, EmojiRawData } from '../../../util/emoji/emoji';
EmojiData,
EmojiModule,
EmojiRawData,
} from '../../../util/emoji/emoji';
import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config'; import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config';
import animateHorizontalScroll from '../../../util/animateHorizontalScroll'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
@ -34,6 +27,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import './EmojiPicker.scss'; import './EmojiPicker.scss';
@ -206,46 +200,43 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
} }
const containerClassName = buildClassName('EmojiPicker', className); const containerClassName = buildClassName('EmojiPicker', className);
if (!shouldRenderContent) {
return (
<div className={containerClassName}>
<Loading />
</div>
);
}
const headerClassName = buildClassName( const headerClassName = buildClassName(
'EmojiPicker-header', 'EmojiPicker-header',
!shouldHideTopBorder && 'with-top-border', !shouldHideTopBorder && 'with-top-border',
); );
return ( return (
<div className={containerClassName}> <Transition className={containerClassName} activeKey={shouldRenderContent ? 1 : 0} name="fade" shouldCleanup>
<div {!shouldRenderContent ? (
ref={headerRef} <Loading />
className={headerClassName} ) : (
dir={lang.isRtl ? 'rtl' : undefined} <>
> <div
{allCategories.map(renderCategoryButton)} ref={headerRef}
</div> className={headerClassName}
<div dir={lang.isRtl ? 'rtl' : undefined}
ref={containerRef} >
onScroll={handleContentScroll} {allCategories.map(renderCategoryButton)}
className={buildClassName('EmojiPicker-main', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')} </div>
> <div
{allCategories.map((category, i) => ( ref={containerRef}
<EmojiCategory onScroll={handleContentScroll}
category={category} className={buildClassName('EmojiPicker-main', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
index={i} >
allEmojis={emojis} {allCategories.map((category, i) => (
observeIntersection={observeIntersection} <EmojiCategory
shouldRender={activeCategoryIndex >= i - 1 && activeCategoryIndex <= i + 1} category={category}
onEmojiSelect={handleEmojiSelect} index={i}
/> allEmojis={emojis}
))} observeIntersection={observeIntersection}
</div> shouldRender={activeCategoryIndex >= i - 1 && activeCategoryIndex <= i + 1}
</div> onEmojiSelect={handleEmojiSelect}
/>
))}
</div>
</>
)}
</Transition>
); );
}; };

View File

@ -3,11 +3,6 @@
top: 0.1875rem; top: 0.1875rem;
overflow-y: auto; overflow-y: auto;
display: grid;
grid-auto-flow: dense;
grid-auto-rows: 6.25rem;
grid-gap: 0.125rem;
grid-template-columns: repeat(6, 1fr);
height: calc(100% - 0.1875rem); height: calc(100% - 0.1875rem);
margin: 0 0.1875rem; margin: 0 0.1875rem;
@ -30,3 +25,11 @@
border-radius: var(--border-radius-default) !important; border-radius: var(--border-radius-default) !important;
} }
} }
.GifPickerGrid {
display: grid;
grid-auto-flow: dense;
grid-auto-rows: 6.25rem;
grid-gap: 0.125rem;
grid-template-columns: repeat(6, 1fr);
}

View File

@ -1,7 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { import { memo, useEffect, useRef } from '../../../lib/teact/teact';
memo, useEffect, useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiVideo } from '../../../api/types'; import type { ApiVideo } from '../../../api/types';
@ -17,6 +15,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import GifButton from '../../common/GifButton'; import GifButton from '../../common/GifButton';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import './GifPicker.scss'; import './GifPicker.scss';
@ -61,34 +60,37 @@ const GifPicker: FC<OwnProps & StateProps> = ({
}); });
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION); const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
const isLoading = canSendGifs && (!canRenderContents || !savedGifs);
return ( return (
<div> <Transition
<div ref={containerRef}
ref={containerRef} className={buildClassName('GifPicker', className, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
className={buildClassName('GifPicker', className, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')} slideClassName="GifPickerGrid"
> activeKey={isLoading ? 0 : 1}
{!canSendGifs ? ( name="fade"
<div className="picker-disabled">Sending GIFs is not allowed in this chat.</div> shouldCleanup
) : canRenderContents && savedGifs && savedGifs.length ? ( >
savedGifs.map((gif) => ( {!canSendGifs ? (
<GifButton <div className="picker-disabled">Sending GIFs is not allowed in this chat.</div>
key={gif.id} ) : canRenderContents && savedGifs && savedGifs.length ? (
gif={gif} savedGifs.map((gif) => (
observeIntersection={observeIntersection} <GifButton
isDisabled={!loadAndPlay} key={gif.id}
onClick={canSendGifs ? onGifSelect : undefined} gif={gif}
onUnsaveClick={handleUnsaveClick} observeIntersection={observeIntersection}
isSavedMessages={isSavedMessages} isDisabled={!loadAndPlay}
/> onClick={canSendGifs ? onGifSelect : undefined}
)) onUnsaveClick={handleUnsaveClick}
) : canRenderContents && savedGifs ? ( isSavedMessages={isSavedMessages}
<div className="picker-disabled">No saved GIFs.</div> />
) : ( ))
<Loading /> ) : canRenderContents && savedGifs ? (
)} <div className="picker-disabled">No saved GIFs.</div>
</div> ) : (
</div> <Loading color="yellow" />
)}
</Transition>
); );
}; };

View File

@ -1,8 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { import { memo, useEffect, useMemo, useRef } from '../../../lib/teact/teact';
memo, useEffect, useMemo,
useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types'; import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types';
@ -19,7 +16,11 @@ import {
STICKER_SIZE_PICKER_HEADER, STICKER_SIZE_PICKER_HEADER,
} from '../../../config'; } from '../../../config';
import { import {
selectChat, selectChatFullInfo, selectIsChatWithSelf, selectIsCurrentUserPremium, selectShouldLoopStickers, selectChat,
selectChatFullInfo,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectShouldLoopStickers,
} from '../../../global/selectors'; } from '../../../global/selectors';
import animateHorizontalScroll from '../../../util/animateHorizontalScroll'; import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
@ -43,6 +44,7 @@ import StickerButton from '../../common/StickerButton';
import StickerSet from '../../common/StickerSet'; import StickerSet from '../../common/StickerSet';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import StickerSetCover from './StickerSetCover'; import StickerSetCover from './StickerSetCover';
import styles from './StickerPicker.module.scss'; import styles from './StickerPicker.module.scss';
@ -329,76 +331,75 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
} }
const fullClassName = buildClassName(styles.root, className); const fullClassName = buildClassName(styles.root, className);
if (!shouldRenderContents) {
return (
<div className={fullClassName}>
{!canSendStickers && !isForEffects ? (
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
) : noPopulatedSets ? (
<div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
) : (
<Loading />
)}
</div>
);
}
const headerClassName = buildClassName( const headerClassName = buildClassName(
styles.header, styles.header,
'no-scrollbar', 'no-scrollbar',
!shouldHideTopBorder && styles.headerWithBorder, !shouldHideTopBorder && styles.headerWithBorder,
); );
const isLoading = !shouldRenderContents && (canSendStickers || isForEffects) && !noPopulatedSets;
return ( return (
<div className={fullClassName}> <Transition className={fullClassName} activeKey={isLoading ? 0 : 1} name="fade" shouldCleanup>
{!isForEffects && ( {!shouldRenderContents ? (
<div ref={headerRef} className={headerClassName}> !canSendStickers && !isForEffects ? (
<div className="shared-canvas-container"> <div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
<canvas ref={sharedCanvasRef} className="shared-canvas" /> ) : noPopulatedSets ? (
{allSets.map(renderCover)} <div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
) : (
<Loading />
)
) : (
<>
{!isForEffects && (
<div ref={headerRef} className={headerClassName}>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
)}
<div
ref={containerRef}
onMouseMove={handleMouseMove}
onScroll={handleContentScroll}
className={
buildClassName(
styles.main,
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
!isForEffects && styles.hasHeader,
)
}
>
{allSets.map((stickerSet, i) => (
<StickerSet
key={stickerSet.id}
stickerSet={stickerSet}
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
noContextMenus={noContextMenus}
index={i}
idPrefix={prefix}
observeIntersection={observeIntersectionForSet}
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
favoriteStickers={favoriteStickers}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
isTranslucent={isTranslucent}
isChatStickerSet={stickerSet.id === chatStickerSetId}
onStickerSelect={handleStickerSelect}
onStickerUnfave={handleStickerUnfave}
onStickerFave={handleStickerFave}
onStickerRemoveRecent={handleRemoveRecentSticker}
forcePlayback
shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID}
/>
))}
</div> </div>
</div> </>
)} )}
<div </Transition>
ref={containerRef}
onMouseMove={handleMouseMove}
onScroll={handleContentScroll}
className={
buildClassName(
styles.main,
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
!isForEffects && styles.hasHeader,
)
}
>
{allSets.map((stickerSet, i) => (
<StickerSet
key={stickerSet.id}
stickerSet={stickerSet}
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
noContextMenus={noContextMenus}
index={i}
idPrefix={prefix}
observeIntersection={observeIntersectionForSet}
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
favoriteStickers={favoriteStickers}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
isTranslucent={isTranslucent}
isChatStickerSet={stickerSet.id === chatStickerSetId}
onStickerSelect={handleStickerSelect}
onStickerUnfave={handleStickerUnfave}
onStickerFave={handleStickerFave}
onStickerRemoveRecent={handleRemoveRecentSticker}
forcePlayback
shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID}
/>
))}
</div>
</div>
); );
}; };

View File

@ -331,7 +331,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV} noCloseOnBackdrop={!IS_TOUCH_ENV}
noCompact noCompact
{...(isAttachmentModal ? menuPositionOptions : { {...(isAttachmentModal ? menuPositionOptions : {
positionX: 'left', positionX: 'left',
positionY: 'bottom', positionY: 'bottom',

View File

@ -1,8 +1,5 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '@teact';
import { import { memo, useCallback, useEffect, useMemo, useRef, useState } from '@teact';
memo, useCallback,
useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
@ -16,20 +13,12 @@ import type {
ApiUserStatus, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import type { import type { AnimationLevel, ProfileState, ProfileTabType, SharedMediaType, ThemeKey, ThreadId } from '../../types';
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, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
import { import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION } from '../../config';
MEMBERS_SLICE,
PROFILE_SENSITIVE_AREA,
SHARED_MEDIA_SLICE,
SLIDE_TRANSITION_DURATION,
} from '../../config';
import { import {
getHasAdminRight, getHasAdminRight,
getIsDownloading, getIsDownloading,
@ -592,8 +581,19 @@ const Profile: FC<OwnProps & StateProps> = ({
); );
} }
if ((!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById) { const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById;
const noSpinner = isFirstTab && !canRenderContent; const noSpinner = isFirstTab && !canRenderContent;
const isSpinner = noContent && !noSpinner;
return (
<Transition activeKey={isSpinner ? 0 : 1} name="fade">
{renderSpinnerOrContent(noContent, noSpinner)}
</Transition>
);
}
function renderSpinnerOrContent(noContent: boolean, noSpinner: boolean) {
if (noContent) {
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
return ( return (
@ -651,6 +651,11 @@ const Profile: FC<OwnProps & StateProps> = ({
); );
} }
if (!messagesById) {
// A TypeScript assertion, should never be really reached
return;
}
return ( return (
<div <div
className={`content ${resultType}-list`} className={`content ${resultType}-list`}

View File

@ -57,7 +57,6 @@ const ResponsiveHoverButton: FC<OwnProps> = ({ onActivate, ...buttonProps }) =>
return ( return (
<Button <Button
{...buttonProps} {...buttonProps}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}

View File

@ -241,12 +241,12 @@
&-fadeBackwards { &-fadeBackwards {
> .Transition_slide-from { > .Transition_slide-from {
opacity: 1; opacity: 1;
animation: fade-out-opacity 0.15s ease; animation: fade-out-opacity 0.2s ease;
} }
> .Transition_slide-to { > .Transition_slide-to {
opacity: 0; opacity: 0;
animation: fade-in-opacity 0.15s ease; animation: fade-in-opacity 0.2s ease;
} }
} }

View File

@ -49,6 +49,8 @@ export type TransitionProps = {
isBlockingAnimation?: boolean; isBlockingAnimation?: boolean;
onStart?: NoneToVoidFunction; onStart?: NoneToVoidFunction;
onStop?: NoneToVoidFunction; onStop?: NoneToVoidFunction;
onScroll?: NoneToVoidFunction;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
children: React.ReactNode | ChildrenFn; children: React.ReactNode | ChildrenFn;
}; };
@ -89,6 +91,8 @@ function Transition({
isBlockingAnimation, isBlockingAnimation,
onStart, onStart,
onStop, onStop,
onScroll,
onMouseDown,
children, children,
}: TransitionProps) { }: TransitionProps) {
const currentKeyRef = useRef<number>(); const currentKeyRef = useRef<number>();
@ -379,6 +383,8 @@ function Transition({
id={id} id={id}
className={buildClassName('Transition', className)} className={buildClassName('Transition', className)}
teactFastList={asFastList} teactFastList={asFastList}
onScroll={onScroll}
onMouseDown={onMouseDown}
> >
{contents} {contents}
</div> </div>