Message List: Appearance animation

This commit is contained in:
Alexander Zinchuk 2021-04-23 19:28:42 +03:00
parent 240e78de0b
commit 8c818080be
4 changed files with 64 additions and 1 deletions

View File

@ -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 { withGlobal } from '../../lib/teact/teactn';
import { ApiUser, ApiMessage, ApiChat } from '../../api/types'; import { ApiUser, ApiMessage, ApiChat } from '../../api/types';
@ -21,11 +23,14 @@ import useFocusMessage from './message/hooks/useFocusMessage';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import ContextMenuContainer from './message/ContextMenuContainer.async'; import ContextMenuContainer from './message/ContextMenuContainer.async';
import useFlag from '../../hooks/useFlag';
import useShowTransition from '../../hooks/useShowTransition';
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
isEmbedded?: boolean; isEmbedded?: boolean;
appearanceOrder?: number;
}; };
type StateProps = { type StateProps = {
@ -38,10 +43,13 @@ type StateProps = {
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
}; };
const APPEARANCE_DELAY = 10;
const ActionMessage: FC<OwnProps & StateProps> = ({ const ActionMessage: FC<OwnProps & StateProps> = ({
message, message,
observeIntersection, observeIntersection,
isEmbedded, isEmbedded,
appearanceOrder = 0,
sender, sender,
targetUser, targetUser,
targetMessage, targetMessage,
@ -59,6 +67,17 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
useLang(); 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( const content = renderActionMessageText(
message, message,
sender, sender,
@ -86,6 +105,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
'ActionMessage message-list-item', 'ActionMessage message-list-item',
isFocused && !noFocusHighlight && 'focused', isFocused && !noFocusHighlight && 'focused',
isContextMenuShown && 'has-menu-open', isContextMenuShown && 'has-menu-open',
transitionClassNames,
)} )}
data-message-id={message.id} data-message-id={message.id}
onMouseDown={handleBeforeContextMenu} onMouseDown={handleBeforeContextMenu}

View File

@ -153,6 +153,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
const memoFirstUnreadIdRef = useRef<number>(); const memoFirstUnreadIdRef = useRef<number>();
const memoFocusingIdRef = useRef<number>(); const memoFocusingIdRef = useRef<number>();
const isScrollTopJustUpdatedRef = useRef(false); const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(!messageIds);
const [containerHeight, setContainerHeight] = useState<number | undefined>(); const [containerHeight, setContainerHeight] = useState<number | undefined>();
const [hasFocusing, setHasFocusing] = useState<boolean>(Boolean(focusingId)); const [hasFocusing, setHasFocusing] = useState<boolean>(Boolean(focusingId));
@ -165,6 +166,12 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
// We update local cached `scrollOffsetRef` when opening chat. // We update local cached `scrollOffsetRef` when opening chat.
// Then we update global version every second on scrolling. // Then we update global version every second on scrolling.
scrollOffsetRef.current = (type === 'thread' && selectScrollOffset(getGlobal(), chatId, threadId)) || 0; 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)]); }, [Boolean(messageIds)]);
useOnChange(() => { useOnChange(() => {
@ -516,6 +523,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
{messageGroups && renderMessages( {messageGroups && renderMessages(
lang, lang,
messageGroups, messageGroups,
shouldAnimateAppearanceRef.current ? messageIds.length : 0,
observeIntersectionForReading, observeIntersectionForReading,
observeIntersectionForMedia, observeIntersectionForMedia,
observeIntersectionForAnimatedStickers, observeIntersectionForAnimatedStickers,
@ -537,6 +545,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
{renderMessages( {renderMessages(
lang, lang,
groupMessages([lastMessage]), groupMessages([lastMessage]),
0,
observeIntersectionForReading, observeIntersectionForReading,
observeIntersectionForMedia, observeIntersectionForMedia,
observeIntersectionForAnimatedStickers, observeIntersectionForAnimatedStickers,
@ -561,6 +570,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
function renderMessages( function renderMessages(
lang: LangFn, lang: LangFn,
messageGroups: MessageDateGroup[], messageGroups: MessageDateGroup[],
messageCountToAnimate: number,
observeIntersectionForReading: ObserveFn, observeIntersectionForReading: ObserveFn,
observeIntersectionForMedia: ObserveFn, observeIntersectionForMedia: ObserveFn,
observeIntersectionForAnimatedStickers: ObserveFn, observeIntersectionForAnimatedStickers: ObserveFn,
@ -580,6 +590,8 @@ function renderMessages(
</div> </div>
); );
let appearanceIndex = 0;
const dateGroups = messageGroups.map(( const dateGroups = messageGroups.map((
dateGroup: MessageDateGroup, dateGroup: MessageDateGroup,
dateGroupIndex: number, dateGroupIndex: number,
@ -599,6 +611,7 @@ function renderMessages(
key={message.id} key={message.id}
message={message} message={message}
observeIntersection={observeIntersectionForReading} observeIntersection={observeIntersectionForReading}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
/>, />,
]); ]);
} }
@ -660,6 +673,7 @@ function renderMessages(
threadId={threadId} threadId={threadId}
messageListType={type} messageListType={type}
noComments={hasLinkedChat === false} noComments={hasLinkedChat === false}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isFirstInGroup={position.isFirstInGroup} isFirstInGroup={position.isFirstInGroup}
isLastInGroup={position.isLastInGroup} isLastInGroup={position.isLastInGroup}
isFirstInDocumentGroup={position.isFirstInDocumentGroup} isFirstInDocumentGroup={position.isFirstInDocumentGroup}

View File

@ -19,6 +19,15 @@
--select-message-scale: 0.9; --select-message-scale: 0.9;
--select-background-color: white; --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, > .Avatar,
> .message-content-wrapper { > .message-content-wrapper {
opacity: 1; opacity: 1;
@ -299,6 +308,7 @@
.message-select-control:not(.group-select) { .message-select-control:not(.group-select) {
background: var(--background-color); background: var(--background-color);
border-color: var(--background-color); border-color: var(--background-color);
&::after { &::after {
background: var(--background-color); background: var(--background-color);
border-color: rgba(var(--color-text-green-rgb), 0.5); border-color: rgba(var(--color-text-green-rgb), 0.5);
@ -309,6 +319,7 @@
} }
} }
&.focused, &.focused,
&.has-menu-open, &.has-menu-open,
&.is-forwarding, &.is-forwarding,

View File

@ -3,6 +3,7 @@ import React, {
FC, FC,
memo, memo,
useCallback, useCallback,
useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
@ -64,6 +65,8 @@ import { ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserve
import useFocusMessage from './hooks/useFocusMessage'; import useFocusMessage from './hooks/useFocusMessage';
import useWindowSize from '../../../hooks/useWindowSize'; import useWindowSize from '../../../hooks/useWindowSize';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useShowTransition from '../../../hooks/useShowTransition';
import useFlag from '../../../hooks/useFlag';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -106,6 +109,7 @@ type OwnProps = {
threadId: number; threadId: number;
messageListType: MessageListType; messageListType: MessageListType;
noComments: boolean; noComments: boolean;
appearanceOrder: number;
} & MessagePositionProperties; } & MessagePositionProperties;
type StateProps = { type StateProps = {
@ -153,6 +157,7 @@ const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
const APPENDIX_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>'; const APPENDIX_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const APPENDIX_NOT_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>'; const APPENDIX_NOT_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>';
const APPEARANCE_DELAY = 10;
const Message: FC<OwnProps & StateProps & DispatchProps> = ({ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
message, message,
@ -163,6 +168,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
withAvatar, withAvatar,
withSenderName, withSenderName,
noComments, noComments,
appearanceOrder,
isFirstInGroup, isFirstInGroup,
isLastInGroup, isLastInGroup,
isFirstInDocumentGroup, isFirstInDocumentGroup,
@ -226,6 +232,17 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = 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 { chatId, id: messageId, threadInfo } = message;
const isOwn = isOwnMessage(message); const isOwn = isOwnMessage(message);
@ -264,6 +281,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
isInSelectMode && 'is-in-selection-mode', isInSelectMode && 'is-in-selection-mode',
isThreadTop && 'is-thread-top', isThreadTop && 'is-thread-top',
Boolean(message.inlineButtons) && 'has-inline-buttons', Boolean(message.inlineButtons) && 'has-inline-buttons',
transitionClassNames,
); );
const contentClassName = buildContentClassName(message, { const contentClassName = buildContentClassName(message, {
hasReply, hasReply,