diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index e91264339..ad84aaeb9 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -1,4 +1,6 @@ -import React, { FC, memo, useRef } from '../../lib/teact/teact'; +import React, { + FC, memo, useEffect, useRef, +} from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; import { ApiUser, ApiMessage, ApiChat } from '../../api/types'; @@ -21,11 +23,14 @@ import useFocusMessage from './message/hooks/useFocusMessage'; import useLang from '../../hooks/useLang'; import ContextMenuContainer from './message/ContextMenuContainer.async'; +import useFlag from '../../hooks/useFlag'; +import useShowTransition from '../../hooks/useShowTransition'; type OwnProps = { message: ApiMessage; observeIntersection?: ObserveFn; isEmbedded?: boolean; + appearanceOrder?: number; }; type StateProps = { @@ -38,10 +43,13 @@ type StateProps = { noFocusHighlight?: boolean; }; +const APPEARANCE_DELAY = 10; + const ActionMessage: FC = ({ message, observeIntersection, isEmbedded, + appearanceOrder = 0, sender, targetUser, targetMessage, @@ -59,6 +67,17 @@ const ActionMessage: FC = ({ useLang(); + const noAppearanceAnimation = appearanceOrder <= 0; + const [isShown, markShown] = useFlag(noAppearanceAnimation); + useEffect(() => { + if (noAppearanceAnimation) { + return; + } + + setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY); + }, [appearanceOrder, markShown, noAppearanceAnimation]); + const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); + const content = renderActionMessageText( message, sender, @@ -86,6 +105,7 @@ const ActionMessage: FC = ({ 'ActionMessage message-list-item', isFocused && !noFocusHighlight && 'focused', isContextMenuShown && 'has-menu-open', + transitionClassNames, )} data-message-id={message.id} onMouseDown={handleBeforeContextMenu} diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 816f197b7..d0c4edeab 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -153,6 +153,7 @@ const MessageList: FC = ({ const memoFirstUnreadIdRef = useRef(); const memoFocusingIdRef = useRef(); const isScrollTopJustUpdatedRef = useRef(false); + const shouldAnimateAppearanceRef = useRef(!messageIds); const [containerHeight, setContainerHeight] = useState(); const [hasFocusing, setHasFocusing] = useState(Boolean(focusingId)); @@ -165,6 +166,12 @@ const MessageList: FC = ({ // We update local cached `scrollOffsetRef` when opening chat. // Then we update global version every second on scrolling. scrollOffsetRef.current = (type === 'thread' && selectScrollOffset(getGlobal(), chatId, threadId)) || 0; + + // We need it just first time when message list appears + // TODO Figure out why `onTickEnd`/100ms is not enough + setTimeout(() => { + shouldAnimateAppearanceRef.current = false; + }, 1000); }, [Boolean(messageIds)]); useOnChange(() => { @@ -516,6 +523,7 @@ const MessageList: FC = ({ {messageGroups && renderMessages( lang, messageGroups, + shouldAnimateAppearanceRef.current ? messageIds.length : 0, observeIntersectionForReading, observeIntersectionForMedia, observeIntersectionForAnimatedStickers, @@ -537,6 +545,7 @@ const MessageList: FC = ({ {renderMessages( lang, groupMessages([lastMessage]), + 0, observeIntersectionForReading, observeIntersectionForMedia, observeIntersectionForAnimatedStickers, @@ -561,6 +570,7 @@ const MessageList: FC = ({ function renderMessages( lang: LangFn, messageGroups: MessageDateGroup[], + messageCountToAnimate: number, observeIntersectionForReading: ObserveFn, observeIntersectionForMedia: ObserveFn, observeIntersectionForAnimatedStickers: ObserveFn, @@ -580,6 +590,8 @@ function renderMessages( ); + let appearanceIndex = 0; + const dateGroups = messageGroups.map(( dateGroup: MessageDateGroup, dateGroupIndex: number, @@ -599,6 +611,7 @@ function renderMessages( key={message.id} message={message} observeIntersection={observeIntersectionForReading} + appearanceOrder={messageCountToAnimate - ++appearanceIndex} />, ]); } @@ -660,6 +673,7 @@ function renderMessages( threadId={threadId} messageListType={type} noComments={hasLinkedChat === false} + appearanceOrder={messageCountToAnimate - ++appearanceIndex} isFirstInGroup={position.isFirstInGroup} isLastInGroup={position.isLastInGroup} isFirstInDocumentGroup={position.isFirstInDocumentGroup} diff --git a/src/components/middle/message/Message.scss b/src/components/middle/message/Message.scss index ef80285a6..c4c9798b7 100644 --- a/src/components/middle/message/Message.scss +++ b/src/components/middle/message/Message.scss @@ -19,6 +19,15 @@ --select-message-scale: 0.9; --select-background-color: white; + opacity: 1; + transform: scale(1); + transition: opacity .2s ease, transform .2s ease; + + &:not(.open) { + transform: scale(0.8); + opacity: 0; + } + > .Avatar, > .message-content-wrapper { opacity: 1; @@ -299,6 +308,7 @@ .message-select-control:not(.group-select) { background: var(--background-color); border-color: var(--background-color); + &::after { background: var(--background-color); border-color: rgba(var(--color-text-green-rgb), 0.5); @@ -309,6 +319,7 @@ } } + &.focused, &.has-menu-open, &.is-forwarding, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 90cb133af..a166573b9 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -3,6 +3,7 @@ import React, { FC, memo, useCallback, + useEffect, useLayoutEffect, useMemo, useRef, @@ -64,6 +65,8 @@ import { ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserve import useFocusMessage from './hooks/useFocusMessage'; import useWindowSize from '../../../hooks/useWindowSize'; import useLang from '../../../hooks/useLang'; +import useShowTransition from '../../../hooks/useShowTransition'; +import useFlag from '../../../hooks/useFlag'; import Button from '../../ui/Button'; import Avatar from '../../common/Avatar'; @@ -106,6 +109,7 @@ type OwnProps = { threadId: number; messageListType: MessageListType; noComments: boolean; + appearanceOrder: number; } & MessagePositionProperties; type StateProps = { @@ -153,6 +157,7 @@ const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover'; const APPENDIX_OWN = ''; // eslint-disable-next-line max-len const APPENDIX_NOT_OWN = ''; +const APPEARANCE_DELAY = 10; const Message: FC = ({ message, @@ -163,6 +168,7 @@ const Message: FC = ({ withAvatar, withSenderName, noComments, + appearanceOrder, isFirstInGroup, isLastInGroup, isFirstInDocumentGroup, @@ -226,6 +232,17 @@ const Message: FC = ({ handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(ref); + const noAppearanceAnimation = appearanceOrder <= 0; + const [isShown, markShown] = useFlag(noAppearanceAnimation); + useEffect(() => { + if (noAppearanceAnimation) { + return; + } + + setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY); + }, [appearanceOrder, markShown, noAppearanceAnimation]); + const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); + const { chatId, id: messageId, threadInfo } = message; const isOwn = isOwnMessage(message); @@ -264,6 +281,7 @@ const Message: FC = ({ isInSelectMode && 'is-in-selection-mode', isThreadTop && 'is-thread-top', Boolean(message.inlineButtons) && 'has-inline-buttons', + transitionClassNames, ); const contentClassName = buildContentClassName(message, { hasReply,