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.
- package/README.md +249 -0
- package/lib/commonjs/assets/Icons/ArrowBack2RoundedIcon.js +2 -0
- package/lib/commonjs/assets/Icons/ArrowBack2RoundedIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/CameraIcon.js +2 -0
- package/lib/commonjs/assets/Icons/CameraIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/CheckAllIcon.js +2 -0
- package/lib/commonjs/assets/Icons/CheckAllIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/CheckIcon.js +2 -0
- package/lib/commonjs/assets/Icons/CheckIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/EmojiFunnySquareIcon.js +2 -0
- package/lib/commonjs/assets/Icons/EmojiFunnySquareIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/LoadingIcon.js +2 -0
- package/lib/commonjs/assets/Icons/LoadingIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/MicrophoneIcon.js +2 -0
- package/lib/commonjs/assets/Icons/MicrophoneIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/PaperClipIcon.js +2 -0
- package/lib/commonjs/assets/Icons/PaperClipIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/PaperPlaneIcon.js +2 -0
- package/lib/commonjs/assets/Icons/PaperPlaneIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/PauseIcon.js +2 -0
- package/lib/commonjs/assets/Icons/PauseIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/PlayIcon.js +2 -0
- package/lib/commonjs/assets/Icons/PlayIcon.js.map +1 -0
- package/lib/commonjs/assets/Icons/XIcon.js +2 -0
- package/lib/commonjs/assets/Icons/XIcon.js.map +1 -0
- package/lib/commonjs/components/AudioPlayer/AudioPlayer.js +2 -0
- package/lib/commonjs/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/lib/commonjs/components/ChatBubble/ChatBubble.js +2 -0
- package/lib/commonjs/components/ChatBubble/ChatBubble.js.map +1 -0
- package/lib/commonjs/components/ChatBubble/MessageContent.js +2 -0
- package/lib/commonjs/components/ChatBubble/MessageContent.js.map +1 -0
- package/lib/commonjs/components/ChatBubble/MessageStatus.js +2 -0
- package/lib/commonjs/components/ChatBubble/MessageStatus.js.map +1 -0
- package/lib/commonjs/components/ChatInput/ChatInput.js +2 -0
- package/lib/commonjs/components/ChatInput/ChatInput.js.map +1 -0
- package/lib/commonjs/components/MediaViewer/MediaViewer.js +2 -0
- package/lib/commonjs/components/MediaViewer/MediaViewer.js.map +1 -0
- package/lib/commonjs/components/TypingComponent/TypingIndicator.js +2 -0
- package/lib/commonjs/components/TypingComponent/TypingIndicator.js.map +1 -0
- package/lib/commonjs/context/AudioContext.js +2 -0
- package/lib/commonjs/context/AudioContext.js.map +1 -0
- package/lib/commonjs/context/ChatContext.js +2 -0
- package/lib/commonjs/context/ChatContext.js.map +1 -0
- package/lib/commonjs/index.js +2 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/utils/datefunc.js +2 -0
- package/lib/commonjs/utils/datefunc.js.map +1 -0
- package/lib/module/assets/Icons/ArrowBack2RoundedIcon.js +2 -0
- package/lib/module/assets/Icons/ArrowBack2RoundedIcon.js.map +1 -0
- package/lib/module/assets/Icons/CameraIcon.js +2 -0
- package/lib/module/assets/Icons/CameraIcon.js.map +1 -0
- package/lib/module/assets/Icons/CheckAllIcon.js +2 -0
- package/lib/module/assets/Icons/CheckAllIcon.js.map +1 -0
- package/lib/module/assets/Icons/CheckIcon.js +2 -0
- package/lib/module/assets/Icons/CheckIcon.js.map +1 -0
- package/lib/module/assets/Icons/EmojiFunnySquareIcon.js +2 -0
- package/lib/module/assets/Icons/EmojiFunnySquareIcon.js.map +1 -0
- package/lib/module/assets/Icons/LoadingIcon.js +2 -0
- package/lib/module/assets/Icons/LoadingIcon.js.map +1 -0
- package/lib/module/assets/Icons/MicrophoneIcon.js +2 -0
- package/lib/module/assets/Icons/MicrophoneIcon.js.map +1 -0
- package/lib/module/assets/Icons/PaperClipIcon.js +2 -0
- package/lib/module/assets/Icons/PaperClipIcon.js.map +1 -0
- package/lib/module/assets/Icons/PaperPlaneIcon.js +2 -0
- package/lib/module/assets/Icons/PaperPlaneIcon.js.map +1 -0
- package/lib/module/assets/Icons/PauseIcon.js +2 -0
- package/lib/module/assets/Icons/PauseIcon.js.map +1 -0
- package/lib/module/assets/Icons/PlayIcon.js +2 -0
- package/lib/module/assets/Icons/PlayIcon.js.map +1 -0
- package/lib/module/assets/Icons/XIcon.js +2 -0
- package/lib/module/assets/Icons/XIcon.js.map +1 -0
- package/lib/module/components/AudioPlayer/AudioPlayer.js +2 -0
- package/lib/module/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/lib/module/components/ChatBubble/ChatBubble.js +2 -0
- package/lib/module/components/ChatBubble/ChatBubble.js.map +1 -0
- package/lib/module/components/ChatBubble/MessageContent.js +2 -0
- package/lib/module/components/ChatBubble/MessageContent.js.map +1 -0
- package/lib/module/components/ChatBubble/MessageStatus.js +2 -0
- package/lib/module/components/ChatBubble/MessageStatus.js.map +1 -0
- package/lib/module/components/ChatInput/ChatInput.js +2 -0
- package/lib/module/components/ChatInput/ChatInput.js.map +1 -0
- package/lib/module/components/MediaViewer/MediaViewer.js +2 -0
- package/lib/module/components/MediaViewer/MediaViewer.js.map +1 -0
- package/lib/module/components/TypingComponent/TypingIndicator.js +2 -0
- package/lib/module/components/TypingComponent/TypingIndicator.js.map +1 -0
- package/lib/module/context/AudioContext.js +2 -0
- package/lib/module/context/AudioContext.js.map +1 -0
- package/lib/module/context/ChatContext.js +2 -0
- package/lib/module/context/ChatContext.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/utils/datefunc.js +2 -0
- package/lib/module/utils/datefunc.js.map +1 -0
- package/lib/typescript/assets/Icons/ArrowBack2RoundedIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/CameraIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/CheckAllIcon.d.ts +4 -0
- package/lib/typescript/assets/Icons/CheckIcon.d.ts +4 -0
- package/lib/typescript/assets/Icons/EmojiFunnySquareIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/LoadingIcon.d.ts +4 -0
- package/lib/typescript/assets/Icons/MicrophoneIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/PaperClipIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/PaperPlaneIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/PauseIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/PlayIcon.d.ts +5 -0
- package/lib/typescript/assets/Icons/XIcon.d.ts +4 -0
- package/lib/typescript/components/AudioPlayer/AudioPlayer.d.ts +4 -0
- package/lib/typescript/components/AudioPlayer/types.d.ts +5 -0
- package/lib/typescript/components/ChatBubble/ChatBubble.d.ts +4 -0
- package/lib/typescript/components/ChatBubble/MessageContent.d.ts +4 -0
- package/lib/typescript/components/ChatBubble/MessageStatus.d.ts +4 -0
- package/lib/typescript/components/ChatBubble/types.d.ts +18 -0
- package/lib/typescript/components/ChatInput/ChatInput.d.ts +4 -0
- package/lib/typescript/components/ChatInput/types.d.ts +20 -0
- package/lib/typescript/components/MediaViewer/MediaViewer.d.ts +4 -0
- package/lib/typescript/components/MediaViewer/types.d.ts +5 -0
- package/lib/typescript/components/TypingComponent/TypingIndicator.d.ts +11 -0
- package/lib/typescript/context/AudioContext.d.ts +10 -0
- package/lib/typescript/context/ChatContext.d.ts +19 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/types/index.d.ts +85 -0
- package/lib/typescript/utils/datefunc.d.ts +1 -0
- package/package.json +93 -0
- package/src/assets/Icons/ArrowBack2RoundedIcon.tsx +25 -0
- package/src/assets/Icons/CameraIcon.tsx +20 -0
- package/src/assets/Icons/CheckAllIcon.tsx +13 -0
- package/src/assets/Icons/CheckIcon.tsx +11 -0
- package/src/assets/Icons/EmojiFunnySquareIcon.tsx +41 -0
- package/src/assets/Icons/LoadingIcon.tsx +15 -0
- package/src/assets/Icons/MicrophoneIcon.tsx +24 -0
- package/src/assets/Icons/PaperClipIcon.tsx +17 -0
- package/src/assets/Icons/PaperPlaneIcon.tsx +24 -0
- package/src/assets/Icons/PauseIcon.tsx +21 -0
- package/src/assets/Icons/PlayIcon.tsx +20 -0
- package/src/assets/Icons/XIcon.tsx +20 -0
- package/src/components/AudioPlayer/AudioPlayer.tsx +259 -0
- package/src/components/AudioPlayer/types.ts +5 -0
- package/src/components/ChatBubble/ChatBubble.tsx +137 -0
- package/src/components/ChatBubble/MessageContent.tsx +143 -0
- package/src/components/ChatBubble/MessageStatus.tsx +68 -0
- package/src/components/ChatBubble/types.ts +21 -0
- package/src/components/ChatInput/ChatInput.tsx +207 -0
- package/src/components/ChatInput/types.ts +22 -0
- package/src/components/MediaViewer/MediaViewer.tsx +101 -0
- package/src/components/MediaViewer/types.ts +5 -0
- package/src/components/TypingComponent/TypingIndicator.tsx +119 -0
- package/src/context/AudioContext.tsx +30 -0
- package/src/context/ChatContext.tsx +40 -0
- package/src/index.tsx +103 -0
- package/src/types/index.ts +94 -0
- 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,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;
|