movius-chats 1.0.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.
Files changed (150) hide show
  1. package/README.md +249 -0
  2. package/lib/commonjs/assets/Icons/ArrowBack2RoundedIcon.js +2 -0
  3. package/lib/commonjs/assets/Icons/ArrowBack2RoundedIcon.js.map +1 -0
  4. package/lib/commonjs/assets/Icons/CameraIcon.js +2 -0
  5. package/lib/commonjs/assets/Icons/CameraIcon.js.map +1 -0
  6. package/lib/commonjs/assets/Icons/CheckAllIcon.js +2 -0
  7. package/lib/commonjs/assets/Icons/CheckAllIcon.js.map +1 -0
  8. package/lib/commonjs/assets/Icons/CheckIcon.js +2 -0
  9. package/lib/commonjs/assets/Icons/CheckIcon.js.map +1 -0
  10. package/lib/commonjs/assets/Icons/EmojiFunnySquareIcon.js +2 -0
  11. package/lib/commonjs/assets/Icons/EmojiFunnySquareIcon.js.map +1 -0
  12. package/lib/commonjs/assets/Icons/LoadingIcon.js +2 -0
  13. package/lib/commonjs/assets/Icons/LoadingIcon.js.map +1 -0
  14. package/lib/commonjs/assets/Icons/MicrophoneIcon.js +2 -0
  15. package/lib/commonjs/assets/Icons/MicrophoneIcon.js.map +1 -0
  16. package/lib/commonjs/assets/Icons/PaperClipIcon.js +2 -0
  17. package/lib/commonjs/assets/Icons/PaperClipIcon.js.map +1 -0
  18. package/lib/commonjs/assets/Icons/PaperPlaneIcon.js +2 -0
  19. package/lib/commonjs/assets/Icons/PaperPlaneIcon.js.map +1 -0
  20. package/lib/commonjs/assets/Icons/PauseIcon.js +2 -0
  21. package/lib/commonjs/assets/Icons/PauseIcon.js.map +1 -0
  22. package/lib/commonjs/assets/Icons/PlayIcon.js +2 -0
  23. package/lib/commonjs/assets/Icons/PlayIcon.js.map +1 -0
  24. package/lib/commonjs/assets/Icons/XIcon.js +2 -0
  25. package/lib/commonjs/assets/Icons/XIcon.js.map +1 -0
  26. package/lib/commonjs/components/AudioPlayer/AudioPlayer.js +2 -0
  27. package/lib/commonjs/components/AudioPlayer/AudioPlayer.js.map +1 -0
  28. package/lib/commonjs/components/ChatBubble/ChatBubble.js +2 -0
  29. package/lib/commonjs/components/ChatBubble/ChatBubble.js.map +1 -0
  30. package/lib/commonjs/components/ChatBubble/MessageContent.js +2 -0
  31. package/lib/commonjs/components/ChatBubble/MessageContent.js.map +1 -0
  32. package/lib/commonjs/components/ChatBubble/MessageStatus.js +2 -0
  33. package/lib/commonjs/components/ChatBubble/MessageStatus.js.map +1 -0
  34. package/lib/commonjs/components/ChatInput/ChatInput.js +2 -0
  35. package/lib/commonjs/components/ChatInput/ChatInput.js.map +1 -0
  36. package/lib/commonjs/components/MediaViewer/MediaViewer.js +2 -0
  37. package/lib/commonjs/components/MediaViewer/MediaViewer.js.map +1 -0
  38. package/lib/commonjs/components/TypingComponent/TypingIndicator.js +2 -0
  39. package/lib/commonjs/components/TypingComponent/TypingIndicator.js.map +1 -0
  40. package/lib/commonjs/context/AudioContext.js +2 -0
  41. package/lib/commonjs/context/AudioContext.js.map +1 -0
  42. package/lib/commonjs/context/ChatContext.js +2 -0
  43. package/lib/commonjs/context/ChatContext.js.map +1 -0
  44. package/lib/commonjs/index.js +2 -0
  45. package/lib/commonjs/index.js.map +1 -0
  46. package/lib/commonjs/utils/datefunc.js +2 -0
  47. package/lib/commonjs/utils/datefunc.js.map +1 -0
  48. package/lib/module/assets/Icons/ArrowBack2RoundedIcon.js +2 -0
  49. package/lib/module/assets/Icons/ArrowBack2RoundedIcon.js.map +1 -0
  50. package/lib/module/assets/Icons/CameraIcon.js +2 -0
  51. package/lib/module/assets/Icons/CameraIcon.js.map +1 -0
  52. package/lib/module/assets/Icons/CheckAllIcon.js +2 -0
  53. package/lib/module/assets/Icons/CheckAllIcon.js.map +1 -0
  54. package/lib/module/assets/Icons/CheckIcon.js +2 -0
  55. package/lib/module/assets/Icons/CheckIcon.js.map +1 -0
  56. package/lib/module/assets/Icons/EmojiFunnySquareIcon.js +2 -0
  57. package/lib/module/assets/Icons/EmojiFunnySquareIcon.js.map +1 -0
  58. package/lib/module/assets/Icons/LoadingIcon.js +2 -0
  59. package/lib/module/assets/Icons/LoadingIcon.js.map +1 -0
  60. package/lib/module/assets/Icons/MicrophoneIcon.js +2 -0
  61. package/lib/module/assets/Icons/MicrophoneIcon.js.map +1 -0
  62. package/lib/module/assets/Icons/PaperClipIcon.js +2 -0
  63. package/lib/module/assets/Icons/PaperClipIcon.js.map +1 -0
  64. package/lib/module/assets/Icons/PaperPlaneIcon.js +2 -0
  65. package/lib/module/assets/Icons/PaperPlaneIcon.js.map +1 -0
  66. package/lib/module/assets/Icons/PauseIcon.js +2 -0
  67. package/lib/module/assets/Icons/PauseIcon.js.map +1 -0
  68. package/lib/module/assets/Icons/PlayIcon.js +2 -0
  69. package/lib/module/assets/Icons/PlayIcon.js.map +1 -0
  70. package/lib/module/assets/Icons/XIcon.js +2 -0
  71. package/lib/module/assets/Icons/XIcon.js.map +1 -0
  72. package/lib/module/components/AudioPlayer/AudioPlayer.js +2 -0
  73. package/lib/module/components/AudioPlayer/AudioPlayer.js.map +1 -0
  74. package/lib/module/components/ChatBubble/ChatBubble.js +2 -0
  75. package/lib/module/components/ChatBubble/ChatBubble.js.map +1 -0
  76. package/lib/module/components/ChatBubble/MessageContent.js +2 -0
  77. package/lib/module/components/ChatBubble/MessageContent.js.map +1 -0
  78. package/lib/module/components/ChatBubble/MessageStatus.js +2 -0
  79. package/lib/module/components/ChatBubble/MessageStatus.js.map +1 -0
  80. package/lib/module/components/ChatInput/ChatInput.js +2 -0
  81. package/lib/module/components/ChatInput/ChatInput.js.map +1 -0
  82. package/lib/module/components/MediaViewer/MediaViewer.js +2 -0
  83. package/lib/module/components/MediaViewer/MediaViewer.js.map +1 -0
  84. package/lib/module/components/TypingComponent/TypingIndicator.js +2 -0
  85. package/lib/module/components/TypingComponent/TypingIndicator.js.map +1 -0
  86. package/lib/module/context/AudioContext.js +2 -0
  87. package/lib/module/context/AudioContext.js.map +1 -0
  88. package/lib/module/context/ChatContext.js +2 -0
  89. package/lib/module/context/ChatContext.js.map +1 -0
  90. package/lib/module/index.js +2 -0
  91. package/lib/module/index.js.map +1 -0
  92. package/lib/module/utils/datefunc.js +2 -0
  93. package/lib/module/utils/datefunc.js.map +1 -0
  94. package/lib/typescript/assets/Icons/ArrowBack2RoundedIcon.d.ts +5 -0
  95. package/lib/typescript/assets/Icons/CameraIcon.d.ts +5 -0
  96. package/lib/typescript/assets/Icons/CheckAllIcon.d.ts +4 -0
  97. package/lib/typescript/assets/Icons/CheckIcon.d.ts +4 -0
  98. package/lib/typescript/assets/Icons/EmojiFunnySquareIcon.d.ts +5 -0
  99. package/lib/typescript/assets/Icons/LoadingIcon.d.ts +4 -0
  100. package/lib/typescript/assets/Icons/MicrophoneIcon.d.ts +5 -0
  101. package/lib/typescript/assets/Icons/PaperClipIcon.d.ts +5 -0
  102. package/lib/typescript/assets/Icons/PaperPlaneIcon.d.ts +5 -0
  103. package/lib/typescript/assets/Icons/PauseIcon.d.ts +5 -0
  104. package/lib/typescript/assets/Icons/PlayIcon.d.ts +5 -0
  105. package/lib/typescript/assets/Icons/XIcon.d.ts +4 -0
  106. package/lib/typescript/components/AudioPlayer/AudioPlayer.d.ts +4 -0
  107. package/lib/typescript/components/AudioPlayer/types.d.ts +5 -0
  108. package/lib/typescript/components/ChatBubble/ChatBubble.d.ts +4 -0
  109. package/lib/typescript/components/ChatBubble/MessageContent.d.ts +4 -0
  110. package/lib/typescript/components/ChatBubble/MessageStatus.d.ts +4 -0
  111. package/lib/typescript/components/ChatBubble/types.d.ts +18 -0
  112. package/lib/typescript/components/ChatInput/ChatInput.d.ts +4 -0
  113. package/lib/typescript/components/ChatInput/types.d.ts +20 -0
  114. package/lib/typescript/components/MediaViewer/MediaViewer.d.ts +4 -0
  115. package/lib/typescript/components/MediaViewer/types.d.ts +5 -0
  116. package/lib/typescript/components/TypingComponent/TypingIndicator.d.ts +11 -0
  117. package/lib/typescript/context/AudioContext.d.ts +10 -0
  118. package/lib/typescript/context/ChatContext.d.ts +19 -0
  119. package/lib/typescript/index.d.ts +4 -0
  120. package/lib/typescript/types/index.d.ts +85 -0
  121. package/lib/typescript/utils/datefunc.d.ts +1 -0
  122. package/package.json +93 -0
  123. package/src/assets/Icons/ArrowBack2RoundedIcon.tsx +25 -0
  124. package/src/assets/Icons/CameraIcon.tsx +20 -0
  125. package/src/assets/Icons/CheckAllIcon.tsx +13 -0
  126. package/src/assets/Icons/CheckIcon.tsx +11 -0
  127. package/src/assets/Icons/EmojiFunnySquareIcon.tsx +41 -0
  128. package/src/assets/Icons/LoadingIcon.tsx +15 -0
  129. package/src/assets/Icons/MicrophoneIcon.tsx +24 -0
  130. package/src/assets/Icons/PaperClipIcon.tsx +17 -0
  131. package/src/assets/Icons/PaperPlaneIcon.tsx +24 -0
  132. package/src/assets/Icons/PauseIcon.tsx +21 -0
  133. package/src/assets/Icons/PlayIcon.tsx +20 -0
  134. package/src/assets/Icons/XIcon.tsx +20 -0
  135. package/src/components/AudioPlayer/AudioPlayer.tsx +259 -0
  136. package/src/components/AudioPlayer/types.ts +5 -0
  137. package/src/components/ChatBubble/ChatBubble.tsx +137 -0
  138. package/src/components/ChatBubble/MessageContent.tsx +143 -0
  139. package/src/components/ChatBubble/MessageStatus.tsx +68 -0
  140. package/src/components/ChatBubble/types.ts +21 -0
  141. package/src/components/ChatInput/ChatInput.tsx +207 -0
  142. package/src/components/ChatInput/types.ts +22 -0
  143. package/src/components/MediaViewer/MediaViewer.tsx +101 -0
  144. package/src/components/MediaViewer/types.ts +5 -0
  145. package/src/components/TypingComponent/TypingIndicator.tsx +119 -0
  146. package/src/context/AudioContext.tsx +30 -0
  147. package/src/context/ChatContext.tsx +40 -0
  148. package/src/index.tsx +103 -0
  149. package/src/types/index.ts +94 -0
  150. package/src/utils/datefunc.ts +5 -0
@@ -0,0 +1,259 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { PanResponder, Pressable, Text, View } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ } from 'react-native-reanimated';
8
+ import Sound from 'react-native-sound';
9
+ import tw from 'twrnc';
10
+ import { PauseIcon } from '../../assets/Icons/PauseIcon';
11
+ import { PlayIcon } from '../../assets/Icons/PlayIcon';
12
+ import { useAudio } from '../../context/AudioContext';
13
+ import { useChatContext } from '../../context/ChatContext';
14
+ import { formatDuration } from '../../utils/datefunc';
15
+ import { AudioPlayerProps } from './types';
16
+
17
+ const AudioPlayer: React.FC<AudioPlayerProps> = ({
18
+ audioUrl,
19
+ audioId,
20
+ isVideoPlaying,
21
+ }) => {
22
+ const { theme, CustomPlayIcon, CustomPauseIcon } = useChatContext();
23
+ const { currentlyPlayingId, setCurrentlyPlayingId } = useAudio();
24
+ const [sound, setSound] = useState<Sound | null>(null);
25
+ const [isPlaying, setIsPlaying] = useState(false);
26
+ const [currentTime, setCurrentTime] = useState(0);
27
+ const [duration, setDuration] = useState(0);
28
+ const [isDragging, setIsDragging] = useState(false);
29
+ const progressRef = useRef<View>(null);
30
+ const progressWidth = useRef(0);
31
+ const progressX = useRef(0);
32
+ const startX = useRef(0);
33
+ const knobPosition = useSharedValue(0);
34
+
35
+ // Initialize sound
36
+ useEffect(() => {
37
+ let mounted = true;
38
+ const newSound = new Sound(audioUrl, '', (error) => {
39
+ if (!error && mounted) {
40
+ setDuration(newSound.getDuration());
41
+ }
42
+ });
43
+ setSound(newSound);
44
+
45
+ return () => {
46
+ mounted = false;
47
+ if (newSound) {
48
+ newSound.pause();
49
+ newSound.release();
50
+ }
51
+ };
52
+ }, [audioUrl]);
53
+
54
+ // Handle stopping playback when another audio starts
55
+ useEffect(() => {
56
+ if (
57
+ currentlyPlayingId &&
58
+ currentlyPlayingId !== audioId &&
59
+ isPlaying &&
60
+ sound
61
+ ) {
62
+ sound.pause();
63
+ setIsPlaying(false);
64
+ setCurrentTime(0);
65
+ knobPosition.value = 0;
66
+ }
67
+ }, [currentlyPlayingId, audioId, isPlaying, sound]);
68
+
69
+ // Update progress
70
+ useEffect(() => {
71
+ let interval: ReturnType<typeof setInterval>;
72
+ if (isPlaying && sound && !isDragging) {
73
+ interval = setInterval(() => {
74
+ sound.getCurrentTime((seconds) => {
75
+ if (typeof seconds === 'number' && !isNaN(seconds)) {
76
+ setCurrentTime(seconds);
77
+ if (progressWidth.current > 0 && duration > 0) {
78
+ const progress = (seconds / duration) * progressWidth.current;
79
+ if (!isNaN(progress)) {
80
+ knobPosition.value = withSpring(progress, {
81
+ damping: 15,
82
+ stiffness: 100,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ });
88
+ }, 100);
89
+ }
90
+ return () => {
91
+ if (interval) clearInterval(interval);
92
+ };
93
+ }, [isPlaying, sound, isDragging, duration]);
94
+
95
+ const panResponder = PanResponder.create({
96
+ onStartShouldSetPanResponder: () => true,
97
+ onMoveShouldSetPanResponder: () => true,
98
+ onPanResponderGrant: (evt) => {
99
+ setIsDragging(true);
100
+ startX.current = evt.nativeEvent.pageX - knobPosition.value;
101
+ },
102
+ onPanResponderMove: (evt) => {
103
+ if (progressWidth.current > 0) {
104
+ const newPosition = evt.nativeEvent.pageX - startX.current;
105
+ const boundedPosition = Math.max(
106
+ 0,
107
+ Math.min(newPosition, progressWidth.current)
108
+ );
109
+ knobPosition.value = boundedPosition;
110
+
111
+ const percentage = boundedPosition / progressWidth.current;
112
+ const newTime = percentage * duration;
113
+ if (!isNaN(newTime)) {
114
+ setCurrentTime(newTime);
115
+ }
116
+ }
117
+ },
118
+ onPanResponderRelease: () => {
119
+ setIsDragging(false);
120
+ if (sound && progressWidth.current > 0) {
121
+ const percentage = knobPosition.value / progressWidth.current;
122
+ const newTime = percentage * duration;
123
+ if (!isNaN(newTime)) {
124
+ sound.setCurrentTime(newTime);
125
+ }
126
+ }
127
+ },
128
+ onPanResponderTerminate: () => {
129
+ setIsDragging(false);
130
+ },
131
+ });
132
+
133
+ const animatedStyle = useAnimatedStyle(() => {
134
+ return {
135
+ transform: [{ translateX: knobPosition.value }],
136
+ };
137
+ });
138
+
139
+ const togglePlay = () => {
140
+ if (!sound) return;
141
+
142
+ if (isPlaying) {
143
+ sound.pause(() => {
144
+ setIsPlaying(false);
145
+ setCurrentlyPlayingId(null);
146
+ });
147
+ } else {
148
+ setCurrentlyPlayingId(audioId);
149
+ sound.play((success) => {
150
+ if (success) {
151
+ setIsPlaying(false);
152
+ setCurrentTime(0);
153
+ knobPosition.value = withSpring(0);
154
+ setCurrentlyPlayingId(null);
155
+ }
156
+ });
157
+ setIsPlaying(true);
158
+ }
159
+ };
160
+
161
+ // Stop audio when video starts playing
162
+ useEffect(() => {
163
+ if (isVideoPlaying && isPlaying && sound) {
164
+ sound.pause(() => {
165
+ setIsPlaying(false);
166
+ setCurrentlyPlayingId(null);
167
+ });
168
+ }
169
+ }, [isVideoPlaying]);
170
+
171
+ return (
172
+ <View style={tw`rounded-lg w-56`}>
173
+ <View style={tw`flex-row items-center gap-2 px-2 pt-2`}>
174
+ <Pressable
175
+ onPress={togglePlay}
176
+ style={[
177
+ tw`bg-black/40 rounded-full p-2`,
178
+ theme?.messageStyle?.audioPlayButtonStyle,
179
+ ]}
180
+ >
181
+ {isPlaying ? (
182
+ CustomPauseIcon ? (
183
+ <CustomPauseIcon />
184
+ ) : (
185
+ <PauseIcon
186
+ style={tw.style('h-6 w-6')}
187
+ color={theme?.colors?.audioPauseIconColor || 'white'}
188
+ />
189
+ )
190
+ ) : CustomPlayIcon ? (
191
+ <CustomPlayIcon />
192
+ ) : (
193
+ <PlayIcon
194
+ style={tw.style('h-6 w-6')}
195
+ color={theme?.colors?.audioPlayIconColor || 'white'}
196
+ />
197
+ )}
198
+ </Pressable>
199
+
200
+ <View
201
+ ref={progressRef}
202
+ onLayout={(e) => {
203
+ const { width } = e.nativeEvent.layout;
204
+ progressWidth.current = width;
205
+ progressRef.current?.measure((_, __, ___, ____, pageX) => {
206
+ progressX.current = pageX;
207
+ });
208
+ }}
209
+ style={[
210
+ tw`relative h-1 bg-zinc-400 rounded overflow-visible w-[75%]`,
211
+ theme?.messageStyle?.progressBarStyle,
212
+ ]}
213
+ >
214
+ <View
215
+ style={[
216
+ tw`absolute h-full bg-slate-200`,
217
+ {
218
+ width: `${duration > 0 ? (currentTime / duration) * 100 : 0}%`,
219
+ },
220
+ theme?.messageStyle?.activeProgressBarStyle,
221
+ ]}
222
+ />
223
+ <Animated.View
224
+ {...panResponder.panHandlers}
225
+ style={[
226
+ animatedStyle,
227
+ {
228
+ position: 'absolute',
229
+ top: -6,
230
+ width: 16,
231
+ height: 16,
232
+ borderRadius: 8,
233
+ backgroundColor: 'white',
234
+ shadowColor: '#000',
235
+ shadowOffset: { width: 0, height: 2 },
236
+ shadowOpacity: 0.25,
237
+ shadowRadius: 3.84,
238
+ elevation: 5,
239
+ },
240
+ { ...theme?.messageStyle?.audioKnobStyle },
241
+ ]}
242
+ />
243
+ </View>
244
+ </View>
245
+ <View style={tw`px-4 py-1`}>
246
+ <Text
247
+ style={[
248
+ tw`text-xs text-gray-500`,
249
+ theme?.messageStyle?.audioDurationStyle,
250
+ ]}
251
+ >
252
+ {!isNaN(currentTime) ? formatDuration(currentTime) : '0:00'}
253
+ </Text>
254
+ </View>
255
+ </View>
256
+ );
257
+ };
258
+
259
+ export default React.memo(AudioPlayer);
@@ -0,0 +1,5 @@
1
+ export interface AudioPlayerProps {
2
+ audioUrl: string;
3
+ audioId: string;
4
+ isVideoPlaying: boolean;
5
+ }
@@ -0,0 +1,137 @@
1
+ import React from 'react';
2
+ import { Image, Pressable, Text, View } from 'react-native';
3
+ import tw from 'twrnc';
4
+ import { ArrowBack2RoundedIcon } from '../../assets/Icons/ArrowBack2RoundedIcon';
5
+ import { useChatContext } from '../../context/ChatContext';
6
+ import MessageContent from './MessageContent';
7
+ import MessageStatus from './MessageStatus';
8
+ import { ChatBubbleProps } from './types';
9
+
10
+ const ChatBubble: React.FC<ChatBubbleProps> = ({
11
+ message,
12
+ isCurrentUser,
13
+ isFirstInSequence,
14
+ onLongPress,
15
+ }) => {
16
+ const {
17
+ theme,
18
+ showAvatars,
19
+ showUserNames,
20
+ showBubbleTail,
21
+ setMediaUrl,
22
+ setIsVideoPlaying,
23
+ isVideoPlaying,
24
+ } = useChatContext();
25
+
26
+ const handleMediaPress = (type: 'image' | 'video', url: string) => {
27
+ setMediaUrl({
28
+ imageUrl: type === 'image' ? url : '',
29
+ videoUrl: type === 'video' ? url : '',
30
+ });
31
+ if (type === 'video') {
32
+ setIsVideoPlaying(true);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <Pressable
38
+ onLongPress={onLongPress}
39
+ style={[
40
+ tw`px-2 my-1 max-w-[75%] relative`,
41
+ isCurrentUser ? tw`self-end mr-3` : tw`self-start ml-9`,
42
+ isFirstInSequence
43
+ ? isCurrentUser
44
+ ? tw`bg-green-500 rounded-tr-none`
45
+ : tw`bg-white rounded-tl-none`
46
+ : isCurrentUser
47
+ ? tw`bg-green-500`
48
+ : tw`bg-white`,
49
+ {
50
+ borderRadius: 8,
51
+ ...(isCurrentUser
52
+ ? theme?.bubbleStyle?.sent
53
+ : theme?.bubbleStyle?.received),
54
+ },
55
+ ]}
56
+ >
57
+ {/* Avatar & Sender Name for Group Chat */}
58
+ {!isCurrentUser && isFirstInSequence && showAvatars && (
59
+ <>
60
+ <View
61
+ style={tw`absolute w-6 h-6 rounded-full top-0 -left-9 flex-row items-center`}
62
+ >
63
+ {message.senderAvatar ? (
64
+ <Image
65
+ source={{ uri: message.senderAvatar }}
66
+ style={[
67
+ tw`w-full h-full rounded-full`,
68
+ theme?.bubbleStyle?.avatarImageStyle,
69
+ ]}
70
+ resizeMode="cover"
71
+ />
72
+ ) : (
73
+ <Text
74
+ style={[
75
+ tw`text-sm text-black font-semibold capitalize rounded-full bg-zinc-300 w-full h-full text-center pt-0.5`,
76
+ theme?.bubbleStyle?.avatarTextStyle,
77
+ ]}
78
+ >
79
+ {message.senderName?.charAt(0)}
80
+ </Text>
81
+ )}
82
+ </View>
83
+ {showUserNames && message.senderName && (
84
+ <Text
85
+ style={[
86
+ tw`text-sm text-black font-semibold mt-1 capitalize`,
87
+ theme?.bubbleStyle?.userNameStyle,
88
+ ]}
89
+ >
90
+ {message.senderName}
91
+ </Text>
92
+ )}
93
+ </>
94
+ )}
95
+
96
+ {/* Bubble Tail */}
97
+ {isFirstInSequence && showBubbleTail && (
98
+ <ArrowBack2RoundedIcon
99
+ style={tw.style(
100
+ 'absolute -top-1 w-6 h-6',
101
+ isCurrentUser ? '-right-3.5' : '-left-3.5 mt-[1.25px]',
102
+ {
103
+ transform: [{ rotate: isCurrentUser ? '90deg' : '180deg' }],
104
+ }
105
+ )}
106
+ color={
107
+ isCurrentUser
108
+ ? `${
109
+ theme?.colors?.sentMessageTailColor || 'rgba(34, 197, 94,1)'
110
+ }`
111
+ : `${theme?.colors?.receivedMessageTailColor || 'white'}`
112
+ }
113
+ />
114
+ )}
115
+
116
+ {/* Message Content */}
117
+ <MessageContent
118
+ message={message}
119
+ isCurrentUser={isCurrentUser}
120
+ isFirstInSequence={isFirstInSequence}
121
+ onMediaPress={handleMediaPress}
122
+ isVideoPlaying={isVideoPlaying}
123
+ />
124
+
125
+ {/* Message Status */}
126
+ <MessageStatus
127
+ time={message.time}
128
+ status={isCurrentUser ? message.status : undefined}
129
+ isCurrentUser={isCurrentUser}
130
+ hasText={!!message.text}
131
+ hasAudio={!!message.audio}
132
+ />
133
+ </Pressable>
134
+ );
135
+ };
136
+
137
+ export default React.memo(ChatBubble);
@@ -0,0 +1,143 @@
1
+ import React from 'react';
2
+ import { Image, Pressable, Text, View } from 'react-native';
3
+ import Video, { VideoRef } from 'react-native-video';
4
+ import tw from 'twrnc';
5
+ import { LoadingIcon } from '../../assets/Icons/LoadingIcon';
6
+ import { PlayIcon } from '../../assets/Icons/PlayIcon';
7
+ import { useChatContext } from '../../context/ChatContext';
8
+ import { formatDuration } from '../../utils/datefunc';
9
+ import AudioPlayer from '../AudioPlayer/AudioPlayer';
10
+ import { MessageContentProps } from './types';
11
+
12
+ const MessageContent: React.FC<MessageContentProps> = ({
13
+ message,
14
+ onMediaPress,
15
+ isVideoPlaying,
16
+ }) => {
17
+ const {
18
+ theme,
19
+ showMessageStatus,
20
+ CustomPlayIcon,
21
+ renderCustomVideoBubbleError,
22
+ } = useChatContext();
23
+ const videoRef = React.useRef<VideoRef>(null);
24
+ const [duration, setDuration] = React.useState(0);
25
+ const [videoIsLoading, setVideoIsLoading] = React.useState(false);
26
+ const [videoHasError, setVideoHasError] = React.useState(false);
27
+
28
+ return (
29
+ <View>
30
+ {message.image && (
31
+ <Pressable
32
+ onPress={() => onMediaPress('image', message.image as string)}
33
+ style={tw`w-60 h-80 my-2`}
34
+ >
35
+ <Image
36
+ source={{ uri: message.image }}
37
+ style={tw`w-full h-full object-contain rounded-lg`}
38
+ />
39
+ </Pressable>
40
+ )}
41
+
42
+ {message.video && (
43
+ <Pressable
44
+ onPress={() => onMediaPress('video', message.video as string)}
45
+ style={tw`w-60 h-80 my-2 justify-center items-center`}
46
+ disabled={videoIsLoading}
47
+ >
48
+ <Video
49
+ source={{ uri: message.video }}
50
+ ref={videoRef}
51
+ paused={true}
52
+ style={{
53
+ width: '100%',
54
+ height: '100%',
55
+ borderRadius: 8,
56
+ position: 'relative',
57
+ }}
58
+ resizeMode="cover"
59
+ onLoadStart={() => {
60
+ setVideoIsLoading(true);
61
+ setVideoHasError(false);
62
+ }}
63
+ onLoad={(data) => {
64
+ setDuration(data.duration);
65
+ setVideoIsLoading(false);
66
+ }}
67
+ onBuffer={({ isBuffering }) => setVideoIsLoading(isBuffering)}
68
+ onError={() => {
69
+ setVideoHasError(true);
70
+ setVideoIsLoading(false);
71
+ }}
72
+ />
73
+ {videoIsLoading ? (
74
+ <View
75
+ style={tw`absolute inset-0 flex items-center justify-center bg-black/40 rounded-full`}
76
+ >
77
+ <LoadingIcon
78
+ style={tw.style('h-12 w-12 fill-white animate-spin')}
79
+ />
80
+ </View>
81
+ ) : videoHasError ? (
82
+ renderCustomVideoBubbleError ? (
83
+ renderCustomVideoBubbleError()
84
+ ) : (
85
+ <View
86
+ style={tw`absolute inset-0 flex items-center justify-center bg-red-500/60 p-2`}
87
+ >
88
+ <Text style={tw`text-white font-bold`}>
89
+ Failed to load video
90
+ </Text>
91
+ </View>
92
+ )
93
+ ) : (
94
+ <>
95
+ <View style={tw`absolute bg-black/40 rounded-full`}>
96
+ {CustomPlayIcon ? (
97
+ <CustomPlayIcon />
98
+ ) : (
99
+ <PlayIcon
100
+ style={tw.style('h-16 w-16')}
101
+ color={theme?.colors?.audioPlayIconColor || 'white'}
102
+ />
103
+ )}
104
+ </View>
105
+ <View
106
+ style={tw`absolute bottom-2 left-2 bg-black/50 px-2 py-1 rounded-md`}
107
+ >
108
+ <Text style={tw`text-white text-xs font-semibold`}>
109
+ {formatDuration(duration)}
110
+ </Text>
111
+ </View>
112
+ </>
113
+ )}
114
+ </Pressable>
115
+ )}
116
+
117
+ {message.audio && (
118
+ <View style={tw`my-2`}>
119
+ <AudioPlayer
120
+ audioUrl={message.audio}
121
+ audioId={message.id}
122
+ isVideoPlaying={isVideoPlaying as boolean}
123
+ />
124
+ </View>
125
+ )}
126
+
127
+ {message.text && (
128
+ <Text
129
+ style={[
130
+ tw`text-gray-800 pt-1`,
131
+ showMessageStatus ? tw`pb-0` : tw`pb-2`,
132
+ { wordBreak: 'break-word', overflowWrap: 'break-word' },
133
+ theme?.messageStyle?.textStyle,
134
+ ]}
135
+ >
136
+ {message.text}
137
+ </Text>
138
+ )}
139
+ </View>
140
+ );
141
+ };
142
+
143
+ export default React.memo(MessageContent);
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import tw from 'twrnc';
4
+ import { CheckAllIcon } from '../../assets/Icons/CheckAllIcon';
5
+ import { CheckIcon } from '../../assets/Icons/CheckIcon';
6
+ import { useChatContext } from '../../context/ChatContext';
7
+ import { MessageStatusProps } from './types';
8
+
9
+ const MessageStatus: React.FC<MessageStatusProps> = ({
10
+ time,
11
+ status,
12
+ isCurrentUser,
13
+ hasText,
14
+ hasAudio,
15
+ }) => {
16
+ const { theme, showMessageStatus } = useChatContext();
17
+ return (
18
+ <>
19
+ {showMessageStatus && (
20
+ <View
21
+ style={[
22
+ tw`flex-row items-center`,
23
+ hasText
24
+ ? tw`justify-end pb-1 ml-4`
25
+ : hasAudio
26
+ ? tw`absolute right-3 bottom-3`
27
+ : tw`absolute right-3 bottom-4 bg-black/50 px-2 py-1 rounded-md`,
28
+ ]}
29
+ >
30
+ <Text
31
+ style={[
32
+ tw`text-xs`,
33
+ {
34
+ color:
35
+ hasText || hasAudio
36
+ ? theme?.colors?.timestamp || 'rgba(107, 114, 128, 0.7)'
37
+ : 'white',
38
+ },
39
+ ]}
40
+ >
41
+ {time}
42
+ </Text>
43
+ {isCurrentUser && (
44
+ <View style={tw`ml-1 flex-row items-center`}>
45
+ {status === 'sent' && (
46
+ <CheckIcon
47
+ style={tw.style('fill-[#6B7280] h-4 w-4', { opacity: 0.7 })}
48
+ />
49
+ )}
50
+ {status === 'delivered' && (
51
+ <CheckAllIcon
52
+ style={tw.style('fill-[#6B7280] h-4 w-4', { opacity: 0.7 })}
53
+ />
54
+ )}
55
+ {status === 'read' && (
56
+ <CheckAllIcon
57
+ style={tw.style('fill-[#3B82F6] h-4 w-4', { opacity: 0.9 })}
58
+ />
59
+ )}
60
+ </View>
61
+ )}
62
+ </View>
63
+ )}
64
+ </>
65
+ );
66
+ };
67
+
68
+ export default React.memo(MessageStatus);
@@ -0,0 +1,21 @@
1
+ import { Message } from "../../types";
2
+
3
+ export interface ChatBubbleProps {
4
+ message: Message;
5
+ isCurrentUser: boolean;
6
+ isFirstInSequence: boolean;
7
+ onLongPress?: () => void;
8
+ }
9
+
10
+ export interface MessageContentProps extends ChatBubbleProps {
11
+ onMediaPress: (type: "image" | "video", url: string) => void;
12
+ isVideoPlaying?: boolean;
13
+ }
14
+
15
+ export interface MessageStatusProps {
16
+ time: string;
17
+ status?: "read" | "delivered" | "sent";
18
+ isCurrentUser: boolean;
19
+ hasText: boolean;
20
+ hasAudio: boolean;
21
+ }