Floating Actions Buttons: Fix animations (#3575)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2024-04-19 13:38:50 +04:00
parent 2688cd768a
commit 11cbab7d39
13 changed files with 91 additions and 47 deletions

View File

@ -38,6 +38,7 @@ type AnyLiteral = Record<string, any>;
type AnyClass = new (...args: any[]) => any; type AnyClass = new (...args: any[]) => any;
type AnyFunction = (...args: any[]) => any; type AnyFunction = (...args: any[]) => any;
type AnyToVoidFunction = (...args: any[]) => void; type AnyToVoidFunction = (...args: any[]) => void;
type BooleanToVoidFunction = (value: boolean) => void;
type NoneToVoidFunction = () => void; type NoneToVoidFunction = () => void;
type EmojiCategory = { type EmojiCategory = {

View File

@ -55,7 +55,7 @@
} }
} }
&.only-reactions { &.hide-scroll-down {
transform: translateY(4rem); transform: translateY(4rem);
.unread { .unread {

View File

@ -16,7 +16,7 @@ import ScrollDownButton from './ScrollDownButton';
import styles from './FloatingActionButtons.module.scss'; import styles from './FloatingActionButtons.module.scss';
type OwnProps = { type OwnProps = {
isShown: boolean; withScrollDown: boolean;
canPost?: boolean; canPost?: boolean;
withExtraShift?: boolean; withExtraShift?: boolean;
}; };
@ -32,7 +32,7 @@ type StateProps = {
const FOCUS_MARGIN = 20; const FOCUS_MARGIN = 20;
const FloatingActionButtons: FC<OwnProps & StateProps> = ({ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
isShown, withScrollDown,
canPost, canPost,
messageListType, messageListType,
chatId, chatId,
@ -64,15 +64,17 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
} }
}, [chatId, fetchUnreadMentions, hasUnreadMentions]); }, [chatId, fetchUnreadMentions, hasUnreadMentions]);
const handleClick = useLastCallback(() => { const handleScrollDownClick = useLastCallback(() => {
if (!isShown) { if (!withScrollDown) {
return; return;
} }
if (messageListType === 'thread') { if (messageListType === 'thread') {
focusNextReply(); focusNextReply();
} else { } else {
const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>('.MessageList')!; const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>(
'.Transition_slide-active > .MessageList',
)!;
const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item'); const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item');
const lastMessageElement = messageElements[messageElements.length - 1]; const lastMessageElement = messageElements[messageElements.length - 1];
if (!lastMessageElement) { if (!lastMessageElement) {
@ -85,8 +87,8 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
const fabClassName = buildClassName( const fabClassName = buildClassName(
styles.root, styles.root,
(isShown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed, (withScrollDown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed,
(Boolean(reactionsCount) || Boolean(mentionsCount)) && !isShown && styles.onlyReactions, (Boolean(reactionsCount) || Boolean(mentionsCount)) && !withScrollDown && styles.hideScrollDown,
!canPost && styles.noComposer, !canPost && styles.noComposer,
!withExtraShift && styles.noExtraShift, !withExtraShift && styles.noExtraShift,
); );
@ -118,7 +120,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
<ScrollDownButton <ScrollDownButton
icon="arrow-down" icon="arrow-down"
ariaLabelLang="AccDescrPageDown" ariaLabelLang="AccDescrPageDown"
onClick={handleClick} onClick={handleScrollDownClick}
unreadCount={unreadCount} unreadCount={unreadCount}
className={styles.unread} className={styles.unread}
/> />

View File

@ -90,8 +90,8 @@ type OwnProps = {
isComments?: boolean; isComments?: boolean;
canPost: boolean; canPost: boolean;
isReady: boolean; isReady: boolean;
onFabToggle: (shouldShow: boolean) => void; onScrollDownToggle: BooleanToVoidFunction;
onNotchToggle: (shouldShow: boolean) => void; onNotchToggle: BooleanToVoidFunction;
hasTools?: boolean; hasTools?: boolean;
withBottomShift?: boolean; withBottomShift?: boolean;
withDefaultBg: boolean; withDefaultBg: boolean;
@ -148,7 +148,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
threadId, threadId,
type, type,
hasTools, hasTools,
onFabToggle, onScrollDownToggle,
onNotchToggle, onNotchToggle,
isCurrentUserPremium, isCurrentUserPremium,
isChatLoaded, isChatLoaded,
@ -597,6 +597,12 @@ const MessageList: FC<OwnProps & StateProps> = ({
const hasMessages = (messageIds && messageGroups) || lastMessage; const hasMessages = (messageIds && messageGroups) || lastMessage;
useEffect(() => {
if (hasMessages) return;
onScrollDownToggle(false);
}, [hasMessages, onScrollDownToggle]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@ -650,7 +656,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isSchedule={messageGroups ? type === 'scheduled' : false} isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderBotInfo={isBot} shouldRenderBotInfo={isBot}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onFabToggle={onFabToggle} onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle} onNotchToggle={onNotchToggle}
onPinnedIntersectionChange={onPinnedIntersectionChange} onPinnedIntersectionChange={onPinnedIntersectionChange}
/> />

View File

@ -56,7 +56,7 @@ interface OwnProps {
shouldRenderBotInfo?: boolean; shouldRenderBotInfo?: boolean;
noAppearanceAnimation: boolean; noAppearanceAnimation: boolean;
isSavedDialog?: boolean; isSavedDialog?: boolean;
onFabToggle: AnyToVoidFunction; onScrollDownToggle: BooleanToVoidFunction;
onNotchToggle: AnyToVoidFunction; onNotchToggle: AnyToVoidFunction;
onPinnedIntersectionChange: PinnedIntersectionChangedCallback; onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
} }
@ -88,7 +88,7 @@ const MessageListContent: FC<OwnProps> = ({
shouldRenderBotInfo, shouldRenderBotInfo,
noAppearanceAnimation, noAppearanceAnimation,
isSavedDialog, isSavedDialog,
onFabToggle, onScrollDownToggle,
onNotchToggle, onNotchToggle,
onPinnedIntersectionChange, onPinnedIntersectionChange,
}) => { }) => {
@ -115,7 +115,7 @@ const MessageListContent: FC<OwnProps> = ({
getContainerHeight, getContainerHeight,
isViewportNewest, isViewportNewest,
isUnread, isUnread,
onFabToggle, onScrollDownToggle,
onNotchToggle, onNotchToggle,
isReady, isReady,
); );

View File

@ -238,7 +238,7 @@ function MiddleColumn({
const lang = useLang(); const lang = useLang();
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None); const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
const [isFabShown, setIsFabShown] = useState<boolean | undefined>(); const [isScrollDownShown, setIsScrollDownShown] = useState(false);
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>(); const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
@ -273,7 +273,9 @@ function MiddleColumn({
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock && !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock
&& chatId !== TMP_CHAT_ID && !isContactRequirePremium; && chatId !== TMP_CHAT_ID && !isContactRequirePremium;
const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration); 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 renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration);
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration); const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration);
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration); const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration);
@ -489,9 +491,9 @@ function MiddleColumn({
); );
const withMessageListBottomShift = Boolean( const withMessageListBottomShift = Boolean(
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
|| isPinnedMessageList || canShowOpenChatButton || renderingCanUnblock, || (isPinnedMessageList && canUnpin) || canShowOpenChatButton || renderingCanUnblock,
); );
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList); const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive);
return ( return (
<div <div
@ -552,7 +554,7 @@ function MiddleColumn({
isComments={isComments} isComments={isComments}
canPost={renderingCanPost!} canPost={renderingCanPost!}
hasTools={renderingHasTools} hasTools={renderingHasTools}
onFabToggle={setIsFabShown} onScrollDownToggle={setIsScrollDownShown}
onNotchToggle={setIsNotchShown} onNotchToggle={setIsNotchShown}
isReady={isReady} isReady={isReady}
isContactRequirePremium={isContactRequirePremium} isContactRequirePremium={isContactRequirePremium}
@ -693,7 +695,7 @@ function MiddleColumn({
</Transition> </Transition>
<FloatingActionButtons <FloatingActionButtons
isShown={renderingIsFabShown!} withScrollDown={renderingIsScrollDownShown}
canPost={renderingCanPost} canPost={renderingCanPost}
withExtraShift={withExtraShift} withExtraShift={withExtraShift}
/> />

View File

@ -64,7 +64,8 @@
top: -0.3125rem; top: -0.3125rem;
right: -0.3125rem; right: -0.3125rem;
background: var(--color-green); background-color: var(--color-green);
border: 1px solid var(--color-background);
color: white; color: white;
pointer-events: none; pointer-events: none;

View File

@ -28,8 +28,8 @@ export default function useScrollHooks(
getContainerHeight: Signal<number | undefined>, getContainerHeight: Signal<number | undefined>,
isViewportNewest: boolean, isViewportNewest: boolean,
isUnread: boolean, isUnread: boolean,
onFabToggle: AnyToVoidFunction, onScrollDownToggle: BooleanToVoidFunction,
onNotchToggle: AnyToVoidFunction, onNotchToggle: BooleanToVoidFunction,
isReady: boolean, isReady: boolean,
) { ) {
const { loadViewportMessages } = getActions(); const { loadViewportMessages } = getActions();
@ -54,13 +54,13 @@ export default function useScrollHooks(
if (!isReady) return; if (!isReady) return;
if (!messageIds?.length) { if (!messageIds?.length) {
onFabToggle(false); onScrollDownToggle(false);
onNotchToggle(false); onNotchToggle(false);
return; return;
} }
if (!isViewportNewest) { if (!isViewportNewest) {
onFabToggle(true); onScrollDownToggle(true);
onNotchToggle(true); onNotchToggle(true);
return; return;
} }
@ -77,7 +77,7 @@ export default function useScrollHooks(
if (scrollHeight === 0) return; if (scrollHeight === 0) return;
onFabToggle(isUnread ? !isAtBottom : !isNearBottom); onScrollDownToggle(isUnread ? !isAtBottom : !isNearBottom);
onNotchToggle(!isAtBottom); onNotchToggle(!isAtBottom);
}); });

View File

@ -410,6 +410,7 @@ const Message: FC<OwnProps & StateProps> = ({
disableContextMenuHint, disableContextMenuHint,
animateUnreadReaction, animateUnreadReaction,
focusLastMessage, focusLastMessage,
markMentionsRead,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -795,10 +796,16 @@ const Message: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
const bottomMarker = bottomMarkerRef.current; const bottomMarker = bottomMarkerRef.current;
if (hasUnreadReaction && bottomMarker && isElementInViewport(bottomMarker)) { if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
if (hasUnreadReaction) {
animateUnreadReaction({ messageIds: [messageId] }); animateUnreadReaction({ messageIds: [messageId] });
} }
}, [hasUnreadReaction, messageId, animateUnreadReaction]);
if (message.hasUnreadMention) {
markMentionsRead({ messageIds: [messageId] });
}
}, [hasUnreadReaction, messageId, animateUnreadReaction, message.hasUnreadMention]);
const albumLayout = useMemo(() => { const albumLayout = useMemo(() => {
return isAlbum return isAlbum

View File

@ -1550,6 +1550,10 @@ addActionHandler('clickSponsoredMessage', (global, actions, payload): ActionRetu
addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => { addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => {
const { chatId, offsetId } = payload; const { chatId, offsetId } = payload;
await fetchUnreadMentions(global, chatId, offsetId);
});
async function fetchUnreadMentions<T extends GlobalState>(global: T, chatId: string, offsetId?: number) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
@ -1571,7 +1575,7 @@ addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promis
}); });
setGlobal(global); setGlobal(global);
}); }
addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => { addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => {
const { messageIds, tabId = getCurrentTabId() } = payload; const { messageIds, tabId = getCurrentTabId() } = payload;
@ -1579,8 +1583,15 @@ addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnTyp
const chat = selectCurrentChat(global, tabId); const chat = selectCurrentChat(global, tabId);
if (!chat) return; 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, { global = updateChat(global, chat.id, {
...(chat.unreadMentionsCount && {
unreadMentionsCount: Math.max(chat.unreadMentionsCount - removedCount, 0) || undefined,
}),
unreadMentions, unreadMentions,
}); });
@ -1589,12 +1600,20 @@ addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnTyp
actions.markMessagesRead({ messageIds, tabId }); actions.markMessagesRead({ messageIds, tabId });
}); });
addActionHandler('focusNextMention', (global, actions, payload): ActionReturnType => { addActionHandler('focusNextMention', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {}; 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 }); actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0], tabId });
}); });

View File

@ -17,7 +17,7 @@ import {
} from '../../helpers'; } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
addChatMessagesById, addChats, addUsers, updateChatMessage, addChatMessagesById, addChats, addUsers, updateChat, updateChatMessage,
} from '../../reducers'; } from '../../reducers';
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions'; import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
@ -437,9 +437,17 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const chat = selectCurrentChat(global, tabId); 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 }); actions.focusMessage({ chatId: chat.id, messageId: chat.unreadReactions[0], tabId });
return undefined;
}); });
addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => { addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => {

View File

@ -7,7 +7,7 @@ import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { isLocalMessageId } from '../../../util/messageKey'; import { isLocalMessageId } from '../../../util/messageKey';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications'; import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages'; import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
import { isChatChannel } from '../../helpers'; import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';
import { import {
addActionHandler, getGlobal, setGlobal, addActionHandler, getGlobal, setGlobal,
} from '../../index'; } from '../../index';
@ -206,24 +206,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateCommonBoxMessages': case 'updateCommonBoxMessages':
case 'updateChannelMessages': { case 'updateChannelMessages': {
const { ids, messageUpdate } = update; const { ids, messageUpdate } = update;
if (messageUpdate.hasUnreadMention !== false) {
return undefined;
}
ids.forEach((id) => { ids.forEach((id) => {
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!; const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (chat?.unreadReactionsCount) { if (messageUpdate.reactions && chat?.unreadReactionsCount
&& !checkIfHasUnreadReactions(global, messageUpdate.reactions)) {
global = updateUnreadReactions(global, chatId, { global = updateUnreadReactions(global, chatId, {
unreadReactionsCount: (chat.unreadReactionsCount - 1) || undefined, unreadReactionsCount: Math.max(chat.unreadReactionsCount - 1, 0) || undefined,
unreadReactions: chat.unreadReactions?.filter((i) => i !== id), unreadReactions: chat.unreadReactions?.filter((i) => i !== id),
}); });
} }
if (chat?.unreadMentionsCount) { if (!messageUpdate.hasUnreadMention && chat?.unreadMentionsCount) {
global = updateChat(global, chatId, { global = updateChat(global, chatId, {
unreadMentionsCount: (chat.unreadMentionsCount - 1) || undefined, unreadMentionsCount: Math.max(chat.unreadMentionsCount - 1, 0) || undefined,
unreadMentions: chat.unreadMentions?.filter((i) => i !== id), unreadMentions: chat.unreadMentions?.filter((i) => i !== id),
}); });
} }

View File

@ -4,7 +4,7 @@ import useForceUpdate from './useForceUpdate';
import usePrevious from './usePrevious'; import usePrevious from './usePrevious';
import useSyncEffect from './useSyncEffect'; import useSyncEffect from './useSyncEffect';
export default function usePrevDuringAnimation<T>(current: T, duration?: number) { export default function usePrevDuringAnimation<T>(current: T, duration?: number): T {
const prev = usePrevious(current, true); const prev = usePrevious(current, true);
const timeoutRef = useRef<number>(); const timeoutRef = useRef<number>();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
@ -28,5 +28,5 @@ export default function usePrevDuringAnimation<T>(current: T, duration?: number)
} }
}, [duration, forceUpdate, isCurrentPresent, isPrevPresent]); }, [duration, forceUpdate, isCurrentPresent, isPrevPresent]);
return !timeoutRef.current || !duration || isCurrentPresent ? current : prev; return (!timeoutRef.current || !duration || isCurrentPresent ? current : prev)!;
} }