@codewithvincent/react-native-love-chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/Chat.d.ts +3 -0
- package/dist/Chat.js +78 -0
- package/dist/components/Bubble.d.ts +21 -0
- package/dist/components/Bubble.js +55 -0
- package/dist/components/Composer.d.ts +14 -0
- package/dist/components/Composer.js +44 -0
- package/dist/components/FooterReplyPreview.d.ts +7 -0
- package/dist/components/FooterReplyPreview.js +130 -0
- package/dist/components/Icons.d.ts +21 -0
- package/dist/components/Icons.js +71 -0
- package/dist/components/InputToolbar.d.ts +17 -0
- package/dist/components/InputToolbar.js +110 -0
- package/dist/components/Message.d.ts +21 -0
- package/dist/components/Message.js +131 -0
- package/dist/components/MessageList.d.ts +6 -0
- package/dist/components/MessageList.js +111 -0
- package/dist/components/MessageStatus.d.ts +6 -0
- package/dist/components/MessageStatus.js +59 -0
- package/dist/components/ReactionBubble.d.ts +55 -0
- package/dist/components/ReactionBubble.js +262 -0
- package/dist/components/ReplyPreview.d.ts +7 -0
- package/dist/components/ReplyPreview.js +102 -0
- package/dist/components/UploadFooter.d.ts +4 -0
- package/dist/components/UploadFooter.js +33 -0
- package/dist/components/adapters/UniversalAudio.d.ts +8 -0
- package/dist/components/adapters/UniversalAudio.js +23 -0
- package/dist/components/adapters/UniversalBlurView.d.ts +3 -0
- package/dist/components/adapters/UniversalBlurView.js +26 -0
- package/dist/components/adapters/UniversalVideo.d.ts +8 -0
- package/dist/components/adapters/UniversalVideo.js +20 -0
- package/dist/components/media/AudioCard.d.ts +7 -0
- package/dist/components/media/AudioCard.js +187 -0
- package/dist/components/media/FileCard.d.ts +8 -0
- package/dist/components/media/FileCard.js +68 -0
- package/dist/components/media/ImageCard.d.ts +9 -0
- package/dist/components/media/ImageCard.js +76 -0
- package/dist/components/media/VideoCard.d.ts +10 -0
- package/dist/components/media/VideoCard.js +232 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +36 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +2 -0
- package/dist/utils/index.d.ts +14 -0
- package/dist/utils/index.js +44 -0
- package/dist/utils/theme.d.ts +77 -0
- package/dist/utils/theme.js +55 -0
- package/package.json +67 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IMessage } from '../types';
|
|
3
|
+
interface InputToolbarProps {
|
|
4
|
+
onSend?: (messages: any[]) => void;
|
|
5
|
+
text?: string;
|
|
6
|
+
onTextChanged?: (text: string) => void;
|
|
7
|
+
renderComposer?: (props: any) => React.ReactNode;
|
|
8
|
+
renderSend?: (props: any) => React.ReactNode;
|
|
9
|
+
user: any;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
onPressAttachment?: (type: string) => void;
|
|
12
|
+
replyMessage?: IMessage | null;
|
|
13
|
+
onClearReply?: () => void;
|
|
14
|
+
renderUploadFooter?: (props: any) => React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
declare const InputToolbar: (props: InputToolbarProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default InputToolbar;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const Composer_1 = __importDefault(require("./Composer"));
|
|
10
|
+
const theme_1 = require("../utils/theme");
|
|
11
|
+
const Icons_1 = require("./Icons");
|
|
12
|
+
const UploadFooter_1 = __importDefault(require("./UploadFooter"));
|
|
13
|
+
const FooterReplyPreview_1 = __importDefault(require("./FooterReplyPreview"));
|
|
14
|
+
const InputToolbar = (props) => {
|
|
15
|
+
const theme = (0, theme_1.useTheme)();
|
|
16
|
+
const { onSend, text, renderComposer, renderSend, user, onPressAttachment, replyMessage, onClearReply, renderUploadFooter, ...rest } = props;
|
|
17
|
+
const [showUploadFooter, setShowUploadFooter] = (0, react_1.useState)(false);
|
|
18
|
+
const [keyboardHeight] = (0, react_1.useState)(new react_native_1.Animated.Value(0));
|
|
19
|
+
(0, react_1.useEffect)(() => {
|
|
20
|
+
const showSub = react_native_1.Keyboard.addListener(react_native_1.Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', e => {
|
|
21
|
+
react_native_1.Animated.timing(keyboardHeight, {
|
|
22
|
+
toValue: e.endCoordinates.height,
|
|
23
|
+
duration: e.duration || 250,
|
|
24
|
+
useNativeDriver: false,
|
|
25
|
+
}).start();
|
|
26
|
+
setShowUploadFooter(false);
|
|
27
|
+
});
|
|
28
|
+
const hideSub = react_native_1.Keyboard.addListener(react_native_1.Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', e => {
|
|
29
|
+
react_native_1.Animated.timing(keyboardHeight, {
|
|
30
|
+
toValue: 0,
|
|
31
|
+
duration: e.duration || 250,
|
|
32
|
+
useNativeDriver: false,
|
|
33
|
+
}).start();
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
showSub.remove();
|
|
37
|
+
hideSub.remove();
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
const handleSend = () => {
|
|
41
|
+
if (text && text.trim().length > 0) {
|
|
42
|
+
const message = {
|
|
43
|
+
_id: Math.round(Math.random() * 1000000),
|
|
44
|
+
text: text.trim(),
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
user: user,
|
|
47
|
+
replyTo: replyMessage,
|
|
48
|
+
};
|
|
49
|
+
onSend === null || onSend === void 0 ? void 0 : onSend([message]);
|
|
50
|
+
onClearReply === null || onClearReply === void 0 ? void 0 : onClearReply();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const toggleUploadFooter = () => {
|
|
54
|
+
if (showUploadFooter) {
|
|
55
|
+
setShowUploadFooter(false);
|
|
56
|
+
react_native_1.Keyboard.dismiss();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
setShowUploadFooter(true);
|
|
60
|
+
react_native_1.Keyboard.dismiss();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const handleActionPress = (type) => {
|
|
64
|
+
onPressAttachment === null || onPressAttachment === void 0 ? void 0 : onPressAttachment(type);
|
|
65
|
+
setShowUploadFooter(false);
|
|
66
|
+
};
|
|
67
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { children: [replyMessage && ((0, jsx_runtime_1.jsx)(FooterReplyPreview_1.default, { chatMessage: replyMessage, clearReply: onClearReply, userId: user._id })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, { borderTopColor: theme.colors.borderGray, backgroundColor: theme.colors.white }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: toggleUploadFooter, style: styles.addButton, children: (0, jsx_runtime_1.jsx)(Icons_1.PlusIcon, { size: 24, color: theme.colors.darkRed }) }), renderComposer ? (renderComposer(props)) : ((0, jsx_runtime_1.jsx)(Composer_1.default, { ...rest, text: text, onSend: handleSend, textInputProps: {
|
|
68
|
+
onFocus: () => setShowUploadFooter(false)
|
|
69
|
+
} })), renderSend ? (renderSend({ ...props, onSend: handleSend })) : ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: handleSend, style: styles.sendButton, disabled: !text || text.trim().length === 0, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.sendIconWrapper, { backgroundColor: theme.colors.darkRed }, (!text || text.trim().length === 0) && { backgroundColor: theme.colors.gray }], children: (0, jsx_runtime_1.jsx)(Icons_1.SendIcon, { size: 18, color: theme.colors.white }) }) }))] }), showUploadFooter && (renderUploadFooter ? (renderUploadFooter({
|
|
70
|
+
onActionPress: handleActionPress,
|
|
71
|
+
theme,
|
|
72
|
+
})) : ((0, jsx_runtime_1.jsx)(UploadFooter_1.default, { onActionPress: handleActionPress })))] }));
|
|
73
|
+
};
|
|
74
|
+
const styles = react_native_1.StyleSheet.create({
|
|
75
|
+
container: {
|
|
76
|
+
borderTopWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
77
|
+
borderTopColor: theme_1.defaultTheme.colors.borderGray,
|
|
78
|
+
backgroundColor: theme_1.defaultTheme.colors.white,
|
|
79
|
+
flexDirection: 'row',
|
|
80
|
+
alignItems: 'flex-end',
|
|
81
|
+
paddingVertical: 6,
|
|
82
|
+
paddingHorizontal: 8,
|
|
83
|
+
},
|
|
84
|
+
addButton: {
|
|
85
|
+
paddingHorizontal: 8,
|
|
86
|
+
paddingVertical: 10,
|
|
87
|
+
justifyContent: 'center',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
marginBottom: 0,
|
|
90
|
+
},
|
|
91
|
+
sendButton: {
|
|
92
|
+
paddingHorizontal: 8,
|
|
93
|
+
paddingVertical: 10,
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
marginBottom: 0,
|
|
97
|
+
},
|
|
98
|
+
sendIconWrapper: {
|
|
99
|
+
backgroundColor: theme_1.defaultTheme.colors.darkRed,
|
|
100
|
+
width: 36,
|
|
101
|
+
height: 36,
|
|
102
|
+
borderRadius: 18,
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
},
|
|
106
|
+
disabledSendWrapper: {
|
|
107
|
+
backgroundColor: theme_1.defaultTheme.colors.gray,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
exports.default = InputToolbar;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IMessage, IUser } from '../types';
|
|
3
|
+
interface MessageProps {
|
|
4
|
+
currentMessage: IMessage;
|
|
5
|
+
nextMessage?: IMessage;
|
|
6
|
+
previousMessage?: IMessage;
|
|
7
|
+
user: IUser;
|
|
8
|
+
renderBubble?: (props: any) => React.ReactNode;
|
|
9
|
+
onLongPress?: (context: any, message: IMessage) => void;
|
|
10
|
+
onPress?: (context: any, message: IMessage) => void;
|
|
11
|
+
onReply?: (message: IMessage) => void;
|
|
12
|
+
onReaction?: (message: IMessage, reaction: string) => void;
|
|
13
|
+
onDeleteMessage?: (message: IMessage) => void;
|
|
14
|
+
onDownloadFile?: (message: IMessage) => void;
|
|
15
|
+
renderMessageText?: (props: any) => React.ReactNode;
|
|
16
|
+
renderMessageImage?: (props: any) => React.ReactNode;
|
|
17
|
+
renderMessageVideo?: (props: any) => React.ReactNode;
|
|
18
|
+
renderMessageAudio?: (props: any) => React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
declare const Message: (props: MessageProps) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export default Message;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const ReanimatedSwipeable_1 = __importDefault(require("react-native-gesture-handler/ReanimatedSwipeable"));
|
|
10
|
+
const Bubble_1 = __importDefault(require("./Bubble"));
|
|
11
|
+
const MessageStatus_1 = __importDefault(require("./MessageStatus"));
|
|
12
|
+
const theme_1 = require("../utils/theme");
|
|
13
|
+
const ReplyPreview_1 = __importDefault(require("./ReplyPreview"));
|
|
14
|
+
const ImageCard_1 = __importDefault(require("./media/ImageCard"));
|
|
15
|
+
const VideoCard_1 = __importDefault(require("./media/VideoCard"));
|
|
16
|
+
const AudioCard_1 = __importDefault(require("./media/AudioCard"));
|
|
17
|
+
const FileCard_1 = __importDefault(require("./media/FileCard"));
|
|
18
|
+
const ReactionBubble_1 = __importDefault(require("./ReactionBubble"));
|
|
19
|
+
const Message = (props) => {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
const [isFullScreen, setFullScreen] = (0, react_1.useState)(false);
|
|
22
|
+
const theme = (0, theme_1.useTheme)();
|
|
23
|
+
const { currentMessage, user, onLongPress, onPress, renderBubble, onReply, onReaction, onDeleteMessage, onDownloadFile, } = props;
|
|
24
|
+
const isMine = currentMessage.user._id === user._id || currentMessage.isMine;
|
|
25
|
+
const swipeableRef = (0, react_1.useRef)(null);
|
|
26
|
+
const fileType = (_a = currentMessage.fileType) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
27
|
+
const isMedia = !!(currentMessage.image || currentMessage.video || currentMessage.audio || fileType === 'image' || fileType === 'video' || fileType === 'file' || fileType === 'audio');
|
|
28
|
+
const timestamp = new Date(currentMessage.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
29
|
+
const handleLongPress = () => {
|
|
30
|
+
onLongPress === null || onLongPress === void 0 ? void 0 : onLongPress(null, currentMessage);
|
|
31
|
+
};
|
|
32
|
+
const handlePress = () => {
|
|
33
|
+
onPress === null || onPress === void 0 ? void 0 : onPress(null, currentMessage);
|
|
34
|
+
if (!isMedia)
|
|
35
|
+
return;
|
|
36
|
+
if (currentMessage.image || fileType === 'image') {
|
|
37
|
+
setFullScreen(!isFullScreen);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const handleReactionPress = (reaction) => {
|
|
41
|
+
if (reaction && onReaction) {
|
|
42
|
+
onReaction(currentMessage, reaction);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const handleSwipeReply = () => {
|
|
46
|
+
var _a;
|
|
47
|
+
(_a = swipeableRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
48
|
+
onReply === null || onReply === void 0 ? void 0 : onReply(currentMessage);
|
|
49
|
+
};
|
|
50
|
+
const renderLeftActions = () => {
|
|
51
|
+
// Invisible view for swipe action trigger
|
|
52
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { width: 50 } });
|
|
53
|
+
};
|
|
54
|
+
const renderContent = () => {
|
|
55
|
+
if (currentMessage.image || fileType === 'image') {
|
|
56
|
+
if (props.renderMessageImage)
|
|
57
|
+
return props.renderMessageImage(props);
|
|
58
|
+
return ((0, jsx_runtime_1.jsx)(ImageCard_1.default, { uri: currentMessage.image || currentMessage.fileUrl || '', time: timestamp, setFullScreen: setFullScreen, isFullScreen: isFullScreen }));
|
|
59
|
+
}
|
|
60
|
+
if (currentMessage.video || fileType === 'video') {
|
|
61
|
+
if (props.renderMessageVideo)
|
|
62
|
+
return props.renderMessageVideo(props);
|
|
63
|
+
return ((0, jsx_runtime_1.jsx)(VideoCard_1.default, { file: { uri: currentMessage.video || currentMessage.fileUrl || '' }, time: timestamp, onLongPress: handleLongPress }));
|
|
64
|
+
}
|
|
65
|
+
if (currentMessage.audio || fileType === 'audio') {
|
|
66
|
+
if (props.renderMessageAudio)
|
|
67
|
+
return props.renderMessageAudio(props);
|
|
68
|
+
return (0, jsx_runtime_1.jsx)(AudioCard_1.default, { uri: currentMessage.audio || currentMessage.fileUrl || '', isMine: !!isMine });
|
|
69
|
+
}
|
|
70
|
+
if (fileType === 'file') {
|
|
71
|
+
return (0, jsx_runtime_1.jsx)(FileCard_1.default, { fileName: currentMessage.fileName || 'File', isMine: !!isMine, time: timestamp });
|
|
72
|
+
}
|
|
73
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { children: currentMessage.text ? (props.renderMessageText ? (props.renderMessageText(props)) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.text, isMine ? { color: theme.colors.ownMessageText } : { color: theme.colors.otherMessageText }], children: currentMessage.text }))) : null }));
|
|
74
|
+
};
|
|
75
|
+
// Reactions logic
|
|
76
|
+
const reactions = ['👍', '❤️', '😂', '😮', '😢', '🤲'];
|
|
77
|
+
const myReaction = (_c = (_b = currentMessage.reactions) === null || _b === void 0 ? void 0 : _b.find((r) => r.userId === user._id)) === null || _c === void 0 ? void 0 : _c.emoji;
|
|
78
|
+
const bubbleContent = ((0, jsx_runtime_1.jsxs)(Bubble_1.default, { isOwnMessage: !!isMine, isMedia: isMedia, children: [currentMessage.replyTo && ((0, jsx_runtime_1.jsx)(ReplyPreview_1.default, { replyTo: currentMessage.replyTo })), renderContent(), !isMedia && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.footer, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.time, isMine ? styles.timeMine : { color: theme.colors.timestamp }], children: timestamp }), isMine && (0, jsx_runtime_1.jsx)(MessageStatus_1.default, { status: currentMessage.status, isMine: !!isMine })] }))] }));
|
|
79
|
+
const messageView = ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, isMine ? styles.containerMine : styles.containerOther], children: (0, jsx_runtime_1.jsx)(ReactionBubble_1.default, { reactions: reactions, isMine: !!isMine, selectedReaction: myReaction, onReactionPress: handleReactionPress, onPress: handlePress, onLongPress: handleLongPress, bubbleStyle: styles.reactionPopup, onReply: () => onReply === null || onReply === void 0 ? void 0 : onReply(currentMessage), onDelete: onDeleteMessage ? () => onDeleteMessage(currentMessage) : undefined, onDownload: onDownloadFile ? () => onDownloadFile(currentMessage) : undefined, isFile: isMedia, children: renderBubble ? renderBubble({
|
|
80
|
+
...props,
|
|
81
|
+
isMine,
|
|
82
|
+
children: renderContent(),
|
|
83
|
+
}) : bubbleContent }) }));
|
|
84
|
+
if (onReply) {
|
|
85
|
+
return ((0, jsx_runtime_1.jsx)(ReanimatedSwipeable_1.default, { ref: swipeableRef, onSwipeableOpen: handleSwipeReply, renderLeftActions: renderLeftActions, leftThreshold: 40, overshootLeft: false, children: messageView }));
|
|
86
|
+
}
|
|
87
|
+
return messageView;
|
|
88
|
+
};
|
|
89
|
+
const styles = react_native_1.StyleSheet.create({
|
|
90
|
+
container: {
|
|
91
|
+
marginBottom: 10,
|
|
92
|
+
flexDirection: 'row',
|
|
93
|
+
paddingHorizontal: 8,
|
|
94
|
+
},
|
|
95
|
+
containerMine: {
|
|
96
|
+
justifyContent: 'flex-end',
|
|
97
|
+
},
|
|
98
|
+
containerOther: {
|
|
99
|
+
justifyContent: 'flex-start',
|
|
100
|
+
},
|
|
101
|
+
text: {
|
|
102
|
+
fontSize: 16,
|
|
103
|
+
lineHeight: 20,
|
|
104
|
+
},
|
|
105
|
+
textMine: {
|
|
106
|
+
color: theme_1.defaultTheme.colors.ownMessageText,
|
|
107
|
+
},
|
|
108
|
+
textOther: {
|
|
109
|
+
color: theme_1.defaultTheme.colors.otherMessageText,
|
|
110
|
+
},
|
|
111
|
+
footer: {
|
|
112
|
+
flexDirection: 'row',
|
|
113
|
+
justifyContent: 'flex-end',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
marginTop: 4,
|
|
116
|
+
},
|
|
117
|
+
time: {
|
|
118
|
+
fontSize: 10,
|
|
119
|
+
marginRight: 4,
|
|
120
|
+
},
|
|
121
|
+
timeMine: {
|
|
122
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
123
|
+
},
|
|
124
|
+
timeOther: {
|
|
125
|
+
color: '#aaa',
|
|
126
|
+
},
|
|
127
|
+
reactionPopup: {
|
|
128
|
+
backgroundColor: '#202A30',
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
exports.default = Message;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const flash_list_1 = require("@shopify/flash-list");
|
|
10
|
+
const Message_1 = __importDefault(require("./Message"));
|
|
11
|
+
const utils_1 = require("../utils");
|
|
12
|
+
const theme_1 = require("../utils/theme");
|
|
13
|
+
const MessageList = (props) => {
|
|
14
|
+
const { messages, user, onLoadEarlier, loadEarlier, isLoadingEarlier } = props;
|
|
15
|
+
const listRef = (0, react_1.useRef)(null);
|
|
16
|
+
const isMounted = (0, react_1.useRef)(false);
|
|
17
|
+
const theme = (0, theme_1.useTheme)();
|
|
18
|
+
const listData = (0, react_1.useMemo)(() => {
|
|
19
|
+
return Array.isArray(messages) ? [...messages].reverse() : [];
|
|
20
|
+
}, [messages]);
|
|
21
|
+
// Automatic scrolling logic
|
|
22
|
+
(0, react_1.useEffect)(() => {
|
|
23
|
+
if (listData.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
if (!isMounted.current) {
|
|
26
|
+
// Scenario 1: Initial mount - scroll to bottom immediately
|
|
27
|
+
isMounted.current = true;
|
|
28
|
+
// Use a small timeout to ensure list is ready
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
var _a;
|
|
31
|
+
(_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ animated: false });
|
|
32
|
+
}, 0);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Scenario 2: New message added - smooth scroll to bottom
|
|
36
|
+
// We can also check if we are already at the bottom to avoid annoyance,
|
|
37
|
+
// but requirements state "always displays the most recent message".
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
var _a;
|
|
40
|
+
(_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ animated: true });
|
|
41
|
+
}, 200);
|
|
42
|
+
}
|
|
43
|
+
}, [listData.length]);
|
|
44
|
+
const renderItem = (0, react_1.useCallback)(({ item, index }) => {
|
|
45
|
+
const previousMessage = listData[index - 1];
|
|
46
|
+
const nextMessage = listData[index + 1];
|
|
47
|
+
const messageProps = {
|
|
48
|
+
...props,
|
|
49
|
+
currentMessage: item,
|
|
50
|
+
previousMessage,
|
|
51
|
+
nextMessage,
|
|
52
|
+
user,
|
|
53
|
+
};
|
|
54
|
+
if (props.renderMessage) {
|
|
55
|
+
return props.renderMessage(messageProps);
|
|
56
|
+
}
|
|
57
|
+
const showDateHeader = shouldRenderDateHeader(item, previousMessage);
|
|
58
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { children: [showDateHeader && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.dateHeader, { backgroundColor: theme.colors.lightGrey }], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.dateText, { color: theme.colors.gray }], children: new Date(item.createdAt).toLocaleDateString(undefined, {
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: 'short',
|
|
61
|
+
day: 'numeric',
|
|
62
|
+
}) }) })), (0, jsx_runtime_1.jsx)(Message_1.default, { ...messageProps })] }));
|
|
63
|
+
}, [listData, user, props]);
|
|
64
|
+
const keyExtractor = (0, react_1.useCallback)((item) => item._id.toString(), []);
|
|
65
|
+
const renderHeader = () => {
|
|
66
|
+
if (loadEarlier) {
|
|
67
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loadEarlierContainer, children: isLoadingEarlier ? ((0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: theme.colors.primary })) : ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onLoadEarlier, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.loadEarlierText, { color: theme.colors.darkRed }], children: "Load earlier messages" }) })) }));
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
return ((0, jsx_runtime_1.jsx)(flash_list_1.FlashList, { ref: listRef, data: listData, renderItem: renderItem, keyExtractor: keyExtractor, contentContainerStyle: styles.listContent, ListHeaderComponent: renderHeader, keyboardShouldPersistTaps: props.keyboardShouldPersistTaps, maintainVisibleContentPosition: {
|
|
72
|
+
autoscrollToBottomThreshold: 0.2,
|
|
73
|
+
animateAutoScrollToBottom: true,
|
|
74
|
+
startRenderingFromBottom: true,
|
|
75
|
+
}, onStartReached: loadEarlier ? onLoadEarlier : undefined, onStartReachedThreshold: 0.1, estimatedItemSize: 72, ...props.listViewProps }));
|
|
76
|
+
};
|
|
77
|
+
const shouldRenderDateHeader = (currentMessage, previousMessage) => {
|
|
78
|
+
if (!previousMessage)
|
|
79
|
+
return true;
|
|
80
|
+
return !(0, utils_1.isSameDay)(currentMessage, previousMessage);
|
|
81
|
+
};
|
|
82
|
+
const styles = react_native_1.StyleSheet.create({
|
|
83
|
+
listContent: {
|
|
84
|
+
paddingVertical: 10,
|
|
85
|
+
paddingHorizontal: 10,
|
|
86
|
+
paddingBottom: 20,
|
|
87
|
+
},
|
|
88
|
+
dateHeader: {
|
|
89
|
+
alignSelf: 'center',
|
|
90
|
+
backgroundColor: theme_1.defaultTheme.colors.lightGrey,
|
|
91
|
+
paddingVertical: 4,
|
|
92
|
+
paddingHorizontal: 12,
|
|
93
|
+
borderRadius: 16,
|
|
94
|
+
marginBottom: 16,
|
|
95
|
+
marginTop: 16,
|
|
96
|
+
},
|
|
97
|
+
dateText: {
|
|
98
|
+
fontSize: 12,
|
|
99
|
+
color: theme_1.defaultTheme.colors.gray,
|
|
100
|
+
fontWeight: '500',
|
|
101
|
+
},
|
|
102
|
+
loadEarlierContainer: {
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
paddingVertical: 10,
|
|
105
|
+
},
|
|
106
|
+
loadEarlierText: {
|
|
107
|
+
color: theme_1.defaultTheme.colors.darkRed,
|
|
108
|
+
fontWeight: '600',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
exports.default = MessageList;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
37
|
+
const react_native_svg_1 = __importStar(require("react-native-svg"));
|
|
38
|
+
const theme_1 = require("../utils/theme");
|
|
39
|
+
const ClockIcon = ({ color }) => ((0, jsx_runtime_1.jsxs)(react_native_svg_1.default, { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)(react_native_svg_1.Path, { d: "M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" }), (0, jsx_runtime_1.jsx)(react_native_svg_1.Path, { d: "M12 6V12L16 14" })] }));
|
|
40
|
+
const CheckIcon = ({ color }) => ((0, jsx_runtime_1.jsx)(react_native_svg_1.default, { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)(react_native_svg_1.Path, { d: "M20 6L9 17L4 12" }) }));
|
|
41
|
+
const DoubleCheckIcon = ({ color }) => ((0, jsx_runtime_1.jsx)(react_native_svg_1.default, { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)(react_native_svg_1.Path, { d: "M7 12l5 5L22 7M2 12l5 5m5-5l5-5" }) }));
|
|
42
|
+
const MessageStatus = ({ status, isMine }) => {
|
|
43
|
+
const theme = (0, theme_1.useTheme)();
|
|
44
|
+
if (!isMine)
|
|
45
|
+
return null;
|
|
46
|
+
switch (status) {
|
|
47
|
+
case 'pending':
|
|
48
|
+
return (0, jsx_runtime_1.jsx)(ClockIcon, { color: theme.colors.gray });
|
|
49
|
+
case 'sent':
|
|
50
|
+
return (0, jsx_runtime_1.jsx)(CheckIcon, { color: theme.colors.gray });
|
|
51
|
+
case 'delivered':
|
|
52
|
+
return (0, jsx_runtime_1.jsx)(DoubleCheckIcon, { color: theme.colors.gray });
|
|
53
|
+
case 'read':
|
|
54
|
+
return (0, jsx_runtime_1.jsx)(DoubleCheckIcon, { color: theme.colors.lightRed }); // Use theme color or blue
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
exports.default = MessageStatus;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export declare const computePositions: ({ pos, reactionsCount, isMine, screenW, screenH, popupItemWidth, popupHeight, menuWidth, menuItemCount, menuItemHeight, verticalGap, horizontalMargin, reactionCoreWidth, reactionHorizontalPadding, popupHorizontalPadding, }: {
|
|
3
|
+
pos: {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
reactionsCount: number;
|
|
10
|
+
isMine: boolean;
|
|
11
|
+
screenW: number;
|
|
12
|
+
screenH: number;
|
|
13
|
+
popupItemWidth?: number;
|
|
14
|
+
popupHeight?: number;
|
|
15
|
+
menuWidth?: number;
|
|
16
|
+
menuItemCount?: number;
|
|
17
|
+
menuItemHeight?: number;
|
|
18
|
+
verticalGap?: number;
|
|
19
|
+
horizontalMargin?: number;
|
|
20
|
+
reactionCoreWidth?: number;
|
|
21
|
+
reactionHorizontalPadding?: number;
|
|
22
|
+
popupHorizontalPadding?: number;
|
|
23
|
+
}) => {
|
|
24
|
+
emojiPanel: {
|
|
25
|
+
left: number;
|
|
26
|
+
top: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
menu: {
|
|
31
|
+
left: number;
|
|
32
|
+
top: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
above: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type ReactionBubbleProps = {
|
|
39
|
+
reactions: string[];
|
|
40
|
+
isMine: boolean;
|
|
41
|
+
selectedReaction?: string;
|
|
42
|
+
onReactionPress: (reaction: string | undefined) => void;
|
|
43
|
+
style?: any;
|
|
44
|
+
bubbleStyle?: any;
|
|
45
|
+
reactionStyle?: any;
|
|
46
|
+
highlightColor?: string;
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
onPress?: () => void;
|
|
49
|
+
onLongPress?: () => void;
|
|
50
|
+
onReply?: () => void;
|
|
51
|
+
onDelete?: () => void;
|
|
52
|
+
onDownload?: () => void;
|
|
53
|
+
isFile?: boolean;
|
|
54
|
+
};
|
|
55
|
+
export default function ReactionBubble({ reactions, selectedReaction, onReactionPress, style, bubbleStyle, reactionStyle, highlightColor, children, isMine, onPress, onLongPress, onReply, onDelete, onDownload, isFile, ...rest }: ReactionBubbleProps): import("react/jsx-runtime").JSX.Element;
|