Message: Fix parallel emoji interactions (#1678)

This commit is contained in:
Alexander Zinchuk 2022-02-20 13:39:33 +02:00
parent e33e22c982
commit 74d348b00a
10 changed files with 122 additions and 96 deletions

View File

@ -27,7 +27,7 @@ type OwnProps = {
forceLoadPreview?: boolean;
messageId?: number;
chatId?: string;
activeEmojiInteraction?: ActiveEmojiInteraction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
};
const QUALITY = 1;
@ -43,7 +43,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
forceLoadPreview,
messageId,
chatId,
activeEmojiInteraction,
activeEmojiInteractions,
}) => {
const {
markAnimationLoaded,
@ -53,7 +53,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
style,
handleClick,
playKey,
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, undefined, effect?.emoji);
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteractions, isOwn, undefined, effect?.emoji);
const localMediaHash = `sticker${sticker.id}`;

View File

@ -23,7 +23,7 @@ type OwnProps = {
forceLoadPreview?: boolean;
messageId?: number;
chatId?: string;
activeEmojiInteraction?: ActiveEmojiInteraction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
};
const LocalAnimatedEmoji: FC<OwnProps> = ({
@ -35,7 +35,7 @@ const LocalAnimatedEmoji: FC<OwnProps> = ({
observeIntersection,
messageId,
chatId,
activeEmojiInteraction,
activeEmojiInteractions,
}) => {
const {
playKey,
@ -44,7 +44,7 @@ const LocalAnimatedEmoji: FC<OwnProps> = ({
width,
handleClick,
markAnimationLoaded,
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, localEffect);
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteractions, isOwn, localEffect);
const id = `local_emoji_${localSticker}`;
const isIntersecting = useIsIntersecting(ref, observeIntersection);

View File

@ -22,7 +22,7 @@ export default function useAnimatedEmoji(
chatId?: string,
messageId?: number,
soundId?: string,
activeEmojiInteraction?: ActiveEmojiInteraction,
activeEmojiInteractions?: ActiveEmojiInteraction[],
isOwn?: boolean,
localEffect?: string,
emoji?: string,
@ -120,16 +120,21 @@ export default function useAnimatedEmoji(
useEffect(() => {
const container = ref.current;
if (!container || !activeEmojiInteraction) return;
if (!container || !activeEmojiInteractions) return;
const {
messageId: selectedMessageId, endX, endY,
} = activeEmojiInteraction;
activeEmojiInteractions.forEach(({
id,
startSize,
messageId: interactionMessageId,
}) => {
if (startSize || messageId !== interactionMessageId) {
return;
}
if (!endX && !endY && selectedMessageId === messageId) {
const { x, y } = container.getBoundingClientRect();
sendWatchingEmojiInteraction({
id,
chatId,
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
startSize: width,
@ -138,9 +143,9 @@ export default function useAnimatedEmoji(
isReversed: !isOwn,
});
play();
}
});
}, [
activeEmojiInteraction, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, width,
activeEmojiInteractions, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, width,
]);
return {

View File

@ -5,8 +5,9 @@ import { Bundles } from '../../util/moduleLoader';
import useModuleLoader from '../../hooks/useModuleLoader';
const EmojiInteractionAnimationAsync: FC<OwnProps> = (props) => {
const { emojiInteraction } = props;
const EmojiInteractionAnimation = useModuleLoader(Bundles.Extra, 'EmojiInteractionAnimation', !emojiInteraction);
const { activeEmojiInteraction } = props;
const EmojiInteractionAnimation = useModuleLoader(Bundles.Extra, 'EmojiInteractionAnimation',
!activeEmojiInteraction);
// eslint-disable-next-line react/jsx-props-no-spreading
return EmojiInteractionAnimation ? <EmojiInteractionAnimation {...props} /> : undefined;

View File

@ -1,5 +1,5 @@
import React, {
FC, memo, useCallback, useEffect, useLayoutEffect, useState,
FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
} from '../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../lib/teact/teactn';
@ -21,13 +21,12 @@ import AnimatedSticker from '../common/AnimatedSticker';
import './EmojiInteractionAnimation.scss';
export type OwnProps = {
emojiInteraction: ActiveEmojiInteraction;
activeEmojiInteraction: ActiveEmojiInteraction;
};
type StateProps = {
effectAnimationId?: string;
localEffectAnimation?: string;
isReversed?: boolean;
};
const HIDE_ANIMATION_DURATION = 250;
@ -35,41 +34,50 @@ const PLAYING_DURATION = 3000;
const EFFECT_SIZE = 240;
const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
emojiInteraction,
effectAnimationId,
localEffectAnimation,
isReversed,
activeEmojiInteraction,
}) => {
const { stopActiveEmojiInteraction } = getDispatch();
const [isHiding, startHiding] = useFlag(false);
const [isPlaying, startPlaying] = useFlag(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const stop = useCallback(() => {
startHiding();
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setTimeout(() => {
stopActiveEmojiInteraction();
stopActiveEmojiInteraction({ id: activeEmojiInteraction.id });
}, HIDE_ANIMATION_DURATION);
}, [startHiding, stopActiveEmojiInteraction]);
}, [activeEmojiInteraction.id, startHiding, stopActiveEmojiInteraction]);
const handleCancelAnimation = useCallback((e: UIEvent) => {
if (!(e.target as HTMLElement)?.closest('.AnimatedEmoji')) {
stop();
}
}, [stop]);
useEffect(() => {
document.addEventListener('touchstart', stop);
document.addEventListener('touchmove', stop);
document.addEventListener('mousedown', stop);
document.addEventListener('wheel', stop);
document.addEventListener('touchstart', handleCancelAnimation);
document.addEventListener('touchmove', handleCancelAnimation);
document.addEventListener('mousedown', handleCancelAnimation);
document.addEventListener('wheel', handleCancelAnimation);
return () => {
document.removeEventListener('touchstart', stop);
document.removeEventListener('touchmove', stop);
document.removeEventListener('mousedown', stop);
document.removeEventListener('wheel', stop);
document.removeEventListener('touchstart', handleCancelAnimation);
document.removeEventListener('touchmove', handleCancelAnimation);
document.removeEventListener('mousedown', handleCancelAnimation);
document.removeEventListener('wheel', handleCancelAnimation);
};
}, [stop]);
}, [handleCancelAnimation]);
useLayoutEffect(() => {
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
setTimeout(() => {
timeoutRef.current = setTimeout(() => {
stop();
dispatchHeavyAnimationStop();
}, PLAYING_DURATION);
@ -86,16 +94,24 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
}
}, [localEffectAnimation]);
const scale = (emojiInteraction.startSize || 0) / EFFECT_SIZE;
if (!activeEmojiInteraction.startSize) {
return undefined;
}
const scale = (activeEmojiInteraction.startSize || 0) / EFFECT_SIZE;
return (
<div
className={buildClassName(
'EmojiInteractionAnimation', isHiding && 'hiding', isPlaying && 'playing', isReversed && 'reversed',
'EmojiInteractionAnimation',
isHiding && 'hiding',
isPlaying && 'playing',
activeEmojiInteraction.isReversed && 'reversed',
)}
style={`--scale: ${scale}; --start-x: ${emojiInteraction.x}px; --start-y: ${emojiInteraction.y}px;`}
style={`--scale: ${scale}; --start-x: ${activeEmojiInteraction.x}px; --start-y: ${activeEmojiInteraction.y}px;`}
>
<AnimatedSticker
key={`effect_${effectAnimationId}`}
id={`effect_${effectAnimationId}`}
size={EFFECT_SIZE}
animationData={localEffectAnimationData || effectAnimationData}
@ -110,15 +126,14 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { emojiInteraction }): StateProps => {
const animatedEffect = emojiInteraction.animatedEffect !== undefined
&& selectAnimatedEmojiEffect(global, emojiInteraction.animatedEffect);
(global, { activeEmojiInteraction }): StateProps => {
const animatedEffect = activeEmojiInteraction.animatedEffect !== undefined
&& selectAnimatedEmojiEffect(global, activeEmojiInteraction.animatedEffect);
return {
effectAnimationId: animatedEffect ? animatedEffect.id : undefined,
localEffectAnimation: !animatedEffect && emojiInteraction.animatedEffect
&& Object.keys(ANIMATED_STICKERS_PATHS).includes(emojiInteraction.animatedEffect)
? emojiInteraction.animatedEffect : undefined,
isReversed: emojiInteraction.isReversed,
localEffectAnimation: !animatedEffect && activeEmojiInteraction.animatedEffect
&& Object.keys(ANIMATED_STICKERS_PATHS).includes(activeEmojiInteraction.animatedEffect)
? activeEmojiInteraction.animatedEffect : undefined,
};
},
)(EmojiInteractionAnimation));

View File

@ -109,7 +109,7 @@ type StateProps = {
canSubscribe?: boolean;
canStartBot?: boolean;
canRestartBot?: boolean;
activeEmojiInteraction?: ActiveEmojiInteraction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
};
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
@ -150,7 +150,7 @@ const MiddleColumn: FC<StateProps> = ({
canSubscribe,
canStartBot,
canRestartBot,
activeEmojiInteraction,
activeEmojiInteractions,
}) => {
const {
openChat,
@ -522,9 +522,15 @@ const MiddleColumn: FC<StateProps> = ({
onUnpin={handleUnpinAllMessages}
/>
)}
{activeEmojiInteraction && (
<EmojiInteractionAnimation emojiInteraction={activeEmojiInteraction} />
)}
<div teactFastList>
{activeEmojiInteractions?.map((activeEmojiInteraction, i) => (
<EmojiInteractionAnimation
teactOrderKey={i}
key={activeEmojiInteraction.id}
activeEmojiInteraction={activeEmojiInteraction}
/>
))}
</div>
</div>
);
};
@ -538,7 +544,7 @@ export default memo(withGlobal(
const { messageLists } = global.messages;
const currentMessageList = selectCurrentMessageList(global);
const { isLeftColumnShown, chats: { listIds }, activeEmojiInteraction } = global;
const { isLeftColumnShown, chats: { listIds }, activeEmojiInteractions } = global;
const state: StateProps = {
theme,
@ -556,7 +562,7 @@ export default memo(withGlobal(
isReactorListModalOpen: Boolean(global.reactorModal),
animationLevel: global.settings.byKey.animationLevel,
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
activeEmojiInteraction,
activeEmojiInteractions,
};
if (!currentMessageList || !listIds.active) {

View File

@ -185,7 +185,7 @@ type StateProps = {
availableReactions?: ApiAvailableReaction[];
defaultReaction?: string;
activeReaction?: ActiveReaction;
activeEmojiInteraction?: ActiveEmojiInteraction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
};
type MetaPosition =
@ -261,7 +261,7 @@ const Message: FC<OwnProps & StateProps> = ({
availableReactions,
defaultReaction,
activeReaction,
activeEmojiInteraction,
activeEmojiInteractions,
messageListType,
isPinnedList,
isDownloading,
@ -637,7 +637,7 @@ const Message: FC<OwnProps & StateProps> = ({
forceLoadPreview={isLocal}
messageId={messageId}
chatId={chatId}
activeEmojiInteraction={activeEmojiInteraction}
activeEmojiInteractions={activeEmojiInteractions}
/>
)}
{localSticker && (
@ -652,7 +652,7 @@ const Message: FC<OwnProps & StateProps> = ({
forceLoadPreview={isLocal}
messageId={messageId}
chatId={chatId}
activeEmojiInteraction={activeEmojiInteraction}
activeEmojiInteractions={activeEmojiInteractions}
/>
)}
{isAlbum && (
@ -1055,7 +1055,7 @@ export default memo(withGlobal<OwnProps>(
availableReactions: global.availableReactions,
defaultReaction: isMessageLocal(message) ? undefined : selectDefaultReaction(global, chatId),
activeReaction: reactionMessage && global.activeReactions[reactionMessage.id],
activeEmojiInteraction: global.activeEmojiInteraction,
activeEmojiInteractions: global.activeEmojiInteractions,
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
...(typeof uploadProgress === 'number' && { uploadProgress }),
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),

View File

@ -65,13 +65,11 @@ export interface MessageList {
}
export interface ActiveEmojiInteraction {
id: number;
x: number;
y: number;
messageId?: number;
endX?: number;
endY?: number;
startSize?: number;
reaction?: string;
animatedEffect?: string;
isReversed?: boolean;
}
@ -332,7 +330,7 @@ export type GlobalState = {
};
availableReactions?: ApiAvailableReaction[];
activeEmojiInteraction?: ActiveEmojiInteraction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
activeReactions: Record<number, ActiveReaction>;
localTextSearch: {

View File

@ -15,6 +15,10 @@ import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { ANIMATION_LEVEL_MAX } from '../../../config';
import { isMessageLocal } from '../../helpers';
const INTERACTION_RANDOM_OFFSET = 40;
let interactionLocalId = 0;
addReducer('loadAvailableReactions', () => {
(async () => {
const result = await callApi('getAvailableReactions');
@ -45,28 +49,31 @@ addReducer('interactWithAnimatedEmoji', (global, actions, payload) => {
emoji, x, y, localEffect, startSize, isReversed,
} = payload!;
const activeEmojiInteraction = {
id: interactionLocalId++,
animatedEffect: emoji || localEffect,
x: subtractXForEmojiInteraction(global, x) + Math.random()
* INTERACTION_RANDOM_OFFSET - INTERACTION_RANDOM_OFFSET / 2,
y: y + Math.random() * INTERACTION_RANDOM_OFFSET - INTERACTION_RANDOM_OFFSET / 2,
startSize,
isReversed,
};
return {
...global,
activeEmojiInteraction: {
animatedEffect: emoji || localEffect,
x: subtractXForEmojiInteraction(global, x),
y,
startSize,
isReversed,
},
activeEmojiInteractions: [...(global.activeEmojiInteractions || []), activeEmojiInteraction],
};
});
addReducer('sendEmojiInteraction', (global, actions, payload) => {
const {
messageId, chatId, emoji, interactions, localEffect,
x, y, startX, startY, startSize,
} = payload!;
const chat = selectChat(global, chatId);
if (!chat || (!emoji && !localEffect) || chatId === global.currentUserId) {
return undefined;
return;
}
void callApi('sendEmojiInteraction', {
@ -75,20 +82,6 @@ addReducer('sendEmojiInteraction', (global, actions, payload) => {
emoticon: emoji || selectLocalAnimatedEmojiEffectByName(localEffect),
timestamps: interactions,
});
if (!global.activeEmojiInteraction) return undefined;
return {
...global,
activeEmojiInteraction: {
...global.activeEmojiInteraction,
endX: subtractXForEmojiInteraction(global, x),
endY: y,
...(startX && { x: subtractXForEmojiInteraction(global, startX) }),
...(startY && { y: startY }),
...(startSize && { startSize }),
},
};
});
addReducer('sendDefaultReaction', (global, actions, payload) => {
@ -224,10 +217,12 @@ addReducer('setDefaultReaction', (global, actions, payload) => {
})();
});
addReducer('stopActiveEmojiInteraction', (global) => {
addReducer('stopActiveEmojiInteraction', (global, actions, payload) => {
const { id } = payload;
return {
...global,
activeEmojiInteraction: undefined,
activeEmojiInteractions: global.activeEmojiInteractions?.filter((l) => l.id !== id),
};
});
@ -287,12 +282,12 @@ addReducer('loadMessageReactions', (global, actions, payload) => {
addReducer('sendWatchingEmojiInteraction', (global, actions, payload) => {
const {
chatId, emoticon, x, y, startSize, isReversed,
chatId, emoticon, x, y, startSize, isReversed, id,
} = payload;
const chat = selectChat(global, chatId);
if (!chat || !global.activeEmojiInteraction || chatId === global.currentUserId) {
if (!chat || !global.activeEmojiInteractions?.some((l) => l.id === id) || chatId === global.currentUserId) {
return undefined;
}
@ -300,12 +295,17 @@ addReducer('sendWatchingEmojiInteraction', (global, actions, payload) => {
return {
...global,
activeEmojiInteraction: {
...global.activeEmojiInteraction,
x: subtractXForEmojiInteraction(global, x),
y,
startSize,
isReversed,
},
activeEmojiInteractions: global.activeEmojiInteractions.map((activeEmojiInteraction) => {
if (activeEmojiInteraction.id === id) {
return {
...activeEmojiInteraction,
x: subtractXForEmojiInteraction(global, x),
y,
startSize,
isReversed,
};
}
return activeEmojiInteraction;
}),
};
});

View File

@ -118,7 +118,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
case 'updateStartEmojiInteraction': {
const { chatId: currentChatId } = selectCurrentMessageList(global) || {};
if (global.activeEmojiInteraction || currentChatId !== update.id) return;
if (currentChatId !== update.id) return;
const message = selectChatMessage(global, currentChatId, update.messageId);
if (!message) return;
@ -130,10 +130,11 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
global = {
...global,
activeEmojiInteraction: {
activeEmojiInteractions: [...(global.activeEmojiInteractions || []), {
id: global.activeEmojiInteractions?.length || 0,
animatedEffect: localEmoji ? selectLocalAnimatedEmojiEffect(localEmoji) : update.emoji,
messageId: update.messageId,
} as ActiveEmojiInteraction,
} as ActiveEmojiInteraction],
};
setGlobal(global);