Story ribbon animation (#3798)

This commit is contained in:
Alexander Zinchuk 2023-09-25 18:53:21 +02:00
parent b090b80863
commit e8c4b8f713
17 changed files with 477 additions and 37 deletions

View File

@ -24,6 +24,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
z-index: 1;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-image: linear-gradient(var(--color-white) -125%, var(--color-user)); background-image: linear-gradient(var(--color-white) -125%, var(--color-user));
@ -168,32 +169,49 @@
left: -0.25rem; left: -0.25rem;
top: -0.25rem; top: -0.25rem;
border-radius: 50%; border-radius: 50%;
padding: 0.125rem;
background: var(--color-borders-read-story); 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 { &.size-tiny {
width: 2rem; width: 1.75rem;
height: 2rem; height: 1.75rem;
&::before { &::before {
width: 2.25rem; width: 2.25rem;
height: 2.25rem; height: 2.25rem;
} }
&::after {
width: 2rem;
height: 2rem;
}
} }
&.size-medium { &.size-medium {
width: 2.75rem; width: 2.5rem;
height: 2.75rem; height: 2.5rem;
&::before { &::before {
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
} }
&::after {
width: 2.75rem;
height: 2.75rem;
}
} }
&.online::after { &.online::after {

View File

@ -244,6 +244,7 @@ const Avatar: FC<OwnProps> = ({
ref={ref} ref={ref}
className={fullClassName} className={fullClassName}
id={peer?.id && withStory ? getUserStoryHtmlId(peer.id) : undefined} id={peer?.id && withStory ? getUserStoryHtmlId(peer.id) : undefined}
data-peer-id={peer?.id}
data-test-sender-id={IS_TEST ? peer?.id : undefined} data-test-sender-id={IS_TEST ? peer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined} aria-label={typeof content === 'string' ? author : undefined}
onClick={handleClick} onClick={handleClick}

View File

@ -1,6 +1,7 @@
.ArchivedChats { .ArchivedChats {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background: var(--color-background);
.left-header { .left-header {
position: relative; position: relative;

View File

@ -6,7 +6,9 @@ import type { GlobalState } from '../../global/types';
import type { FolderEditDispatch } from '../../hooks/reducers/useFoldersReducer'; import type { FolderEditDispatch } from '../../hooks/reducers/useFoldersReducer';
import type { LeftColumnContent, SettingsScreens } from '../../types'; import type { LeftColumnContent, SettingsScreens } from '../../types';
import { ANIMATION_END_DELAY } from '../../config';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { ANIMATION_DURATION } from '../story/helpers/ribbonAnimation';
import useForumPanelRender from '../../hooks/useForumPanelRender'; import useForumPanelRender from '../../hooks/useForumPanelRender';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
@ -80,7 +82,7 @@ const ArchivedChats: FC<OwnProps> = ({
shouldRender: shouldRenderStoryRibbon, shouldRender: shouldRenderStoryRibbon,
transitionClassNames: storyRibbonClassNames, transitionClassNames: storyRibbonClassNames,
isClosing: isStoryRibbonClosing, isClosing: isStoryRibbonClosing,
} = useShowTransition(isStoryRibbonShown, undefined, undefined, ''); } = useShowTransition(isStoryRibbonShown, undefined, undefined, '', false, ANIMATION_DURATION + ANIMATION_END_DELAY);
return ( return (
<div className="ArchivedChats"> <div className="ArchivedChats">

View File

@ -4,6 +4,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background: var(--color-background);
> .Transition { > .Transition {
flex: 1; flex: 1;
@ -47,6 +48,7 @@
padding-left: 0.625rem; padding-left: 0.625rem;
padding-right: 0.625rem; padding-right: 0.625rem;
/* stylelint-disable-next-line */
> span { > span {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;

View File

@ -1,19 +1,21 @@
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import type { ApiDimensions, ApiMessage } from '../../../api/types'; import type { ApiDimensions, ApiMessage } from '../../../api/types';
import { MediaViewerOrigin } from '../../../types'; import { MediaViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config'; 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 { import {
calculateDimensions, calculateDimensions,
getMediaViewerAvailableDimensions, getMediaViewerAvailableDimensions,
MEDIA_VIEWER_MEDIA_QUERY, MEDIA_VIEWER_MEDIA_QUERY,
REM, REM,
} from '../../common/helpers/mediaDimensions'; } 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; const ANIMATION_DURATION = 200;
@ -283,10 +285,6 @@ function getTopOffset(hasFooter: boolean) {
return topOffsetRem * REM; return topOffsetRem * REM;
} }
function applyStyles(element: HTMLElement, styles: Record<string, string>) {
Object.assign(element.style, styles);
}
function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) { function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
let containerSelector; let containerSelector;
let mediaSelector; let mediaSelector;

View File

@ -4,7 +4,6 @@
column-gap: 0.625rem; column-gap: 0.625rem;
padding: 0.25rem 0.5rem 0.5rem 1rem; padding: 0.25rem 0.5rem 0.5rem 1rem;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
white-space: nowrap; white-space: nowrap;
max-height: 5.5rem; max-height: 5.5rem;
position: relative; position: relative;
@ -37,6 +36,10 @@
&:focus { &:focus {
outline: none; outline: none;
} }
&:global(.animating) {
opacity: 0;
}
} }
.name { .name {
@ -44,6 +47,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
unicode-bidi: plaintext; unicode-bidi: plaintext;
white-space: nowrap;
max-width: 110%; max-width: 110%;
&_hasUnreadStory { &_hasUnreadStory {

View File

@ -44,6 +44,7 @@ function StoryRibbon({
return ( return (
<div <div
ref={ref} ref={ref}
id="StoryRibbon"
className={fullClassName} className={fullClassName}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >

View File

@ -95,6 +95,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) {
<div <div
ref={ref} ref={ref}
role="button" role="button"
data-peer-id={user.id}
tabIndex={0} tabIndex={0}
className={styles.user} className={styles.user}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}

View File

@ -1,7 +1,10 @@
/* stylelint-disable-next-line */
@value name from "./StoryRibbon.module.scss";
.root { .root {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0.125rem; right: 0.25rem;
transform: translateY(-50%); transform: translateY(-50%);
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -19,17 +22,48 @@
} }
.avatar { .avatar {
border: 0.125rem solid var(--color-background); z-index: 1;
&::before { &:not(:first-child):before {
z-index: -2; mask-image: linear-gradient(90deg, #fff 75%, transparent 0);
mask-composite: exclude;
} }
&:global(.has-unread-story)::before { &:global(.animating) {
z-index: -1; opacity: 0;
} }
} }
.avatarHidden {
display: none;
}
.avatar + .avatar { .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;
} }

View File

@ -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 { getActions, withGlobal } from '../../global';
import type { ApiUser } from '../../api/types'; import type { ApiUser } from '../../api/types';
import { PREVIEW_AVATAR_COUNT } from '../../config'; import { ANIMATION_END_DELAY, PREVIEW_AVATAR_COUNT } from '../../config';
import { selectTabState } from '../../global/selectors'; import { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { animateClosing, animateOpening, ANIMATION_DURATION } from './helpers/ribbonAnimation';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useStoryPreloader from './hooks/useStoryPreloader'; import useStoryPreloader from './hooks/useStoryPreloader';
@ -24,6 +26,7 @@ interface StateProps {
currentUserId: string; currentUserId: string;
orderedUserIds: string[]; orderedUserIds: string[];
isShown: boolean; isShown: boolean;
withAnimation?: boolean;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
} }
@ -36,6 +39,7 @@ function StoryToggler({
canShow, canShow,
isShown, isShown,
isArchived, isArchived,
withAnimation,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { toggleStoryRibbon } = getActions(); const { toggleStoryRibbon } = getActions();
@ -58,8 +62,20 @@ function StoryToggler({
}, [orderedUserIds]); }, [orderedUserIds]);
useStoryPreloader(preloadUserIds); useStoryPreloader(preloadUserIds);
const isVisible = canShow && isShown;
// For some reason, setting 'slow' here also fixes scroll freezes on iOS when collapsing Story Ribbon // 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) { if (!shouldRender) {
return undefined; return undefined;
@ -68,6 +84,7 @@ function StoryToggler({
return ( return (
<button <button
type="button" type="button"
id="StoryToggler"
className={buildClassName(styles.root, transitionClassNames)} className={buildClassName(styles.root, transitionClassNames)}
aria-label={lang('Chat.Context.Peer.OpenStory')} aria-label={lang('Chat.Context.Peer.OpenStory')}
onClick={() => toggleStoryRibbon({ isShown: true, isArchived })} onClick={() => toggleStoryRibbon({ isShown: true, isArchived })}
@ -89,11 +106,13 @@ function StoryToggler({
export default memo(withGlobal<OwnProps>((global, { isArchived }): StateProps => { export default memo(withGlobal<OwnProps>((global, { isArchived }): StateProps => {
const { orderedUserIds: { archived, active } } = global.stories; const { orderedUserIds: { archived, active } } = global.stories;
const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global); const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global);
const withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations');
return { return {
currentUserId: global.currentUserId!, currentUserId: global.currentUserId!,
orderedUserIds: isArchived ? archived : active, orderedUserIds: isArchived ? archived : active,
isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown, isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown,
withAnimation,
usersById: global.users.byId, usersById: global.users.byId,
}; };
})(StoryToggler)); })(StoryToggler));

View File

@ -22,7 +22,7 @@
} }
&:global(.opacity-transition) { &:global(.opacity-transition) {
transition: opacity 200ms; transition: opacity 250ms;
} }
:global(.text-entity-link) { :global(.text-entity-link) {

View File

@ -5,6 +5,7 @@ import { ANIMATION_END_DELAY } from '../../../config';
import fastBlur from '../../../lib/fastBlur'; import fastBlur from '../../../lib/fastBlur';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getUserStoryHtmlId } from '../../../global/helpers'; import { getUserStoryHtmlId } from '../../../global/helpers';
import { applyStyles } from '../../../util/animation';
import stopEvent from '../../../util/stopEvent'; import stopEvent from '../../../util/stopEvent';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
@ -231,7 +232,3 @@ function getNodes(origin: StoryViewerOrigin, userId: string) {
mediaEl: mediaEls?.[0], mediaEl: mediaEls?.[0],
}; };
} }
function applyStyles(element: HTMLElement, css: Record<string, string>) {
Object.assign(element.style, css);
}

View 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();
}

View File

@ -28,6 +28,7 @@ export const INITIAL_PERFORMANCE_STATE_MAX: PerformanceType = {
reactionEffects: true, reactionEffects: true,
rightColumnAnimations: true, rightColumnAnimations: true,
stickerEffects: true, stickerEffects: true,
storyRibbonAnimations: true,
}; };
export const INITIAL_PERFORMANCE_STATE_MID: PerformanceType = { export const INITIAL_PERFORMANCE_STATE_MID: PerformanceType = {
@ -44,6 +45,7 @@ export const INITIAL_PERFORMANCE_STATE_MID: PerformanceType = {
reactionEffects: true, reactionEffects: true,
rightColumnAnimations: false, rightColumnAnimations: false,
stickerEffects: false, stickerEffects: false,
storyRibbonAnimations: false,
}; };
export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = { export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
@ -60,6 +62,7 @@ export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
reactionEffects: false, reactionEffects: false,
rightColumnAnimations: false, rightColumnAnimations: false,
stickerEffects: false, stickerEffects: false,
storyRibbonAnimations: false,
}; };
export const INITIAL_GLOBAL_STATE: GlobalState = { export const INITIAL_GLOBAL_STATE: GlobalState = {

View File

@ -35,6 +35,7 @@ export type PerformanceTypeKey = (
'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations' 'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations'
| 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations' | 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations'
| 'animatedEmoji' | 'loopAnimatedStickers' | 'reactionEffects' | 'stickerEffects' | 'autoplayGifs' | 'autoplayVideos' | 'animatedEmoji' | 'loopAnimatedStickers' | 'reactionEffects' | 'stickerEffects' | 'autoplayGifs' | 'autoplayVideos'
| 'storyRibbonAnimations'
); );
export type PerformanceType = { export type PerformanceType = {
[key in PerformanceTypeKey]: boolean; [key in PerformanceTypeKey]: boolean;

View File

@ -110,3 +110,7 @@ export function animateNumber<T extends number | number[]>({
if (onEnd) onEnd(); if (onEnd) onEnd();
}; };
} }
export function applyStyles(element: HTMLElement, css: Record<string, string>) {
Object.assign(element.style, css);
}