@plusscommunities/pluss-core-app 7.0.1-beta.1 → 7.0.2-auth.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 (34) hide show
  1. package/dist/module/apis/fileActions.js +21 -28
  2. package/dist/module/apis/fileActions.js.map +1 -1
  3. package/dist/module/components/DocumentUploader.js +237 -0
  4. package/dist/module/components/DocumentUploader.js.map +1 -0
  5. package/dist/module/components/ImagePopup.js +42 -20
  6. package/dist/module/components/ImagePopup.js.map +1 -1
  7. package/dist/module/components/ImageUploader.js +50 -25
  8. package/dist/module/components/ImageUploader.js.map +1 -1
  9. package/dist/module/components/MediaPlayer.js +2 -2
  10. package/dist/module/components/MediaPlayer.js.map +1 -1
  11. package/dist/module/components/PlussChat.js +80 -6
  12. package/dist/module/components/PlussChat.js.map +1 -1
  13. package/dist/module/components/PlussChatMessage.js +41 -4
  14. package/dist/module/components/PlussChatMessage.js.map +1 -1
  15. package/dist/module/components/index.js +1 -0
  16. package/dist/module/components/index.js.map +1 -1
  17. package/dist/module/components/react-native-expo-image-cropper/ExpoImageManipulator.js +2 -1
  18. package/dist/module/components/react-native-expo-image-cropper/ExpoImageManipulator.js.map +1 -1
  19. package/dist/module/config.js +6 -1
  20. package/dist/module/config.js.map +1 -1
  21. package/dist/module/session.js +2 -2
  22. package/dist/module/session.js.map +1 -1
  23. package/package.json +4 -1
  24. package/src/apis/fileActions.js +19 -27
  25. package/src/components/DocumentUploader.js +207 -0
  26. package/src/components/ImagePopup.js +41 -26
  27. package/src/components/ImageUploader.js +66 -28
  28. package/src/components/MediaPlayer.js +2 -2
  29. package/src/components/PlussChat.js +88 -2
  30. package/src/components/PlussChatMessage.js +40 -3
  31. package/src/components/index.js +1 -0
  32. package/src/components/react-native-expo-image-cropper/ExpoImageManipulator.js +1 -1
  33. package/src/config.js +5 -0
  34. package/src/session.js +2 -2
@@ -0,0 +1,207 @@
1
+ import React, { Component } from 'react';
2
+ import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
3
+ import { Icon } from '@rneui/themed';
4
+ // import * as DocumentPicker from 'expo-document-picker';
5
+ import { connect } from 'react-redux';
6
+ import Config from '../config';
7
+ import { fileActions } from '../apis';
8
+ import { getValueOrDefault } from '../helper';
9
+ import { TEXT_DARK, getMainBrandingColourFromState } from '../colours';
10
+
11
+ const DEFAULT_DOCUMENT_NAME = 'document';
12
+
13
+ class DocumentUploader extends Component {
14
+ static defaultProps = {
15
+ allowedTypes: ['application/pdf'],
16
+ buttonTitle: 'Upload Document',
17
+ buttonStyle: {},
18
+ buttonTextStyle: {},
19
+ onUploadStarted: () => {},
20
+ onUploadSuccess: () => {},
21
+ onUploadFailed: () => {},
22
+ onUploadProgress: null,
23
+ userId: null,
24
+ fileName: null,
25
+ disabled: false,
26
+ };
27
+
28
+ state = {
29
+ isUploading: false,
30
+ uploadProgress: 0,
31
+ };
32
+
33
+ pickDocument = async () => {
34
+ const { allowedTypes, multiple } = this.props;
35
+
36
+ try {
37
+ const result = {};
38
+ // const result = await DocumentPicker.getDocumentAsync({
39
+ // type: allowedTypes,
40
+ // copyToCacheDirectory: true,
41
+ // multiple: multiple || false,
42
+ // });
43
+ // console.log('pickDocument', JSON.stringify(result, null, 2));
44
+
45
+ if (!result.canceled) {
46
+ await this.handleDocumentPicked(result);
47
+ }
48
+ } catch (error) {
49
+ console.log('Document picker error:', error);
50
+ this.props.onUploadFailed(null, error.message);
51
+ }
52
+ };
53
+
54
+ handleDocumentPicked = async documentResult => {
55
+ const { assets } = documentResult;
56
+ const { userId } = this.props;
57
+
58
+ try {
59
+ this.setState({ isUploading: true, uploadProgress: 0 });
60
+
61
+ // Process each asset in parallel
62
+ const uploadPromises = assets.map(async asset => {
63
+ const { name, uri } = asset;
64
+
65
+ // Generate a unique filename if not provided
66
+ const [file, fileExt] = name.split('.');
67
+ const fileName = `${getValueOrDefault(this.props.fileName, DEFAULT_DOCUMENT_NAME)}_${Date.now()}.${fileExt}`;
68
+ const uploadUri = fileActions.getUploadUrl(userId, fileName);
69
+ // console.log('handleDocumentPicked', JSON.stringify({ uploadUri, uri, name, fileName }, null, 2));
70
+
71
+ try {
72
+ // Notify parent component that upload has started for this file
73
+ this.props.onUploadStarted(uploadUri, uri, file, fileExt.toUpperCase());
74
+
75
+ const res = await fileActions.uploadUserMediaWithProgress(uri, uploadUri, progress => {
76
+ if (this.props.onUploadProgress) this.props.onUploadProgress(progress);
77
+ });
78
+
79
+ const fileUrl = Config.env.baseUploadsUrl + res.key;
80
+ this.props.onUploadSuccess(fileUrl, uploadUri);
81
+ console.log('Upload success', fileUrl);
82
+ return { success: true, url: fileUrl, uploadUri };
83
+ } catch (error) {
84
+ console.error(`Upload failed for ${name}:`, error);
85
+ this.props.onUploadFailed(uploadUri, error.message);
86
+ return { success: false, error, uploadUri };
87
+ }
88
+ });
89
+
90
+ // Wait for all uploads to complete
91
+ const results = await Promise.all(uploadPromises);
92
+ return results;
93
+ } catch (error) {
94
+ console.error('Document upload error:', error);
95
+ return [];
96
+ } finally {
97
+ this.setState({ isUploading: false, uploadProgress: 0 });
98
+ }
99
+ };
100
+
101
+ renderUploadButton = () => {
102
+ const { buttonTitle, buttonStyle, buttonTextStyle, disabled } = this.props;
103
+ const { isUploading } = this.state;
104
+ const mainColor = getMainBrandingColourFromState(this.props);
105
+
106
+ return (
107
+ <TouchableOpacity
108
+ style={[styles.uploadButton, { borderColor: mainColor }, buttonStyle]}
109
+ onPress={this.pickDocument}
110
+ disabled={isUploading || disabled}
111
+ activeOpacity={0.7}
112
+ >
113
+ <View style={styles.buttonContent}>
114
+ {isUploading ? (
115
+ <ActivityIndicator color={mainColor} />
116
+ ) : (
117
+ <Icon name="attachment" type="entypo" color={mainColor} size={18} style={styles.icon} />
118
+ )}
119
+ <Text style={[styles.buttonText, { color: mainColor }, buttonTextStyle]}>{buttonTitle}</Text>
120
+ </View>
121
+ </TouchableOpacity>
122
+ );
123
+ };
124
+
125
+ renderProgress = () => {
126
+ const { uploadProgress } = this.state;
127
+ if (uploadProgress <= 0 || uploadProgress >= 1) return null;
128
+
129
+ return (
130
+ <View style={styles.progressContainer}>
131
+ <View style={styles.progressBar}>
132
+ <View style={[styles.progressFill, { width: `${uploadProgress * 100}%` }]} />
133
+ </View>
134
+ <Text style={styles.progressText}>{Math.round(uploadProgress * 100)}%</Text>
135
+ </View>
136
+ );
137
+ };
138
+
139
+ render() {
140
+ return (
141
+ <View style={styles.container}>
142
+ {this.renderUploadButton()}
143
+ {this.renderProgress()}
144
+ </View>
145
+ );
146
+ }
147
+ }
148
+
149
+ const styles = StyleSheet.create({
150
+ container: {
151
+ marginVertical: 10,
152
+ },
153
+ uploadButton: {
154
+ flexDirection: 'row',
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ paddingVertical: 8,
158
+ paddingHorizontal: 16,
159
+ borderRadius: 6,
160
+ backgroundColor: '#fff',
161
+ borderWidth: 1,
162
+ borderColor: '#007AFF',
163
+ },
164
+ buttonContent: {
165
+ flexDirection: 'row',
166
+ alignItems: 'center',
167
+ },
168
+ buttonText: {
169
+ color: '#fff',
170
+ fontSize: 16,
171
+ fontWeight: '500',
172
+ marginLeft: 8,
173
+ },
174
+ icon: {
175
+ marginRight: 8,
176
+ },
177
+ progressContainer: {
178
+ marginTop: 8,
179
+ alignItems: 'center',
180
+ },
181
+ progressBar: {
182
+ height: 4,
183
+ width: '100%',
184
+ backgroundColor: '#E0E0E0',
185
+ borderRadius: 2,
186
+ overflow: 'hidden',
187
+ },
188
+ progressFill: {
189
+ height: '100%',
190
+ backgroundColor: '#4CAF50',
191
+ },
192
+ progressText: {
193
+ marginTop: 4,
194
+ fontSize: 12,
195
+ color: TEXT_DARK,
196
+ },
197
+ });
198
+
199
+ const mapStateToProps = state => {
200
+ const { user } = state;
201
+ return {
202
+ user,
203
+ colourBrandingMain: getMainBrandingColourFromState(state),
204
+ };
205
+ };
206
+
207
+ export default connect(mapStateToProps)(DocumentUploader);
@@ -1,9 +1,10 @@
1
1
  import React, { Component } from 'react';
2
2
  import _ from 'lodash';
3
3
  import moment from 'moment';
4
- import { Dimensions, Modal, TouchableOpacity, StyleSheet, View, ImageBackground, Text } from 'react-native';
4
+ import { Dimensions, Modal, TouchableOpacity, StyleSheet, View, Text } from 'react-native';
5
5
  import ImageViewer from 'react-native-image-zoom-viewer';
6
6
  import { Icon } from '@rneui/themed';
7
+ import AutoHeightImage from 'react-native-auto-height-image';
7
8
  import { TEXT_DARK } from '../colours';
8
9
  import { StatusBarHeight, isVideo, get1400 } from '../helper';
9
10
  import { Pl60Icon } from '../fonts';
@@ -71,9 +72,8 @@ class ImagePopup extends Component {
71
72
  );
72
73
  }
73
74
 
74
- renderImage = props => {
75
- const media = this.state.images.find(image => image.url === props.source.uri);
76
- const isVideo = !_.isNil(media) && media.isVideo;
75
+ renderImageFooter = (media, style) => {
76
+ if (!media.user || !media.date) return null;
77
77
 
78
78
  let dateText, timeText;
79
79
  if (!_.isNil(media.date)) {
@@ -83,25 +83,31 @@ class ImagePopup extends Component {
83
83
  }
84
84
 
85
85
  return (
86
- <View>
87
- <ImageBackground {...props}>
88
- {isVideo && (
89
- <View style={styles.videoOverlay}>
90
- <TouchableOpacity onPress={this.toggleFullscreenVideo.bind(this, media.original)}>
91
- <Icon name="play" type="font-awesome" iconStyle={styles.videoPlayIcon} />
92
- </TouchableOpacity>
93
- </View>
94
- )}
95
- </ImageBackground>
96
- {(!_.isNil(media.user) || !_.isNil(media.date)) && (
97
- <View style={styles.imageInfoContainer}>
98
- <ProfilePic Diameter={42} ProfilePic={media?.user?.profilePic} />
99
- <View style={styles.imageTextContainer}>
100
- <Text style={styles.imageTextName}>{media?.user?.displayName}</Text>
101
- <Text numberOfLines={2} style={styles.iamgeTextDate}>{`Uploaded ${dateText} • ${timeText}`}</Text>
102
- </View>
86
+ <View style={[styles.imageInfoContainer, style]}>
87
+ <ProfilePic Diameter={42} ProfilePic={media?.user?.profilePic} />
88
+ <View style={styles.imageTextContainer}>
89
+ <Text style={styles.imageTextName}>{media?.user?.displayName}</Text>
90
+ <Text numberOfLines={2} style={styles.iamgeTextDate}>{`Uploaded ${dateText} • ${timeText}`}</Text>
91
+ </View>
92
+ </View>
93
+ );
94
+ };
95
+
96
+ renderImage = props => {
97
+ const media = this.state.images.find(image => image.url === props.source.uri);
98
+ const isVideo = !_.isNil(media) && media.isVideo;
99
+
100
+ return (
101
+ <View style={styles.imageContainer}>
102
+ <AutoHeightImage source={{ uri: media.url }} width={SCREEN_WIDTH} resizeMode="contain" />
103
+ {isVideo && (
104
+ <View style={styles.videoOverlay}>
105
+ <TouchableOpacity onPress={this.toggleFullscreenVideo.bind(this, media.original)}>
106
+ <Icon name="play" type="font-awesome" iconStyle={styles.videoPlayIcon} />
107
+ </TouchableOpacity>
103
108
  </View>
104
109
  )}
110
+ {this.renderImageFooter(media)}
105
111
  </View>
106
112
  );
107
113
  };
@@ -120,6 +126,15 @@ class ImagePopup extends Component {
120
126
  if (_.isEmpty(images)) {
121
127
  return null;
122
128
  }
129
+
130
+ // This is required for the ImageViewer to render the image info section properly
131
+ const imagesWithSizes = images.map(image => {
132
+ return {
133
+ ...image,
134
+ width: SCREEN_WIDTH,
135
+ height: SCREEN_HEIGHT,
136
+ };
137
+ });
123
138
  return (
124
139
  <Modal visible={visible} animationType="slide" onRequestClose={onClose} style={styles.modal}>
125
140
  <ImageViewer
@@ -128,7 +143,7 @@ class ImagePopup extends Component {
128
143
  onChange={this.onChange}
129
144
  onSwipeDown={onClose}
130
145
  enableSwipeDown
131
- imageUrls={images}
146
+ imageUrls={imagesWithSizes}
132
147
  saveToLocalByLongPress={false}
133
148
  renderImage={this.renderImage}
134
149
  />
@@ -149,10 +164,10 @@ const styles = StyleSheet.create({
149
164
  height: SCREEN_HEIGHT,
150
165
  backgroundColor: '#000',
151
166
  },
152
- image: {
153
- width: SCREEN_WIDTH,
154
- height: SCREEN_HEIGHT,
155
- backgroundColor: '#000',
167
+ imageContainer: {
168
+ flex: 1,
169
+ justifyContent: 'center',
170
+ alignItems: 'center',
156
171
  },
157
172
  menuIconContainer: {
158
173
  position: 'absolute',
@@ -142,23 +142,25 @@ class ImageUploader extends Component {
142
142
  let uploadUri;
143
143
  try {
144
144
  const fileName = `${getValueOrDefault(this.props.fileName, DEFAULT_IMAGE_NAME)}.${DEFULAT_IMAGE_TYPE}`;
145
- uploadUri = fileActions.getUploadUrl(this.props.userId, fileName);
146
145
 
147
- this.props.onUploadStarted(uploadUri, imageUri);
148
146
  this.hideUploadMenu();
149
147
 
150
148
  const resized = await this.resizeImageAsync(imageUri);
149
+ const blob = await fileActions.imageToBlob(resized.uri);
150
+ const presignedUriRes = await fileActions.getPresignedUrl(_.last(resized.uri.split('/')), blob.type);
151
+ uploadUri = presignedUriRes.key;
152
+ this.props.onUploadStarted(uploadUri, imageUri);
151
153
  if (this.props.onlySelectImage) {
152
154
  this.props.onImageSelected(resized, fileName);
153
155
  return;
154
156
  }
155
157
 
156
- const res = await fileActions.uploadUserMediaWithProgress(resized.uri, uploadUri, progress => {
158
+ const uploadResult = await fileActions.uploadUserMediaWithProgress(blob, presignedUriRes.url, progress => {
157
159
  if (this.props.onUploadProgress) this.props.onUploadProgress(progress);
158
160
  });
159
161
 
160
- this.props.onUploadSuccess(Config.env.baseUploadsUrl + res.key, uploadUri);
161
- console.log('upload success', Config.env.baseUploadsUrl + res.key);
162
+ this.props.onUploadSuccess(Config.env.baseUploadsUrl + uploadUri, uploadUri);
163
+ console.log('upload success', Config.env.baseUploadsUrl + uploadUri);
162
164
  } catch (e) {
163
165
  console.log('handleImagePicked error', e);
164
166
  this.props.onUploadFailed(uploadUri);
@@ -170,7 +172,6 @@ class ImageUploader extends Component {
170
172
  const imagesUploaded = imagesSelected.map(image => {
171
173
  const fileName = `${getValueOrDefault(this.props.fileName, DEFAULT_IMAGE_NAME)}.${DEFULAT_IMAGE_TYPE}`;
172
174
  const uploadUri = fileActions.getUploadUrl(this.props.userId, fileName);
173
- if (!this.props.onlySelectImage) this.props.onUploadStarted(uploadUri, image.uri);
174
175
  return { imageUri: image.uri, uploadUri, fileName };
175
176
  });
176
177
  this.hideUploadMenu();
@@ -181,16 +182,21 @@ class ImageUploader extends Component {
181
182
  const { imageUri, uploadUri, fileName } = image;
182
183
  try {
183
184
  const resized = await this.resizeImageAsync(imageUri);
185
+ const blob = await fileActions.imageToBlob(resized.uri);
184
186
  if (this.props.onlySelectImage) {
185
187
  this.props.onImageSelected(resized, fileName);
186
188
  return;
187
189
  }
190
+ const presignedUriRes = await fileActions.getPresignedUrl(_.last(resized.uri.split('/')), blob.type);
191
+ const uploadUri = presignedUriRes.key;
192
+ if (!this.props.onlySelectImage) this.props.onUploadStarted(uploadUri, imageUri);
188
193
 
189
- const res = await fileActions.uploadUserMediaWithProgress(resized.uri, uploadUri, progress => {
194
+ const uploadResult = await fileActions.uploadUserMediaWithProgress(blob, presignedUriRes.url, progress => {
190
195
  if (this.props.onUploadProgress) this.props.onUploadProgress(progress);
191
196
  });
192
- this.props.onUploadSuccess(Config.env.baseUploadsUrl + res.key, uploadUri);
193
- console.log('upload success', Config.env.baseUploadsUrl + res.key);
197
+
198
+ this.props.onUploadSuccess(Config.env.baseUploadsUrl + uploadUri, uploadUri);
199
+ console.log('upload success', Config.env.baseUploadsUrl + uploadUri);
194
200
  } catch (e) {
195
201
  console.log('handleMultiImagePicked error', e);
196
202
  this.props.onUploadFailed(uploadUri);
@@ -203,17 +209,19 @@ class ImageUploader extends Component {
203
209
  try {
204
210
  const fileType = uri.substring(uri.lastIndexOf('.') + 1);
205
211
  const fileName = `${getValueOrDefault(this.props.fileName, DEFAULT_VIDEO_NAME)}.${fileType}`;
206
- uploadUri = fileActions.getUploadUrl(this.props.userId, fileName);
212
+ const blob = await fileActions.imageToBlob(uri);
213
+ const presignedUriRes = await fileActions.getPresignedUrl(fileName, blob.type);
214
+ uploadUri = presignedUriRes.key;
207
215
 
208
216
  this.props.onUploadStarted(uploadUri, uri);
209
217
  this.hideUploadMenu();
210
218
 
211
- const res = await fileActions.uploadUserMediaWithProgress(uri, uploadUri, progress => {
219
+ const uploadResult = await fileActions.uploadUserMediaWithProgress(blob, presignedUriRes.url, progress => {
212
220
  if (this.props.onUploadProgress) this.props.onUploadProgress(progress);
213
221
  });
214
222
 
215
- this.props.onUploadSuccess(Config.env.baseUploadsUrl + res.key, uploadUri);
216
- console.log('upload success', Config.env.baseUploadsUrl + res.key);
223
+ this.props.onUploadSuccess(Config.env.baseUploadsUrl + uploadUri, uploadUri);
224
+ console.log('upload success', Config.env.baseUploadsUrl + uploadUri);
217
225
  } catch (e) {
218
226
  console.log('handleVideoPicked error', e);
219
227
  this.props.onUploadFailed(uploadUri);
@@ -266,6 +274,7 @@ class ImageUploader extends Component {
266
274
  };
267
275
 
268
276
  isEditingEnabled = () => {
277
+ return false;
269
278
  return !this.props.multiple && !this.props.allowVideo && getValueOrDefault(this.props.allowsEditing, DEFAULT_ALLOWS_EDITING);
270
279
  };
271
280
 
@@ -304,13 +313,21 @@ class ImageUploader extends Component {
304
313
  };
305
314
 
306
315
  openLibrary = async () => {
307
- this.setState({
308
- showUploadMenu: false,
309
- showPhotos: true,
310
- selected: [],
311
- showRemote: true,
312
- selectedAlbumId: '',
313
- });
316
+ this.setState(
317
+ {
318
+ showUploadMenu: false,
319
+ },
320
+ () => {
321
+ setTimeout(() => {
322
+ this.setState({
323
+ showPhotos: true,
324
+ selected: [],
325
+ showRemote: true,
326
+ selectedAlbumId: '',
327
+ });
328
+ }, 1000);
329
+ },
330
+ );
314
331
  };
315
332
 
316
333
  openPhotos = async () => {
@@ -334,13 +351,21 @@ class ImageUploader extends Component {
334
351
  // iOS behaviour
335
352
  if (!(await this.askPermissionsAsync())) return;
336
353
 
337
- this.setState({
338
- showUploadMenu: false,
339
- showPhotos: true,
340
- selected: [],
341
- showRemote: false,
342
- selectedAlbumId: '',
343
- });
354
+ this.setState(
355
+ {
356
+ showUploadMenu: false,
357
+ },
358
+ () => {
359
+ setTimeout(() => {
360
+ this.setState({
361
+ showPhotos: true,
362
+ selected: [],
363
+ showRemote: false,
364
+ selectedAlbumId: '',
365
+ });
366
+ }, 1000);
367
+ },
368
+ );
344
369
  };
345
370
 
346
371
  hidePhotos = () => {
@@ -348,7 +373,19 @@ class ImageUploader extends Component {
348
373
  };
349
374
 
350
375
  openCropper = () => {
351
- this.setState({ showPhotos: false, showCropper: true });
376
+ // Switching modals - requires a timeout to ensure the modal is closed before opening the next one
377
+ this.setState(
378
+ {
379
+ showPhotos: false,
380
+ },
381
+ () => {
382
+ setTimeout(() => {
383
+ this.setState({
384
+ showCropper: true,
385
+ });
386
+ }, 1000);
387
+ },
388
+ );
352
389
  };
353
390
 
354
391
  toggleCropper = () => {
@@ -390,6 +427,7 @@ class ImageUploader extends Component {
390
427
  } else {
391
428
  if (mediaSelected[0].mediaType === MediaLibrary.MediaType.video) {
392
429
  const uri = mediaSelected[0].localUri || mediaSelected[0].uri;
430
+ console.log('picked a video');
393
431
  this.handleVideoPicked(uri);
394
432
  } else {
395
433
  this.handleMultiImagePicked(mediaSelected);
@@ -3,7 +3,7 @@ import { View, StyleSheet, Dimensions } from 'react-native';
3
3
  import YoutubePlayer, { getYoutubeMeta } from 'react-native-youtube-iframe';
4
4
  import { Vimeo } from 'react-native-vimeo-iframe';
5
5
  import { WebView } from 'react-native-webview';
6
- import { Video } from 'expo-av';
6
+ import { Video, ResizeMode } from 'expo-av';
7
7
  import { Spinner } from './Spinner';
8
8
 
9
9
  const SCREEN_HEIGHT = Dimensions.get('window').height;
@@ -269,7 +269,7 @@ class MediaPlayer extends Component {
269
269
  source={{ uri: embedUrl }}
270
270
  {...EXPO_VIDEO_PROPS}
271
271
  shouldPlay={autoPlay}
272
- resizeMode={Video.RESIZE_MODE_CONTAIN}
272
+ resizeMode={ResizeMode.CONTAIN}
273
273
  useNativeControls
274
274
  style={{ width, height }}
275
275
  onLoadStart={this.onStreamLoadStart}
@@ -1,5 +1,5 @@
1
1
  import React, { Component } from 'react';
2
- import { View, Image, ImageBackground, TouchableOpacity, Text, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
2
+ import { View, Image, ImageBackground, TouchableOpacity, Text, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
3
3
  import { GiftedChat, Bubble, MessageText, Send, InputToolbar, Composer } from 'react-native-gifted-chat';
4
4
  import { connect } from 'react-redux';
5
5
  import _ from 'lodash';
@@ -13,6 +13,7 @@ import { PDFPopup } from './PDFPopup';
13
13
  import { Attachment } from './Attachment';
14
14
  import PlussChatMessage from './PlussChatMessage';
15
15
  import { TextStyle } from './TextStyle';
16
+ import { ConfirmPopup } from './ConfirmPopup';
16
17
  import {
17
18
  TEXT_DARK,
18
19
  LINEGREY,
@@ -62,6 +63,8 @@ class PlussChat extends Component {
62
63
  imagesToUpload: [],
63
64
  showFullscreenVideo: false,
64
65
  currentVideoUrl: '',
66
+ showDeleteMessageConfirm: false,
67
+ messageToDelete: null,
65
68
  };
66
69
  this.checkThumb = null;
67
70
  }
@@ -256,6 +259,55 @@ class PlussChat extends Component {
256
259
  });
257
260
  };
258
261
 
262
+ onDelete = message => {
263
+ // Only proceed if delete handler is provided
264
+ if (!this.props.onDeleteMessage) {
265
+ return;
266
+ }
267
+
268
+ // Only allow deletion of own messages
269
+ if (message.user._id !== this.props.user.uid) {
270
+ return;
271
+ }
272
+
273
+ this.setState({
274
+ showDeleteMessageConfirm: true,
275
+ messageToDelete: message,
276
+ });
277
+ };
278
+
279
+ onCancelDeleteMessage = () => {
280
+ this.setState({
281
+ showDeleteMessageConfirm: false,
282
+ messageToDelete: null,
283
+ });
284
+ };
285
+
286
+ onConfirmDeleteMessage = async () => {
287
+ const { messageToDelete } = this.state;
288
+ if (!messageToDelete) return;
289
+
290
+ this.setState({
291
+ showDeleteMessageConfirm: false,
292
+ });
293
+
294
+ try {
295
+ // Call the parent's delete handler if provided
296
+ if (this.props.onDeleteMessage) {
297
+ await this.props.onDeleteMessage(messageToDelete);
298
+ }
299
+
300
+ this.setState({
301
+ messageToDelete: null,
302
+ });
303
+ } catch (error) {
304
+ // Handle error gracefully if parent handler fails
305
+ console.log('onConfirmDeleteMessage error', error);
306
+ // Error is already handled by parent component
307
+ this.setState({ messageToDelete: null });
308
+ }
309
+ };
310
+
259
311
  closeGallery() {
260
312
  this.setState({
261
313
  imagePopupSource: [],
@@ -353,14 +405,28 @@ class PlussChat extends Component {
353
405
  onPressReply={() => {
354
406
  this.onReply(props.currentMessage);
355
407
  }}
408
+ onPressDelete={this.props.onDeleteMessage ? () => {
409
+ this.onDelete(props.currentMessage);
410
+ } : null}
356
411
  {...props}
357
412
  />
358
413
  );
359
414
  }
360
415
  renderMessageText(messageTextProps) {
416
+ // If message is deleted, show placeholder text
417
+ const props = messageTextProps.currentMessage.deleted
418
+ ? {
419
+ ...messageTextProps,
420
+ currentMessage: {
421
+ ...messageTextProps.currentMessage,
422
+ text: '[Message deleted]',
423
+ },
424
+ }
425
+ : messageTextProps;
426
+
361
427
  return (
362
428
  <MessageText
363
- {...messageTextProps}
429
+ {...props}
364
430
  textStyle={{
365
431
  left: {
366
432
  fontFamily: 'sf-regular',
@@ -408,6 +474,11 @@ class PlussChat extends Component {
408
474
  );
409
475
  }
410
476
  renderCustomView({ currentMessage, position }) {
477
+ // Don't show images or attachments for deleted messages
478
+ if (currentMessage.deleted) {
479
+ return null;
480
+ }
481
+
411
482
  if (currentMessage.image) {
412
483
  const images = typeof currentMessage.image === 'string' ? [currentMessage.image] : currentMessage.image;
413
484
  const containerWidth = (() => {
@@ -765,6 +836,19 @@ class PlussChat extends Component {
765
836
  );
766
837
  }
767
838
 
839
+ renderDeleteConfirmPopup() {
840
+ return (
841
+ <ConfirmPopup
842
+ visible={this.state.showDeleteMessageConfirm}
843
+ onConfirm={this.onConfirmDeleteMessage}
844
+ onCancel={this.onCancelDeleteMessage}
845
+ text="Are you sure you want to delete this message?"
846
+ yesText="Delete"
847
+ noText="Cancel"
848
+ />
849
+ );
850
+ }
851
+
768
852
  render() {
769
853
  if (Platform.OS === 'android' && !this.props.noAndroidAvoid) {
770
854
  return (
@@ -774,6 +858,7 @@ class PlussChat extends Component {
774
858
  {this.renderImagePopup()}
775
859
  {this.renderVideoPlayerPopup()}
776
860
  {this.renderPDF()}
861
+ {this.renderDeleteConfirmPopup()}
777
862
  </KeyboardAvoidingView>
778
863
  );
779
864
  }
@@ -784,6 +869,7 @@ class PlussChat extends Component {
784
869
  {this.renderImagePopup()}
785
870
  {this.renderVideoPlayerPopup()}
786
871
  {this.renderPDF()}
872
+ {this.renderDeleteConfirmPopup()}
787
873
  </View>
788
874
  );
789
875
  }