react-native-ajora
Version:
The most complete AI agent UI for React Native
211 lines • 8.47 kB
JavaScript
import React, { useCallback } from "react";
import { Share, TouchableWithoutFeedback, View } from "react-native";
import { useChatContext } from "../AjoraContext";
import { MessageText } from "../MessageText";
import { MessageImage } from "../MessageImage";
import { MessageToolCall } from "../Tool";
import { isSameUser, isSameDay } from "../utils";
import stylesCommon from "../styles";
import styles from "./styles";
import { MessageFile } from "../MessageFile";
import * as Clipboard from "expo-clipboard";
export * from "./types";
const Bubble = (props) => {
const { currentMessage, nextMessage, position, containerToNextStyle, previousMessage, containerToPreviousStyle, containerStyle, wrapperStyle, } = props;
const context = useChatContext();
const onPress = useCallback(() => {
if (props.onPress)
props.onPress(context, currentMessage);
}, [context, props, currentMessage]);
const handleLongPress = useCallback((context, currentMessage) => {
if (!currentMessage.parts[0].text)
return;
const options = ["Copy text", "Share", "Cancel"];
const cancelButtonIndex = options.length - 1;
context.actionSheet().showActionSheetWithOptions({
options,
cancelButtonIndex,
}, (buttonIndex) => {
switch (buttonIndex) {
case 0:
if (currentMessage.parts[0].text) {
Clipboard.setStringAsync(currentMessage.parts[0].text);
}
break;
case 1:
if (currentMessage.parts[0].text) {
Share.share({
message: currentMessage.parts[0].text,
});
}
break;
default:
break;
}
});
}, []);
const onLongPress = useCallback(() => {
const { onLongPress } = props;
if (onLongPress) {
onLongPress(context, currentMessage);
return;
}
handleLongPress(context, currentMessage);
}, [currentMessage, context, props]);
const styledBubbleToNext = useCallback(() => {
if (currentMessage &&
nextMessage &&
position &&
isSameUser(currentMessage, nextMessage) &&
isSameDay(currentMessage, nextMessage))
return [
styles[position].containerToNext,
containerToNextStyle?.[position],
];
return null;
}, [currentMessage, nextMessage, position, containerToNextStyle]);
const styledBubbleToPrevious = useCallback(() => {
if (currentMessage &&
previousMessage &&
position &&
isSameUser(currentMessage, previousMessage) &&
isSameDay(currentMessage, previousMessage))
return [
styles[position].containerToPrevious,
containerToPreviousStyle && containerToPreviousStyle[position],
];
return null;
}, [currentMessage, previousMessage, position, containerToPreviousStyle]);
const renderMessageText = useCallback(() => {
// Check if there are any text parts in the message
const hasTextParts = currentMessage?.parts?.some((part) => part.text);
if (hasTextParts) {
const {
/* eslint-disable @typescript-eslint/no-unused-vars */
containerStyle, wrapperStyle, optionTitles,
/* eslint-enable @typescript-eslint/no-unused-vars */
...messageTextProps } = props;
if (props.renderMessageText)
return props.renderMessageText(messageTextProps);
return <MessageText {...messageTextProps}/>;
}
return null;
}, [props, currentMessage]);
const renderMessageImage = useCallback(() => {
// Check if there are any image parts in the message
const hasImageParts = currentMessage?.parts?.some((part) => {
const mimeType = part.fileData?.mimeType;
return (mimeType &&
(mimeType === "image/jpeg" ||
mimeType === "image/png" ||
mimeType === "image/jpg" ||
mimeType === "image/webp" ||
mimeType === "image/heic" ||
mimeType === "image/heif"));
});
if (hasImageParts) {
const {
/* eslint-disable @typescript-eslint/no-unused-vars */
containerStyle, wrapperStyle,
/* eslint-enable @typescript-eslint/no-unused-vars */
...messageImageProps } = props;
if (props.renderMessageImage)
return props.renderMessageImage(messageImageProps);
return <MessageImage {...messageImageProps}/>;
}
return null;
}, [props, currentMessage]);
const renderMessageFile = useCallback(() => {
// Check if there are any file parts in the message
const hasFileParts = currentMessage?.parts?.some((part) => {
const mimeType = part.fileData?.mimeType;
return mimeType && mimeType === "application/pdf";
});
if (!hasFileParts)
return null;
const {
/* eslint-disable @typescript-eslint/no-unused-vars */
containerStyle, wrapperStyle,
/* eslint-enable @typescript-eslint/no-unused-vars */
...messageFileProps } = props;
return <MessageFile {...messageFileProps}/>;
}, [props, currentMessage]);
const renderMessageToolCall = useCallback(() => {
// Check if there are any tool call parts in the message
const hasToolCallParts = currentMessage?.parts?.some((part) => part.functionCall);
if (!hasToolCallParts)
return null;
const {
/* eslint-disable @typescript-eslint/no-unused-vars */
containerStyle, wrapperStyle,
/* eslint-enable @typescript-eslint/no-unused-vars */
...messageToolCallProps } = props;
if (props.renderMessageToolCall)
return props.renderMessageToolCall(messageToolCallProps);
return <MessageToolCall {...messageToolCallProps}/>;
}, [props, currentMessage]);
const renderBubbleContent = useCallback(() => {
return (<View>
{renderMessageImage()}
{renderMessageText()}
{renderMessageToolCall()}
{renderMessageFile()}
</View>);
}, [
renderMessageImage,
renderMessageText,
renderMessageFile,
renderMessageToolCall,
]);
const getBubbleWrapperStyle = useCallback(() => {
// Check if message has text parts
const hasTextParts = currentMessage?.parts?.some((part) => part.text);
// Base wrapper styles
const baseWrapperStyle = styles[position].wrapper;
// If it's not a text message, make background transparent
if (!hasTextParts) {
const transparentStyle = {
...baseWrapperStyle,
backgroundColor: "transparent",
borderWidth: 0,
borderColor: "transparent",
paddingHorizontal: 0,
paddingVertical: 0,
shadowOpacity: 0,
elevation: 0,
};
return [
transparentStyle,
styledBubbleToNext(),
styledBubbleToPrevious(),
wrapperStyle && wrapperStyle[position],
];
}
// For text messages, use normal bubble styles
return [
baseWrapperStyle,
styledBubbleToNext(),
styledBubbleToPrevious(),
wrapperStyle && wrapperStyle[position],
];
}, [
position,
styledBubbleToNext,
styledBubbleToPrevious,
wrapperStyle,
currentMessage,
]);
return (<View style={[
stylesCommon.fill,
styles[position].container,
containerStyle && containerStyle[position],
]}>
<View style={getBubbleWrapperStyle()}>
<TouchableWithoutFeedback onPress={onPress} onLongPress={onLongPress} accessibilityRole="text" {...props.touchableProps}>
<View>{renderBubbleContent()}</View>
</TouchableWithoutFeedback>
</View>
</View>);
};
export default Bubble;
//# sourceMappingURL=index.js.map