diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index c07e90feec..de9b37372a 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -112,7 +112,14 @@ const ActionMessage: FC = ({ message.replyInfo?.type === 'message' ? message.replyInfo.replyToMsgId : undefined, targetMessage, ); - useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded); + useFocusMessage({ + elementRef: ref, + chatId: message.chatId, + isFocused, + focusDirection, + noFocusHighlight, + isJustAdded, + }); useEffect(() => { if (!message.isPinned) return undefined; diff --git a/src/components/middle/FloatingActionButtons.tsx b/src/components/middle/FloatingActionButtons.tsx index ed01706363..0356f04277 100644 --- a/src/components/middle/FloatingActionButtons.tsx +++ b/src/components/middle/FloatingActionButtons.tsx @@ -2,12 +2,14 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; +import type { ApiSponsoredMessage } from '../../api/types'; import type { MessageListType } from '../../global/types'; import { MAIN_THREAD_ID } from '../../api/types'; -import { selectChat, selectCurrentMessageList } from '../../global/selectors'; +import { selectChat, selectCurrentMessageList, selectSponsoredMessage } from '../../global/selectors'; import animateScroll from '../../util/animateScroll'; import buildClassName from '../../util/buildClassName'; +import { REM } from '../common/helpers/mediaDimensions'; import useLastCallback from '../../hooks/useLastCallback'; @@ -27,9 +29,17 @@ type StateProps = { unreadCount?: number; reactionsCount?: number; mentionsCount?: number; + sponsoredMessage?: ApiSponsoredMessage; }; -const FOCUS_MARGIN = 20; +const FOCUS_MARGIN = 5.625 * REM; +const FOCUS_SPONSORED_MARGIN = 10 * REM + FOCUS_MARGIN; + +const getLastMessageElement = (container: HTMLDivElement): HTMLDivElement | undefined => { + const messageElements = container.querySelectorAll('.message-list-item'); + + return messageElements[messageElements.length - 1]; +}; const FloatingActionButtons: FC = ({ isShown, @@ -40,6 +50,7 @@ const FloatingActionButtons: FC = ({ reactionsCount, mentionsCount, withExtraShift, + sponsoredMessage, }) => { const { focusNextReply, focusNextReaction, focusNextMention, fetchUnreadReactions, @@ -64,22 +75,29 @@ const FloatingActionButtons: FC = ({ } }, [chatId, fetchUnreadMentions, hasUnreadMentions]); + const animateScrollToLastMessage = (container?: HTMLDivElement | null) => { + if (!container) return; + const lastMessageElement = getLastMessageElement(container); + + if (!lastMessageElement) return; + animateScroll(container, lastMessageElement, 'end', sponsoredMessage ? FOCUS_SPONSORED_MARGIN : FOCUS_MARGIN); + }; + const handleClick = useLastCallback(() => { - if (!isShown) { - return; + if (!isShown) return; + + if (messageListType === 'thread') focusNextReply(); + + if (messageListType === 'pinned') { + const container = elementRef.current?.parentElement?.querySelector('.MessageList.type-pinned'); + + animateScrollToLastMessage(container); } - if (messageListType === 'thread') { - focusNextReply(); - } else { - const messagesContainer = elementRef.current!.parentElement!.querySelector('.MessageList')!; - const messageElements = messagesContainer.querySelectorAll('.message-list-item'); - const lastMessageElement = messageElements[messageElements.length - 1]; - if (!lastMessageElement) { - return; - } - - animateScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN); + if (messageListType === 'scheduled') { + const container = elementRef.current?.parentElement?.querySelector('.MessageList.type-scheduled'); + + animateScrollToLastMessage(container); } }); @@ -144,6 +162,7 @@ export default memo(withGlobal( reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined, mentionsCount: shouldShowCount ? chat.unreadMentionsCount : undefined, unreadCount: shouldShowCount ? chat.unreadCount : undefined, + sponsoredMessage: selectSponsoredMessage(global, chatId), }; }, )(FloatingActionButtons)); diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 1a086e4891..019fecaf58 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -76,19 +76,32 @@ } } + @mixin last-element-margin { + margin-bottom: 5.625rem; + + @media (max-width: 600px) { + margin-bottom: 4.25rem; + } + } + &.select-mode-active, &.type-pinned { margin-bottom: 0; - .last-in-list { - margin-bottom: 5.625rem; - - @media (max-width: 600px) { - margin-bottom: 4.25rem; - } + &:not(.with-sponsored-message, .is-channel, .is-group-chat), + &.select-mode-active:not(.with-sponsored-message) { + .last-in-list { + @include last-element-margin; + + &.ActionMessage { + padding-bottom: 0.125rem; + } + } + } - &.ActionMessage { - padding-bottom: 0.125rem; + &.select-mode-active { + .SponsoredMessage { + @include last-element-margin; } } } @@ -97,12 +110,14 @@ &.with-bottom-shift { margin-bottom: 0; - .last-in-list { - margin-bottom: 4.25rem; - - @supports (margin-bottom: calc(4.25rem + env(safe-area-inset-bottom))) { - body:not(.keyboard-visible) & { - margin-bottom: calc(4.25rem + env(safe-area-inset-bottom)); + &:not(.is-channel) { + .last-in-list { + margin-bottom: 4.25rem; + + @supports (margin-bottom: calc(4.25rem + env(safe-area-inset-bottom))) { + body:not(.keyboard-visible) & { + margin-bottom: calc(4.25rem + env(safe-area-inset-bottom)); + } } } } diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 5f924df5bf..14af1b1c22 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -9,7 +9,7 @@ import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { - ApiMessage, ApiRestrictionReason, ApiTopic, + ApiMessage, ApiRestrictionReason, ApiSponsoredMessage, ApiTopic, } from '../../api/types'; import type { MessageListType } from '../../global/types'; import type { Signal } from '../../util/signals'; @@ -47,6 +47,7 @@ import { selectLastScrollOffset, selectPerformanceSettingsValue, selectScrollOffset, + selectSponsoredMessage, selectTabState, selectThreadInfo, } from '../../global/selectors'; @@ -119,6 +120,7 @@ type StateProps = { isServiceNotificationsChat?: boolean; isEmptyThread?: boolean; isForum?: boolean; + sponsoredMessage?: ApiSponsoredMessage; }; const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; @@ -173,6 +175,7 @@ const MessageList: FC = ({ isServiceNotificationsChat, onPinnedIntersectionChange, getForceNextPinnedInHeader, + sponsoredMessage, }) => { const { loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, @@ -569,12 +572,17 @@ const MessageList: FC = ({ 'MessageList custom-scroll', noAvatars && 'no-avatars', !canPost && 'no-composer', + type === 'scheduled' && 'type-scheduled', type === 'pinned' && 'type-pinned', + type === 'thread' && 'type-thread', withBottomShift && 'with-bottom-shift', withDefaultBg && 'with-default-bg', isSelectModeActive && 'select-mode-active', isScrolled && 'scrolled', !isReady && 'is-animating', + sponsoredMessage && 'with-sponsored-message', + isChannelChat && 'is-channel', + isGroupChat && 'is-group-chat', ); const hasMessages = (messageIds && messageGroups) || lastMessage; @@ -674,6 +682,7 @@ export default memo(withGlobal( const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount; return { + sponsoredMessage: selectSponsoredMessage(global, chatId), isCurrentUserPremium: selectIsCurrentUserPremium(global), isChatLoaded: true, isRestricted, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index b3f9101369..a4cc01acf4 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -85,6 +85,7 @@ import { selectSenderFromHeader, selectShouldDetectChatLanguage, selectShouldLoopStickers, + selectSponsoredMessage, selectTabState, selectTheme, selectThreadInfo, @@ -273,6 +274,7 @@ type StateProps = { isConnected: boolean; isLoadingComments?: boolean; shouldWarnAboutSvg?: boolean; + hasSponsoredMessage?: boolean; }; type MetaPosition = @@ -387,6 +389,7 @@ const Message: FC = ({ getIsMessageListReady, shouldWarnAboutSvg, onPinnedIntersectionChange, + hasSponsoredMessage, }) => { const { toggleMessageSelection, @@ -731,9 +734,29 @@ const Message: FC = ({ replyStory, ); - useFocusMessage( - ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded, Boolean(focusedQuote), - ); + const WITH_BOTTOM_ELEMENT_GAP = 5.625 * REM; + const SPONSORED_MESSAGE_GAP = 11 * REM; + const SELECT_MODE_WITH_SPONSORED_GAP = WITH_BOTTOM_ELEMENT_GAP + SPONSORED_MESSAGE_GAP; + + const getFocusMargin = () => { + if (messageListType === 'pinned' || (isInSelectMode && !hasSponsoredMessage)) return WITH_BOTTOM_ELEMENT_GAP; + if (!isInSelectMode && hasSponsoredMessage) return SPONSORED_MESSAGE_GAP; + if (isInSelectMode && hasSponsoredMessage) return SELECT_MODE_WITH_SPONSORED_GAP; + + return undefined; + }; + + useFocusMessage({ + elementRef: ref, + chatId, + isFocused, + focusDirection, + noFocusHighlight, + isResizingContainer, + isJustAdded, + isQuote: Boolean(focusedQuote), + focusMargin: getFocusMargin(), + }); const signature = (isChannel && message.postAuthorTitle) || (!asForwarded && forwardInfo?.postAuthorTitle) @@ -1552,6 +1575,7 @@ export default memo(withGlobal( const hasActiveReactions = Boolean(reactionMessage && activeReactions[getMessageKey(reactionMessage)]?.length); return { + hasSponsoredMessage: Boolean(selectSponsoredMessage(global, chatId)), theme: selectTheme(global), forceSenderName, sender, diff --git a/src/components/middle/message/hooks/useFocusMessage.ts b/src/components/middle/message/hooks/useFocusMessage.ts index be937b1cff..748ef4dbb1 100644 --- a/src/components/middle/message/hooks/useFocusMessage.ts +++ b/src/components/middle/message/hooks/useFocusMessage.ts @@ -13,16 +13,29 @@ const BOTTOM_FOCUS_OFFSET = 500; const RELOCATED_FOCUS_OFFSET = 750; const FOCUS_MARGIN = 20; -export default function useFocusMessage( - elementRef: { current: HTMLDivElement | null }, - chatId: string, - isFocused?: boolean, - focusDirection?: FocusDirection, - noFocusHighlight?: boolean, - isResizingContainer?: boolean, - isJustAdded?: boolean, - isQuote?: boolean, -) { +interface OwnProps { + elementRef: { current: HTMLDivElement | null }; + chatId: string; + isFocused?: boolean; + focusDirection?: FocusDirection; + noFocusHighlight?: boolean; + isResizingContainer?: boolean; + isJustAdded?: boolean; + isQuote?: boolean; + focusMargin?: number; +} + +export default function useFocusMessage({ + elementRef, + chatId, + isFocused, + focusDirection, + noFocusHighlight, + isResizingContainer, + isJustAdded, + isQuote, + focusMargin = FOCUS_MARGIN, +}: OwnProps) { const isRelocatedRef = useRef(!isJustAdded); useLayoutEffect(() => { @@ -39,7 +52,7 @@ export default function useFocusMessage( messagesContainer, elementRef.current!, isToBottom ? 'end' : 'centerOrTop', - FOCUS_MARGIN, + focusMargin, focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined, focusDirection, undefined, @@ -69,6 +82,6 @@ export default function useFocusMessage( } } }, [ - elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, + elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, focusMargin, ]); }