@codewithvincent/react-native-love-chat 0.1.0 → 0.2.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.
@@ -7,7 +7,6 @@ const theme_1 = require("../utils/theme");
7
7
  const Icons_1 = require("./Icons");
8
8
  const FooterReplyPreview = ({ chatMessage, clearReply, userId }) => {
9
9
  var _a, _b;
10
- console.log('chatMessage', chatMessage);
11
10
  const theme = (0, theme_1.useTheme)();
12
11
  if (!chatMessage)
13
12
  return null;
@@ -81,7 +80,7 @@ const styles = react_native_1.StyleSheet.create({
81
80
  borderLeftWidth: 4,
82
81
  borderLeftColor: theme_1.defaultTheme.colors.primary,
83
82
  borderBottomWidth: 0.3,
84
- borderBottomColor: '#ddd',
83
+ borderBottomColor: theme_1.defaultTheme.colors.replyPreviewBorder,
85
84
  },
86
85
  replyLine: {
87
86
  width: 4,
@@ -115,7 +114,7 @@ const styles = react_native_1.StyleSheet.create({
115
114
  height: 40,
116
115
  justifyContent: 'center',
117
116
  alignItems: 'center',
118
- backgroundColor: '#eee',
117
+ backgroundColor: theme_1.defaultTheme.colors.replyMediaBg,
119
118
  borderRadius: 4,
120
119
  },
121
120
  replyFileContainer: {
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -6,26 +39,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
39
  const jsx_runtime_1 = require("react/jsx-runtime");
7
40
  const react_1 = require("react");
8
41
  const react_native_1 = require("react-native");
9
- const ReanimatedSwipeable_1 = __importDefault(require("react-native-gesture-handler/ReanimatedSwipeable"));
42
+ const react_native_gesture_handler_1 = require("react-native-gesture-handler");
43
+ const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
10
44
  const Bubble_1 = __importDefault(require("./Bubble"));
11
45
  const MessageStatus_1 = __importDefault(require("./MessageStatus"));
12
46
  const theme_1 = require("../utils/theme");
13
47
  const ReplyPreview_1 = __importDefault(require("./ReplyPreview"));
14
48
  const ImageCard_1 = __importDefault(require("./media/ImageCard"));
15
- const VideoCard_1 = __importDefault(require("./media/VideoCard"));
16
- const AudioCard_1 = __importDefault(require("./media/AudioCard"));
17
49
  const FileCard_1 = __importDefault(require("./media/FileCard"));
18
50
  const ReactionBubble_1 = __importDefault(require("./ReactionBubble"));
19
51
  const Message = (props) => {
20
52
  var _a, _b, _c;
21
53
  const [isFullScreen, setFullScreen] = (0, react_1.useState)(false);
54
+ const [isVideo, setVideo] = (0, react_1.useState)(false);
22
55
  const theme = (0, theme_1.useTheme)();
23
56
  const { currentMessage, user, onLongPress, onPress, renderBubble, onReply, onReaction, onDeleteMessage, onDownloadFile, } = props;
24
57
  const isMine = currentMessage.user._id === user._id || currentMessage.isMine;
25
- const swipeableRef = (0, react_1.useRef)(null);
26
58
  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' });
59
+ const isMedia = !!(currentMessage.image ||
60
+ currentMessage.video ||
61
+ currentMessage.audio ||
62
+ fileType === 'image' ||
63
+ fileType === 'video' ||
64
+ fileType === 'file' ||
65
+ fileType === 'audio');
66
+ const timestamp = new Date(currentMessage.createdAt).toLocaleTimeString([], {
67
+ hour: '2-digit',
68
+ minute: '2-digit',
69
+ });
29
70
  const handleLongPress = () => {
30
71
  onLongPress === null || onLongPress === void 0 ? void 0 : onLongPress(null, currentMessage);
31
72
  };
@@ -35,6 +76,11 @@ const Message = (props) => {
35
76
  return;
36
77
  if (currentMessage.image || fileType === 'image') {
37
78
  setFullScreen(!isFullScreen);
79
+ return;
80
+ }
81
+ if (currentMessage.video || currentMessage.fileType === 'video') {
82
+ setVideo(true);
83
+ return;
38
84
  }
39
85
  };
40
86
  const handleReactionPress = (reaction) => {
@@ -42,47 +88,83 @@ const Message = (props) => {
42
88
  onReaction(currentMessage, reaction);
43
89
  }
44
90
  };
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
- };
91
+ // =============================
92
+ // Universal Swipe Implementation
93
+ // =============================
94
+ const translateX = (0, react_native_reanimated_1.useSharedValue)(0);
95
+ const threshold = 60;
96
+ const panGesture = react_native_gesture_handler_1.Gesture.Pan()
97
+ .activeOffsetX([-20, 20])
98
+ .failOffsetY([-15, 15])
99
+ .minDistance(15)
100
+ .maxPointers(1)
101
+ .onUpdate((event) => {
102
+ if (event.translationX > 0) {
103
+ translateX.value = event.translationX;
104
+ }
105
+ })
106
+ .onEnd(() => {
107
+ if (translateX.value > threshold && onReply) {
108
+ (0, react_native_reanimated_1.runOnJS)(onReply)(currentMessage);
109
+ }
110
+ translateX.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 200 });
111
+ });
112
+ const animatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => ({
113
+ transform: [{ translateX: translateX.value }],
114
+ }));
115
+ // =============================
116
+ // Content Rendering
117
+ // =============================
54
118
  const renderContent = () => {
55
119
  if (currentMessage.image || fileType === 'image') {
56
120
  if (props.renderMessageImage)
57
- return props.renderMessageImage(props);
121
+ return props.renderMessageImage(currentMessage);
58
122
  return ((0, jsx_runtime_1.jsx)(ImageCard_1.default, { uri: currentMessage.image || currentMessage.fileUrl || '', time: timestamp, setFullScreen: setFullScreen, isFullScreen: isFullScreen }));
59
123
  }
60
124
  if (currentMessage.video || fileType === 'video') {
125
+ const currentProps = {
126
+ ...currentMessage,
127
+ setFullScreen: setVideo,
128
+ isFullScreen: isVideo,
129
+ };
61
130
  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 }));
131
+ return props.renderMessageVideo(currentProps);
132
+ return null;
64
133
  }
65
134
  if (currentMessage.audio || fileType === 'audio') {
66
135
  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 });
136
+ return props.renderMessageAudio(currentMessage);
137
+ return null;
69
138
  }
70
139
  if (fileType === 'file') {
71
- return (0, jsx_runtime_1.jsx)(FileCard_1.default, { fileName: currentMessage.fileName || 'File', isMine: !!isMine, time: timestamp });
140
+ return ((0, jsx_runtime_1.jsx)(FileCard_1.default, { fileName: currentMessage.fileName || 'File', isMine: !!isMine, time: timestamp }));
72
141
  }
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 }));
142
+ 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: [
143
+ styles.text,
144
+ isMine
145
+ ? { color: theme.colors.ownMessageText }
146
+ : { color: theme.colors.otherMessageText },
147
+ ], children: currentMessage.text }))) : null }));
74
148
  };
75
- // Reactions logic
149
+ // =============================
150
+ // Reactions
151
+ // =============================
76
152
  const reactions = ['👍', '❤️', '😂', '😮', '😢', '🤲'];
77
153
  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 }) }));
154
+ const contentEl = renderContent();
155
+ const bubbleContent = contentEl ? ((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 }), contentEl, !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 })] }))] })) : null;
156
+ 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
157
+ ? renderBubble({
158
+ ...props,
159
+ isMine,
160
+ children: contentEl,
161
+ })
162
+ : bubbleContent }) }));
163
+ // =============================
164
+ // Conditional Swipe Wrapper
165
+ // =============================
84
166
  if (onReply) {
85
- return ((0, jsx_runtime_1.jsx)(ReanimatedSwipeable_1.default, { ref: swipeableRef, onSwipeableOpen: handleSwipeReply, renderLeftActions: renderLeftActions, leftThreshold: 40, overshootLeft: false, children: messageView }));
167
+ return ((0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureDetector, { gesture: react_native_gesture_handler_1.Gesture.Simultaneous(panGesture, react_native_gesture_handler_1.Gesture.Native()), children: (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.View, { style: animatedStyle, children: messageView }) }));
86
168
  }
87
169
  return messageView;
88
170
  };
@@ -119,13 +201,13 @@ const styles = react_native_1.StyleSheet.create({
119
201
  marginRight: 4,
120
202
  },
121
203
  timeMine: {
122
- color: 'rgba(255, 255, 255, 0.7)',
204
+ color: theme_1.defaultTheme.colors.timestampMine,
123
205
  },
124
206
  timeOther: {
125
- color: '#aaa',
207
+ color: theme_1.defaultTheme.colors.timestamp,
126
208
  },
127
209
  reactionPopup: {
128
- backgroundColor: '#202A30',
129
- }
210
+ backgroundColor: theme_1.defaultTheme.colors.reactionPopupBg,
211
+ },
130
212
  });
131
213
  exports.default = Message;
@@ -1,5 +1,5 @@
1
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, }: {
2
+ export declare const computePositions: ({ pos, reactionsCount, isMine, screenW, screenH, popupHeight, menuWidth, menuItemCount, menuItemHeight, verticalGap, horizontalMargin, reactionCoreWidth, reactionHorizontalPadding, popupHorizontalPadding, }: {
3
3
  pos: {
4
4
  x: number;
5
5
  y: number;
@@ -50,7 +50,7 @@ const Icons_1 = require("./Icons");
50
50
  const SCREEN = react_native_1.Dimensions.get('window');
51
51
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
52
52
  // Helper exported for tests (non-breaking)
53
- const computePositions = ({ pos, reactionsCount, isMine, screenW, screenH, popupItemWidth = 42, popupHeight = 60, menuWidth = 200, menuItemCount = 3, menuItemHeight = 44, verticalGap = 12, horizontalMargin = 8, reactionCoreWidth = 28, reactionHorizontalPadding = 8, popupHorizontalPadding = 12, }) => {
53
+ const computePositions = ({ pos, reactionsCount, isMine, screenW, screenH, popupHeight = 60, menuWidth = 200, menuItemCount = 3, menuItemHeight = 44, verticalGap = 12, horizontalMargin = 8, reactionCoreWidth = 28, reactionHorizontalPadding = 8, popupHorizontalPadding = 12, }) => {
54
54
  // More accurate width estimation: per-reaction width + container paddings
55
55
  const perReactionWidth = reactionCoreWidth + reactionHorizontalPadding * 2;
56
56
  const emojiPanelWidth = reactionsCount * perReactionWidth + popupHorizontalPadding * 2;
@@ -148,10 +148,10 @@ function ReactionBubble({ reactions, selectedReaction, onReactionPress, style, b
148
148
  }
149
149
  left = clamp(left, 10, screenW - popupWidth - 10);
150
150
  menuTop = pos.y + pos.height + 10;
151
- let _menuLeft = isMine ? (pos.x + pos.width - menuWidth) : pos.x;
151
+ let _menuLeft = isMine ? pos.x + pos.width - menuWidth : pos.x;
152
152
  menuLeft = clamp(_menuLeft, 10, screenW - menuWidth - 10);
153
153
  }
154
- siblingRef.current = new react_native_root_siblings_1.default(((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.overlayContainer, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: react_native_1.StyleSheet.absoluteFill, onPress: hidePopup, children: (0, jsx_runtime_1.jsx)(UniversalBlurView_1.UniversalBlurView, { style: react_native_1.StyleSheet.absoluteFill, blurType: 'dark', blurAmount: 12, tint: "dark", intensity: 60, reducedTransparencyFallbackColor: "#000000CC" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
154
+ siblingRef.current = new react_native_root_siblings_1.default((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.overlayContainer, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: react_native_1.StyleSheet.absoluteFill, onPress: hidePopup, children: (0, jsx_runtime_1.jsx)(UniversalBlurView_1.UniversalBlurView, { style: react_native_1.StyleSheet.absoluteFill, blurType: 'dark', blurAmount: 12, tint: "dark", intensity: 60, reducedTransparencyFallbackColor: theme.colors.blurFallback }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
155
155
  styles.messageDuplicateWrapper,
156
156
  {
157
157
  top: pos.y,
@@ -168,8 +168,17 @@ function ReactionBubble({ reactions, selectedReaction, onReactionPress, style, b
168
168
  top: menuTop,
169
169
  left: menuLeft,
170
170
  width: menuWidth,
171
- }
172
- ], children: [onReply && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: styles.menuItem, onPress: () => { hidePopup(); onReply(); }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.menuText, children: "Reply" }), (0, jsx_runtime_1.jsx)(Icons_1.ReplyIcon, { size: 18, color: "#fff" })] })), isFile && onDownload && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: styles.menuItem, onPress: () => { hidePopup(); onDownload(); }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.menuText, children: "Download" }), (0, jsx_runtime_1.jsx)(Icons_1.DownloadIcon, { size: 18, color: "#fff" })] })), onDelete && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: [styles.menuItem, { borderBottomWidth: 0 }], onPress: () => { hidePopup(); onDelete(); }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.menuText, { color: theme_1.defaultTheme.colors.darkRed }], children: "Delete" }), (0, jsx_runtime_1.jsx)(Icons_1.TrashIcon, { size: 18 })] }))] })] })));
171
+ },
172
+ ], children: [onReply && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: styles.menuItem, onPress: () => {
173
+ hidePopup();
174
+ onReply();
175
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.menuText, children: "Reply" }), (0, jsx_runtime_1.jsx)(Icons_1.ReplyIcon, { size: 18, color: theme.colors.white })] })), isFile && onDownload && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: styles.menuItem, onPress: () => {
176
+ hidePopup();
177
+ onDownload();
178
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.menuText, children: "Download" }), (0, jsx_runtime_1.jsx)(Icons_1.DownloadIcon, { size: 18, color: theme.colors.white })] })), onDelete && ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: [styles.menuItem, { borderBottomWidth: 0 }], onPress: () => {
179
+ hidePopup();
180
+ onDelete();
181
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.menuText, { color: theme_1.defaultTheme.colors.darkRed }], children: "Delete" }), (0, jsx_runtime_1.jsx)(Icons_1.TrashIcon, { size: 18 })] }))] })] }));
173
182
  requestAnimationFrame(() => {
174
183
  scale.value = 1;
175
184
  });
@@ -219,7 +228,7 @@ const styles = react_native_1.StyleSheet.create({
219
228
  paddingVertical: 8,
220
229
  paddingHorizontal: 12,
221
230
  borderRadius: 40,
222
- backgroundColor: '#202A30',
231
+ backgroundColor: theme_1.defaultTheme.colors.reactionPopupBg,
223
232
  zIndex: 100,
224
233
  },
225
234
  reaction: {
@@ -239,7 +248,7 @@ const styles = react_native_1.StyleSheet.create({
239
248
  },
240
249
  contextMenu: {
241
250
  position: 'absolute',
242
- backgroundColor: 'rgba(30, 30, 30, 0.95)',
251
+ backgroundColor: theme_1.defaultTheme.colors.reactionMenuBg,
243
252
  borderRadius: 12,
244
253
  paddingVertical: 4,
245
254
  width: 200,
@@ -252,10 +261,10 @@ const styles = react_native_1.StyleSheet.create({
252
261
  paddingVertical: 12,
253
262
  paddingHorizontal: 16,
254
263
  borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
255
- borderBottomColor: 'rgba(255, 255, 255, 0.1)',
264
+ borderBottomColor: theme_1.defaultTheme.colors.reactionMenuSeparator,
256
265
  },
257
266
  menuText: {
258
- color: '#fff',
267
+ color: theme_1.defaultTheme.colors.white,
259
268
  fontSize: 16,
260
269
  fontWeight: '400',
261
270
  },
@@ -86,7 +86,7 @@ const styles = react_native_1.StyleSheet.create({
86
86
  borderRadius: 6,
87
87
  marginLeft: 10,
88
88
  overflow: 'hidden',
89
- backgroundColor: '#ccc',
89
+ backgroundColor: theme_1.defaultTheme.colors.mediaThumbBg,
90
90
  justifyContent: 'center',
91
91
  alignItems: 'center',
92
92
  },
@@ -4,5 +4,5 @@ interface FileCardProps {
4
4
  isMine?: boolean;
5
5
  time?: string;
6
6
  }
7
- export default function FileCard({ fileName, isMine, time, }: FileCardProps): import("react/jsx-runtime").JSX.Element;
7
+ export default function FileCard({ fileName, isMine, time }: FileCardProps): import("react/jsx-runtime").JSX.Element;
8
8
  export {};
@@ -6,14 +6,14 @@ const react_native_1 = require("react-native");
6
6
  const utils_1 = require("../../utils");
7
7
  const theme_1 = require("../../utils/theme");
8
8
  const Icons_1 = require("../Icons");
9
- function FileCard({ fileName, isMine = false, time, }) {
9
+ function FileCard({ fileName, isMine = false, time }) {
10
10
  const theme = (0, theme_1.useTheme)();
11
11
  // WhatsApp-style colors
12
12
  // Sent: Lighter green bubble, file card is slightly darker green/transparent
13
13
  // Received: White bubble, file card is light gray
14
14
  const cardBackgroundColor = isMine ? theme.colors.ownFileBg : theme.colors.otherFileBg;
15
15
  const textColor = isMine ? theme.colors.ownMessageText : theme.colors.otherMessageText;
16
- const subTextColor = isMine ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.5)';
16
+ const subTextColor = isMine ? theme.colors.timestampMine : theme.colors.mediumGray;
17
17
  const iconColor = isMine ? theme.colors.white : theme.colors.darkRed;
18
18
  const getExtension = (name) => {
19
19
  var _a;
@@ -64,5 +64,5 @@ const styles = react_native_1.StyleSheet.create({
64
64
  },
65
65
  timeText: {
66
66
  fontSize: 10,
67
- }
67
+ },
68
68
  });
@@ -7,10 +7,12 @@ const react_native_1 = require("react-native");
7
7
  const utils_1 = require("../../utils");
8
8
  const Icons_1 = require("../Icons");
9
9
  const react_native_safe_area_context_1 = require("react-native-safe-area-context");
10
+ const theme_1 = require("../../utils/theme");
10
11
  const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
11
12
  const ImageComponent = react_native_1.Image;
12
13
  function ImageCard({ uri, maxWidth, time, isFullScreen, setFullScreen, }) {
13
14
  const inset = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
15
+ const theme = (0, theme_1.useTheme)();
14
16
  const bubbleWidth = maxWidth || utils_1.appSize.width(65);
15
17
  const [bubbleHeight, setBubbleHeight] = (0, react_1.useState)(bubbleWidth * 0.75);
16
18
  (0, react_1.useEffect)(() => {
@@ -30,13 +32,13 @@ function ImageCard({ uri, maxWidth, time, isFullScreen, setFullScreen, }) {
30
32
  ], children: [(0, jsx_runtime_1.jsx)(ImageComponent, { source: { uri }, style: styles.image, resizeMode: "cover" }), time && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.timeOverlay, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.timeText, children: time }) }))] }), (0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: isFullScreen, transparent: false, onRequestClose: () => setFullScreen(false), animationType: "fade", children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.modal, children: [(0, jsx_runtime_1.jsx)(ImageComponent, { source: { uri }, style: styles.fullScreenImage, resizeMode: "contain" }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [
31
33
  styles.closeButton,
32
34
  { top: inset.top + 10 },
33
- ], onPress: () => setFullScreen(false), children: (0, jsx_runtime_1.jsx)(Icons_1.CloseIcon, { size: 24, color: "#fff" }) })] }) })] }));
35
+ ], onPress: () => setFullScreen(false), children: (0, jsx_runtime_1.jsx)(Icons_1.CloseIcon, { size: 24, color: theme.colors.white }) })] }) })] }));
34
36
  }
35
37
  const styles = react_native_1.StyleSheet.create({
36
38
  container: {
37
39
  borderRadius: utils_1.appSize.width(2),
38
40
  overflow: 'hidden',
39
- backgroundColor: '#000',
41
+ backgroundColor: theme_1.defaultTheme.colors.black,
40
42
  margin: -2,
41
43
  },
42
44
  image: {
@@ -47,7 +49,7 @@ const styles = react_native_1.StyleSheet.create({
47
49
  flex: 1,
48
50
  justifyContent: 'center',
49
51
  alignItems: 'center',
50
- backgroundColor: '#000',
52
+ backgroundColor: theme_1.defaultTheme.colors.black,
51
53
  },
52
54
  fullScreenImage: {
53
55
  width: SCREEN_WIDTH,
@@ -63,13 +65,13 @@ const styles = react_native_1.StyleSheet.create({
63
65
  position: 'absolute',
64
66
  bottom: 5,
65
67
  right: 5,
66
- backgroundColor: 'rgba(0,0,0,0.5)',
68
+ backgroundColor: theme_1.defaultTheme.colors.overlayBlack50,
67
69
  borderRadius: 10,
68
70
  paddingHorizontal: 6,
69
71
  paddingVertical: 2,
70
72
  },
71
73
  timeText: {
72
- color: '#fff',
74
+ color: theme_1.defaultTheme.colors.white,
73
75
  fontSize: 10,
74
76
  fontWeight: '600',
75
77
  },
package/dist/types.d.ts CHANGED
@@ -45,6 +45,7 @@ export interface ChatProps {
45
45
  renderMessageImage?: (props: any) => React.ReactNode;
46
46
  renderMessageVideo?: (props: any) => React.ReactNode;
47
47
  renderMessageAudio?: (props: any) => React.ReactNode;
48
+ renderAudio?: (props: any) => React.ReactNode;
48
49
  renderFooter?: () => React.ReactNode;
49
50
  renderChatFooter?: () => React.ReactNode;
50
51
  renderUploadFooter?: (props: any) => React.ReactNode;
@@ -23,6 +23,20 @@ export declare const defaultTheme: {
23
23
  otherMessageText: string;
24
24
  ownFileBg: string;
25
25
  otherFileBg: string;
26
+ timestampMine: string;
27
+ reactionPopupBg: string;
28
+ reactionMenuBg: string;
29
+ reactionMenuSeparator: string;
30
+ overlayBlack50: string;
31
+ overlayBlack35: string;
32
+ overlayBlack55: string;
33
+ overlayWhite20: string;
34
+ mediaThumbBg: string;
35
+ replyMediaBg: string;
36
+ replyPreviewBorder: string;
37
+ progressTrackBg: string;
38
+ mediumGray: string;
39
+ blurFallback: string;
26
40
  };
27
41
  fonts: {
28
42
  light: string;
@@ -61,6 +75,20 @@ export declare const useTheme: () => {
61
75
  otherMessageText: string;
62
76
  ownFileBg: string;
63
77
  otherFileBg: string;
78
+ timestampMine: string;
79
+ reactionPopupBg: string;
80
+ reactionMenuBg: string;
81
+ reactionMenuSeparator: string;
82
+ overlayBlack50: string;
83
+ overlayBlack35: string;
84
+ overlayBlack55: string;
85
+ overlayWhite20: string;
86
+ mediaThumbBg: string;
87
+ replyMediaBg: string;
88
+ replyPreviewBorder: string;
89
+ progressTrackBg: string;
90
+ mediumGray: string;
91
+ blurFallback: string;
64
92
  };
65
93
  fonts: {
66
94
  light: string;
@@ -27,6 +27,20 @@ exports.defaultTheme = {
27
27
  otherMessageText: '#000',
28
28
  ownFileBg: 'rgba(0, 0, 0, 0.1)',
29
29
  otherFileBg: 'rgba(0, 0, 0, 0.05)',
30
+ timestampMine: 'rgba(255, 255, 255, 0.7)',
31
+ reactionPopupBg: '#202A30',
32
+ reactionMenuBg: 'rgba(30, 30, 30, 0.95)',
33
+ reactionMenuSeparator: 'rgba(255, 255, 255, 0.1)',
34
+ overlayBlack50: 'rgba(0,0,0,0.5)',
35
+ overlayBlack35: 'rgba(0,0,0,0.35)',
36
+ overlayBlack55: 'rgba(0,0,0,0.55)',
37
+ overlayWhite20: 'rgba(255,255,255,0.2)',
38
+ mediaThumbBg: '#ccc',
39
+ replyMediaBg: '#eee',
40
+ replyPreviewBorder: '#ddd',
41
+ progressTrackBg: '#e0e0e0',
42
+ mediumGray: '#666',
43
+ blurFallback: '#000000CC',
30
44
  },
31
45
  fonts: {
32
46
  light: 'System',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewithvincent/react-native-love-chat",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A fully featured React Native chat UI component with replies, reactions, media, and theming.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -23,7 +23,7 @@
23
23
  "react": ">=18.0.0",
24
24
  "react-native": ">=0.73.0",
25
25
  "react-native-gesture-handler": ">=2.12.0",
26
- "react-native-reanimated": ">=3.6.0",
26
+ "react-native-reanimated": ">=3.6.0 <5.0.0",
27
27
  "react-native-safe-area-context": ">=4.10.0",
28
28
  "react-native-root-siblings": ">=4.2.1",
29
29
  "@shopify/flash-list": ">=1.6.3",
@@ -1,8 +0,0 @@
1
- type EngineType = 'expo-audio' | 'rnsound' | 'none';
2
- export interface AudioEngineType {
3
- type: EngineType;
4
- ExpoAudio: any;
5
- RNSound: any;
6
- }
7
- export declare const AudioEngine: AudioEngineType;
8
- export {};
@@ -1,23 +0,0 @@
1
- "use strict";
2
- // src/adapters/UniversalAudio.ts
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.AudioEngine = void 0;
5
- let ExpoAudio = null;
6
- let RNSound = null;
7
- // Try expo-audio first
8
- try {
9
- ExpoAudio = require('expo-audio');
10
- }
11
- catch { }
12
- // Fallback to react-native-sound
13
- if (!ExpoAudio) {
14
- try {
15
- RNSound = require('react-native-sound');
16
- }
17
- catch { }
18
- }
19
- exports.AudioEngine = {
20
- type: ExpoAudio ? 'expo-audio' : RNSound ? 'rnsound' : 'none',
21
- ExpoAudio,
22
- RNSound,
23
- };
@@ -1,8 +0,0 @@
1
- type EngineType = 'expo-video' | 'rn-video' | 'none';
2
- export interface VideoEngineType {
3
- type: EngineType;
4
- ExpoVideo: any;
5
- RNVideo: any;
6
- }
7
- export declare const VideoEngine: VideoEngineType;
8
- export {};
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VideoEngine = void 0;
4
- let ExpoVideo = null;
5
- let RNVideo = null;
6
- try {
7
- ExpoVideo = require('expo-video');
8
- }
9
- catch { }
10
- if (!ExpoVideo) {
11
- try {
12
- RNVideo = require('react-native-video').default;
13
- }
14
- catch { }
15
- }
16
- exports.VideoEngine = {
17
- type: ExpoVideo ? 'expo-video' : RNVideo ? 'rn-video' : 'none',
18
- ExpoVideo,
19
- RNVideo,
20
- };
@@ -1,7 +0,0 @@
1
- interface AudioMessageProps {
2
- uri: string;
3
- maxWidth?: number;
4
- isMine?: boolean;
5
- }
6
- export default function AudioCard({ uri, maxWidth, isMine }: AudioMessageProps): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,187 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = AudioCard;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const react_1 = require("react");
6
- const react_native_1 = require("react-native");
7
- const utils_1 = require("../../utils");
8
- const theme_1 = require("../../utils/theme");
9
- const Icons_1 = require("../Icons");
10
- const UniversalAudio_1 = require("../adapters/UniversalAudio");
11
- function AudioCard({ uri, maxWidth, isMine }) {
12
- const soundRef = (0, react_1.useRef)(null);
13
- const intervalRef = (0, react_1.useRef)(null);
14
- const progressAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
15
- const totalDuration = (0, react_1.useRef)(0);
16
- const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
17
- const [isLoading, setIsLoading] = (0, react_1.useState)(false);
18
- const [currentTime, setCurrentTime] = (0, react_1.useState)('0:00');
19
- const theme = (0, theme_1.useTheme)();
20
- (0, react_1.useEffect)(() => {
21
- return () => {
22
- cleanup();
23
- };
24
- }, []);
25
- const cleanup = async () => {
26
- var _a, _b;
27
- if (UniversalAudio_1.AudioEngine.type === 'expo-audio' && soundRef.current) {
28
- await ((_b = (_a = soundRef.current) === null || _a === void 0 ? void 0 : _a.stop) === null || _b === void 0 ? void 0 : _b.call(_a));
29
- }
30
- if (UniversalAudio_1.AudioEngine.type === 'rnsound' && soundRef.current) {
31
- soundRef.current.release();
32
- }
33
- if (intervalRef.current)
34
- clearInterval(intervalRef.current);
35
- };
36
- const formatTime = (sec) => {
37
- const m = Math.floor(sec / 60);
38
- const s = Math.floor(sec % 60);
39
- return `${m}:${s < 10 ? '0' : ''}${s}`;
40
- };
41
- const onFinish = () => {
42
- setIsPlaying(false);
43
- progressAnim.setValue(0);
44
- setCurrentTime('0:00');
45
- if (intervalRef.current)
46
- clearInterval(intervalRef.current);
47
- };
48
- const play = async () => {
49
- var _a;
50
- if (!uri)
51
- return;
52
- // 🔵 EXPO-AUDIO MODE
53
- if (UniversalAudio_1.AudioEngine.type === 'expo-audio') {
54
- try {
55
- setIsLoading(true);
56
- const { createAudioPlayer } = UniversalAudio_1.AudioEngine.ExpoAudio;
57
- const player = createAudioPlayer({ uri });
58
- soundRef.current = player;
59
- player.play();
60
- setIsPlaying(true);
61
- setIsLoading(false);
62
- totalDuration.current = (_a = player.duration) !== null && _a !== void 0 ? _a : 0;
63
- intervalRef.current = setInterval(() => {
64
- var _a;
65
- const pos = (_a = player.currentTime) !== null && _a !== void 0 ? _a : 0;
66
- if (totalDuration.current > 0) {
67
- progressAnim.setValue(pos / totalDuration.current);
68
- }
69
- setCurrentTime(formatTime(pos));
70
- if (player.didJustFinish) {
71
- onFinish();
72
- }
73
- }, 200);
74
- }
75
- catch (e) {
76
- utils_1.Logger.error('expo-audio error', e);
77
- setIsLoading(false);
78
- }
79
- return;
80
- }
81
- // 🟠 RN SOUND MODE
82
- if (UniversalAudio_1.AudioEngine.type === 'rnsound') {
83
- const Sound = UniversalAudio_1.AudioEngine.RNSound;
84
- if (!soundRef.current) {
85
- setIsLoading(true);
86
- const sound = new Sound(uri, '', (err) => {
87
- setIsLoading(false);
88
- if (err) {
89
- utils_1.Logger.error('RN Sound error', err);
90
- return;
91
- }
92
- soundRef.current = sound;
93
- totalDuration.current = sound.getDuration();
94
- sound.play(onFinish);
95
- setIsPlaying(true);
96
- intervalRef.current = setInterval(() => {
97
- sound.getCurrentTime((sec) => {
98
- if (totalDuration.current > 0) {
99
- progressAnim.setValue(sec / totalDuration.current);
100
- }
101
- setCurrentTime(formatTime(sec));
102
- });
103
- }, 200);
104
- });
105
- return;
106
- }
107
- soundRef.current.play(onFinish);
108
- setIsPlaying(true);
109
- return;
110
- }
111
- utils_1.Logger.error('No audio engine available.');
112
- };
113
- const pause = async () => {
114
- var _a, _b;
115
- if (!soundRef.current)
116
- return;
117
- if (UniversalAudio_1.AudioEngine.type === 'expo-audio') {
118
- (_b = (_a = soundRef.current).pause) === null || _b === void 0 ? void 0 : _b.call(_a);
119
- }
120
- if (UniversalAudio_1.AudioEngine.type === 'rnsound') {
121
- soundRef.current.pause();
122
- }
123
- setIsPlaying(false);
124
- if (intervalRef.current)
125
- clearInterval(intervalRef.current);
126
- };
127
- const progressWidth = progressAnim.interpolate({
128
- inputRange: [0, 1],
129
- outputRange: ['0%', '100%'],
130
- });
131
- const cardBackgroundColor = isMine ? theme.colors.ownFileBg : theme.colors.otherFileBg;
132
- const textColor = isMine ? theme.colors.ownMessageText : theme.colors.otherMessageText;
133
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
134
- styles.container,
135
- {
136
- maxWidth: maxWidth || utils_1.appSize.width(60),
137
- backgroundColor: cardBackgroundColor
138
- },
139
- ], children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [styles.playButton, { backgroundColor: theme.colors.darkRed }], onPress: isPlaying ? pause : play, disabled: isLoading, children: isLoading ? ((0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: "#fff" })) : isPlaying ? ((0, jsx_runtime_1.jsx)(Icons_1.PauseIcon, { size: 18, color: "#fff" })) : ((0, jsx_runtime_1.jsx)(Icons_1.PlayIcon, { size: 18, color: "#fff" })) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.middle, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressBackground }), (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [
140
- styles.progressForeground,
141
- { width: progressWidth, backgroundColor: theme.colors.darkRed },
142
- ] })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.timeText, { color: textColor }], children: currentTime })] }));
143
- }
144
- const styles = react_native_1.StyleSheet.create({
145
- container: {
146
- flexDirection: 'row',
147
- minWidth: utils_1.appSize.width(60),
148
- alignItems: 'center',
149
- backgroundColor: theme_1.defaultTheme.colors.white,
150
- paddingVertical: 8,
151
- paddingHorizontal: 10,
152
- borderRadius: utils_1.appSize.width(2),
153
- flexShrink: 1,
154
- },
155
- playButton: {
156
- width: 38,
157
- height: 38,
158
- borderRadius: 19,
159
- backgroundColor: theme_1.defaultTheme.colors.darkRed,
160
- justifyContent: 'center',
161
- alignItems: 'center',
162
- marginRight: 12,
163
- },
164
- timeText: {
165
- marginLeft: 12,
166
- width: 45,
167
- fontSize: 12,
168
- color: '#666',
169
- textAlign: 'right',
170
- },
171
- middle: {
172
- flex: 1,
173
- justifyContent: 'center',
174
- height: 4,
175
- borderRadius: 2,
176
- backgroundColor: '#e0e0e0',
177
- overflow: 'hidden',
178
- },
179
- progressBackground: {
180
- ...react_native_1.StyleSheet.absoluteFillObject,
181
- backgroundColor: '#e0e0e0',
182
- },
183
- progressForeground: {
184
- ...react_native_1.StyleSheet.absoluteFillObject,
185
- backgroundColor: theme_1.defaultTheme.colors.darkRed,
186
- },
187
- });
@@ -1,10 +0,0 @@
1
- interface VideoMessageProps {
2
- file: {
3
- uri: string;
4
- };
5
- maxWidth?: number;
6
- time?: string;
7
- onLongPress?: () => void;
8
- }
9
- export default function VideoCard({ file, maxWidth, time, onLongPress, }: VideoMessageProps): import("react/jsx-runtime").JSX.Element;
10
- export {};
@@ -1,232 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = VideoCard;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const react_1 = require("react");
6
- const react_native_1 = require("react-native");
7
- const utils_1 = require("../../utils");
8
- const theme_1 = require("../../utils/theme");
9
- const Icons_1 = require("../Icons");
10
- const react_native_safe_area_context_1 = require("react-native-safe-area-context");
11
- const UniversalVideo_1 = require("../adapters/UniversalVideo");
12
- const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
13
- const DEFAULT_ASPECT_RATIO = 16 / 9;
14
- function VideoCard({ file, maxWidth, time, onLongPress, // ✅ ADDED
15
- }) {
16
- const inset = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
17
- const videoRef = (0, react_1.useRef)(null);
18
- const theme = (0, theme_1.useTheme)();
19
- const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
20
- const [duration, setDuration] = (0, react_1.useState)(0);
21
- const [current, setCurrent] = (0, react_1.useState)(0);
22
- const [isFullScreen, setFullScreen] = (0, react_1.useState)(false);
23
- const progressAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
24
- const bubbleWidth = maxWidth || utils_1.appSize.width(65);
25
- const initialHeight = Math.min(Math.max(bubbleWidth / DEFAULT_ASPECT_RATIO, 120), 350);
26
- const [videoHeight, setVideoHeight] = (0, react_1.useState)(initialHeight);
27
- (0, react_1.useEffect)(() => {
28
- progressAnim.setValue(current / (duration || 1));
29
- }, [current, duration]);
30
- const formatTime = (secs) => {
31
- if (!secs)
32
- return '0:00';
33
- const m = Math.floor(secs / 60);
34
- const s = Math.floor(secs % 60);
35
- return `${m}:${s < 10 ? '0' + s : s}`;
36
- };
37
- const progressWidth = progressAnim.interpolate({
38
- inputRange: [0, 1],
39
- outputRange: ['0%', '100%'],
40
- });
41
- if (UniversalVideo_1.VideoEngine.type === 'none') {
42
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
43
- styles.wrapper,
44
- {
45
- width: bubbleWidth,
46
- height: 120,
47
- justifyContent: 'center',
48
- alignItems: 'center',
49
- backgroundColor: theme.colors.black,
50
- },
51
- ], children: [(0, jsx_runtime_1.jsx)(Icons_1.VideoIcon, { size: 40, color: theme.colors.white }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: 'white', marginTop: 10 }, children: "Video not supported" })] }));
52
- }
53
- const VideoComponent = UniversalVideo_1.VideoEngine.type === 'expo-video'
54
- ? UniversalVideo_1.VideoEngine.ExpoVideo.Video
55
- : UniversalVideo_1.VideoEngine.RNVideo;
56
- const togglePlayPause = () => {
57
- setIsPlaying(prev => !prev);
58
- };
59
- const renderVideoPlayer = (isFull) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: isFull
60
- ? styles.fullScreenWrapper
61
- : [styles.videoArea, { width: bubbleWidth, height: videoHeight }], children: [(0, jsx_runtime_1.jsx)(VideoComponent, { ref: videoRef, source: { uri: file.uri }, style: isFull ? styles.fullScreenVideo : styles.video, resizeMode: isFull ? 'contain' : 'cover', paused: !isPlaying, onLoad: (meta) => {
62
- var _a;
63
- const dur = (_a = meta === null || meta === void 0 ? void 0 : meta.duration) !== null && _a !== void 0 ? _a : ((meta === null || meta === void 0 ? void 0 : meta.durationMillis)
64
- ? meta.durationMillis / 1000
65
- : 0);
66
- setDuration(dur);
67
- if (!isFull && (meta === null || meta === void 0 ? void 0 : meta.naturalSize)) {
68
- const { width: w, height: h } = meta.naturalSize;
69
- if (w && h) {
70
- const ratio = h / w;
71
- let finalHeight = bubbleWidth * ratio;
72
- finalHeight = Math.min(Math.max(finalHeight, 120), 350);
73
- setVideoHeight(finalHeight);
74
- }
75
- }
76
- }, onProgress: (data) => {
77
- const time = (data === null || data === void 0 ? void 0 : data.currentTime) ||
78
- (data === null || data === void 0 ? void 0 : data.positionMillis) / 1000 ||
79
- 0;
80
- if (time !== undefined && time !== null) {
81
- setCurrent(time);
82
- }
83
- }, onEnd: () => {
84
- setIsPlaying(false);
85
- setCurrent(duration);
86
- progressAnim.setValue(1);
87
- } }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: react_native_1.StyleSheet.absoluteFill, onPress: togglePlayPause, onLongPress: onLongPress, delayLongPress: 250, activeOpacity: 1, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.centerButton, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.controlCircle, children: isPlaying ? ((0, jsx_runtime_1.jsx)(Icons_1.PauseIcon, { size: 24 })) : ((0, jsx_runtime_1.jsx)(Icons_1.PlayIcon, { size: 24 })) }) }) }), !isFull && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [
88
- styles.progress,
89
- {
90
- width: progressWidth,
91
- backgroundColor: theme.colors.darkRed,
92
- },
93
- ] }) }), time && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.timeOverlay, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.timeText, children: time }) }))] })), isFull && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
94
- styles.modalCloseButton,
95
- { top: inset.top + 10 },
96
- ], children: (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: () => setFullScreen(false), children: (0, jsx_runtime_1.jsx)(Icons_1.CloseIcon, { size: 24, color: "#fff" }) }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.modalProgressWrapper, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.modalTime, children: formatTime(current) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.modalProgressBackground, children: (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [
97
- styles.modalProgressForeground,
98
- {
99
- width: progressWidth,
100
- backgroundColor: theme.colors.darkRed,
101
- },
102
- ] }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.modalTime, children: formatTime(duration) })] })] }))] }));
103
- if (isFullScreen) {
104
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: react_native_1.StyleSheet.absoluteFill, children: renderVideoPlayer(true) }));
105
- }
106
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.wrapper, { width: bubbleWidth }], children: renderVideoPlayer(false) }));
107
- }
108
- const styles = react_native_1.StyleSheet.create({
109
- wrapper: {
110
- borderRadius: utils_1.appSize.width(2),
111
- overflow: 'hidden',
112
- backgroundColor: theme_1.defaultTheme.colors.black,
113
- margin: -2,
114
- },
115
- videoArea: {
116
- borderRadius: utils_1.appSize.width(2),
117
- overflow: 'hidden',
118
- backgroundColor: theme_1.defaultTheme.colors.black,
119
- },
120
- video: {
121
- width: '100%',
122
- height: '100%',
123
- },
124
- bottomGradient: {
125
- position: 'absolute',
126
- bottom: 0,
127
- width: '100%',
128
- height: utils_1.appSize.height(5),
129
- backgroundColor: 'rgba(0,0,0,0.35)',
130
- justifyContent: 'flex-end',
131
- paddingBottom: 6,
132
- paddingHorizontal: 8,
133
- },
134
- durationBadge: {
135
- flexDirection: 'row',
136
- alignItems: 'center',
137
- alignSelf: 'flex-end',
138
- backgroundColor: 'rgba(0,0,0,0.55)',
139
- paddingHorizontal: 8,
140
- paddingVertical: 3,
141
- borderRadius: 10,
142
- },
143
- durationText: {
144
- color: theme_1.defaultTheme.colors.white,
145
- fontSize: 13,
146
- marginLeft: 5,
147
- },
148
- centerButton: {
149
- position: 'absolute',
150
- top: '40%',
151
- left: '40%',
152
- },
153
- controlCircle: {
154
- width: utils_1.appSize.width(10),
155
- height: utils_1.appSize.width(10),
156
- borderRadius: 31,
157
- backgroundColor: 'rgba(0,0,0,0.55)',
158
- justifyContent: 'center',
159
- alignItems: 'center',
160
- },
161
- progressContainer: {
162
- height: 4,
163
- backgroundColor: 'rgba(255,255,255,0.2)',
164
- justifyContent: 'center',
165
- },
166
- progressBackground: {
167
- ...react_native_1.StyleSheet.absoluteFillObject,
168
- backgroundColor: theme_1.defaultTheme.colors.black,
169
- },
170
- progress: {
171
- height: 4,
172
- backgroundColor: theme_1.defaultTheme.colors.darkRed,
173
- },
174
- fullScreenWrapper: {
175
- width: SCREEN_WIDTH,
176
- height: SCREEN_HEIGHT,
177
- backgroundColor: '#000',
178
- justifyContent: 'center',
179
- alignItems: 'center',
180
- },
181
- fullScreenVideo: {
182
- width: SCREEN_WIDTH,
183
- height: SCREEN_HEIGHT,
184
- },
185
- modalPlayButton: {
186
- position: 'absolute',
187
- alignSelf: 'center',
188
- top: '45%',
189
- },
190
- modalCloseButton: {
191
- position: 'absolute',
192
- right: 20,
193
- zIndex: 9999,
194
- },
195
- modalProgressWrapper: {
196
- position: 'absolute',
197
- bottom: 40,
198
- width: SCREEN_WIDTH - 40,
199
- flexDirection: 'row',
200
- alignItems: 'center',
201
- },
202
- modalProgressBackground: {
203
- flex: 1,
204
- height: 6,
205
- backgroundColor: '#e0e0e0',
206
- borderRadius: 3,
207
- marginHorizontal: 10,
208
- overflow: 'hidden',
209
- },
210
- modalProgressForeground: {
211
- height: '100%',
212
- backgroundColor: theme_1.defaultTheme.colors.darkRed,
213
- },
214
- modalTime: {
215
- color: '#fff',
216
- fontSize: 14,
217
- },
218
- timeOverlay: {
219
- position: 'absolute',
220
- bottom: 25, // Above the progress bar
221
- right: 5,
222
- backgroundColor: 'rgba(0,0,0,0.5)',
223
- borderRadius: 10,
224
- paddingHorizontal: 6,
225
- paddingVertical: 2,
226
- },
227
- timeText: {
228
- color: '#fff',
229
- fontSize: 10,
230
- fontWeight: '600',
231
- },
232
- });