FAB: Scroll to unread reaction on click (#4709)

This commit is contained in:
Alexander Zinchuk 2024-07-15 15:50:30 +02:00
parent e2c6199431
commit a4b72cfd3e
8 changed files with 37 additions and 10 deletions

View File

@ -26,7 +26,8 @@ import type {
} from '../../../global/types'; } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { import type {
FocusDirection, IAlbum, ISettings, ThreadId, FocusDirection, IAlbum, ISettings, ScrollTargetPosition,
ThreadId,
} from '../../../types'; } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
@ -234,6 +235,7 @@ type StateProps = {
focusDirection?: FocusDirection; focusDirection?: FocusDirection;
focusedQuote?: string; focusedQuote?: string;
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
scrollTargetPosition?: ScrollTargetPosition;
isResizingContainer?: boolean; isResizingContainer?: boolean;
isForwarding?: boolean; isForwarding?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
@ -354,6 +356,7 @@ const Message: FC<OwnProps & StateProps> = ({
focusDirection, focusDirection,
focusedQuote, focusedQuote,
noFocusHighlight, noFocusHighlight,
scrollTargetPosition,
isResizingContainer, isResizingContainer,
isForwarding, isForwarding,
isChatWithSelf, isChatWithSelf,
@ -770,7 +773,15 @@ const Message: FC<OwnProps & StateProps> = ({
); );
useFocusMessage( 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; const viaBusinessBotTitle = viaBusinessBot ? getSenderTitle(lang, viaBusinessBot) : undefined;
@ -1659,7 +1670,8 @@ export default memo(withGlobal<OwnProps>(
); );
const { const {
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, quote: focusedQuote, direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
quote: focusedQuote, scrollTargetPosition,
} = (isFocused && focusedMessage) || {}; } = (isFocused && focusedMessage) || {};
const { query: highlight } = selectCurrentTextSearch(global) || {}; const { query: highlight } = selectCurrentTextSearch(global) || {};
@ -1795,6 +1807,7 @@ export default memo(withGlobal<OwnProps>(
noFocusHighlight, noFocusHighlight,
isResizingContainer, isResizingContainer,
focusedQuote, focusedQuote,
scrollTargetPosition,
}), }),
senderBoosts, senderBoosts,
tags: global.savedReactionTags?.byKey, tags: global.savedReactionTags?.byKey,

View File

@ -1,7 +1,7 @@
import { useLayoutEffect, useRef } from '../../../../lib/teact/teact'; import { useLayoutEffect, useRef } from '../../../../lib/teact/teact';
import { addExtraClass } from '../../../../lib/teact/teact-dom'; import { addExtraClass } from '../../../../lib/teact/teact-dom';
import type { FocusDirection } from '../../../../types'; import type { FocusDirection, ScrollTargetPosition } from '../../../../types';
import { import {
requestForcedReflow, requestMeasure, requestMutation, requestForcedReflow, requestMeasure, requestMutation,
@ -22,6 +22,7 @@ export default function useFocusMessage(
isResizingContainer?: boolean, isResizingContainer?: boolean,
isJustAdded?: boolean, isJustAdded?: boolean,
isQuote?: boolean, isQuote?: boolean,
scrollTargetPosition?: ScrollTargetPosition,
) { ) {
const isRelocatedRef = useRef(!isJustAdded); const isRelocatedRef = useRef(!isJustAdded);
@ -33,12 +34,13 @@ export default function useFocusMessage(
const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!; const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!;
// `noFocusHighlight` is always called with “scroll-to-bottom” buttons // `noFocusHighlight` is always called with “scroll-to-bottom” buttons
const isToBottom = noFocusHighlight; const isToBottom = noFocusHighlight;
const scrollPosition = scrollTargetPosition || isToBottom ? 'end' : 'centerOrTop';
const exec = () => { const exec = () => {
const result = animateScroll( const result = animateScroll(
messagesContainer, messagesContainer,
elementRef.current!, elementRef.current!,
isToBottom ? 'end' : 'centerOrTop', scrollPosition,
FOCUS_MARGIN, FOCUS_MARGIN,
focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined, focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined,
focusDirection, focusDirection,
@ -69,6 +71,6 @@ export default function useFocusMessage(
} }
} }
}, [ }, [
elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, scrollTargetPosition,
]); ]);
} }

View File

@ -440,7 +440,9 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy
return undefined; 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 }); actions.markMessagesRead({ messageIds: [chat.unreadReactions[0]], tabId });
return undefined; return undefined;
}); });

View File

@ -400,7 +400,7 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => { addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
const { const {
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId, chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
@ -445,6 +445,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
noHighlight, noHighlight,
isResizingContainer, isResizingContainer,
quote, quote,
scrollTargetPosition,
}, tabId); }, tabId);
global = updateFocusDirection(global, undefined, tabId); global = updateFocusDirection(global, undefined, tabId);

View File

@ -1,7 +1,7 @@
import type { import type {
ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo, ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
} from '../../api/types'; } from '../../api/types';
import type { FocusDirection, ThreadId } from '../../types'; import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types';
import type { import type {
GlobalState, MessageList, MessageListType, TabArgs, TabThread, Thread, GlobalState, MessageList, MessageListType, TabArgs, TabThread, Thread,
} from '../types'; } from '../types';
@ -643,6 +643,7 @@ export function updateFocusedMessage<T extends GlobalState>({
noHighlight = false, noHighlight = false,
isResizingContainer = false, isResizingContainer = false,
quote, quote,
scrollTargetPosition,
}: { }: {
global: T; global: T;
chatId?: string; chatId?: string;
@ -651,6 +652,7 @@ export function updateFocusedMessage<T extends GlobalState>({
noHighlight?: boolean; noHighlight?: boolean;
isResizingContainer?: boolean; isResizingContainer?: boolean;
quote?: string; quote?: string;
scrollTargetPosition?: ScrollTargetPosition;
}, },
...[tabId = getCurrentTabId()]: TabArgs<T>): T { ...[tabId = getCurrentTabId()]: TabArgs<T>): T {
return updateTabState(global, { return updateTabState(global, {
@ -662,6 +664,7 @@ export function updateFocusedMessage<T extends GlobalState>({
noHighlight, noHighlight,
isResizingContainer, isResizingContainer,
quote, quote,
scrollTargetPosition,
}, },
}, tabId); }, tabId);
} }

View File

@ -115,6 +115,7 @@ import type {
PrivacyVisibility, PrivacyVisibility,
ProfileEditProgress, ProfileEditProgress,
ProfileTabType, ProfileTabType,
ScrollTargetPosition,
SettingsScreens, SettingsScreens,
SharedMediaType, SharedMediaType,
ShippingOption, ShippingOption,
@ -311,6 +312,7 @@ export type TabState = {
noHighlight?: boolean; noHighlight?: boolean;
isResizingContainer?: boolean; isResizingContainer?: boolean;
quote?: string; quote?: string;
scrollTargetPosition?: ScrollTargetPosition;
}; };
selectedMessages?: { selectedMessages?: {
@ -1908,6 +1910,7 @@ export interface ActionPayloads {
shouldReplaceHistory?: boolean; shouldReplaceHistory?: boolean;
noForumTopicPanel?: boolean; noForumTopicPanel?: boolean;
quote?: string; quote?: string;
scrollTargetPosition?: ScrollTargetPosition;
} & WithTabId; } & WithTabId;
focusLastMessage: WithTabId | undefined; focusLastMessage: WithTabId | undefined;

View File

@ -30,6 +30,8 @@ export enum FocusDirection {
Static, Static,
} }
export type ScrollTargetPosition = ScrollLogicalPosition | 'centerOrTop';
export interface IAlbum { export interface IAlbum {
albumId: string; albumId: string;
messages: ApiMessage[]; messages: ApiMessage[];

View File

@ -1,5 +1,6 @@
import { getGlobal } from '../global'; import { getGlobal } from '../global';
import type { ScrollTargetPosition } from '../types';
import { FocusDirection } from '../types'; import { FocusDirection } from '../types';
import { import {
@ -49,7 +50,7 @@ export function restartCurrentScrollAnimation() {
function createMutateFunction( function createMutateFunction(
container: HTMLElement, container: HTMLElement,
element: HTMLElement, element: HTMLElement,
position: ScrollLogicalPosition | 'centerOrTop', position: ScrollTargetPosition,
margin = 0, margin = 0,
maxDistance = FAST_SMOOTH_MAX_DISTANCE, maxDistance = FAST_SMOOTH_MAX_DISTANCE,
forceDirection?: FocusDirection, forceDirection?: FocusDirection,