Files
pi-mobile/apps/mobile/components/CollapsibleMessage.tsx
T
2026-07-09 12:07:57 -05:00

92 lines
2.1 KiB
TypeScript

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<ViewStyle>;
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 (
<View style={style}>
<View style={collapsed ? { maxHeight: collapsedMaxHeight, overflow: "hidden" } : undefined}>
{children}
</View>
{long && !forceExpanded ? (
<Pressable
onPress={() => setExpanded((value) => !value)}
style={[styles.toggle, toggleAlign === "right" && styles.toggleRight]}
hitSlop={8}
>
<Text style={styles.toggleText}>{expanded ? "Show less" : "Show more"}</Text>
</Pressable>
) : null}
</View>
);
}
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",
},
});