1 | import PropTypes from 'prop-types';
|
2 | import React from 'react';
|
3 | import { Text, Clipboard, StyleSheet, TouchableWithoutFeedback, View, } from 'react-native';
|
4 | import QuickReplies from './QuickReplies';
|
5 | import MessageText from './MessageText';
|
6 | import MessageImage from './MessageImage';
|
7 | import MessageVideo from './MessageVideo';
|
8 | import MessageAudio from './MessageAudio';
|
9 | import Time from './Time';
|
10 | import Color from './Color';
|
11 | import { StylePropType, isSameUser, isSameDay } from './utils';
|
12 | const styles = {
|
13 | left: StyleSheet.create({
|
14 | container: {
|
15 | flex: 1,
|
16 | alignItems: 'flex-start',
|
17 | },
|
18 | wrapper: {
|
19 | borderRadius: 15,
|
20 | backgroundColor: Color.leftBubbleBackground,
|
21 | marginRight: 60,
|
22 | minHeight: 20,
|
23 | justifyContent: 'flex-end',
|
24 | },
|
25 | containerToNext: {
|
26 | borderBottomLeftRadius: 3,
|
27 | },
|
28 | containerToPrevious: {
|
29 | borderTopLeftRadius: 3,
|
30 | },
|
31 | bottom: {
|
32 | flexDirection: 'row',
|
33 | justifyContent: 'flex-start',
|
34 | },
|
35 | }),
|
36 | right: StyleSheet.create({
|
37 | container: {
|
38 | flex: 1,
|
39 | alignItems: 'flex-end',
|
40 | },
|
41 | wrapper: {
|
42 | borderRadius: 15,
|
43 | backgroundColor: Color.defaultBlue,
|
44 | marginLeft: 60,
|
45 | minHeight: 20,
|
46 | justifyContent: 'flex-end',
|
47 | },
|
48 | containerToNext: {
|
49 | borderBottomRightRadius: 3,
|
50 | },
|
51 | containerToPrevious: {
|
52 | borderTopRightRadius: 3,
|
53 | },
|
54 | bottom: {
|
55 | flexDirection: 'row',
|
56 | justifyContent: 'flex-end',
|
57 | },
|
58 | }),
|
59 | content: StyleSheet.create({
|
60 | tick: {
|
61 | fontSize: 10,
|
62 | backgroundColor: Color.backgroundTransparent,
|
63 | color: Color.white,
|
64 | },
|
65 | tickView: {
|
66 | flexDirection: 'row',
|
67 | marginRight: 10,
|
68 | },
|
69 | username: {
|
70 | top: -3,
|
71 | left: 0,
|
72 | fontSize: 12,
|
73 | backgroundColor: 'transparent',
|
74 | color: '#aaa',
|
75 | },
|
76 | usernameView: {
|
77 | flexDirection: 'row',
|
78 | marginHorizontal: 10,
|
79 | },
|
80 | }),
|
81 | };
|
82 | const DEFAULT_OPTION_TITLES = ['Copy Text', 'Cancel'];
|
83 | export default class Bubble extends React.Component {
|
84 | constructor() {
|
85 | super(...arguments);
|
86 | this.onLongPress = () => {
|
87 | const { currentMessage } = this.props;
|
88 | if (this.props.onLongPress) {
|
89 | this.props.onLongPress(this.context, this.props.currentMessage);
|
90 | }
|
91 | else if (currentMessage && currentMessage.text) {
|
92 | const { optionTitles } = this.props;
|
93 | const options = optionTitles && optionTitles.length > 0
|
94 | ? optionTitles.slice(0, 2)
|
95 | : DEFAULT_OPTION_TITLES;
|
96 | const cancelButtonIndex = options.length - 1;
|
97 | this.context.actionSheet().showActionSheetWithOptions({
|
98 | options,
|
99 | cancelButtonIndex,
|
100 | }, (buttonIndex) => {
|
101 | switch (buttonIndex) {
|
102 | case 0:
|
103 | Clipboard.setString(currentMessage.text);
|
104 | break;
|
105 | default:
|
106 | break;
|
107 | }
|
108 | });
|
109 | }
|
110 | };
|
111 | }
|
112 | styledBubbleToNext() {
|
113 | const { currentMessage, nextMessage, position, containerToNextStyle, } = this.props;
|
114 | if (currentMessage &&
|
115 | nextMessage &&
|
116 | position &&
|
117 | isSameUser(currentMessage, nextMessage) &&
|
118 | isSameDay(currentMessage, nextMessage)) {
|
119 | return [
|
120 | styles[position].containerToNext,
|
121 | containerToNextStyle && containerToNextStyle[position],
|
122 | ];
|
123 | }
|
124 | return null;
|
125 | }
|
126 | styledBubbleToPrevious() {
|
127 | const { currentMessage, previousMessage, position, containerToPreviousStyle, } = this.props;
|
128 | if (currentMessage &&
|
129 | previousMessage &&
|
130 | position &&
|
131 | isSameUser(currentMessage, previousMessage) &&
|
132 | isSameDay(currentMessage, previousMessage)) {
|
133 | return [
|
134 | styles[position].containerToPrevious,
|
135 | containerToPreviousStyle && containerToPreviousStyle[position],
|
136 | ];
|
137 | }
|
138 | return null;
|
139 | }
|
140 | renderQuickReplies() {
|
141 | const { currentMessage, onQuickReply, nextMessage, renderQuickReplySend, quickReplyStyle, } = this.props;
|
142 | if (currentMessage && currentMessage.quickReplies) {
|
143 | const { containerStyle, wrapperStyle, ...quickReplyProps } = this.props;
|
144 | if (this.props.renderQuickReplies) {
|
145 | return this.props.renderQuickReplies(quickReplyProps);
|
146 | }
|
147 | return (<QuickReplies {...{
|
148 | currentMessage,
|
149 | onQuickReply,
|
150 | nextMessage,
|
151 | renderQuickReplySend,
|
152 | quickReplyStyle,
|
153 | }}/>);
|
154 | }
|
155 | return null;
|
156 | }
|
157 | renderMessageText() {
|
158 | if (this.props.currentMessage && this.props.currentMessage.text) {
|
159 | const { containerStyle, wrapperStyle, optionTitles, ...messageTextProps } = this.props;
|
160 | if (this.props.renderMessageText) {
|
161 | return this.props.renderMessageText(messageTextProps);
|
162 | }
|
163 | return <MessageText {...messageTextProps}/>;
|
164 | }
|
165 | return null;
|
166 | }
|
167 | renderMessageImage() {
|
168 | if (this.props.currentMessage && this.props.currentMessage.image) {
|
169 | const { containerStyle, wrapperStyle, ...messageImageProps } = this.props;
|
170 | if (this.props.renderMessageImage) {
|
171 | return this.props.renderMessageImage(messageImageProps);
|
172 | }
|
173 | return <MessageImage {...messageImageProps}/>;
|
174 | }
|
175 | return null;
|
176 | }
|
177 | renderMessageVideo() {
|
178 | if (this.props.currentMessage && this.props.currentMessage.video) {
|
179 | const { containerStyle, wrapperStyle, ...messageVideoProps } = this.props;
|
180 | if (this.props.renderMessageVideo) {
|
181 | return this.props.renderMessageVideo(messageVideoProps);
|
182 | }
|
183 | return <MessageVideo {...messageVideoProps}/>;
|
184 | }
|
185 | return null;
|
186 | }
|
187 | renderMessageAudio() {
|
188 | if (this.props.currentMessage && this.props.currentMessage.audio) {
|
189 | const { containerStyle, wrapperStyle, ...messageAudioProps } = this.props;
|
190 | if (this.props.renderMessageAudio) {
|
191 | return this.props.renderMessageAudio(messageAudioProps);
|
192 | }
|
193 | return <MessageAudio {...messageAudioProps}/>;
|
194 | }
|
195 | return null;
|
196 | }
|
197 | renderTicks() {
|
198 | const { currentMessage, renderTicks, user } = this.props;
|
199 | if (renderTicks && currentMessage) {
|
200 | return renderTicks(currentMessage);
|
201 | }
|
202 | if (currentMessage &&
|
203 | user &&
|
204 | currentMessage.user &&
|
205 | currentMessage.user._id !== user._id) {
|
206 | return null;
|
207 | }
|
208 | if (currentMessage &&
|
209 | (currentMessage.sent || currentMessage.received || currentMessage.pending)) {
|
210 | return (<View style={styles.content.tickView}>
|
211 | {!!currentMessage.sent && (<Text style={[styles.content.tick, this.props.tickStyle]}>✓</Text>)}
|
212 | {!!currentMessage.received && (<Text style={[styles.content.tick, this.props.tickStyle]}>✓</Text>)}
|
213 | {!!currentMessage.pending && (<Text style={[styles.content.tick, this.props.tickStyle]}>🕓</Text>)}
|
214 | </View>);
|
215 | }
|
216 | return null;
|
217 | }
|
218 | renderTime() {
|
219 | if (this.props.currentMessage && this.props.currentMessage.createdAt) {
|
220 | const { containerStyle, wrapperStyle, textStyle, ...timeProps } = this.props;
|
221 | if (this.props.renderTime) {
|
222 | return this.props.renderTime(timeProps);
|
223 | }
|
224 | return <Time {...timeProps}/>;
|
225 | }
|
226 | return null;
|
227 | }
|
228 | renderUsername() {
|
229 | const { currentMessage, user } = this.props;
|
230 | if (this.props.renderUsernameOnMessage && currentMessage) {
|
231 | if (user && currentMessage.user._id === user._id) {
|
232 | return null;
|
233 | }
|
234 | return (<View style={styles.content.usernameView}>
|
235 | <Text style={[styles.content.username, this.props.usernameStyle]}>
|
236 | ~ {currentMessage.user.name}
|
237 | </Text>
|
238 | </View>);
|
239 | }
|
240 | return null;
|
241 | }
|
242 | renderCustomView() {
|
243 | if (this.props.renderCustomView) {
|
244 | return this.props.renderCustomView(this.props);
|
245 | }
|
246 | return null;
|
247 | }
|
248 | renderBubbleContent() {
|
249 | return this.props.isCustomViewBottom ? (<View>
|
250 | {this.renderMessageImage()}
|
251 | {this.renderMessageVideo()}
|
252 | {this.renderMessageAudio()}
|
253 | {this.renderMessageText()}
|
254 | {this.renderCustomView()}
|
255 | </View>) : (<View>
|
256 | {this.renderCustomView()}
|
257 | {this.renderMessageImage()}
|
258 | {this.renderMessageVideo()}
|
259 | {this.renderMessageAudio()}
|
260 | {this.renderMessageText()}
|
261 | </View>);
|
262 | }
|
263 | render() {
|
264 | const { position, containerStyle, wrapperStyle, bottomContainerStyle, } = this.props;
|
265 | return (<View style={[
|
266 | styles[position].container,
|
267 | containerStyle && containerStyle[position],
|
268 | ]}>
|
269 | <View style={[
|
270 | styles[position].wrapper,
|
271 | this.styledBubbleToNext(),
|
272 | this.styledBubbleToPrevious(),
|
273 | wrapperStyle && wrapperStyle[position],
|
274 | ]}>
|
275 | <TouchableWithoutFeedback onLongPress={this.onLongPress} accessibilityTraits='text' {...this.props.touchableProps}>
|
276 | <View>
|
277 | {this.renderBubbleContent()}
|
278 | <View style={[
|
279 | styles[position].bottom,
|
280 | bottomContainerStyle && bottomContainerStyle[position],
|
281 | ]}>
|
282 | {this.renderUsername()}
|
283 | {this.renderTime()}
|
284 | {this.renderTicks()}
|
285 | </View>
|
286 | </View>
|
287 | </TouchableWithoutFeedback>
|
288 | </View>
|
289 | {this.renderQuickReplies()}
|
290 | </View>);
|
291 | }
|
292 | }
|
293 | Bubble.contextTypes = {
|
294 | actionSheet: PropTypes.func,
|
295 | };
|
296 | Bubble.defaultProps = {
|
297 | touchableProps: {},
|
298 | onLongPress: null,
|
299 | renderMessageImage: null,
|
300 | renderMessageVideo: null,
|
301 | renderMessageAudio: null,
|
302 | renderMessageText: null,
|
303 | renderCustomView: null,
|
304 | renderUsername: null,
|
305 | renderTicks: null,
|
306 | renderTime: null,
|
307 | renderQuickReplies: null,
|
308 | onQuickReply: null,
|
309 | position: 'left',
|
310 | optionTitles: DEFAULT_OPTION_TITLES,
|
311 | currentMessage: {
|
312 | text: null,
|
313 | createdAt: null,
|
314 | image: null,
|
315 | },
|
316 | nextMessage: {},
|
317 | previousMessage: {},
|
318 | containerStyle: {},
|
319 | wrapperStyle: {},
|
320 | bottomContainerStyle: {},
|
321 | tickStyle: {},
|
322 | usernameStyle: {},
|
323 | containerToNextStyle: {},
|
324 | containerToPreviousStyle: {},
|
325 | };
|
326 | Bubble.propTypes = {
|
327 | user: PropTypes.object.isRequired,
|
328 | touchableProps: PropTypes.object,
|
329 | onLongPress: PropTypes.func,
|
330 | renderMessageImage: PropTypes.func,
|
331 | renderMessageVideo: PropTypes.func,
|
332 | renderMessageAudio: PropTypes.func,
|
333 | renderMessageText: PropTypes.func,
|
334 | renderCustomView: PropTypes.func,
|
335 | isCustomViewBottom: PropTypes.bool,
|
336 | renderUsernameOnMessage: PropTypes.bool,
|
337 | renderUsername: PropTypes.func,
|
338 | renderTime: PropTypes.func,
|
339 | renderTicks: PropTypes.func,
|
340 | renderQuickReplies: PropTypes.func,
|
341 | onQuickReply: PropTypes.func,
|
342 | position: PropTypes.oneOf(['left', 'right']),
|
343 | optionTitles: PropTypes.arrayOf(PropTypes.string),
|
344 | currentMessage: PropTypes.object,
|
345 | nextMessage: PropTypes.object,
|
346 | previousMessage: PropTypes.object,
|
347 | containerStyle: PropTypes.shape({
|
348 | left: StylePropType,
|
349 | right: StylePropType,
|
350 | }),
|
351 | wrapperStyle: PropTypes.shape({
|
352 | left: StylePropType,
|
353 | right: StylePropType,
|
354 | }),
|
355 | bottomContainerStyle: PropTypes.shape({
|
356 | left: StylePropType,
|
357 | right: StylePropType,
|
358 | }),
|
359 | tickStyle: StylePropType,
|
360 | usernameStyle: StylePropType,
|
361 | containerToNextStyle: PropTypes.shape({
|
362 | left: StylePropType,
|
363 | right: StylePropType,
|
364 | }),
|
365 | containerToPreviousStyle: PropTypes.shape({
|
366 | left: StylePropType,
|
367 | right: StylePropType,
|
368 | }),
|
369 | };
|
370 | //# sourceMappingURL=Bubble.js.map |
\ | No newline at end of file |