/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import styles from '@app/components/channel/BlockedMessageGroups.module.css'; import {Divider} from '@app/components/channel/Divider'; import {MessageGroup} from '@app/components/channel/MessageGroup'; import type {ChannelRecord} from '@app/records/ChannelRecord'; import type {MessageRecord} from '@app/records/MessageRecord'; import {type ChannelStreamItem, ChannelStreamType} from '@app/utils/MessageGroupingUtils'; import {useLingui} from '@lingui/react/macro'; import React, {useCallback, useMemo, useRef} from 'react'; interface BlockedMessageGroupsProps { channel: ChannelRecord; messageGroups: Array; onReveal: (messageId: string | null) => void; revealed: boolean; compact: boolean; messageGroupSpacing: number; } const arePropsEqual = (prevProps: BlockedMessageGroupsProps, nextProps: BlockedMessageGroupsProps): boolean => { if (prevProps.channel.id !== nextProps.channel.id) return false; if (prevProps.revealed !== nextProps.revealed) return false; if (prevProps.compact !== nextProps.compact) return false; if (prevProps.messageGroupSpacing !== nextProps.messageGroupSpacing) return false; if (prevProps.messageGroups.length !== nextProps.messageGroups.length) return false; for (let i = 0; i < prevProps.messageGroups.length; i++) { const prevGroup = prevProps.messageGroups[i]; const nextGroup = nextProps.messageGroups[i]; if (!nextGroup) return false; if (prevGroup.type !== nextGroup.type) return false; if (prevGroup.type === ChannelStreamType.MESSAGE) { const prevMessage = prevGroup.content as MessageRecord; const nextMessage = nextGroup.content as MessageRecord; if (prevMessage.id !== nextMessage.id) return false; if (prevMessage.editedTimestamp !== nextMessage.editedTimestamp) return false; } } return true; }; export const BlockedMessageGroups = React.memo((props) => { const {t} = useLingui(); const {messageGroups, channel, compact, revealed, messageGroupSpacing, onReveal} = props; const containerRef = useRef(null); const handleClick = useCallback(() => { const container = containerRef.current; const scroller = container?.closest('[data-scroller]') as HTMLElement | null; if (scroller) { const wasAtBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 1; if (revealed) { onReveal(null); if (wasAtBottom) { requestAnimationFrame(() => { scroller.scrollTop = scroller.scrollHeight; }); } } else { const firstMessage = messageGroups.find((item) => item.type === ChannelStreamType.MESSAGE); if (firstMessage) { onReveal((firstMessage.content as MessageRecord).id); if (wasAtBottom) { requestAnimationFrame(() => { scroller.scrollTop = scroller.scrollHeight; }); } } } } else { if (revealed) { onReveal(null); } else { const firstMessage = messageGroups.find((item) => item.type === ChannelStreamType.MESSAGE); if (firstMessage) { onReveal((firstMessage.content as MessageRecord).id); } } } }, [messageGroups, onReveal, revealed]); const totalMessageCount = useMemo(() => { return messageGroups.filter((item) => item.type === ChannelStreamType.MESSAGE).length; }, [messageGroups]); const messageNodes = useMemo(() => { if (!revealed) return null; const nodes: Array = []; let currentGroupMessages: Array = []; let groupId: string | undefined; const flushGroup = () => { if (currentGroupMessages.length > 0) { nodes.push( , ); currentGroupMessages = []; groupId = undefined; } }; messageGroups.forEach((item, itemIndex) => { if (item.type === ChannelStreamType.DIVIDER) { flushGroup(); nodes.push( {item.content as string} , ); } else if (item.type === ChannelStreamType.MESSAGE) { const message = item.content as MessageRecord; if (groupId !== item.groupId) { flushGroup(); groupId = item.groupId; } currentGroupMessages.push(message); } }); flushGroup(); return nodes; }, [revealed, messageGroups, messageGroupSpacing, channel, compact]); return (
{revealed && (
{messageNodes}
)}
); }, arePropsEqual);