diff --git a/fluxer_app/src/features/channel/components/ChannelMessageStream.tsx b/fluxer_app/src/features/channel/components/ChannelMessageStream.tsx index 54ea942f1..cdc4e4c2e 100644 --- a/fluxer_app/src/features/channel/components/ChannelMessageStream.tsx +++ b/fluxer_app/src/features/channel/components/ChannelMessageStream.tsx @@ -115,9 +115,6 @@ export function renderChannelStream(props: RenderChannelStreamProps): Array { - return position === 'before' && unreadDividerBeforeMessageId === messageId; - }; nodes.push( 0, focusAnchorId: null, - unloadedSpacerHeight: placeholderSpecs.totalHeight, + unloadedSpacerHeight: selectChannelMessagesSpacerHeight(windowStatus, placeholderSpecs.totalHeight), allowHistoryFetch: true, windowId, notifyPinnedToBottom: () => { @@ -237,6 +255,10 @@ export const Messages = observer(function Messages({ canAutoAck, handleJumpHighlight, }); + const resolveMessageScrollSurface = useMemo( + () => () => scrollManager.ref.current?.getViewportElement() ?? null, + [scrollManager], + ); useEffect(() => { ChannelMessages.retainChannel(channel.id); return () => { @@ -370,9 +392,9 @@ export const Messages = observer(function Messages({ const data = (payload ?? {}) as {channelId?: string}; if (!data.channelId || data.channelId !== channel.id) return; const scroller = scrollManager.ref.current?.getViewportElement(); - const scrollerInner = scrollerInnerRef.current; - if (!scroller || !scrollerInner) return; - const messageElements = scrollerInner.querySelectorAll('[data-message-id]'); + const innerElement = scrollerInnerRef.current; + if (!scroller || !innerElement) return; + const messageElements = innerElement.querySelectorAll('[data-message-id]'); if (!messageElements.length) return; const scrollerRect = scroller.getBoundingClientRect(); let bottomMostVisibleMessage: HTMLElement | null = null; @@ -424,16 +446,7 @@ export const Messages = observer(function Messages({ } }, [state.editingMessageId, scrollManager]); useEffect(() => { - const messages = state.messages; - if ( - !messages || - messages.ready || - messages.loadingMore || - messages.error || - messages.length > 0 || - !isGatewayConnected || - selectedChannelId !== channel.id - ) { + if (!windowNeedsPage || !isGatewayConnected || selectedChannelId !== channel.id) { if (recoveryFetchChannelIdRef.current === channel.id) { recoveryFetchChannelIdRef.current = null; } @@ -448,15 +461,7 @@ export const Messages = observer(function Messages({ recoveryFetchChannelIdRef.current = null; } }); - }, [ - channel.id, - isGatewayConnected, - selectedChannelId, - state.messages?.ready, - state.messages?.loadingMore, - state.messages?.error, - state.messageVersion, - ]); + }, [channel.id, isGatewayConnected, selectedChannelId, windowNeedsPage, state.messageVersion]); useMessageListKeyboardNavigation({ containerRef: scrollManager.ref, channelId: channel.id, @@ -506,14 +511,14 @@ export const Messages = observer(function Messages({ }, [channel.id]); const spammerOverrideVersion = LocalUserSpamOverride.version; const channelStream = useMemo>(() => { - if (!state.messages?.ready) return []; + if (!state.messages) return []; return createChannelStream({ channel, messages: state.messages, oldestUnreadMessageId: state.visualUnreadMessageId, treatSpam: true, }); - }, [channel, state.messages?.ready, state.messageVersion, state.visualUnreadMessageId, spammerOverrideVersion]); + }, [channel, state.messages, state.messageVersion, state.visualUnreadMessageId, spammerOverrideVersion]); useEffect(() => { const messages = state.messages; if (!messages?.ready) { @@ -552,7 +557,7 @@ export const Messages = observer(function Messages({ [channel.id, channel.guildId, state.permissionVersion], ); const streamMarkup = useMemo(() => { - if (!state.messages?.ready) return null; + if (!state.messages) return null; return renderChannelStream({ channelStream, messages: state.messages, @@ -566,7 +571,7 @@ export const Messages = observer(function Messages({ }); }, [ channelStream, - state.messages?.ready, + state.messages, channel, highlightedMessageId, state.messageDisplayCompact, @@ -575,9 +580,7 @@ export const Messages = observer(function Messages({ onMessageEdit, onReveal, ]); - const hasJumpToPresentBar = Boolean(state.messages?.ready && state.messages.hasMoreAfter); - const hasLoadErrorBar = Boolean(state.messages?.error); - const hasBottomBar = hasJumpToPresentBar || hasLoadErrorBar; + const hasBottomBar = windowBar !== 'none'; useEffect(() => { onBottomBarVisibilityChange?.(hasBottomBar); }, [hasBottomBar, onBottomBarVisibilityChange]); @@ -586,34 +589,33 @@ export const Messages = observer(function Messages({ onBottomBarVisibilityChange?.(false); }; }, [onBottomBarVisibilityChange]); - const jumpToPresentBar = hasJumpToPresentBar ? ( - - ) : null; - const loadErrorBar = hasLoadErrorBar ? ( - - ) : null; - const showNewMessagesBar = Boolean(state.messages?.ready && state.unreadCount > 0); + const bottomBar = + windowBar === 'retry' ? ( + + ) : windowBar === 'present' ? ( + + ) : null; + const showNewMessagesBar = Boolean(windowStatus.phase === 'stream' && state.unreadCount > 0); const unreadTimestampMessageId = state.oldestUnreadMessageId ?? state.ackMessageId; const topBar = showNewMessagesBar ? ( ) : null; - const readyMessages = state.messages?.ready ? state.messages : null; const messagesWrapperStyle = useMemo( () => ({ '--message-group-spacing': remFromPx(state.messageGroupSpacing), @@ -638,24 +640,36 @@ export const Messages = observer(function Messages({ ? i18n._(MESSAGE_LIST_FOR_DESCRIPTOR, {channelName: channel.name}) : i18n._(MESSAGE_LIST_DESCRIPTOR); const messageListLiveMode = Accessibility.screenReaderAnnounceNewMessages && state.isAtBottom ? 'polite' : 'off'; - const scrollerInner = readyMessages ? ( + const topFillerVisible = selectChannelMessagesFillerVisible({ + reducedMotion: Accessibility.useReducedMotion, + scrollManagerInitialized: scrollManager.lifecycleIsInitialized(), + ready: safeMessages.ready, + }); + const headFillerVisible = windowStatus.olderPageAvailable && topFillerVisible; + const tailFillerVisible = windowStatus.newerPageAvailable; + const streamHasRows = streamMarkup != null && streamMarkup.length > 0; + const spacerFollowsFiller = tailFillerVisible || (headFillerVisible && !streamHasRows); + const scrollerInner = ( <> - {!readyMessages.hasMoreBefore && ( + {headFillerVisible && ( + + )} + {windowStatus.olderPageAvailable && safeMessages.length > 0 && ( +
+ )} + {!windowStatus.olderPageAvailable && ( )} - {readyMessages.hasMoreBefore && ( - <> -
- - - )} {streamMarkup} - {readyMessages.hasMoreAfter && ( + {tailFillerVisible && ( )} -
+
- ) : null; + ); return (
- - {scrollerInner} - + + + {scrollerInner} + +
- {loadErrorBar !== null ? loadErrorBar : jumpToPresentBar} + {bottomBar}
); }); const JumpToPresentBar = observer(function JumpToPresentBar({ loadingMore, - jumpedToPresent, + landedAtLiveEdge, onJumpToPresent, }: { loadingMore: boolean; - jumpedToPresent: boolean; + landedAtLiveEdge: boolean; onJumpToPresent: () => void; }) { const {i18n} = useLingui(); - const jumpIsActiveNow = loadingMore && jumpedToPresent; + const jumpIsActiveNow = loadingMore && landedAtLiveEdge; return (
document.querySelector(MESSAGE_SCROLLER_SELECTOR); diff --git a/fluxer_app/src/features/channel/components/MessageGroup.tsx b/fluxer_app/src/features/channel/components/MessageGroup.tsx index 4071a757e..1d0878f4c 100644 --- a/fluxer_app/src/features/channel/components/MessageGroup.tsx +++ b/fluxer_app/src/features/channel/components/MessageGroup.tsx @@ -12,7 +12,7 @@ import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; import type React from 'react'; -import {Fragment, useMemo} from 'react'; +import {Fragment, memo, useMemo} from 'react'; const MESSAGE_GROUP_DESCRIPTOR = msg({ message: 'Message group', @@ -31,11 +31,12 @@ export interface MessageGroupProps { messages: Array; channel: Channel; onEdit?: (targetNode: HTMLElement) => void; - jumpSequenceId?: number; + jumpTicket?: number; highlightedMessageId?: string | null; messageDisplayCompact?: boolean; flashKey?: number; - getUnreadDividerVisibility?: (messageId: string, position: 'before' | 'after') => boolean; + showUnreadDividerSlots?: boolean; + unreadDividerBeforeMessageId?: string | null; idPrefix?: string; messageRowClassName?: string; messageActionsClassName?: string; @@ -47,16 +48,17 @@ export interface MessageGroupProps { getMessageHeadingActivate?: (message: Message) => (() => void) | undefined; } -export const MessageGroup: React.FC = observer((props) => { +const MessageGroupBase: React.FC = observer((props) => { const {i18n} = useLingui(); const { messages, channel, onEdit, - jumpSequenceId, + jumpTicket, highlightedMessageId, messageDisplayCompact = false, - getUnreadDividerVisibility, + showUnreadDividerSlots = false, + unreadDividerBeforeMessageId = null, idPrefix, messageRowClassName, messageActionsClassName, @@ -109,10 +111,10 @@ export const MessageGroup: React.FC = observer((props) => { ); return ( - {getUnreadDividerVisibility && ( + {showUnreadDividerSlots && ( )} @@ -145,7 +147,8 @@ export const MessageGroup: React.FC = observer((props) => { highlightedMessageId, messageDisplayCompact, idPrefix, - getUnreadDividerVisibility, + showUnreadDividerSlots, + unreadDividerBeforeMessageId, messageRowClassName, messageActionsClassName, renderMessageActions, @@ -158,7 +161,7 @@ export const MessageGroup: React.FC = observer((props) => { ); return (
= observer((props) => {
); }); + +function areMessagesEqual(previous: Array, next: Array): boolean { + if (previous === next) { + return true; + } + if (previous.length !== next.length) { + return false; + } + for (let index = 0; index < previous.length; index++) { + if (previous[index] !== next[index]) { + return false; + } + } + return true; +} + +function arePropsEqual(previous: MessageGroupProps, next: MessageGroupProps): boolean { + const previousKeys = Object.keys(previous) as Array; + if (previousKeys.length !== Object.keys(next).length) { + return false; + } + for (const key of previousKeys) { + if (key === 'messages') { + continue; + } + if (previous[key] !== next[key]) { + return false; + } + } + return areMessagesEqual(previous.messages, next.messages); +} + +export const MessageGroup = memo(MessageGroupBase, arePropsEqual); diff --git a/fluxer_app/src/features/channel/components/NewMessagesBar.tsx b/fluxer_app/src/features/channel/components/NewMessagesBar.tsx index c829a7f7c..3e12ec0ec 100644 --- a/fluxer_app/src/features/channel/components/NewMessagesBar.tsx +++ b/fluxer_app/src/features/channel/components/NewMessagesBar.tsx @@ -36,22 +36,22 @@ const MARK_READ_DESCRIPTOR = msg({ export const NewMessagesBar = observer( ({ unreadCount, - oldestUnreadTimestamp, + oldestUnreadMessageTimestamp, isEstimated, onJumpToOldestUnread, onJumpToNewMessages, }: { unreadCount: number; - oldestUnreadTimestamp: number; + oldestUnreadMessageTimestamp: number; isEstimated: boolean; onJumpToOldestUnread: () => void; onJumpToNewMessages: () => void; }) => { const {i18n} = useLingui(); const isMobile = MobileLayout.isMobileLayout(); - const sameDay = isSameDayBase(oldestUnreadTimestamp); - const compactTime = DateUtils.getFormattedCompactDateTime(oldestUnreadTimestamp); - const shortTime = sameDay ? DateUtils.getFormattedTime(oldestUnreadTimestamp) : compactTime; + const sameDay = isSameDayBase(oldestUnreadMessageTimestamp); + const compactTime = DateUtils.getFormattedCompactDateTime(oldestUnreadMessageTimestamp); + const shortTime = sameDay ? DateUtils.getFormattedTime(oldestUnreadMessageTimestamp) : compactTime; return (