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,207 @@
1
+ import { CameraIcon } from "../../assets/Icons/CameraIcon";
2
+ import { EmojiFunnySquareIcon } from "../../assets/Icons/EmojiFunnySquareIcon";
3
+ import { MicrophoneIcon } from "../../assets/Icons/MicrophoneIcon";
4
+ import { PaperClipIcon } from "../../assets/Icons/PaperClipIcon";
5
+ import { PaperPlaneIcon } from "../../assets/Icons/PaperPlaneIcon";
6
+ import React, { useCallback, useEffect, useState } from "react";
7
+ import { Platform, Pressable, TextInput, View } from "react-native";
8
+ import { useChatContext } from "../../context/ChatContext";
9
+ import { ChatInputProps, InputHeightState } from "./types";
10
+ import tw from 'twrnc';
11
+
12
+ const MIN_INPUT_HEIGHT = Platform.OS === "ios" ? 32 : 30;
13
+ const MAX_INPUT_HEIGHT = 118;
14
+
15
+ const ChatInput: React.FC<ChatInputProps> = ({
16
+ onSendMessage,
17
+ onTypingStart,
18
+ onTypingEnd,
19
+ onAttachmentPress,
20
+ onCameraPress,
21
+ onAudioRecordStart,
22
+ onAudioRecordEnd,
23
+ CustomEmojiIcon,
24
+ CustomAttachmentIcon,
25
+ CustomCameraIcon,
26
+ CustomSendIcon,
27
+ CustomMicrophoneIcon,
28
+ }) => {
29
+ const [inputText, setInputText] = useState("");
30
+ const [inputHeight, setInputHeight] = useState<InputHeightState>({
31
+ height: MIN_INPUT_HEIGHT,
32
+ isMultiline: false,
33
+ });
34
+ const {
35
+ theme,
36
+ currentUserId,
37
+ showEmojiButton,
38
+ showAttachmentsButton,
39
+ showCameraButton,
40
+ showVoiceRecordButton,
41
+ placeholder,
42
+ } = useChatContext();
43
+
44
+ const handleContentSizeChange = useCallback(
45
+ (event: { nativeEvent: { contentSize: { height: number } } }) => {
46
+ const newHeight = Math.min(
47
+ Math.max(event.nativeEvent.contentSize.height, MIN_INPUT_HEIGHT),
48
+ MAX_INPUT_HEIGHT
49
+ );
50
+ setInputHeight({
51
+ height: newHeight,
52
+ isMultiline: newHeight > MIN_INPUT_HEIGHT,
53
+ });
54
+ },
55
+ []
56
+ );
57
+
58
+ const handleSendMessage = useCallback(() => {
59
+ if (inputText.trim()) {
60
+ onSendMessage({
61
+ text: inputText.trim(),
62
+ senderId: currentUserId,
63
+ });
64
+ setInputText("");
65
+ setInputHeight({ height: MIN_INPUT_HEIGHT, isMultiline: false });
66
+ }
67
+ }, [inputText, onSendMessage, currentUserId]);
68
+
69
+ useEffect(() => {
70
+ if (inputText.trim()) {
71
+ onTypingStart?.();
72
+ } else {
73
+ onTypingEnd?.();
74
+ }
75
+ }, [inputText, onTypingStart, onTypingEnd]);
76
+
77
+ return (
78
+ <View
79
+ style={[
80
+ tw`flex-row gap-2`,
81
+ theme?.inputStyles?.inputSectionContainerStyle,
82
+ ]}
83
+ >
84
+ <View
85
+ style={[
86
+ tw`flex-1 bg-white px-3.5 gap-1 flex-row justify-between`,
87
+ inputHeight.isMultiline
88
+ ? tw`rounded-3xl items-end`
89
+ : tw`rounded-full items-center`,
90
+ theme?.inputStyles?.inputContainerStyle,
91
+ ]}
92
+ >
93
+ {showEmojiButton && (
94
+ <Pressable>
95
+ {CustomEmojiIcon ? (
96
+ <CustomEmojiIcon />
97
+ ) : (
98
+ <EmojiFunnySquareIcon
99
+ style={tw.style(
100
+ `${Platform.OS === 'ios' ? 'h-6 w-6' : 'w-7 h-7'}`,
101
+ inputHeight.isMultiline ? 'pb-14' : 'pb-0'
102
+ )}
103
+ color={theme?.colors?.inputsIconsColor || 'rgba(0,0,0,0.7)'}
104
+ />
105
+ )}
106
+ </Pressable>
107
+ )}
108
+
109
+ <TextInput
110
+ value={inputText}
111
+ onChangeText={setInputText}
112
+ placeholder={placeholder || 'Message'}
113
+ style={[
114
+ tw`bg-transparent flex-1 pl-2 my-3`,
115
+ Platform.OS === 'ios' ? tw`text-[17px]` : tw`text-[16px]`,
116
+ { minHeight: MIN_INPUT_HEIGHT, maxHeight: MAX_INPUT_HEIGHT },
117
+ ]}
118
+ placeholderTextColor={
119
+ theme?.colors?.placeholderTextColor || 'rgba(0, 0, 0, 0.4)'
120
+ }
121
+ multiline
122
+ textAlignVertical="center"
123
+ onContentSizeChange={handleContentSizeChange}
124
+ />
125
+
126
+ <View
127
+ style={[
128
+ tw`gap-4 flex-row`,
129
+ inputHeight.isMultiline ? tw`pb-4` : tw`pb-0`,
130
+ ]}
131
+ >
132
+ {showAttachmentsButton && (
133
+ <Pressable onPress={onAttachmentPress}>
134
+ {CustomAttachmentIcon ? (
135
+ <CustomAttachmentIcon />
136
+ ) : (
137
+ <PaperClipIcon
138
+ style={tw.style(
139
+ Platform.OS === 'ios' ? 'h-6 w-6' : 'w-7 h-7'
140
+ )}
141
+ color={theme?.colors?.inputsIconsColor || 'rgba(0,0,0,0.7)'}
142
+ />
143
+ )}
144
+ </Pressable>
145
+ )}
146
+ {showCameraButton && !inputText.trim() && (
147
+ <Pressable onPress={onCameraPress}>
148
+ {CustomCameraIcon ? (
149
+ <CustomCameraIcon />
150
+ ) : (
151
+ <CameraIcon
152
+ style={tw.style(
153
+ Platform.OS === 'ios' ? 'h-6 w-6' : 'w-7 h-7'
154
+ )}
155
+ color={theme?.colors?.inputsIconsColor || 'rgba(0,0,0,0.7)'}
156
+ />
157
+ )}
158
+ </Pressable>
159
+ )}
160
+ </View>
161
+ </View>
162
+
163
+ <Pressable
164
+ style={[
165
+ tw`p-2 rounded-full bg-green-600 justify-center items-center`,
166
+ {
167
+ height: Platform.OS === 'ios' ? 50 : 48,
168
+ width: Platform.OS === 'ios' ? 50 : 48,
169
+ ...theme?.inputStyles?.sendButtonStyle,
170
+ },
171
+ ]}
172
+ onPress={inputText.trim() ? handleSendMessage : onAudioRecordStart}
173
+ onLongPress={onAudioRecordStart}
174
+ onPressOut={onAudioRecordEnd}
175
+ >
176
+ {inputText.trim() ? (
177
+ CustomSendIcon ? (
178
+ <CustomSendIcon />
179
+ ) : (
180
+ <PaperPlaneIcon
181
+ style={tw.style('h-6 w-6')}
182
+ color={theme?.colors?.sendIconsColor || 'rgba(255,255,255,0.7)'}
183
+ />
184
+ )
185
+ ) : showVoiceRecordButton ? (
186
+ CustomMicrophoneIcon ? (
187
+ <CustomMicrophoneIcon />
188
+ ) : (
189
+ <MicrophoneIcon
190
+ style={tw.style('h-8 w-8')}
191
+ color={theme?.colors?.sendIconsColor || 'rgba(255,255,255,0.7)'}
192
+ />
193
+ )
194
+ ) : CustomSendIcon ? (
195
+ <CustomSendIcon />
196
+ ) : (
197
+ <PaperPlaneIcon
198
+ style={tw.style('h-6 w-6')}
199
+ color={theme?.colors?.sendIconsColor || 'rgba(255,255,255,0.7)'}
200
+ />
201
+ )}
202
+ </Pressable>
203
+ </View>
204
+ );
205
+ };
206
+
207
+ export default React.memo(ChatInput);
@@ -0,0 +1,22 @@
1
+ import { Message } from "../../types";
2
+
3
+ export interface ChatInputProps {
4
+ onSendMessage: (message: Omit<Message, "id" | "time" | "status">) => void;
5
+ onTypingStart?: () => void;
6
+ onTypingEnd?: () => void;
7
+ onAttachmentPress?: () => void;
8
+ onCameraPress?: () => void;
9
+ onAudioRecordStart?: () => void;
10
+ onAudioRecordEnd?: () => void;
11
+ placeholder?: string;
12
+ CustomEmojiIcon?: () => React.ReactNode;
13
+ CustomAttachmentIcon?: () => React.ReactNode;
14
+ CustomCameraIcon?: () => React.ReactNode;
15
+ CustomSendIcon?: () => React.ReactNode;
16
+ CustomMicrophoneIcon?: () => React.ReactNode;
17
+ }
18
+
19
+ export interface InputHeightState {
20
+ height: number;
21
+ isMultiline: boolean;
22
+ }
@@ -0,0 +1,101 @@
1
+ import { LoadingIcon } from '../../assets/Icons/LoadingIcon';
2
+ import { XIcon } from '../../assets/Icons/XIcon';
3
+ import React, { useRef, useState } from 'react';
4
+ import { Modal, Pressable, Text, View } from 'react-native';
5
+ import ImageViewer from 'react-native-image-zoom-viewer';
6
+ import Video, { VideoRef } from 'react-native-video';
7
+ import tw from 'twrnc';
8
+ import { MediaViewerProps } from './types';
9
+
10
+ const MediaViewer: React.FC<MediaViewerProps> = ({
11
+ imageUrl,
12
+ videoUrl,
13
+ onClose,
14
+ }) => {
15
+ const videoRef = useRef<VideoRef>(null);
16
+ const [videoIsLoading, setVideoIsLoading] = useState(false);
17
+ const [videoHasError, setVideoHasError] = useState(false);
18
+
19
+ if (!imageUrl && !videoUrl) return null;
20
+
21
+ return (
22
+ <Modal visible={!!imageUrl || !!videoUrl} transparent={true}>
23
+ <View
24
+ style={tw`top-0 bottom-0 left-0 right-0 bg-black/80 flex-1 absolute`}
25
+ >
26
+ <Pressable
27
+ onPress={onClose}
28
+ style={tw`absolute right-4 top-4 p-2 rounded-full bg-slate-100/70 z-10`}
29
+ >
30
+ <XIcon style={tw`h-8 w-8 stroke-black`} />
31
+ </Pressable>
32
+
33
+ {imageUrl && (
34
+ <ImageViewer
35
+ imageUrls={[{ url: imageUrl }]}
36
+ enableSwipeDown
37
+ onSwipeDown={onClose}
38
+ backgroundColor="rgba(0,0,0,0.8)"
39
+ enableImageZoom
40
+ onSave={() => imageUrl}
41
+ renderIndicator={() => <></>}
42
+ />
43
+ )}
44
+
45
+ {videoUrl && (
46
+ <View style={tw`justify-center items-center`}>
47
+ <Video
48
+ source={{ uri: videoUrl }}
49
+ ref={videoRef}
50
+ shutterColor="transparent"
51
+ controls={true}
52
+ style={{
53
+ width: '100%',
54
+ height: '100%',
55
+ borderRadius: 8,
56
+ position: 'relative',
57
+ marginHorizontal: 48,
58
+ }}
59
+ controlsStyles={{
60
+ hideSettingButton: false,
61
+ hideNext: true,
62
+ hidePrevious: true,
63
+ }}
64
+ resizeMode="contain"
65
+ onLoadStart={() => {
66
+ setVideoIsLoading(true);
67
+ setVideoHasError(false);
68
+ }}
69
+ onLoad={() => setVideoIsLoading(false)}
70
+ onBuffer={({ isBuffering }) => setVideoIsLoading(isBuffering)}
71
+ onError={() => {
72
+ setVideoHasError(true);
73
+ setVideoIsLoading(false);
74
+ }}
75
+ />
76
+ {videoIsLoading && (
77
+ <View
78
+ style={tw`absolute inset-0 flex items-center justify-center bg-black/40 rounded-full`}
79
+ >
80
+ <LoadingIcon
81
+ style={tw.style('h-12 w-12 fill-white animate-spin')}
82
+ />
83
+ </View>
84
+ )}
85
+ {videoHasError && (
86
+ <View
87
+ style={tw`absolute inset-0 flex items-center justify-center bg-red-500/60 p-2`}
88
+ >
89
+ <Text style={tw`text-white font-bold`}>
90
+ Failed to load video
91
+ </Text>
92
+ </View>
93
+ )}
94
+ </View>
95
+ )}
96
+ </View>
97
+ </Modal>
98
+ );
99
+ };
100
+
101
+ export default React.memo(MediaViewer);
@@ -0,0 +1,5 @@
1
+ export interface MediaViewerProps {
2
+ imageUrl: string;
3
+ videoUrl: string;
4
+ onClose: () => void;
5
+ }
@@ -0,0 +1,119 @@
1
+ import { Image, Text, View } from 'react-native';
2
+ import tw from 'twrnc';
3
+ import { ArrowBack2RoundedIcon } from '../../assets/Icons/ArrowBack2RoundedIcon';
4
+ import { useChatContext } from '../../context/ChatContext';
5
+
6
+ export interface TypingUser {
7
+ id: string;
8
+ avatar: string;
9
+ name: string;
10
+ }
11
+
12
+ interface TypingIndicatorProps {
13
+ typingUsers: TypingUser[];
14
+ currentUserId: string;
15
+ }
16
+
17
+ export const TypingIndicator = ({
18
+ typingUsers,
19
+ currentUserId,
20
+ }: TypingIndicatorProps) => {
21
+ const { theme, showAvatars, renderCustomTyping, showBubbleTail } =
22
+ useChatContext();
23
+
24
+ const otherTypingUsers = typingUsers.filter(
25
+ (user) => user.id !== currentUserId
26
+ );
27
+
28
+ if (!otherTypingUsers.length) return null;
29
+
30
+ const displayedUsers = otherTypingUsers.slice(0, 2);
31
+ const additionalUsers = otherTypingUsers.length - 2;
32
+
33
+ return (
34
+ <View style={tw`my-1 max-w-[75%] self-start flex-row`}>
35
+ {showAvatars && (
36
+ <View style={tw`flex-row`}>
37
+ {displayedUsers.map((user, index) => (
38
+ <View
39
+ key={user.id}
40
+ style={[
41
+ tw`bg-gray-400 w-6 h-6 rounded-full items-center`,
42
+ {
43
+ marginLeft: index > 0 ? -10 : 0,
44
+ zIndex: displayedUsers.length + index,
45
+ },
46
+ ]}
47
+ >
48
+ {user.avatar ? (
49
+ <Image
50
+ source={{ uri: user.avatar }}
51
+ style={[
52
+ tw`w-full h-full object-cover rounded-full`,
53
+ theme?.bubbleStyle?.avatarImageStyle,
54
+ ]}
55
+ />
56
+ ) : (
57
+ <Text
58
+ style={[
59
+ tw`text-sm text-black font-semibold capitalize rounded-full bg-zinc-300 w-full h-full text-center pt-0.5`,
60
+ theme?.bubbleStyle?.avatarTextStyle,
61
+ ]}
62
+ >
63
+ {user.name?.charAt(0)}
64
+ </Text>
65
+ )}
66
+ </View>
67
+ ))}
68
+ {additionalUsers > 0 && (
69
+ <View
70
+ style={[
71
+ tw`bg-gray-400 w-6 h-6 rounded-full items-center justify-center`,
72
+ {
73
+ marginLeft: -10,
74
+ zIndex: 3,
75
+ },
76
+ { ...theme?.bubbleStyle?.additionalTypingUsersContainerStyle },
77
+ ]}
78
+ >
79
+ <Text
80
+ style={[
81
+ tw`text-white text-xs font-semibold`,
82
+ theme?.bubbleStyle?.additionalTypingUsersTextStyle,
83
+ ]}
84
+ >
85
+ +{additionalUsers}
86
+ </Text>
87
+ </View>
88
+ )}
89
+ </View>
90
+ )}
91
+ {showBubbleTail && (
92
+ <ArrowBack2RoundedIcon
93
+ style={tw.style(
94
+ 'w-6 h-6 fill-white mt-[1.25px]',
95
+ {
96
+ transform: [{ rotate: '180deg' }, { translateX: 6 }],
97
+ }
98
+ )}
99
+ color={`${theme?.colors?.receivedMessageTailColor || 'white'}`}
100
+ />
101
+ )}
102
+
103
+ <View
104
+ style={[
105
+ tw`px-2 my-1 bg-white rounded-tl-none rounded-lg`,
106
+ theme?.bubbleStyle?.typingContainerStyle,
107
+ ]}
108
+ >
109
+ {renderCustomTyping ? (
110
+ renderCustomTyping()
111
+ ) : (
112
+ <View style={tw`flex-row items-center py-3 px-2 justify-center`}>
113
+ <Text style={tw`text-gray-600`}>Typing...</Text>
114
+ </View>
115
+ )}
116
+ </View>
117
+ </View>
118
+ );
119
+ };
@@ -0,0 +1,30 @@
1
+ import React, { createContext, useContext, useState } from "react";
2
+
3
+ interface AudioContextType {
4
+ currentlyPlayingId: string | null;
5
+ setCurrentlyPlayingId: (id: string | null) => void;
6
+ }
7
+
8
+ const AudioContext = createContext<AudioContextType | undefined>(undefined);
9
+
10
+ export const AudioProvider: React.FC<{ children: React.ReactNode }> = ({
11
+ children,
12
+ }) => {
13
+ const [currentlyPlayingId, setCurrentlyPlayingId] = useState<string | null>(
14
+ null
15
+ );
16
+
17
+ return (
18
+ <AudioContext.Provider value={{ currentlyPlayingId, setCurrentlyPlayingId }}>
19
+ {children}
20
+ </AudioContext.Provider>
21
+ );
22
+ };
23
+
24
+ export const useAudio = () => {
25
+ const context = useContext(AudioContext);
26
+ if (!context) {
27
+ throw new Error("useAudio must be used within an AudioProvider");
28
+ }
29
+ return context;
30
+ };
@@ -0,0 +1,40 @@
1
+ import React, { createContext, useContext, useState } from "react";
2
+ import { ChatScreenProps } from "../types";
3
+
4
+ interface ChatContextType extends ChatScreenProps {
5
+ mediaUrl: { imageUrl: string; videoUrl: string };
6
+ setMediaUrl: (url: { imageUrl: string; videoUrl: string }) => void;
7
+ isVideoPlaying: boolean;
8
+ setIsVideoPlaying: (playing: boolean) => void;
9
+ }
10
+
11
+ const ChatContext = createContext<ChatContextType | undefined>(undefined);
12
+
13
+ export const ChatProvider: React.FC<
14
+ ChatScreenProps & { children: React.ReactNode }
15
+ > = ({ children, ...props }) => {
16
+ const [mediaUrl, setMediaUrl] = useState({ imageUrl: "", videoUrl: "" });
17
+ const [isVideoPlaying, setIsVideoPlaying] = useState(false);
18
+
19
+ return (
20
+ <ChatContext.Provider
21
+ value={{
22
+ ...props,
23
+ mediaUrl,
24
+ setMediaUrl,
25
+ isVideoPlaying,
26
+ setIsVideoPlaying,
27
+ }}
28
+ >
29
+ {children}
30
+ </ChatContext.Provider>
31
+ );
32
+ };
33
+
34
+ export const useChatContext = () => {
35
+ const context = useContext(ChatContext);
36
+ if (!context) {
37
+ throw new Error("useChatContext must be used within a ChatProvider");
38
+ }
39
+ return context;
40
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { FlatList, View } from 'react-native';
3
+ import tw from 'twrnc';
4
+ import ChatBubble from './components/ChatBubble/ChatBubble';
5
+ import ChatInput from './components/ChatInput/ChatInput';
6
+ import MediaViewer from './components/MediaViewer/MediaViewer';
7
+ import { TypingIndicator } from './components/TypingComponent/TypingIndicator';
8
+ import { AudioProvider } from './context/AudioContext';
9
+ import { ChatProvider, useChatContext } from './context/ChatContext';
10
+ import { ChatScreenProps } from './types';
11
+
12
+ const ChatScreenContent = () => {
13
+ const {
14
+ messages,
15
+ currentUserId,
16
+ onMessageLongPress,
17
+ mediaUrl,
18
+ setMediaUrl,
19
+ setIsVideoPlaying,
20
+ typingUsers,
21
+ onSendMessage,
22
+ onTypingStart,
23
+ onTypingEnd,
24
+ onAttachmentPress,
25
+ onAudioRecordEnd,
26
+ onAudioRecordStart,
27
+ onCameraPress,
28
+ renderCustomInput,
29
+ CustomEmojiIcon,
30
+ CustomAttachmentIcon,
31
+ CustomCameraIcon,
32
+ CustomMicrophoneIcon,
33
+ CustomSendIcon,
34
+ } = useChatContext();
35
+
36
+ return (
37
+ <View style={tw`flex-1 px-2 pb-4 gap-2 relative`}>
38
+ <FlatList
39
+ data={messages}
40
+ keyExtractor={(item) => item.id}
41
+ renderItem={({ item, index }) => (
42
+ <ChatBubble
43
+ message={item}
44
+ isCurrentUser={item.senderId === currentUserId}
45
+ onLongPress={() => onMessageLongPress?.(item)}
46
+ isFirstInSequence={
47
+ index === messages.length - 1 ||
48
+ messages[index + 1]?.senderId !== item.senderId
49
+ }
50
+ />
51
+ )}
52
+ ListHeaderComponent={
53
+ <TypingIndicator
54
+ typingUsers={typingUsers || []}
55
+ currentUserId={currentUserId}
56
+ />
57
+ }
58
+ showsVerticalScrollIndicator={false}
59
+ inverted
60
+ />
61
+
62
+ {renderCustomInput ? (
63
+ renderCustomInput()
64
+ ) : (
65
+ <ChatInput
66
+ onSendMessage={onSendMessage}
67
+ onTypingStart={onTypingStart}
68
+ onTypingEnd={onTypingEnd}
69
+ onAttachmentPress={onAttachmentPress}
70
+ onAudioRecordEnd={onAudioRecordEnd}
71
+ onAudioRecordStart={onAudioRecordStart}
72
+ onCameraPress={onCameraPress}
73
+ CustomEmojiIcon={CustomEmojiIcon}
74
+ CustomAttachmentIcon={CustomAttachmentIcon}
75
+ CustomCameraIcon={CustomCameraIcon}
76
+ CustomMicrophoneIcon={CustomMicrophoneIcon}
77
+ CustomSendIcon={CustomSendIcon}
78
+ />
79
+ )}
80
+
81
+ <MediaViewer
82
+ imageUrl={mediaUrl.imageUrl}
83
+ videoUrl={mediaUrl.videoUrl}
84
+ onClose={() => {
85
+ setMediaUrl({ imageUrl: '', videoUrl: '' });
86
+ setIsVideoPlaying(false);
87
+ }}
88
+ />
89
+ </View>
90
+ );
91
+ };
92
+
93
+ const ChatScreen: React.FC<ChatScreenProps> = (props) => {
94
+ return (
95
+ <AudioProvider>
96
+ <ChatProvider {...props}>
97
+ <ChatScreenContent />
98
+ </ChatProvider>
99
+ </AudioProvider>
100
+ );
101
+ };
102
+
103
+ export default ChatScreen;