From 11cbab7d39019fe288a8d61d05828c41486d073b Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 19 Apr 2024 13:38:50 +0400 Subject: [PATCH] Floating Actions Buttons: Fix animations (#3575) Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> --- src/@types/global.d.ts | 1 + .../middle/FloatingActionButtons.module.scss | 2 +- .../middle/FloatingActionButtons.tsx | 18 +++++++----- src/components/middle/MessageList.tsx | 14 ++++++--- src/components/middle/MessageListContent.tsx | 6 ++-- src/components/middle/MiddleColumn.tsx | 14 +++++---- .../middle/ScrollDownButton.module.scss | 3 +- src/components/middle/hooks/useScrollHooks.ts | 10 +++---- src/components/middle/message/Message.tsx | 11 +++++-- src/global/actions/api/messages.ts | 29 +++++++++++++++---- src/global/actions/api/reactions.ts | 12 ++++++-- src/global/actions/apiUpdaters/chats.ts | 14 ++++----- src/hooks/usePrevDuringAnimation.ts | 4 +-- 13 files changed, 91 insertions(+), 47 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 3bc24ea3c..a62c73d94 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -38,6 +38,7 @@ type AnyLiteral = Record; type AnyClass = new (...args: any[]) => any; type AnyFunction = (...args: any[]) => any; type AnyToVoidFunction = (...args: any[]) => void; +type BooleanToVoidFunction = (value: boolean) => void; type NoneToVoidFunction = () => void; type EmojiCategory = { diff --git a/src/components/middle/FloatingActionButtons.module.scss b/src/components/middle/FloatingActionButtons.module.scss index b0d09daf7..d0c4cdcb4 100644 --- a/src/components/middle/FloatingActionButtons.module.scss +++ b/src/components/middle/FloatingActionButtons.module.scss @@ -55,7 +55,7 @@ } } - &.only-reactions { + &.hide-scroll-down { transform: translateY(4rem); .unread { diff --git a/src/components/middle/FloatingActionButtons.tsx b/src/components/middle/FloatingActionButtons.tsx index ed0170636..71366b47d 100644 --- a/src/components/middle/FloatingActionButtons.tsx +++ b/src/components/middle/FloatingActionButtons.tsx @@ -16,7 +16,7 @@ import ScrollDownButton from './ScrollDownButton'; import styles from './FloatingActionButtons.module.scss'; type OwnProps = { - isShown: boolean; + withScrollDown: boolean; canPost?: boolean; withExtraShift?: boolean; }; @@ -32,7 +32,7 @@ type StateProps = { const FOCUS_MARGIN = 20; const FloatingActionButtons: FC = ({ - isShown, + withScrollDown, canPost, messageListType, chatId, @@ -64,15 +64,17 @@ const FloatingActionButtons: FC = ({ } }, [chatId, fetchUnreadMentions, hasUnreadMentions]); - const handleClick = useLastCallback(() => { - if (!isShown) { + const handleScrollDownClick = useLastCallback(() => { + if (!withScrollDown) { return; } if (messageListType === 'thread') { focusNextReply(); } else { - const messagesContainer = elementRef.current!.parentElement!.querySelector('.MessageList')!; + const messagesContainer = elementRef.current!.parentElement!.querySelector( + '.Transition_slide-active > .MessageList', + )!; const messageElements = messagesContainer.querySelectorAll('.message-list-item'); const lastMessageElement = messageElements[messageElements.length - 1]; if (!lastMessageElement) { @@ -85,8 +87,8 @@ const FloatingActionButtons: FC = ({ const fabClassName = buildClassName( styles.root, - (isShown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed, - (Boolean(reactionsCount) || Boolean(mentionsCount)) && !isShown && styles.onlyReactions, + (withScrollDown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed, + (Boolean(reactionsCount) || Boolean(mentionsCount)) && !withScrollDown && styles.hideScrollDown, !canPost && styles.noComposer, !withExtraShift && styles.noExtraShift, ); @@ -118,7 +120,7 @@ const FloatingActionButtons: FC = ({ diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 1104f8f71..6189172b9 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -90,8 +90,8 @@ type OwnProps = { isComments?: boolean; canPost: boolean; isReady: boolean; - onFabToggle: (shouldShow: boolean) => void; - onNotchToggle: (shouldShow: boolean) => void; + onScrollDownToggle: BooleanToVoidFunction; + onNotchToggle: BooleanToVoidFunction; hasTools?: boolean; withBottomShift?: boolean; withDefaultBg: boolean; @@ -148,7 +148,7 @@ const MessageList: FC = ({ threadId, type, hasTools, - onFabToggle, + onScrollDownToggle, onNotchToggle, isCurrentUserPremium, isChatLoaded, @@ -597,6 +597,12 @@ const MessageList: FC = ({ const hasMessages = (messageIds && messageGroups) || lastMessage; + useEffect(() => { + if (hasMessages) return; + + onScrollDownToggle(false); + }, [hasMessages, onScrollDownToggle]); + return (
= ({ isSchedule={messageGroups ? type === 'scheduled' : false} shouldRenderBotInfo={isBot} noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} - onFabToggle={onFabToggle} + onScrollDownToggle={onScrollDownToggle} onNotchToggle={onNotchToggle} onPinnedIntersectionChange={onPinnedIntersectionChange} /> diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 9d9eff96e..e49e65038 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -56,7 +56,7 @@ interface OwnProps { shouldRenderBotInfo?: boolean; noAppearanceAnimation: boolean; isSavedDialog?: boolean; - onFabToggle: AnyToVoidFunction; + onScrollDownToggle: BooleanToVoidFunction; onNotchToggle: AnyToVoidFunction; onPinnedIntersectionChange: PinnedIntersectionChangedCallback; } @@ -88,7 +88,7 @@ const MessageListContent: FC = ({ shouldRenderBotInfo, noAppearanceAnimation, isSavedDialog, - onFabToggle, + onScrollDownToggle, onNotchToggle, onPinnedIntersectionChange, }) => { @@ -115,7 +115,7 @@ const MessageListContent: FC = ({ getContainerHeight, isViewportNewest, isUnread, - onFabToggle, + onScrollDownToggle, onNotchToggle, isReady, ); diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index e4389bf39..1686f2407 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -238,7 +238,7 @@ function MiddleColumn({ const lang = useLang(); const [dropAreaState, setDropAreaState] = useState(DropAreaState.None); - const [isFabShown, setIsFabShown] = useState(); + const [isScrollDownShown, setIsScrollDownShown] = useState(false); const [isNotchShown, setIsNotchShown] = useState(); const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); @@ -273,7 +273,9 @@ function MiddleColumn({ && !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock && chatId !== TMP_CHAT_ID && !isContactRequirePremium; const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration); - const renderingIsFabShown = usePrevDuringAnimation(isFabShown, closeAnimationDuration) && chatId !== TMP_CHAT_ID; + const renderingIsScrollDownShown = usePrevDuringAnimation( + isScrollDownShown, closeAnimationDuration, + ) && chatId !== TMP_CHAT_ID; const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration); const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration); const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration); @@ -489,9 +491,9 @@ function MiddleColumn({ ); const withMessageListBottomShift = Boolean( renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot - || isPinnedMessageList || canShowOpenChatButton || renderingCanUnblock, + || (isPinnedMessageList && canUnpin) || canShowOpenChatButton || renderingCanUnblock, ); - const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList); + const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive); return (
diff --git a/src/components/middle/ScrollDownButton.module.scss b/src/components/middle/ScrollDownButton.module.scss index b395fcaa6..f165bd787 100644 --- a/src/components/middle/ScrollDownButton.module.scss +++ b/src/components/middle/ScrollDownButton.module.scss @@ -64,7 +64,8 @@ top: -0.3125rem; right: -0.3125rem; - background: var(--color-green); + background-color: var(--color-green); + border: 1px solid var(--color-background); color: white; pointer-events: none; diff --git a/src/components/middle/hooks/useScrollHooks.ts b/src/components/middle/hooks/useScrollHooks.ts index c247fcee6..5c3d0f7c1 100644 --- a/src/components/middle/hooks/useScrollHooks.ts +++ b/src/components/middle/hooks/useScrollHooks.ts @@ -28,8 +28,8 @@ export default function useScrollHooks( getContainerHeight: Signal, isViewportNewest: boolean, isUnread: boolean, - onFabToggle: AnyToVoidFunction, - onNotchToggle: AnyToVoidFunction, + onScrollDownToggle: BooleanToVoidFunction, + onNotchToggle: BooleanToVoidFunction, isReady: boolean, ) { const { loadViewportMessages } = getActions(); @@ -54,13 +54,13 @@ export default function useScrollHooks( if (!isReady) return; if (!messageIds?.length) { - onFabToggle(false); + onScrollDownToggle(false); onNotchToggle(false); return; } if (!isViewportNewest) { - onFabToggle(true); + onScrollDownToggle(true); onNotchToggle(true); return; } @@ -77,7 +77,7 @@ export default function useScrollHooks( if (scrollHeight === 0) return; - onFabToggle(isUnread ? !isAtBottom : !isNearBottom); + onScrollDownToggle(isUnread ? !isAtBottom : !isNearBottom); onNotchToggle(!isAtBottom); }); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 3b797cf57..ab2bc27a9 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -410,6 +410,7 @@ const Message: FC = ({ disableContextMenuHint, animateUnreadReaction, focusLastMessage, + markMentionsRead, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -795,10 +796,16 @@ const Message: FC = ({ useEffect(() => { const bottomMarker = bottomMarkerRef.current; - if (hasUnreadReaction && bottomMarker && isElementInViewport(bottomMarker)) { + if (!bottomMarker || !isElementInViewport(bottomMarker)) return; + + if (hasUnreadReaction) { animateUnreadReaction({ messageIds: [messageId] }); } - }, [hasUnreadReaction, messageId, animateUnreadReaction]); + + if (message.hasUnreadMention) { + markMentionsRead({ messageIds: [messageId] }); + } + }, [hasUnreadReaction, messageId, animateUnreadReaction, message.hasUnreadMention]); const albumLayout = useMemo(() => { return isAlbum diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index cb042a35d..54f0e5dc5 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -1550,6 +1550,10 @@ addActionHandler('clickSponsoredMessage', (global, actions, payload): ActionRetu addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise => { const { chatId, offsetId } = payload; + await fetchUnreadMentions(global, chatId, offsetId); +}); + +async function fetchUnreadMentions(global: T, chatId: string, offsetId?: number) { const chat = selectChat(global, chatId); if (!chat) return; @@ -1571,7 +1575,7 @@ addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promis }); setGlobal(global); -}); +} addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => { const { messageIds, tabId = getCurrentTabId() } = payload; @@ -1579,8 +1583,15 @@ addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnTyp const chat = selectCurrentChat(global, tabId); if (!chat) return; - const unreadMentions = (chat.unreadMentions || []).filter((id) => !messageIds.includes(id)); + const currentUnreadMentions = chat.unreadMentions || []; + + const unreadMentions = currentUnreadMentions.filter((id) => !messageIds.includes(id)); + const removedCount = currentUnreadMentions.length - unreadMentions.length; + global = updateChat(global, chat.id, { + ...(chat.unreadMentionsCount && { + unreadMentionsCount: Math.max(chat.unreadMentionsCount - removedCount, 0) || undefined, + }), unreadMentions, }); @@ -1589,12 +1600,20 @@ addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnTyp actions.markMessagesRead({ messageIds, tabId }); }); -addActionHandler('focusNextMention', (global, actions, payload): ActionReturnType => { +addActionHandler('focusNextMention', async (global, actions, payload): Promise => { const { tabId = getCurrentTabId() } = payload || {}; - const chat = selectCurrentChat(global, tabId); + let chat = selectCurrentChat(global, tabId); - if (!chat?.unreadMentions) return; + if (!chat) return; + + if (!chat.unreadMentions) { + await fetchUnreadMentions(global, chat.id); + global = getGlobal(); + const previousChatId = chat.id; + chat = selectCurrentChat(global, tabId); + if (!chat?.unreadMentions || previousChatId !== chat.id) return; + } actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0], tabId }); }); diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 793acf65f..5b6254b25 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -17,7 +17,7 @@ import { } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { - addChatMessagesById, addChats, addUsers, updateChatMessage, + addChatMessagesById, addChats, addUsers, updateChat, updateChatMessage, } from '../../reducers'; import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions'; import { updateTabState } from '../../reducers/tabs'; @@ -437,9 +437,17 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy const { tabId = getCurrentTabId() } = payload || {}; const chat = selectCurrentChat(global, tabId); - if (!chat?.unreadReactions) return; + if (!chat?.unreadReactions) { + if (chat?.unreadReactionsCount) { + return updateChat(global, chat.id, { + unreadReactionsCount: 0, + }); + } + return undefined; + } actions.focusMessage({ chatId: chat.id, messageId: chat.unreadReactions[0], tabId }); + return undefined; }); addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => { diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index 7444d1583..dfd756be0 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -7,7 +7,7 @@ import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { isLocalMessageId } from '../../../util/messageKey'; import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications'; import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages'; -import { isChatChannel } from '../../helpers'; +import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal, } from '../../index'; @@ -206,24 +206,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { case 'updateCommonBoxMessages': case 'updateChannelMessages': { const { ids, messageUpdate } = update; - if (messageUpdate.hasUnreadMention !== false) { - return undefined; - } ids.forEach((id) => { const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!; const chat = selectChat(global, chatId); - if (chat?.unreadReactionsCount) { + if (messageUpdate.reactions && chat?.unreadReactionsCount + && !checkIfHasUnreadReactions(global, messageUpdate.reactions)) { global = updateUnreadReactions(global, chatId, { - unreadReactionsCount: (chat.unreadReactionsCount - 1) || undefined, + unreadReactionsCount: Math.max(chat.unreadReactionsCount - 1, 0) || undefined, unreadReactions: chat.unreadReactions?.filter((i) => i !== id), }); } - if (chat?.unreadMentionsCount) { + if (!messageUpdate.hasUnreadMention && chat?.unreadMentionsCount) { global = updateChat(global, chatId, { - unreadMentionsCount: (chat.unreadMentionsCount - 1) || undefined, + unreadMentionsCount: Math.max(chat.unreadMentionsCount - 1, 0) || undefined, unreadMentions: chat.unreadMentions?.filter((i) => i !== id), }); } diff --git a/src/hooks/usePrevDuringAnimation.ts b/src/hooks/usePrevDuringAnimation.ts index 31cfb51a8..d5040a7fd 100644 --- a/src/hooks/usePrevDuringAnimation.ts +++ b/src/hooks/usePrevDuringAnimation.ts @@ -4,7 +4,7 @@ import useForceUpdate from './useForceUpdate'; import usePrevious from './usePrevious'; import useSyncEffect from './useSyncEffect'; -export default function usePrevDuringAnimation(current: T, duration?: number) { +export default function usePrevDuringAnimation(current: T, duration?: number): T { const prev = usePrevious(current, true); const timeoutRef = useRef(); const forceUpdate = useForceUpdate(); @@ -28,5 +28,5 @@ export default function usePrevDuringAnimation(current: T, duration?: number) } }, [duration, forceUpdate, isCurrentPresent, isPrevPresent]); - return !timeoutRef.current || !duration || isCurrentPresent ? current : prev; + return (!timeoutRef.current || !duration || isCurrentPresent ? current : prev)!; }