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 AnyFunction = (...args: any[]) => any;
type AnyToVoidFunction = (...args: any[]) => void;
type BooleanToVoidFunction = (value: boolean) => void;
type NoneToVoidFunction = () => void;
type EmojiCategory = {

View File

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

View File

@ -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<OwnProps & StateProps> = ({
isShown,
withScrollDown,
canPost,
messageListType,
chatId,
@ -64,15 +64,17 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
}
}, [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<HTMLDivElement>('.MessageList')!;
const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>(
'.Transition_slide-active > .MessageList',
)!;
const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item');
const lastMessageElement = messageElements[messageElements.length - 1];
if (!lastMessageElement) {
@ -85,8 +87,8 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
<ScrollDownButton
icon="arrow-down"
ariaLabelLang="AccDescrPageDown"
onClick={handleClick}
onClick={handleScrollDownClick}
unreadCount={unreadCount}
className={styles.unread}
/>

View File

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

View File

@ -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<OwnProps> = ({
shouldRenderBotInfo,
noAppearanceAnimation,
isSavedDialog,
onFabToggle,
onScrollDownToggle,
onNotchToggle,
onPinnedIntersectionChange,
}) => {
@ -115,7 +115,7 @@ const MessageListContent: FC<OwnProps> = ({
getContainerHeight,
isViewportNewest,
isUnread,
onFabToggle,
onScrollDownToggle,
onNotchToggle,
isReady,
);

View File

@ -238,7 +238,7 @@ function MiddleColumn({
const lang = useLang();
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
const [isFabShown, setIsFabShown] = useState<boolean | undefined>();
const [isScrollDownShown, setIsScrollDownShown] = useState(false);
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
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 (
<div
@ -552,7 +554,7 @@ function MiddleColumn({
isComments={isComments}
canPost={renderingCanPost!}
hasTools={renderingHasTools}
onFabToggle={setIsFabShown}
onScrollDownToggle={setIsScrollDownShown}
onNotchToggle={setIsNotchShown}
isReady={isReady}
isContactRequirePremium={isContactRequirePremium}
@ -693,7 +695,7 @@ function MiddleColumn({
</Transition>
<FloatingActionButtons
isShown={renderingIsFabShown!}
withScrollDown={renderingIsScrollDownShown}
canPost={renderingCanPost}
withExtraShift={withExtraShift}
/>

View File

@ -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;

View File

@ -28,8 +28,8 @@ export default function useScrollHooks(
getContainerHeight: Signal<number | undefined>,
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);
});

View File

@ -410,6 +410,7 @@ const Message: FC<OwnProps & StateProps> = ({
disableContextMenuHint,
animateUnreadReaction,
focusLastMessage,
markMentionsRead,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -795,10 +796,16 @@ const Message: FC<OwnProps & StateProps> = ({
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

View File

@ -1550,6 +1550,10 @@ addActionHandler('clickSponsoredMessage', (global, actions, payload): ActionRetu
addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => {
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);
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<void> => {
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 });
});

View File

@ -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 => {

View File

@ -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),
});
}

View File

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