// SPDX-License-Identifier: AGPL-3.0-or-later import Accessibility from '@app/features/accessibility/state/Accessibility'; import {useMessageListPlaceholderSpecs} from '@app/features/app/components/skeleton/PlaceholderSpecs'; import {ScrollFillerSkeleton} from '@app/features/app/components/skeleton/ScrollFillerSkeleton'; import {reportSkeletonMessagePresentation} from '@app/features/app/components/skeleton/SkeletonLayoutMemory'; import {measureSkeletonHeightPx, useSkeletonLayoutReport} from '@app/features/app/hooks/useSkeletonLayoutMemoryCapture'; import { type MessageFocusCandidate, resolveBottommostFocusableMessageId, } from '@app/features/channel/components/ChannelMessageFocusTarget'; import {renderChannelStream} from '@app/features/channel/components/ChannelMessageStream'; import styles from '@app/features/channel/components/ChannelMessages.module.css'; import {ChannelWelcomeSection} from '@app/features/channel/components/ChannelWelcomeSection'; import {CollapsedMessageVisibilityProvider} from '@app/features/channel/components/CollapsedMessageVisibilityContext'; import {NewMessagesBar} from '@app/features/channel/components/NewMessagesBar'; import {usePresentableTypingUsers} from '@app/features/channel/components/TypingUsers'; import {UploadManager} from '@app/features/channel/components/UploadManager'; import type {Channel} from '@app/features/channel/models/Channel'; import GatewayConnection from '@app/features/gateway/transport/GatewayConnection'; import GuildVerification from '@app/features/guild/state/GuildVerification'; import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import * as MessageCommands from '@app/features/messaging/commands/MessageCommands'; import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation'; import {useMessageSelectionCopyForMessageGetter} from '@app/features/messaging/hooks/useMessageSelectionCopy'; import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import {ChannelMessages} from '@app/features/messaging/state/ChannelMessages'; import { resolveChannelMessagesWindowStatus, selectChannelMessagesFillerVisible, selectChannelMessagesSpacerHeight, selectChannelMessagesWindowBar, } from '@app/features/messaging/state/ChannelMessagesLoadStateMachine'; import MessageEdit from '@app/features/messaging/state/MessageEdit'; import MessageFocus from '@app/features/messaging/state/MessageFocus'; import MessagesState from '@app/features/messaging/state/MessagingMessages'; import { type ChannelStreamItem, createChannelStream, getCollapsedMessageGroupKey, } from '@app/features/messaging/utils/MessageGroupingUtils'; import {findMessageElement, getMessageSelector} from '@app/features/messaging/utils/MessageNodeSelectors'; import LocalUserSpamOverride from '@app/features/moderation/state/LocalUserSpamOverride'; import SelectedChannel from '@app/features/navigation/state/SelectedChannel'; import Permission from '@app/features/permissions/state/Permission'; import {ComponentBus} from '@app/features/platform/utils/ComponentBus'; import {useScrollManager} from '@app/features/platform/utils/ScrollManager'; import * as ReadStateCommands from '@app/features/read_state/commands/ReadStateCommands'; import ReadStates from '@app/features/read_state/state/ReadStates'; import {shouldAutoAck} from '@app/features/read_state/utils/AutoAckPredicate'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; import {Button} from '@app/features/ui/button/Button'; import {Scroller} from '@app/features/ui/components/Scroller'; import FocusRingScope from '@app/features/ui/focus_ring/FocusRingScope'; import KeyboardMode from '@app/features/ui/state/KeyboardMode'; import MediaViewer from '@app/features/ui/state/MediaViewer'; import Modal from '@app/features/ui/state/Modal'; import type {User} from '@app/features/user/models/User'; import UserSettings from '@app/features/user/state/UserSettings'; import Users from '@app/features/user/state/Users'; import Window from '@app/features/window/state/Window'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {MAX_MESSAGES_PER_CHANNEL} from '@fluxer/constants/src/LimitConstants'; import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {clsx} from 'clsx'; import {runInAction} from 'mobx'; import {observer, useLocalObservable} from 'mobx-react-lite'; import type React from 'react'; import {useCallback, useEffect, useMemo, useRef} from 'react'; const MESSAGE_LIST_FOR_DESCRIPTOR = msg({ message: 'Message list for {channelName}', comment: 'Label in the channel and chat messages. Preserve {channelName}; it is inserted by code.', }); const MESSAGE_LIST_DESCRIPTOR = msg({ message: 'Message list', comment: 'Short label in the channel and chat messages. Keep it concise.', }); const YOU_RE_VIEWING_OLDER_MESSAGES_DESCRIPTOR = msg({ message: "You're viewing older messages", comment: 'Label in the channel and chat messages.', }); const JUMP_TO_PRESENT_DESCRIPTOR = msg({ message: 'Jump to present', comment: 'Short label in the channel and chat messages. Keep it concise.', }); const MESSAGES_FAILED_TO_LOAD_DESCRIPTOR = msg({ message: 'Messages failed to load', comment: 'Error message in the channel and chat messages.', }); function checkPermissions(channel: Channel) { const canSendMessages = Permission.can(Permissions.SEND_MESSAGES, channel); const passesVerification = channel.isPrivate() || GuildVerification.canAccessGuild(channel.guildId || ''); const canChat = channel.isPrivate() || (canSendMessages && passesVerification); const canAttachFiles = channel.isPrivate() ? canChat : canChat && Permission.can(Permissions.ATTACH_FILES, channel); const canManageMessages = Permission.can(Permissions.MANAGE_MESSAGES, channel); return {canSendMessages, canChat, canAttachFiles, canManageMessages}; } interface MessagesStateSnapshot { unreadCount: number; oldestUnreadMessageId: string | null; visualUnreadMessageId: string | null; ackMessageId: string | null; lastReadStateMessageId: string | null; messages: ChannelMessages; messageVersion: number; unblurredMessageId: string | null; permissionVersion: number; messageGroupSpacing: number; fontSize: number; messageDisplayCompact: boolean; editingMessageId: string | null; currentUser: User | undefined; isEstimated: boolean; ackedManually: boolean; } interface MessagesProps { channel: Channel; onBottomBarVisibilityChange?: (visible: boolean) => void; allowAutoAck?: boolean; } type MessagesWrapperStyle = React.CSSProperties & {'--message-group-spacing': string}; const readFromState = (channelId: string): MessagesStateSnapshot => { const messages = MessagesState.getMessages(channelId); const messageDisplayCompact = UserSettings.getMessageDisplayCompact(); return { unreadCount: ReadStates.getUnreadCount(channelId), oldestUnreadMessageId: ReadStates.getOldestUnreadMessageId(channelId), visualUnreadMessageId: ReadStates.getVisualUnreadMessageId(channelId), ackMessageId: ReadStates.ackMessageId(channelId), lastReadStateMessageId: ReadStates.lastMessageId(channelId), messages, messageVersion: messages.version, unblurredMessageId: messages.unblurredMessageId, permissionVersion: Permission.version, messageGroupSpacing: Accessibility.getMessageGroupSpacingValue(messageDisplayCompact), fontSize: Accessibility.fontSize, messageDisplayCompact, editingMessageId: MessageEdit.getEditingMessageId(channelId), currentUser: Users.currentUser ?? undefined, isEstimated: ReadStates.getIfExists(channelId)?.estimated ?? false, ackedManually: ReadStates.getIfExists(channelId)?.ackedManually ?? false, }; }; function shallowEqual(a: T, b: T): boolean { const aKeys = Object.keys(a); if (aKeys.length !== Object.keys(b).length) return false; for (const key of aKeys) { if ((a as Record)[key] !== (b as Record)[key]) return false; } return true; } export const Messages = observer(function Messages({ channel, onBottomBarVisibilityChange, allowAutoAck = true, }: MessagesProps) { const {i18n} = useLingui(); const scrollerInnerRef = useRef(null); const scrollerContainerRef = useRef(null); const lastStateSnapshotRef = useRef(null); const recoveryFetchChannelIdRef = useRef(null); interface MessageState extends MessagesStateSnapshot { highlightedMessageId: string | null; isAtBottom: boolean; } const state = useLocalObservable(() => { const initial = readFromState(channel.id); lastStateSnapshotRef.current = initial; return { ...initial, highlightedMessageId: null, isAtBottom: false, }; }); const windowId = Window.windowId; const isWindowFocused = Window.isFocused(); const isModalOpen = Modal.hasModalOpen(); const isGatewayConnected = GatewayConnection.isConnected; const selectedChannelId = SelectedChannel.currentChannelId; const placeholderSpecs = useMessageListPlaceholderSpecs({ channelId: channel.id, compact: state.messageDisplayCompact, compactAvatarsVisible: Accessibility.showUserAvatarsInCompactMode, groupSpacing: state.messageGroupSpacing, }); const safeMessages = state.messages ?? MessagesState.getMessages(channel.id); const windowStatus = resolveChannelMessagesWindowStatus({ ready: safeMessages.ready, loading: safeMessages.loadingMore, failed: safeMessages.error, messageCount: safeMessages.length, hasMoreBefore: safeMessages.hasMoreBefore, hasMoreAfter: safeMessages.hasMoreAfter, }); const windowBar = selectChannelMessagesWindowBar(windowStatus); const windowNeedsPage = windowStatus.needsPage; const canAutoAck = shouldAutoAck({ channelActive: allowAutoAck, windowFocused: isWindowFocused, atBottom: true, textChatVisible: true, manualAck: state.ackedManually, blockingModalOpen: isModalOpen || MediaViewer.isOpen, }); const jumpHighlightTimeoutRef = useRef(null); const lastJumpSequenceKeyRef = useRef(null); const handleJumpHighlight = useCallback( (targetMessageId: string | null, jumpTicket: number) => { const jumpSequenceKey = `${channel.id}:${jumpTicket}`; if (jumpSequenceKey === lastJumpSequenceKeyRef.current) return; lastJumpSequenceKeyRef.current = jumpSequenceKey; if (jumpHighlightTimeoutRef.current != null) { clearTimeout(jumpHighlightTimeoutRef.current); jumpHighlightTimeoutRef.current = null; } runInAction(() => { state.highlightedMessageId = targetMessageId; }); if (!targetMessageId) return; jumpHighlightTimeoutRef.current = window.setTimeout(() => { runInAction(() => { if (state.highlightedMessageId === targetMessageId) { state.highlightedMessageId = null; } }); jumpHighlightTimeoutRef.current = null; }, 2000); }, [channel.id, state], ); const scrollManager = useScrollManager({ messages: safeMessages, channel, compact: state.messageDisplayCompact, hasPendingUnreads: state.visualUnreadMessageId != null, focusAnchorId: null, unloadedSpacerHeight: selectChannelMessagesSpacerHeight(windowStatus, placeholderSpecs.totalHeight), allowHistoryFetch: true, windowId, notifyPinnedToBottom: () => { runInAction(() => { state.isAtBottom = true; }); }, notifyUnpinnedFromBottom: () => { runInAction(() => { state.isAtBottom = false; }); }, extraListPadding: 48, canAutoAck, handleJumpHighlight, }); const resolveMessageScrollSurface = useMemo( () => () => scrollManager.ref.current?.getViewportElement() ?? null, [scrollManager], ); useEffect(() => { ChannelMessages.retainChannel(channel.id); return () => { ChannelMessages.releaseRetainedChannel(channel.id); }; }, [channel.id]); const updateFromState = useCallback(() => { const snapshot = readFromState(channel.id); const previous = lastStateSnapshotRef.current; if (previous && shallowEqual(previous, snapshot)) return; runInAction(() => { Object.assign(state, snapshot); }); lastStateSnapshotRef.current = snapshot; }, [channel.id, state]); const revealMessageNode = useCallback( (targetNode: HTMLElement) => { const scrollerNode = scrollManager.ref.current?.getViewportElement(); if (!scrollerNode) return; const targetRect = targetNode.getBoundingClientRect(); const scrollerRect = scrollerNode.getBoundingClientRect(); const isAbove = targetRect.top < scrollerRect.top; const isBelow = targetRect.bottom > scrollerRect.bottom; if (isAbove || isBelow) { scrollManager.ref.current?.revealElement({ node: targetNode, padding: 80, animate: false, }); scrollManager.scrollHandle(); } }, [scrollManager], ); const onMessageEdit = useCallback( (targetNode: HTMLElement) => { const scrollerNode = scrollManager.ref.current?.getViewportElement(); if (!scrollerNode) return; if (scrollManager.pinIsAtBottomNow()) { return; } if (KeyboardMode.keyboardModeEnabled) { const focusedMessageId = MessageFocus.focusedMessageId; const focusedChannelId = MessageFocus.focusedChannelId; const editedMessageId = targetNode.getAttribute('data-message-id'); if (focusedChannelId === channel.id && focusedMessageId && editedMessageId === focusedMessageId) { return; } } revealMessageNode(targetNode); }, [scrollManager, channel.id, revealMessageNode], ); const onReveal = useCallback( (messageId: string | null) => { MessageCommands.revealMessage(channel.id, messageId); }, [channel.id], ); const onScrollToPresent = useCallback(() => { if (state.messages?.hasMoreAfter) { MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL); } else { scrollManager.scrollSetToBottom(false); } }, [channel.id, state.messages?.hasMoreAfter, scrollManager]); const onMessageSent = useCallback( (payload?: unknown) => { const data = payload as {channelId?: string} | undefined; if (data?.channelId && data.channelId !== channel.id) return; if (!Accessibility.scrollToBottomOnMessageSend) return; if (state.messages?.hasMoreAfter) return; window.requestAnimationFrame(() => { scrollManager.scrollSetToBottom(false); }); }, [channel.id, state.messages?.hasMoreAfter, scrollManager], ); const onJumpToOldestUnread = useCallback(() => { const messageId = state.oldestUnreadMessageId ?? state.ackMessageId; if (messageId == null) { return; } MessageCommands.jumpToMessage({channelId: channel.id, messageId, flash: true}); }, [channel.id, state.ackMessageId, state.oldestUnreadMessageId]); const onScrollToPresentAndAck = useCallback(() => { if (state.messages?.hasMoreAfter) { MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL); } else { scrollManager.scrollSetToBottom(false); } if (state.visualUnreadMessageId != null) { ReadStateCommands.clearStickyUnread(channel.id); } if (ReadStates.hasUnread(channel.id)) { ReadStateCommands.ack(channel.id, true, false); } }, [channel.id, state.messages?.hasMoreAfter, state.visualUnreadMessageId, scrollManager]); const onEscapePressed = useCallback( (payload?: unknown) => { const data = payload as {channelId?: string} | undefined; if (data?.channelId && data.channelId !== channel.id) return; if (scrollManager.jumpReturnToOrigin()) { return; } onScrollToPresentAndAck(); }, [channel.id, onScrollToPresentAndAck, scrollManager], ); const onRetryLoadMessages = useCallback(() => { void MessageCommands.fetchMessages(channel.id, null, null, MAX_MESSAGES_PER_CHANNEL); }, [channel.id]); const getSelectionCopyMessage = useCallback( (messageId: string) => MessagesState.getMessage(channel.id, messageId), [channel.id], ); const onCopySelectedMessages = useMessageSelectionCopyForMessageGetter(getSelectionCopyMessage); useEffect(() => { const storeUnsubs = [ MessagesState.subscribe(updateFromState), ReadStates.subscribe(updateFromState), Users.subscribe(updateFromState), Permission.subscribe(updateFromState), Accessibility.subscribe(updateFromState), UserSettings.subscribe(updateFromState), MessageEdit.subscribe(updateFromState), ]; const onForceJumpToPresent = () => { MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL); }; const onScrollPageUp = () => scrollManager.pageBackward(true); const onScrollPageDown = () => scrollManager.pageForward(true); const onLayoutResized = (payload?: unknown) => { const data = payload as {channelId?: string} | undefined; if (data?.channelId && data.channelId !== channel.id) return; scrollManager.layoutHandleResized(); }; const onFocusBottommostMessage = (payload?: unknown) => { const data = (payload ?? {}) as {channelId?: string}; if (!data.channelId || data.channelId !== channel.id) return; const scroller = scrollManager.ref.current?.getViewportElement(); const innerElement = scrollerInnerRef.current; if (!scroller || !innerElement) return; const messageElements = innerElement.querySelectorAll(getMessageSelector(channel.id)); if (!messageElements.length) return; const scrollerRect = scroller.getBoundingClientRect(); const candidates: Array = []; for (const messageEl of messageElements) { const messageId = messageEl.dataset.messageId; if (!messageId) continue; const rect = messageEl.getBoundingClientRect(); candidates.push({messageId, top: rect.top, bottom: rect.bottom, height: rect.height}); } const messageId = resolveBottommostFocusableMessageId(candidates, scrollerRect.top, scrollerRect.bottom); if (messageId) { scrollManager.focusRequestForMessage(messageId); } }; const dispatchUnsubs = [ ComponentBus.subscribe('SCROLL_TO_PRESENT', onScrollToPresent), ComponentBus.subscribe('MESSAGE_SENT', onMessageSent), ComponentBus.subscribe('FORCE_JUMP_TO_PRESENT', onForceJumpToPresent), ComponentBus.subscribe('ESCAPE_PRESSED', onEscapePressed), ComponentBus.subscribe('SCROLL_PAGE_UP', onScrollPageUp), ComponentBus.subscribe('SCROLL_PAGE_DOWN', onScrollPageDown), ComponentBus.subscribe('LAYOUT_RESIZED', onLayoutResized), ComponentBus.subscribe('FOCUS_BOTTOMMOST_MESSAGE', onFocusBottommostMessage), ]; updateFromState(); return () => { storeUnsubs.forEach((u) => u()); dispatchUnsubs.forEach((u) => u()); }; }, [channel.id, updateFromState, onScrollToPresent, onMessageSent, onEscapePressed, scrollManager]); useEffect(() => { return ComponentBus.subscribe('MESSAGE_REVEAL', (payload?: unknown) => { const data = (payload ?? {}) as {channelId?: string; messageId?: string}; if (data.channelId !== channel.id || !data.messageId) return; const messageId = data.messageId; window.requestAnimationFrame(() => { const node = findMessageElement( document, scrollManager.ref.current?.getViewportElement(), channel.id, messageId, ); if (node) revealMessageNode(node); }); }); }, [channel.id, scrollManager, revealMessageNode]); useEffect(() => { const editingMessageId = state.editingMessageId; if (editingMessageId) { scrollManager.editEnter(); } else { scrollManager.editExit(); } }, [state.editingMessageId, scrollManager]); useEffect(() => { if (!windowNeedsPage || !isGatewayConnected || selectedChannelId !== channel.id) { if (recoveryFetchChannelIdRef.current === channel.id) { recoveryFetchChannelIdRef.current = null; } return; } if (recoveryFetchChannelIdRef.current === channel.id) { return; } recoveryFetchChannelIdRef.current = channel.id; void MessageCommands.fetchMessages(channel.id, null, null, MAX_MESSAGES_PER_CHANNEL).finally(() => { if (recoveryFetchChannelIdRef.current === channel.id) { recoveryFetchChannelIdRef.current = null; } }); }, [channel.id, isGatewayConnected, selectedChannelId, windowNeedsPage, state.messageVersion]); useMessageListKeyboardNavigation({ containerRef: scrollManager.ref, channelId: channel.id, onFocusMessage: (messageId) => { scrollManager.focusRequestForMessage(messageId); }, onLoadMoreBefore: () => { scrollManager.loadMoreForKeyboardNavigation(false); }, onLoadMoreAfter: () => { scrollManager.loadMoreForKeyboardNavigation(true); }, hasMoreBefore: state.messages?.hasMoreBefore ?? false, hasMoreAfter: state.messages?.hasMoreAfter ?? false, isLoadingMore: state.messages?.loadingMore ?? false, onEscape: () => { if (scrollManager.jumpReturnToOrigin()) { return; } scrollManager.jumpCancel(); ComponentBus.dispatch('FOCUS_TEXTAREA', {channelId: channel.id}); }, onNavigatePastNewest: () => { ComponentBus.dispatch('FOCUS_TEXTAREA', {channelId: channel.id, enterKeyboardMode: true}); }, allowWhenInactive: true, }); useEffect(() => { return () => { if (jumpHighlightTimeoutRef.current != null) { clearTimeout(jumpHighlightTimeoutRef.current); jumpHighlightTimeoutRef.current = null; } }; }, []); useEffect(() => { if (!canAutoAck || !state.isAtBottom || !state.messages?.ready || state.messages.loadingMore) return; if (ReadStates.hasUnread(channel.id)) { ReadStateCommands.ackWithStickyUnread(channel.id); } }, [canAutoAck, state.isAtBottom, state.messages?.ready, state.messages?.loadingMore, channel.id]); useEffect(() => { return () => { const readState = ReadStates.getIfExists(channel.id); if (readState?.ackedManually) { ReadStateCommands.clearManualAck(channel.id); } ReadStateCommands.clearStickyUnread(channel.id); }; }, [channel.id]); const spammerOverrideVersion = LocalUserSpamOverride.version; const channelStream = useMemo>(() => { if (!state.messages) return []; return createChannelStream({ channel, messages: state.messages, oldestUnreadMessageId: state.visualUnreadMessageId, treatSpam: true, }); }, [channel, state.messages, state.messageVersion, state.visualUnreadMessageId, spammerOverrideVersion]); useEffect(() => { const messages = state.messages; if (!messages?.ready) { return; } void MessageCommands.ensureMembersForMessages(messages.toArray()); }, [channel.id, state.messages?.ready, state.messageVersion]); const immediateHighlightedMessageId = (() => { const messages = state.messages; if (!messages?.ready || !messages.hasJumped || !messages.jumpHighlight || !messages.jumpDestinationId) { return null; } return messages.jumpDestinationId !== channel.id ? messages.jumpDestinationId : null; })(); const highlightedMessageId = immediateHighlightedMessageId ?? state.highlightedMessageId; const collapsedMessageVisibility = useMemo( () => ({ isMessageRevealed: (message: Message) => { if (!state.unblurredMessageId || !state.messages || message.channelId !== channel.id) { return false; } return ( getCollapsedMessageGroupKey({ channel, messages: state.messages, messageId: message.id, treatSpam: true, }) === state.unblurredMessageId ); }, }), [channel, state.messages, state.unblurredMessageId, state.messageVersion, spammerOverrideVersion], ); const {canChat, canAttachFiles} = useMemo( () => checkPermissions(channel), [channel.id, channel.guildId, state.permissionVersion], ); const streamMarkup = useMemo(() => { if (!state.messages) return null; return renderChannelStream({ channelStream, messages: state.messages, channel, highlightedMessageId, messageDisplayCompact: state.messageDisplayCompact, messageGroupSpacing: state.messageGroupSpacing, unblurredMessageId: state.unblurredMessageId, onMessageEdit, onReveal, }); }, [ channelStream, state.messages, channel, highlightedMessageId, state.messageDisplayCompact, state.messageGroupSpacing, state.unblurredMessageId, onMessageEdit, onReveal, ]); const hasBottomBar = windowBar !== 'none'; useEffect(() => { onBottomBarVisibilityChange?.(hasBottomBar); }, [hasBottomBar, onBottomBarVisibilityChange]); useEffect(() => { return () => { onBottomBarVisibilityChange?.(false); }; }, [onBottomBarVisibilityChange]); 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 messagesWrapperStyle = useMemo( () => ({ '--message-group-spacing': remFromPx(state.messageGroupSpacing), }), [state.messageGroupSpacing], ); const compactAvatarsVisible = Accessibility.showUserAvatarsInCompactMode; const messageGutter = Accessibility.messageGutter; useSkeletonLayoutReport( () => reportSkeletonMessagePresentation({ compact: state.messageDisplayCompact, messageGutterPx: messageGutter, fontSizePx: state.fontSize, groupSpacingPx: state.messageGroupSpacing, compactAvatarsVisible, viewportHeightPx: measureSkeletonHeightPx(scrollerContainerRef.current), }), `${state.messageDisplayCompact}|${messageGutter}|${state.fontSize}|${state.messageGroupSpacing}|${compactAvatarsVisible}|${channel.id}|${state.messageVersion}`, ); const messageListLabel = channel.name ? i18n._(MESSAGE_LIST_FOR_DESCRIPTOR, {channelName: channel.name}) : i18n._(MESSAGE_LIST_DESCRIPTOR); const messageListLiveMode = Accessibility.screenReaderAnnounceNewMessages && state.isAtBottom ? 'polite' : 'off'; const composerStatusVisible = usePresentableTypingUsers(channel).length > 0 || channel.rateLimitPerUser > 0; 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 = ( <> {headFillerVisible && ( )} {windowStatus.olderPageAvailable && safeMessages.length > 0 && (
)} {!windowStatus.olderPageAvailable && ( )} {streamMarkup} {tailFillerVisible && ( )}
); return (
{topBar}
{scrollerInner}
{composerStatusVisible && ( {bottomBar}
); }); const JumpToPresentBar = observer(function JumpToPresentBar({ loadingMore, landedAtLiveEdge, onJumpToPresent, }: { loadingMore: boolean; landedAtLiveEdge: boolean; onJumpToPresent: () => void; }) { const {i18n} = useLingui(); const jumpIsActiveNow = loadingMore && landedAtLiveEdge; return (
{i18n._(YOU_RE_VIEWING_OLDER_MESSAGES_DESCRIPTOR)}
); }); function LoadErrorBar({loading, onRetry}: {loading: boolean; onRetry: () => void}) { const {i18n} = useLingui(); return (
{i18n._(MESSAGES_FAILED_TO_LOAD_DESCRIPTOR)}
); }