/*
* 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 (