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 {
memo, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
} from '@teact';
import { getGlobal, withGlobal } from '../../global';
import type {
@ -48,6 +48,7 @@ import { useStickerPickerObservers } from './hooks/useStickerPickerObservers';
import StickerSetCover from '../middle/composer/StickerSetCover';
import Button from '../ui/Button';
import Loading from '../ui/Loading';
import Transition from '../ui/Transition.tsx';
import Icon from './icons/Icon';
import StickerButton from './StickerButton';
import StickerSet from './StickerSet';
@ -397,19 +398,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
}
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(
pickerStyles.header,
'no-scrollbar',
@ -423,62 +411,72 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
pickerStyles.hasHeader,
);
return (
<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;
const isLoading = !shouldRenderContent && !noPopulatedSets;
return (
<StickerSet
key={stickerSet.id}
stickerSet={stickerSet}
loadAndPlay={Boolean(canAnimate && canLoadAndPlay)}
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>
</div>
return (
<Transition className={fullClassName} name="fade" activeKey={isLoading ? 0 : 1} shouldCleanup>
{!shouldRenderContent && !noPopulatedSets ? (
<Loading />
) : !shouldRenderContent && noPopulatedSets ? (
<div className={pickerStyles.pickerDisabled}>{oldLang('NoStickers')}</div>
) : (
<>
<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 (
<StickerSet
key={stickerSet.id}
stickerSet={stickerSet}
loadAndPlay={Boolean(canAnimate && canLoadAndPlay)}
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 InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
export type OwnProps = {
isVoice?: boolean;
@ -126,7 +127,12 @@ const AudioResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return (
<div className="LeftSearch--content">
<Transition
slideClassName="LeftSearch--content"
name="fade"
activeKey={canRenderContents ? 1 : 0}
shouldCleanup
>
<InfiniteScroll
className="search-content documents-list custom-scroll"
items={canRenderContents ? foundMessages : undefined}
@ -143,7 +149,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
)}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll>
</div>
</Transition>
);
};

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import Media from '../../common/Media';
import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import ChatMessage from './ChatMessage';
export type OwnProps = {
@ -122,7 +123,13 @@ const MediaResults: FC<OwnProps & StateProps> = ({
);
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
className={classNames}
items={canRenderContents ? foundMessages : undefined}
@ -141,7 +148,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
{isMediaGrid && renderGallery()}
{isMessageList && renderSearchResult()}
</InfiniteScroll>
</div>
</Transition>
);
};

View File

@ -1,16 +1,9 @@
import type { FC } from '../../lib/teact/teact';
import {
beginHeavyAnimation, memo, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
import type { FC } from '@teact';
import { beginHeavyAnimation, memo, useEffect, useMemo, useRef } from '@teact';
import { addExtraClass, removeExtraClass } from '@teact/teact-dom.ts';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiChatFullInfo,
ApiMessage,
ApiRestrictionReason,
ApiTopic,
} from '../../api/types';
import type { ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic } from '../../api/types';
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types';
import { LoadMoreDirection, type MessageListType, type ThreadId } from '../../types';
@ -83,6 +76,7 @@ import useContainerHeight from './hooks/useContainerHeight';
import useStickyDates from './hooks/useStickyDates';
import Loading from '../ui/Loading';
import Transition from '../ui/Transition.tsx';
import ContactGreeting from './ContactGreeting';
import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListContent from './MessageListContent';
@ -150,6 +144,17 @@ type StateProps = {
shouldAutoTranslate?: boolean;
};
enum Content {
Loading,
Restricted,
StarsRequired,
PremiumRequired,
AccountInfo,
ContactGreeting,
NoMessages,
MessageList,
}
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
@ -711,73 +716,98 @@ const MessageList: FC<OwnProps & StateProps> = ({
onScrollDownToggle(false);
}, [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 (
<div
<Transition
ref={containerRef}
className={className}
name="fade"
activeKey={activeKey}
shouldCleanup
onScroll={handleScroll}
onMouseDown={preventMessageInputBlur}
>
{isRestricted ? (
<div className="empty">
<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>
{renderContent()}
</Transition>
);
};

View File

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

View File

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

View File

@ -1,8 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import {
memo, useEffect, useMemo,
useRef,
} from '../../../lib/teact/teact';
import { memo, useEffect, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types';
@ -19,7 +16,11 @@ import {
STICKER_SIZE_PICKER_HEADER,
} from '../../../config';
import {
selectChat, selectChatFullInfo, selectIsChatWithSelf, selectIsCurrentUserPremium, selectShouldLoopStickers,
selectChat,
selectChatFullInfo,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectShouldLoopStickers,
} from '../../../global/selectors';
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
@ -43,6 +44,7 @@ import StickerButton from '../../common/StickerButton';
import StickerSet from '../../common/StickerSet';
import Button from '../../ui/Button';
import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition.tsx';
import StickerSetCover from './StickerSetCover';
import styles from './StickerPicker.module.scss';
@ -329,76 +331,75 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
}
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(
styles.header,
'no-scrollbar',
!shouldHideTopBorder && styles.headerWithBorder,
);
const isLoading = !shouldRenderContents && (canSendStickers || isForEffects) && !noPopulatedSets;
return (
<div className={fullClassName}>
{!isForEffects && (
<div ref={headerRef} className={headerClassName}>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
<Transition className={fullClassName} activeKey={isLoading ? 0 : 1} name="fade" shouldCleanup>
{!shouldRenderContents ? (
!canSendStickers && !isForEffects ? (
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
) : noPopulatedSets ? (
<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
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>
</Transition>
);
};

View File

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

View File

@ -1,8 +1,5 @@
import type { FC } from '../../lib/teact/teact';
import {
memo, useCallback,
useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact';
import type { FC } from '@teact';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from '@teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
@ -16,20 +13,12 @@ import type {
ApiUserStatus,
} from '../../api/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 { MAIN_THREAD_ID } from '../../api/types';
import { AudioOrigin, 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 {
getHasAdminRight,
getIsDownloading,
@ -592,8 +581,19 @@ const Profile: FC<OwnProps & StateProps> = ({
);
}
if ((!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById) {
const noSpinner = isFirstTab && !canRenderContent;
const noContent = (!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById;
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);
return (
@ -651,6 +651,11 @@ const Profile: FC<OwnProps & StateProps> = ({
);
}
if (!messagesById) {
// A TypeScript assertion, should never be really reached
return;
}
return (
<div
className={`content ${resultType}-list`}

View File

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

View File

@ -241,12 +241,12 @@
&-fadeBackwards {
> .Transition_slide-from {
opacity: 1;
animation: fade-out-opacity 0.15s ease;
animation: fade-out-opacity 0.2s ease;
}
> .Transition_slide-to {
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;
onStart?: NoneToVoidFunction;
onStop?: NoneToVoidFunction;
onScroll?: NoneToVoidFunction;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
children: React.ReactNode | ChildrenFn;
};
@ -89,6 +91,8 @@ function Transition({
isBlockingAnimation,
onStart,
onStop,
onScroll,
onMouseDown,
children,
}: TransitionProps) {
const currentKeyRef = useRef<number>();
@ -379,6 +383,8 @@ function Transition({
id={id}
className={buildClassName('Transition', className)}
teactFastList={asFastList}
onScroll={onScroll}
onMouseDown={onMouseDown}
>
{contents}
</div>