import { memo, useEffect, useState, type ReactNode } from "react"; import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native"; import { colors } from "@/lib/theme"; const CHAR_THRESHOLD = 480; const LINE_THRESHOLD = 10; export const COLLAPSED_MAX_HEIGHT = 220; export function isLongMessage(text: string): boolean { const trimmed = text.trim(); if (trimmed.length > CHAR_THRESHOLD) { return true; } let lines = 1; for (let i = 0; i < trimmed.length; i++) { if (trimmed.charCodeAt(i) === 10) { lines += 1; if (lines > LINE_THRESHOLD) { return true; } } } return false; } type Props = { text: string; /** Keep fully expanded (e.g. while streaming). */ forceExpanded?: boolean; collapsedMaxHeight?: number; toggleAlign?: "left" | "right"; style?: StyleProp; children: ReactNode; }; function CollapsibleMessageComponent({ text, forceExpanded = false, collapsedMaxHeight = COLLAPSED_MAX_HEIGHT, toggleAlign = "left", style, children, }: Props) { const long = isLongMessage(text); const [expanded, setExpanded] = useState(false); useEffect(() => { if (forceExpanded) { setExpanded(true); } }, [forceExpanded]); const collapsed = long && !expanded && !forceExpanded; return ( {children} {long && !forceExpanded ? ( setExpanded((value) => !value)} style={[styles.toggle, toggleAlign === "right" && styles.toggleRight]} hitSlop={8} > {expanded ? "Show less" : "Show more"} ) : null} ); } export const CollapsibleMessage = memo(CollapsibleMessageComponent); const styles = StyleSheet.create({ toggle: { alignSelf: "flex-start", paddingTop: 6, paddingBottom: 2, }, toggleRight: { alignSelf: "flex-end", }, toggleText: { color: colors.accent, fontSize: 13, fontWeight: "600", }, });