From 74d348b00adcf9bbb281103ea6a8e32f23bc1148 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 20 Feb 2022 13:39:33 +0200 Subject: [PATCH] Message: Fix parallel emoji interactions (#1678) --- src/components/common/AnimatedEmoji.tsx | 6 +- src/components/common/LocalAnimatedEmoji.tsx | 6 +- .../common/hooks/useAnimatedEmoji.ts | 21 +++--- .../EmojiInteractionAnimation.async.tsx | 5 +- .../middle/EmojiInteractionAnimation.tsx | 69 +++++++++++-------- src/components/middle/MiddleColumn.tsx | 20 ++++-- src/components/middle/message/Message.tsx | 10 +-- src/global/types.ts | 6 +- src/modules/actions/api/reactions.ts | 68 +++++++++--------- src/modules/actions/apiUpdaters/messages.ts | 7 +- 10 files changed, 122 insertions(+), 96 deletions(-) diff --git a/src/components/common/AnimatedEmoji.tsx b/src/components/common/AnimatedEmoji.tsx index 7b51d989e..bfb9e778f 100644 --- a/src/components/common/AnimatedEmoji.tsx +++ b/src/components/common/AnimatedEmoji.tsx @@ -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 = ({ forceLoadPreview, messageId, chatId, - activeEmojiInteraction, + activeEmojiInteractions, }) => { const { markAnimationLoaded, @@ -53,7 +53,7 @@ const AnimatedEmoji: FC = ({ 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}`; diff --git a/src/components/common/LocalAnimatedEmoji.tsx b/src/components/common/LocalAnimatedEmoji.tsx index f85494466..3f4dda699 100644 --- a/src/components/common/LocalAnimatedEmoji.tsx +++ b/src/components/common/LocalAnimatedEmoji.tsx @@ -23,7 +23,7 @@ type OwnProps = { forceLoadPreview?: boolean; messageId?: number; chatId?: string; - activeEmojiInteraction?: ActiveEmojiInteraction; + activeEmojiInteractions?: ActiveEmojiInteraction[]; }; const LocalAnimatedEmoji: FC = ({ @@ -35,7 +35,7 @@ const LocalAnimatedEmoji: FC = ({ observeIntersection, messageId, chatId, - activeEmojiInteraction, + activeEmojiInteractions, }) => { const { playKey, @@ -44,7 +44,7 @@ const LocalAnimatedEmoji: FC = ({ 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); diff --git a/src/components/common/hooks/useAnimatedEmoji.ts b/src/components/common/hooks/useAnimatedEmoji.ts index 1814d1e1d..d8152610b 100644 --- a/src/components/common/hooks/useAnimatedEmoji.ts +++ b/src/components/common/hooks/useAnimatedEmoji.ts @@ -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 { diff --git a/src/components/middle/EmojiInteractionAnimation.async.tsx b/src/components/middle/EmojiInteractionAnimation.async.tsx index dbfb73caf..703aee56f 100644 --- a/src/components/middle/EmojiInteractionAnimation.async.tsx +++ b/src/components/middle/EmojiInteractionAnimation.async.tsx @@ -5,8 +5,9 @@ import { Bundles } from '../../util/moduleLoader'; import useModuleLoader from '../../hooks/useModuleLoader'; const EmojiInteractionAnimationAsync: FC = (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 ? : undefined; diff --git a/src/components/middle/EmojiInteractionAnimation.tsx b/src/components/middle/EmojiInteractionAnimation.tsx index 387b3d026..1832f47fc 100644 --- a/src/components/middle/EmojiInteractionAnimation.tsx +++ b/src/components/middle/EmojiInteractionAnimation.tsx @@ -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 = ({ - emojiInteraction, effectAnimationId, localEffectAnimation, - isReversed, + activeEmojiInteraction, }) => { const { stopActiveEmojiInteraction } = getDispatch(); const [isHiding, startHiding] = useFlag(false); const [isPlaying, startPlaying] = useFlag(false); + const timeoutRef = useRef(); 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 = ({ } }, [localEffectAnimation]); - const scale = (emojiInteraction.startSize || 0) / EFFECT_SIZE; + if (!activeEmojiInteraction.startSize) { + return undefined; + } + + const scale = (activeEmojiInteraction.startSize || 0) / EFFECT_SIZE; return (
= ({ }; export default memo(withGlobal( - (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)); diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index ea2b3305e..54c55cb8f 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -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 = ({ canSubscribe, canStartBot, canRestartBot, - activeEmojiInteraction, + activeEmojiInteractions, }) => { const { openChat, @@ -522,9 +522,15 @@ const MiddleColumn: FC = ({ onUnpin={handleUnpinAllMessages} /> )} - {activeEmojiInteraction && ( - - )} +
+ {activeEmojiInteractions?.map((activeEmojiInteraction, i) => ( + + ))} +
); }; @@ -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) { diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 10784cbe2..eabe8ec03 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -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 = ({ availableReactions, defaultReaction, activeReaction, - activeEmojiInteraction, + activeEmojiInteractions, messageListType, isPinnedList, isDownloading, @@ -637,7 +637,7 @@ const Message: FC = ({ forceLoadPreview={isLocal} messageId={messageId} chatId={chatId} - activeEmojiInteraction={activeEmojiInteraction} + activeEmojiInteractions={activeEmojiInteractions} /> )} {localSticker && ( @@ -652,7 +652,7 @@ const Message: FC = ({ forceLoadPreview={isLocal} messageId={messageId} chatId={chatId} - activeEmojiInteraction={activeEmojiInteraction} + activeEmojiInteractions={activeEmojiInteractions} /> )} {isAlbum && ( @@ -1055,7 +1055,7 @@ export default memo(withGlobal( 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 }), diff --git a/src/global/types.ts b/src/global/types.ts index 0fc376511..61a7e5127 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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; localTextSearch: { diff --git a/src/modules/actions/api/reactions.ts b/src/modules/actions/api/reactions.ts index b2f5c2ef9..735e1304d 100644 --- a/src/modules/actions/api/reactions.ts +++ b/src/modules/actions/api/reactions.ts @@ -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; + }), }; }); diff --git a/src/modules/actions/apiUpdaters/messages.ts b/src/modules/actions/apiUpdaters/messages.ts index 8bbbfdfd0..663623ff9 100644 --- a/src/modules/actions/apiUpdaters/messages.ts +++ b/src/modules/actions/apiUpdaters/messages.ts @@ -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);