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 { 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<OwnProps & StateProps> = ({
message,
observeIntersection,
isEmbedded,
appearanceOrder = 0,
sender,
targetUser,
targetMessage,
@ -59,6 +67,17 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
'ActionMessage message-list-item',
isFocused && !noFocusHighlight && 'focused',
isContextMenuShown && 'has-menu-open',
transitionClassNames,
)}
data-message-id={message.id}
onMouseDown={handleBeforeContextMenu}

View File

@ -153,6 +153,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
const memoFirstUnreadIdRef = useRef<number>();
const memoFocusingIdRef = useRef<number>();
const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(!messageIds);
const [containerHeight, setContainerHeight] = useState<number | undefined>();
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.
// 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<OwnProps & StateProps & DispatchProps> = ({
{messageGroups && renderMessages(
lang,
messageGroups,
shouldAnimateAppearanceRef.current ? messageIds.length : 0,
observeIntersectionForReading,
observeIntersectionForMedia,
observeIntersectionForAnimatedStickers,
@ -537,6 +545,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
{renderMessages(
lang,
groupMessages([lastMessage]),
0,
observeIntersectionForReading,
observeIntersectionForMedia,
observeIntersectionForAnimatedStickers,
@ -561,6 +570,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
function renderMessages(
lang: LangFn,
messageGroups: MessageDateGroup[],
messageCountToAnimate: number,
observeIntersectionForReading: ObserveFn,
observeIntersectionForMedia: ObserveFn,
observeIntersectionForAnimatedStickers: ObserveFn,
@ -580,6 +590,8 @@ function renderMessages(
</div>
);
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}

View File

@ -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,

View File

@ -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 = '<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
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> = ({
message,
@ -163,6 +168,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
withAvatar,
withSenderName,
noComments,
appearanceOrder,
isFirstInGroup,
isLastInGroup,
isFirstInDocumentGroup,
@ -226,6 +232,17 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
isInSelectMode && 'is-in-selection-mode',
isThreadTop && 'is-thread-top',
Boolean(message.inlineButtons) && 'has-inline-buttons',
transitionClassNames,
);
const contentClassName = buildContentClassName(message, {
hasReply,