92 lines
2.1 KiB
TypeScript
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",
|
|
},
|
|
});
|