Story ribbon animation (#3798)
This commit is contained in:
parent
b090b80863
commit
e8c4b8f713
@ -24,6 +24,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: linear-gradient(var(--color-white) -125%, var(--color-user));
|
||||
@ -168,32 +169,49 @@
|
||||
left: -0.25rem;
|
||||
top: -0.25rem;
|
||||
border-radius: 50%;
|
||||
padding: 0.125rem;
|
||||
|
||||
background: var(--color-borders-read-story);
|
||||
mask: linear-gradient(to bottom, #fff 0%, #fff 100%) content-box, linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||
mask-composite: exclude;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
left: -0.125rem;
|
||||
top: -0.125rem;
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
&.size-tiny {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
|
||||
&::before {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-medium {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
|
||||
&::before {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.online::after {
|
||||
|
||||
@ -244,6 +244,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
ref={ref}
|
||||
className={fullClassName}
|
||||
id={peer?.id && withStory ? getUserStoryHtmlId(peer.id) : undefined}
|
||||
data-peer-id={peer?.id}
|
||||
data-test-sender-id={IS_TEST ? peer?.id : undefined}
|
||||
aria-label={typeof content === 'string' ? author : undefined}
|
||||
onClick={handleClick}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.ArchivedChats {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--color-background);
|
||||
|
||||
.left-header {
|
||||
position: relative;
|
||||
|
||||
@ -6,7 +6,9 @@ import type { GlobalState } from '../../global/types';
|
||||
import type { FolderEditDispatch } from '../../hooks/reducers/useFoldersReducer';
|
||||
import type { LeftColumnContent, SettingsScreens } from '../../types';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { ANIMATION_DURATION } from '../story/helpers/ribbonAnimation';
|
||||
|
||||
import useForumPanelRender from '../../hooks/useForumPanelRender';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
@ -80,7 +82,7 @@ const ArchivedChats: FC<OwnProps> = ({
|
||||
shouldRender: shouldRenderStoryRibbon,
|
||||
transitionClassNames: storyRibbonClassNames,
|
||||
isClosing: isStoryRibbonClosing,
|
||||
} = useShowTransition(isStoryRibbonShown, undefined, undefined, '');
|
||||
} = useShowTransition(isStoryRibbonShown, undefined, undefined, '', false, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
|
||||
return (
|
||||
<div className="ArchivedChats">
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--color-background);
|
||||
|
||||
> .Transition {
|
||||
flex: 1;
|
||||
@ -47,6 +48,7 @@
|
||||
padding-left: 0.625rem;
|
||||
padding-right: 0.625rem;
|
||||
|
||||
/* stylelint-disable-next-line */
|
||||
> span {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiDimensions, ApiMessage } from '../../../api/types';
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getMessageHtmlId } from '../../../global/helpers';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
import {
|
||||
calculateDimensions,
|
||||
getMediaViewerAvailableDimensions,
|
||||
MEDIA_VIEWER_MEDIA_QUERY,
|
||||
REM,
|
||||
} from '../../common/helpers/mediaDimensions';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { getMessageHtmlId } from '../../../global/helpers';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { applyStyles } from '../../../util/animation';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
@ -283,10 +285,6 @@ function getTopOffset(hasFooter: boolean) {
|
||||
return topOffsetRem * REM;
|
||||
}
|
||||
|
||||
function applyStyles(element: HTMLElement, styles: Record<string, string>) {
|
||||
Object.assign(element.style, styles);
|
||||
}
|
||||
|
||||
function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
let containerSelector;
|
||||
let mediaSelector;
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
column-gap: 0.625rem;
|
||||
padding: 0.25rem 0.5rem 0.5rem 1rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
max-height: 5.5rem;
|
||||
position: relative;
|
||||
@ -37,6 +36,10 @@
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:global(.animating) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@ -44,6 +47,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
unicode-bidi: plaintext;
|
||||
white-space: nowrap;
|
||||
max-width: 110%;
|
||||
|
||||
&_hasUnreadStory {
|
||||
|
||||
@ -44,6 +44,7 @@ function StoryRibbon({
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id="StoryRibbon"
|
||||
className={fullClassName}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
|
||||
@ -95,6 +95,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
|
||||
<div
|
||||
ref={ref}
|
||||
role="button"
|
||||
data-peer-id={user.id}
|
||||
tabIndex={0}
|
||||
className={styles.user}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
/* stylelint-disable-next-line */
|
||||
@value name from "./StoryRibbon.module.scss";
|
||||
|
||||
.root {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.125rem;
|
||||
right: 0.25rem;
|
||||
transform: translateY(-50%);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@ -19,17 +22,48 @@
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 0.125rem solid var(--color-background);
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
z-index: -2;
|
||||
&:not(:first-child):before {
|
||||
mask-image: linear-gradient(90deg, #fff 75%, transparent 0);
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
&:global(.has-unread-story)::before {
|
||||
z-index: -1;
|
||||
&:global(.animating) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar + .avatar {
|
||||
margin-inline-end: -1.125rem;
|
||||
margin-inline-end: -0.875rem;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
position: absolute;
|
||||
transform-origin: top left;
|
||||
--transform-transition: transform 250ms ease;
|
||||
--opacity-transition: opacity 250ms ease;
|
||||
transition: var(--transform-transition), var(--opacity-transition);
|
||||
}
|
||||
|
||||
.ghostAnimateName {
|
||||
:global(.name) {
|
||||
transition: var(--opacity-transition);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ghostRevealName {
|
||||
:global(.name) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ghostLast:before {
|
||||
mask: none !important;
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiUser } from '../../api/types';
|
||||
|
||||
import { PREVIEW_AVATAR_COUNT } from '../../config';
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import { ANIMATION_END_DELAY, PREVIEW_AVATAR_COUNT } from '../../config';
|
||||
import { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { animateClosing, animateOpening, ANIMATION_DURATION } from './helpers/ribbonAnimation';
|
||||
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useStoryPreloader from './hooks/useStoryPreloader';
|
||||
@ -24,6 +26,7 @@ interface StateProps {
|
||||
currentUserId: string;
|
||||
orderedUserIds: string[];
|
||||
isShown: boolean;
|
||||
withAnimation?: boolean;
|
||||
usersById: Record<string, ApiUser>;
|
||||
}
|
||||
|
||||
@ -36,6 +39,7 @@ function StoryToggler({
|
||||
canShow,
|
||||
isShown,
|
||||
isArchived,
|
||||
withAnimation,
|
||||
}: OwnProps & StateProps) {
|
||||
const { toggleStoryRibbon } = getActions();
|
||||
|
||||
@ -58,8 +62,20 @@ function StoryToggler({
|
||||
}, [orderedUserIds]);
|
||||
useStoryPreloader(preloadUserIds);
|
||||
|
||||
const isVisible = canShow && isShown;
|
||||
// For some reason, setting 'slow' here also fixes scroll freezes on iOS when collapsing Story Ribbon
|
||||
const { shouldRender, transitionClassNames } = useShowTransition(canShow && isShown, undefined, undefined, 'slow');
|
||||
const { shouldRender, transitionClassNames } = useShowTransition(isVisible, undefined, undefined, 'slow');
|
||||
|
||||
useEffect(() => {
|
||||
if (!withAnimation) return;
|
||||
if (isVisible) {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateClosing(isArchived);
|
||||
} else {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateOpening(isArchived);
|
||||
}
|
||||
}, [isArchived, isVisible, withAnimation]);
|
||||
|
||||
if (!shouldRender) {
|
||||
return undefined;
|
||||
@ -68,6 +84,7 @@ function StoryToggler({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id="StoryToggler"
|
||||
className={buildClassName(styles.root, transitionClassNames)}
|
||||
aria-label={lang('Chat.Context.Peer.OpenStory')}
|
||||
onClick={() => toggleStoryRibbon({ isShown: true, isArchived })}
|
||||
@ -89,11 +106,13 @@ function StoryToggler({
|
||||
export default memo(withGlobal<OwnProps>((global, { isArchived }): StateProps => {
|
||||
const { orderedUserIds: { archived, active } } = global.stories;
|
||||
const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global);
|
||||
const withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations');
|
||||
|
||||
return {
|
||||
currentUserId: global.currentUserId!,
|
||||
orderedUserIds: isArchived ? archived : active,
|
||||
isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown,
|
||||
withAnimation,
|
||||
usersById: global.users.byId,
|
||||
};
|
||||
})(StoryToggler));
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
&:global(.opacity-transition) {
|
||||
transition: opacity 200ms;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
:global(.text-entity-link) {
|
||||
|
||||
@ -5,6 +5,7 @@ import { ANIMATION_END_DELAY } from '../../../config';
|
||||
import fastBlur from '../../../lib/fastBlur';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getUserStoryHtmlId } from '../../../global/helpers';
|
||||
import { applyStyles } from '../../../util/animation';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
@ -231,7 +232,3 @@ function getNodes(origin: StoryViewerOrigin, userId: string) {
|
||||
mediaEl: mediaEls?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function applyStyles(element: HTMLElement, css: Record<string, string>) {
|
||||
Object.assign(element.style, css);
|
||||
}
|
||||
|
||||
354
src/components/story/helpers/ribbonAnimation.ts
Normal file
354
src/components/story/helpers/ribbonAnimation.ts
Normal file
@ -0,0 +1,354 @@
|
||||
import { ANIMATION_END_DELAY } from '../../../config';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { applyStyles } from '../../../util/animation';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import ribbonStyles from '../StoryRibbon.module.scss';
|
||||
import togglerStyles from '../StoryToggler.module.scss';
|
||||
|
||||
export const ANIMATION_DURATION = 250;
|
||||
const RIBBON_OFFSET = 0.25 * REM;
|
||||
const RIBBON_Z_INDEX = 11;
|
||||
const STROKE_OFFSET = 0.1875 * REM;
|
||||
const CANVAS_OFFSET = 0.1 * REM;
|
||||
|
||||
const callbacks: Set<NoneToVoidFunction> = new Set();
|
||||
|
||||
export function animateOpening(isArchived?: boolean) {
|
||||
cancelDelayedCallbacks();
|
||||
|
||||
const {
|
||||
container, toggler, leftMainHeader, ribbonUsers, toggleAvatars,
|
||||
} = getHTMLElements(isArchived);
|
||||
|
||||
if (!toggler || !toggleAvatars || !ribbonUsers || !container || !leftMainHeader) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { bottom: headerBottom, right: headerRight } = leftMainHeader.getBoundingClientRect();
|
||||
const toTop = headerBottom + RIBBON_OFFSET;
|
||||
|
||||
// Toggle avatars are in the reverse order
|
||||
const lastToggleAvatar = toggleAvatars[0];
|
||||
const firstToggleAvatar = toggleAvatars[toggleAvatars.length - 1];
|
||||
const lastId = getUserId(lastToggleAvatar);
|
||||
|
||||
Array.from(ribbonUsers).reverse().forEach((user, index, { length }) => {
|
||||
const id = getUserId(user);
|
||||
if (!id) return;
|
||||
const isLast = id === lastId;
|
||||
let toggleAvatar = selectByUserId(toggler, id);
|
||||
let zIndex = RIBBON_Z_INDEX + index + 1;
|
||||
if (!toggleAvatar) {
|
||||
const isSelf = index === length - 1;
|
||||
|
||||
// Self story should appear from the first toggle avatar
|
||||
toggleAvatar = isSelf ? firstToggleAvatar : lastToggleAvatar;
|
||||
zIndex = RIBBON_Z_INDEX;
|
||||
}
|
||||
|
||||
if (!toggleAvatar) return;
|
||||
|
||||
let {
|
||||
// eslint-disable-next-line prefer-const
|
||||
top: fromTop,
|
||||
left: fromLeft,
|
||||
width: fromWidth,
|
||||
} = toggleAvatar.getBoundingClientRect();
|
||||
|
||||
const {
|
||||
left: toLeft,
|
||||
width: toWidth,
|
||||
} = user.getBoundingClientRect();
|
||||
|
||||
if (toLeft > headerRight) {
|
||||
return;
|
||||
}
|
||||
|
||||
fromLeft -= STROKE_OFFSET;
|
||||
fromWidth += 2 * STROKE_OFFSET;
|
||||
|
||||
const fromTranslateX = fromLeft - toLeft;
|
||||
const fromTranslateY = fromTop - toTop;
|
||||
const fromScale = fromWidth / toWidth;
|
||||
|
||||
fromTop -= STROKE_OFFSET;
|
||||
|
||||
const toTranslateX = toLeft - fromLeft;
|
||||
const toTranslateY = toTop - fromTop - CANVAS_OFFSET;
|
||||
const toScale = toWidth / fromWidth;
|
||||
|
||||
requestMutation(() => {
|
||||
if (!toggleAvatar) return;
|
||||
const ghost = createGhost(user);
|
||||
|
||||
let ghost2: HTMLElement | undefined;
|
||||
|
||||
// If this is a toogle avatar we create a second ghost and do crossfade animation
|
||||
if (zIndex > RIBBON_Z_INDEX) {
|
||||
ghost2 = createGhost(toggleAvatar!);
|
||||
if (isLast) {
|
||||
ghost2.classList.add(togglerStyles.ghostLast);
|
||||
}
|
||||
} else {
|
||||
// Else we animate only name
|
||||
ghost.classList.add(togglerStyles.ghostAnimateName);
|
||||
}
|
||||
|
||||
applyStyles(ghost, {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
zIndex: `${zIndex}`,
|
||||
opacity: ghost2 ? '0' : '',
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScale})`,
|
||||
});
|
||||
|
||||
if (ghost2) {
|
||||
applyStyles(ghost2, {
|
||||
top: `${fromTop}px`,
|
||||
left: `${fromLeft}px`,
|
||||
zIndex: `${zIndex}`,
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(ghost);
|
||||
if (ghost2) {
|
||||
container.appendChild(ghost2);
|
||||
}
|
||||
toggleAvatar.classList.add('animating');
|
||||
user.classList.add('animating');
|
||||
|
||||
requestMutation(() => {
|
||||
applyStyles(ghost, {
|
||||
opacity: '',
|
||||
transform: '',
|
||||
});
|
||||
|
||||
if (ghost2) {
|
||||
applyStyles(ghost2, {
|
||||
opacity: '0',
|
||||
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScale})`,
|
||||
});
|
||||
} else {
|
||||
ghost.classList.add(togglerStyles.ghostRevealName);
|
||||
}
|
||||
|
||||
const cb = createDelayedCallback(() => {
|
||||
requestMutation(() => {
|
||||
if (container.contains(ghost)) {
|
||||
container.removeChild(ghost);
|
||||
}
|
||||
if (ghost2 && container.contains(ghost2)) {
|
||||
container.removeChild(ghost2);
|
||||
}
|
||||
toggleAvatar?.classList.remove('animating');
|
||||
user.classList.remove('animating');
|
||||
});
|
||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
|
||||
callbacks.add(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function animateClosing(isArchived?: boolean) {
|
||||
cancelDelayedCallbacks();
|
||||
|
||||
const {
|
||||
container,
|
||||
toggler,
|
||||
toggleAvatars,
|
||||
ribbonUsers,
|
||||
leftMainHeader,
|
||||
} = getHTMLElements(isArchived);
|
||||
|
||||
if (!toggler || !toggleAvatars || !ribbonUsers || !container || !leftMainHeader) {
|
||||
return;
|
||||
}
|
||||
const { right: headerRight } = leftMainHeader.getBoundingClientRect();
|
||||
|
||||
// Toggle avatars are in the reverse order
|
||||
const lastToggleAvatar = toggleAvatars[0];
|
||||
const firstToggleAvatar = toggleAvatars[toggleAvatars.length - 1];
|
||||
const lastId = getUserId(lastToggleAvatar);
|
||||
|
||||
Array.from(ribbonUsers).reverse().forEach((user, index, { length }) => {
|
||||
const id = getUserId(user);
|
||||
if (!id) return;
|
||||
const isLast = id === lastId;
|
||||
let toggleAvatar = selectByUserId(toggler, id);
|
||||
let zIndex = RIBBON_Z_INDEX + index + 1;
|
||||
if (!toggleAvatar) {
|
||||
const isSelf = index === length - 1;
|
||||
|
||||
// Self story should appear from the first toggle avatar
|
||||
toggleAvatar = isSelf ? firstToggleAvatar : lastToggleAvatar;
|
||||
zIndex = RIBBON_Z_INDEX;
|
||||
}
|
||||
|
||||
if (!toggleAvatar) return;
|
||||
|
||||
const {
|
||||
top: fromTop,
|
||||
left: fromLeft,
|
||||
width: fromWidth,
|
||||
} = user.getBoundingClientRect();
|
||||
|
||||
let {
|
||||
left: toLeft,
|
||||
width: toWidth,
|
||||
top: toTop,
|
||||
} = toggleAvatar.getBoundingClientRect();
|
||||
|
||||
if (fromLeft > headerRight) {
|
||||
return;
|
||||
}
|
||||
|
||||
toLeft -= STROKE_OFFSET;
|
||||
toWidth += 2 * STROKE_OFFSET;
|
||||
|
||||
const toTranslateX = toLeft - fromLeft;
|
||||
const toTranslateY = toTop - fromTop - CANVAS_OFFSET;
|
||||
const toScale = toWidth / fromWidth;
|
||||
|
||||
toTop -= STROKE_OFFSET;
|
||||
|
||||
const fromTranslateX = fromLeft - toLeft;
|
||||
const fromTranslateY = fromTop - toTop;
|
||||
const fromScale = fromWidth / toWidth;
|
||||
|
||||
requestMutation(() => {
|
||||
const ghost = createGhost(user);
|
||||
let ghost2: HTMLElement | undefined;
|
||||
|
||||
if (zIndex > RIBBON_Z_INDEX) {
|
||||
ghost2 = createGhost(toggleAvatar!);
|
||||
if (isLast) {
|
||||
ghost2.classList.add(togglerStyles.ghostLast);
|
||||
}
|
||||
} else {
|
||||
ghost.classList.add(togglerStyles.ghostAnimateName, togglerStyles.ghostRevealName);
|
||||
}
|
||||
|
||||
applyStyles(ghost, {
|
||||
top: `${fromTop}px`,
|
||||
left: `${fromLeft}px`,
|
||||
width: `${fromWidth}px`,
|
||||
zIndex: `${zIndex}`,
|
||||
});
|
||||
|
||||
if (ghost2) {
|
||||
applyStyles(ghost2, {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
zIndex: `${zIndex}`,
|
||||
opacity: '0',
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScale})`,
|
||||
});
|
||||
}
|
||||
|
||||
user.classList.add('animating');
|
||||
toggleAvatar!.classList.add('animating');
|
||||
|
||||
container.appendChild(ghost);
|
||||
if (ghost2) {
|
||||
container.appendChild(ghost2);
|
||||
}
|
||||
|
||||
requestMutation(() => {
|
||||
applyStyles(ghost, {
|
||||
opacity: ghost2 ? '0' : '',
|
||||
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScale})`,
|
||||
});
|
||||
|
||||
if (ghost2) {
|
||||
applyStyles(ghost2!, {
|
||||
opacity: '',
|
||||
transform: '',
|
||||
});
|
||||
} else {
|
||||
ghost.classList.remove(togglerStyles.ghostRevealName);
|
||||
}
|
||||
|
||||
const cb = createDelayedCallback(() => {
|
||||
requestMutation(() => {
|
||||
if (container.contains(ghost)) {
|
||||
container.removeChild(ghost);
|
||||
}
|
||||
if (ghost2 && container.contains(ghost2)) {
|
||||
container.removeChild(ghost2);
|
||||
}
|
||||
user.classList.remove('animating');
|
||||
toggleAvatar!.classList.remove('animating');
|
||||
});
|
||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
|
||||
callbacks.add(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getHTMLElements(isArchived?: boolean) {
|
||||
let container = document.getElementById('LeftColumn');
|
||||
if (container && isArchived) {
|
||||
container = container.querySelector<HTMLElement>('.ArchivedChats');
|
||||
}
|
||||
if (!container) return {};
|
||||
const toggler = container.querySelector<HTMLElement>('#StoryToggler');
|
||||
const ribbon = container.querySelector<HTMLElement>('#StoryRibbon');
|
||||
const leftMainHeader = container.querySelector<HTMLElement>('.left-header');
|
||||
const ribbonUsers = ribbon?.querySelectorAll<HTMLElement>(`.${ribbonStyles.user}`);
|
||||
const toggleAvatars = toggler?.querySelectorAll<HTMLElement>('.Avatar');
|
||||
|
||||
return {
|
||||
container,
|
||||
toggler,
|
||||
leftMainHeader,
|
||||
ribbonUsers,
|
||||
toggleAvatars,
|
||||
};
|
||||
}
|
||||
|
||||
function createGhost(sourceEl: HTMLElement) {
|
||||
const ghost = sourceEl.cloneNode(true) as HTMLElement;
|
||||
ghost.classList.add(togglerStyles.ghost);
|
||||
|
||||
// Avoid source animating class being copied to the ghost
|
||||
ghost.classList.remove('animating');
|
||||
|
||||
ghost.draggable = false;
|
||||
ghost.oncontextmenu = stopEvent;
|
||||
|
||||
const sourceCanvas = sourceEl.querySelector('canvas');
|
||||
if (sourceCanvas) {
|
||||
const canvas = ghost.querySelector('canvas');
|
||||
canvas?.getContext('2d')?.drawImage(sourceCanvas, 0, 0);
|
||||
}
|
||||
|
||||
return ghost;
|
||||
}
|
||||
|
||||
function getUserId(el: HTMLElement) {
|
||||
return el.getAttribute('data-peer-id');
|
||||
}
|
||||
|
||||
function selectByUserId(el: HTMLElement, id: string) {
|
||||
return el.querySelector<HTMLElement>(`[data-peer-id="${id}"]`);
|
||||
}
|
||||
|
||||
function createDelayedCallback(callback: NoneToVoidFunction, ms: number) {
|
||||
const timeout = setTimeout(callback, ms);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
function cancelDelayedCallbacks() {
|
||||
callbacks.forEach((cb) => cb());
|
||||
callbacks.clear();
|
||||
}
|
||||
@ -28,6 +28,7 @@ export const INITIAL_PERFORMANCE_STATE_MAX: PerformanceType = {
|
||||
reactionEffects: true,
|
||||
rightColumnAnimations: true,
|
||||
stickerEffects: true,
|
||||
storyRibbonAnimations: true,
|
||||
};
|
||||
|
||||
export const INITIAL_PERFORMANCE_STATE_MID: PerformanceType = {
|
||||
@ -44,6 +45,7 @@ export const INITIAL_PERFORMANCE_STATE_MID: PerformanceType = {
|
||||
reactionEffects: true,
|
||||
rightColumnAnimations: false,
|
||||
stickerEffects: false,
|
||||
storyRibbonAnimations: false,
|
||||
};
|
||||
|
||||
export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
|
||||
@ -60,6 +62,7 @@ export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
|
||||
reactionEffects: false,
|
||||
rightColumnAnimations: false,
|
||||
stickerEffects: false,
|
||||
storyRibbonAnimations: false,
|
||||
};
|
||||
|
||||
export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
|
||||
@ -35,6 +35,7 @@ export type PerformanceTypeKey = (
|
||||
'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations'
|
||||
| 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations'
|
||||
| 'animatedEmoji' | 'loopAnimatedStickers' | 'reactionEffects' | 'stickerEffects' | 'autoplayGifs' | 'autoplayVideos'
|
||||
| 'storyRibbonAnimations'
|
||||
);
|
||||
export type PerformanceType = {
|
||||
[key in PerformanceTypeKey]: boolean;
|
||||
|
||||
@ -110,3 +110,7 @@ export function animateNumber<T extends number | number[]>({
|
||||
if (onEnd) onEnd();
|
||||
};
|
||||
}
|
||||
|
||||
export function applyStyles(element: HTMLElement, css: Record<string, string>) {
|
||||
Object.assign(element.style, css);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user