From a4b72cfd3e7c81ed69f42dea2db8c21e8c5b16cb Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 15 Jul 2024 15:50:30 +0200 Subject: [PATCH] FAB: Scroll to unread reaction on click (#4709) --- src/components/middle/message/Message.tsx | 19 ++++++++++++++++--- .../middle/message/hooks/useFocusMessage.ts | 8 +++++--- src/global/actions/api/reactions.ts | 4 +++- src/global/actions/ui/messages.ts | 3 ++- src/global/reducers/messages.ts | 5 ++++- src/global/types.ts | 3 +++ src/types/index.ts | 2 ++ src/util/animateScroll.ts | 3 ++- 8 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 764cebb5c..b0cf60a38 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -26,7 +26,8 @@ import type { } from '../../../global/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { - FocusDirection, IAlbum, ISettings, ThreadId, + FocusDirection, IAlbum, ISettings, ScrollTargetPosition, + ThreadId, } from '../../../types'; import type { Signal } from '../../../util/signals'; import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage'; @@ -234,6 +235,7 @@ type StateProps = { focusDirection?: FocusDirection; focusedQuote?: string; noFocusHighlight?: boolean; + scrollTargetPosition?: ScrollTargetPosition; isResizingContainer?: boolean; isForwarding?: boolean; isChatWithSelf?: boolean; @@ -354,6 +356,7 @@ const Message: FC = ({ focusDirection, focusedQuote, noFocusHighlight, + scrollTargetPosition, isResizingContainer, isForwarding, isChatWithSelf, @@ -770,7 +773,15 @@ const Message: FC = ({ ); useFocusMessage( - ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded, Boolean(focusedQuote), + ref, + chatId, + isFocused, + focusDirection, + noFocusHighlight, + isResizingContainer, + isJustAdded, + Boolean(focusedQuote), + scrollTargetPosition, ); const viaBusinessBotTitle = viaBusinessBot ? getSenderTitle(lang, viaBusinessBot) : undefined; @@ -1659,7 +1670,8 @@ export default memo(withGlobal( ); const { - direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, quote: focusedQuote, + direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, + quote: focusedQuote, scrollTargetPosition, } = (isFocused && focusedMessage) || {}; const { query: highlight } = selectCurrentTextSearch(global) || {}; @@ -1795,6 +1807,7 @@ export default memo(withGlobal( noFocusHighlight, isResizingContainer, focusedQuote, + scrollTargetPosition, }), senderBoosts, tags: global.savedReactionTags?.byKey, diff --git a/src/components/middle/message/hooks/useFocusMessage.ts b/src/components/middle/message/hooks/useFocusMessage.ts index be937b1cf..05edba787 100644 --- a/src/components/middle/message/hooks/useFocusMessage.ts +++ b/src/components/middle/message/hooks/useFocusMessage.ts @@ -1,7 +1,7 @@ import { useLayoutEffect, useRef } from '../../../../lib/teact/teact'; import { addExtraClass } from '../../../../lib/teact/teact-dom'; -import type { FocusDirection } from '../../../../types'; +import type { FocusDirection, ScrollTargetPosition } from '../../../../types'; import { requestForcedReflow, requestMeasure, requestMutation, @@ -22,6 +22,7 @@ export default function useFocusMessage( isResizingContainer?: boolean, isJustAdded?: boolean, isQuote?: boolean, + scrollTargetPosition?: ScrollTargetPosition, ) { const isRelocatedRef = useRef(!isJustAdded); @@ -33,12 +34,13 @@ export default function useFocusMessage( const messagesContainer = elementRef.current.closest('.MessageList')!; // `noFocusHighlight` is always called with “scroll-to-bottom” buttons const isToBottom = noFocusHighlight; + const scrollPosition = scrollTargetPosition || isToBottom ? 'end' : 'centerOrTop'; const exec = () => { const result = animateScroll( messagesContainer, elementRef.current!, - isToBottom ? 'end' : 'centerOrTop', + scrollPosition, FOCUS_MARGIN, focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined, focusDirection, @@ -69,6 +71,6 @@ export default function useFocusMessage( } } }, [ - elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, + elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, scrollTargetPosition, ]); } diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 8df8ff3f7..077790f97 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -440,7 +440,9 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy return undefined; } - actions.focusMessage({ chatId: chat.id, messageId: chat.unreadReactions[0], tabId }); + actions.focusMessage({ + chatId: chat.id, messageId: chat.unreadReactions[0], tabId, scrollTargetPosition: 'end', + }); actions.markMessagesRead({ messageIds: [chat.unreadReactions[0]], tabId }); return undefined; }); diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index d5f6ef6b9..dd48539e2 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -400,7 +400,7 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => { const { chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId, - replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, + replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition, tabId = getCurrentTabId(), } = payload; @@ -445,6 +445,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => noHighlight, isResizingContainer, quote, + scrollTargetPosition, }, tabId); global = updateFocusDirection(global, undefined, tabId); diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 75a9787ef..8d0131f09 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -1,7 +1,7 @@ import type { ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo, } from '../../api/types'; -import type { FocusDirection, ThreadId } from '../../types'; +import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types'; import type { GlobalState, MessageList, MessageListType, TabArgs, TabThread, Thread, } from '../types'; @@ -643,6 +643,7 @@ export function updateFocusedMessage({ noHighlight = false, isResizingContainer = false, quote, + scrollTargetPosition, }: { global: T; chatId?: string; @@ -651,6 +652,7 @@ export function updateFocusedMessage({ noHighlight?: boolean; isResizingContainer?: boolean; quote?: string; + scrollTargetPosition?: ScrollTargetPosition; }, ...[tabId = getCurrentTabId()]: TabArgs): T { return updateTabState(global, { @@ -662,6 +664,7 @@ export function updateFocusedMessage({ noHighlight, isResizingContainer, quote, + scrollTargetPosition, }, }, tabId); } diff --git a/src/global/types.ts b/src/global/types.ts index 085f0f985..534ca392c 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -115,6 +115,7 @@ import type { PrivacyVisibility, ProfileEditProgress, ProfileTabType, + ScrollTargetPosition, SettingsScreens, SharedMediaType, ShippingOption, @@ -311,6 +312,7 @@ export type TabState = { noHighlight?: boolean; isResizingContainer?: boolean; quote?: string; + scrollTargetPosition?: ScrollTargetPosition; }; selectedMessages?: { @@ -1908,6 +1910,7 @@ export interface ActionPayloads { shouldReplaceHistory?: boolean; noForumTopicPanel?: boolean; quote?: string; + scrollTargetPosition?: ScrollTargetPosition; } & WithTabId; focusLastMessage: WithTabId | undefined; diff --git a/src/types/index.ts b/src/types/index.ts index 7a08969e6..180ef44ae 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -30,6 +30,8 @@ export enum FocusDirection { Static, } +export type ScrollTargetPosition = ScrollLogicalPosition | 'centerOrTop'; + export interface IAlbum { albumId: string; messages: ApiMessage[]; diff --git a/src/util/animateScroll.ts b/src/util/animateScroll.ts index b59637a20..8de840671 100644 --- a/src/util/animateScroll.ts +++ b/src/util/animateScroll.ts @@ -1,5 +1,6 @@ import { getGlobal } from '../global'; +import type { ScrollTargetPosition } from '../types'; import { FocusDirection } from '../types'; import { @@ -49,7 +50,7 @@ export function restartCurrentScrollAnimation() { function createMutateFunction( container: HTMLElement, element: HTMLElement, - position: ScrollLogicalPosition | 'centerOrTop', + position: ScrollTargetPosition, margin = 0, maxDistance = FAST_SMOOTH_MAX_DISTANCE, forceDirection?: FocusDirection,