@messenger-box/platform-mobile 10.0.3-alpha.16 → 10.0.3-alpha.162

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 (95) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/compute.js.map +1 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/module.js.map +1 -1
  6. package/lib/queries/inboxQueries.js +65 -0
  7. package/lib/queries/inboxQueries.js.map +1 -0
  8. package/lib/routes.json +13 -1
  9. package/lib/screens/inbox/DialogMessages.js +5 -3
  10. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  11. package/lib/screens/inbox/DialogThreadMessages.js +3 -7
  12. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  13. package/lib/screens/inbox/DialogThreads.js +57 -18
  14. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  15. package/lib/screens/inbox/Inbox.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  17. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  18. package/lib/screens/inbox/components/CachedImage/index.js +168 -46
  19. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  20. package/lib/screens/inbox/components/DialogItem.js +169 -0
  21. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  22. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  23. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  24. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js.map +1 -1
  25. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +197 -0
  26. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +141 -31
  28. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  29. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  30. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  31. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  32. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -44
  33. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  34. package/lib/screens/inbox/config/config.js +2 -2
  35. package/lib/screens/inbox/config/config.js.map +1 -1
  36. package/lib/screens/inbox/containers/ConversationView.js +1205 -432
  37. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  38. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  39. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadConversationView.js +724 -207
  41. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  42. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  43. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  44. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  45. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  46. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  47. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  48. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  49. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  50. package/package.json +5 -5
  51. package/src/index.ts +2 -0
  52. package/src/queries/inboxQueries.ts +299 -0
  53. package/src/queries/index.d.ts +2 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/screens/inbox/DialogMessages.tsx +2 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +3 -11
  57. package/src/screens/inbox/DialogThreads.tsx +53 -60
  58. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  59. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  60. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  61. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  62. package/src/screens/inbox/components/DialogsListItem.tsx +230 -170
  63. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  64. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  65. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  66. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +330 -171
  67. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  68. package/src/screens/inbox/components/SlackMessageContainer/PaymentMessage.tsx +199 -0
  69. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  70. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  71. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  72. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +41 -24
  73. package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
  74. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  75. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  76. package/src/screens/inbox/config/config.ts +2 -2
  77. package/src/screens/inbox/containers/ConversationView.tsx +2004 -698
  78. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  79. package/src/screens/inbox/containers/Dialogs.tsx +402 -196
  80. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +2 -2
  81. package/src/screens/inbox/containers/ThreadConversationView.tsx +1337 -301
  82. package/src/screens/inbox/containers/ThreadsView.tsx +105 -186
  83. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  84. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  85. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  86. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  87. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  88. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  89. package/src/screens/inbox/index.ts +37 -0
  90. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  91. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  92. package/lib/screens/inbox/components/DialogsListItem.js +0 -175
  93. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  94. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -165
  95. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { Day, utils } from 'react-native-gifted-chat';
4
+ import { Avatar, AvatarFallbackText, AvatarImage, Text } from '@admin-layout/gluestack-ui-mobile';
5
+ import { MaterialCommunityIcons } from '@expo/vector-icons';
6
+
7
+ const { isSameUser, isSameDay } = utils;
8
+
9
+ export default class PaymentMessage extends React.Component<any> {
10
+ static defaultProps = {
11
+ renderAvatar: undefined,
12
+ renderBubble: null,
13
+ renderDay: null,
14
+ currentMessage: {},
15
+ nextMessage: {},
16
+ previousMessage: {},
17
+ user: {},
18
+ containerStyle: {},
19
+ isShowImageViewer: false,
20
+ setImageViewer: (obj: any, v: boolean) => null,
21
+ };
22
+
23
+ getInnerComponentProps() {
24
+ const { containerStyle, ...props } = this.props;
25
+ return {
26
+ ...props,
27
+ position: 'left',
28
+ isSameUser,
29
+ isSameDay,
30
+ };
31
+ }
32
+
33
+ renderDay() {
34
+ if (this.props.currentMessage.createdAt) {
35
+ const dayProps = this.getInnerComponentProps();
36
+ if (this.props.renderDay) {
37
+ return this.props.renderDay(dayProps);
38
+ }
39
+ return <Day {...dayProps} />;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ renderAvatar(isRight: boolean) {
45
+ let extraStyle: any;
46
+ let isSameUserAndSameDay: boolean = false;
47
+ if (
48
+ isSameUser(this.props.currentMessage, this.props.previousMessage) &&
49
+ isSameDay(this.props.currentMessage, this.props.previousMessage)
50
+ ) {
51
+ extraStyle = { height: 0 };
52
+ isSameUserAndSameDay = true;
53
+ }
54
+ const avatarProps: any = this.getInnerComponentProps();
55
+ return (
56
+ <Avatar
57
+ size={'sm'}
58
+ className="bg-transparent rounded-none"
59
+ style={[isRight ? { marginLeft: 8 } : { marginRight: 8 }]}
60
+ >
61
+ <AvatarFallbackText>
62
+ {isSameUserAndSameDay ? '' : avatarProps?.currentMessage?.user?.name?.[0]}
63
+ </AvatarFallbackText>
64
+ {!isSameUserAndSameDay && avatarProps?.currentMessage?.user?.avatar && (
65
+ <AvatarImage
66
+ alt="image"
67
+ style={{ height: 40, width: 40, borderRadius: 3, marginTop: 1 }}
68
+ source={{
69
+ uri: avatarProps?.currentMessage?.user?.avatar,
70
+ }}
71
+ />
72
+ )}
73
+ </Avatar>
74
+ );
75
+ }
76
+
77
+ renderPaymentBubble(isRight: boolean) {
78
+ const { currentMessage } = this.props;
79
+ // Extract payment details
80
+ const paymentDetails = currentMessage?.propsConfiguration?.payment || {};
81
+ const amount = paymentDetails.amount || currentMessage?.amount;
82
+ const currency = paymentDetails.currency || currentMessage?.currency || '₹';
83
+ const status = paymentDetails.status || currentMessage?.status || 'Sent Securely';
84
+ const paymentNote = paymentDetails.note || currentMessage?.text;
85
+ const time = currentMessage?.createdAt
86
+ ? new Date(currentMessage.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
87
+ : '';
88
+
89
+ return (
90
+ <TouchableOpacity
91
+ activeOpacity={0.85}
92
+ style={[
93
+ styles.bubble,
94
+ isRight ? styles.bubbleRight : styles.bubbleLeft,
95
+ { alignSelf: isRight ? 'flex-end' : 'flex-start' },
96
+ ]}
97
+ >
98
+ <Text style={styles.amount}>
99
+ {currency}
100
+ {amount}
101
+ </Text>
102
+ <View style={styles.secureRow}>
103
+ <MaterialCommunityIcons
104
+ name="check-circle-outline"
105
+ size={20}
106
+ color="#4caf50"
107
+ style={{ marginRight: 4 }}
108
+ />
109
+ <Text style={styles.secureText}>{status}</Text>
110
+ <MaterialCommunityIcons name="chevron-right" size={22} color="#7c3aed" style={{ marginLeft: 4 }} />
111
+ </View>
112
+ {paymentNote && paymentNote !== status && <Text style={styles.note}>{paymentNote}</Text>}
113
+ <Text style={[styles.time, { textAlign: isRight ? 'right' : 'left' }]}>{time}</Text>
114
+ </TouchableOpacity>
115
+ );
116
+ }
117
+
118
+ render() {
119
+ const { currentMessage, user } = this.props;
120
+ const isRight = currentMessage?.user?._id === user?._id;
121
+ const marginBottom = isSameUser(currentMessage, this.props.nextMessage) ? 2 : 10;
122
+ return (
123
+ <View>
124
+ {this.renderDay()}
125
+ <View
126
+ style={[styles.container, { marginBottom }, isRight ? styles.containerRight : styles.containerLeft]}
127
+ >
128
+ {isRight ? null : this.renderAvatar(false)}
129
+ {this.renderPaymentBubble(isRight)}
130
+ {isRight ? this.renderAvatar(true) : null}
131
+ </View>
132
+ </View>
133
+ );
134
+ }
135
+ }
136
+
137
+ const styles = StyleSheet.create({
138
+ container: {
139
+ flexDirection: 'row',
140
+ alignItems: 'flex-end',
141
+ width: '100%',
142
+ },
143
+ containerLeft: {
144
+ justifyContent: 'flex-start',
145
+ },
146
+ containerRight: {
147
+ flexDirection: 'row-reverse',
148
+ justifyContent: 'flex-end',
149
+ },
150
+ bubble: {
151
+ backgroundColor: '#f3e8ff',
152
+ borderRadius: 16,
153
+ paddingVertical: 16,
154
+ paddingHorizontal: 20,
155
+ minWidth: 160,
156
+ maxWidth: 320,
157
+ shadowColor: '#000',
158
+ shadowOffset: { width: 0, height: 1 },
159
+ shadowOpacity: 0.08,
160
+ shadowRadius: 4,
161
+ elevation: 2,
162
+ marginBottom: 2,
163
+ },
164
+ bubbleLeft: {
165
+ marginLeft: 0,
166
+ marginRight: 32,
167
+ },
168
+ bubbleRight: {
169
+ marginRight: 0,
170
+ marginLeft: 32,
171
+ },
172
+ amount: {
173
+ fontSize: 26,
174
+ fontWeight: 'bold',
175
+ color: '#18181b',
176
+ marginBottom: 8,
177
+ },
178
+ secureRow: {
179
+ flexDirection: 'row',
180
+ alignItems: 'center',
181
+ marginBottom: 4,
182
+ },
183
+ secureText: {
184
+ fontSize: 16,
185
+ color: '#18181b',
186
+ fontWeight: '500',
187
+ },
188
+ note: {
189
+ fontSize: 14,
190
+ color: '#6b7280',
191
+ marginTop: 2,
192
+ marginBottom: 2,
193
+ },
194
+ time: {
195
+ fontSize: 12,
196
+ color: '#a1a1aa',
197
+ marginTop: 6,
198
+ },
199
+ });
@@ -56,45 +56,255 @@ export default class Bubble extends React.Component<any> {
56
56
  }
57
57
 
58
58
  renderMessageImage() {
59
- if (this.props.currentMessage.image) {
60
- const { containerStyle, wrapperStyle, ...messageImageProps } = this.props;
59
+ const { currentMessage, containerStyle, wrapperStyle, ...messageImageProps } = this.props;
60
+
61
+ // Check if we have multiple images
62
+ if (currentMessage.images && Array.isArray(currentMessage.images) && currentMessage.images.length > 0) {
63
+ // Filter out any invalid or empty image URLs first
64
+ const validImages = currentMessage.images.filter((url) => url && typeof url === 'string');
65
+
66
+ // If no valid images after filtering, don't render anything
67
+ if (validImages.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ // Render multiple images in a scrollable horizontal row
72
+ return (
73
+ <View style={{ marginVertical: 0 }}>
74
+ <View
75
+ style={{
76
+ flexDirection: 'row',
77
+ flexWrap: 'wrap',
78
+ gap: 8,
79
+ }}
80
+ >
81
+ {validImages.map((imageUrl, index) => {
82
+ // Check if this is a local image
83
+ const isLocalImage =
84
+ imageUrl.startsWith('file:') ||
85
+ imageUrl.startsWith('data:') ||
86
+ imageUrl.startsWith('content:') ||
87
+ (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://'));
88
+
89
+ const imageSize =
90
+ validImages.length === 1
91
+ ? { width: windowWidth * 0.6, height: windowWidth * 0.4 }
92
+ : { width: windowWidth * 0.3, height: windowWidth * 0.3 };
93
+
94
+ // Show upload indicator if this message is uploading
95
+ const isUploading = currentMessage.isUploading;
96
+
97
+ return (
98
+ <TouchableHighlight
99
+ key={`image-${index}-${currentMessage._id}`}
100
+ underlayColor={'transparent'}
101
+ onPress={() => {
102
+ // Don't allow viewing while uploading
103
+ if (isUploading) return;
104
+
105
+ // Create a message-like object with this image for the viewer
106
+ const imageObject = {
107
+ _id: `${currentMessage._id}-img-${index}`,
108
+ image: imageUrl,
109
+ };
110
+ this.props.setImageViewer(imageObject, true);
111
+ }}
112
+ >
113
+ <View
114
+ style={{
115
+ ...imageSize,
116
+ maxHeight: 200,
117
+ borderRadius: 8,
118
+ overflow: 'hidden',
119
+ borderWidth: 1,
120
+ borderColor: '#e0e0e0',
121
+ backgroundColor: '#f7f7f7',
122
+ }}
123
+ >
124
+ {isLocalImage ? (
125
+ // For local images, use direct Image component with no loading state
126
+ <View style={{ width: '100%', height: '100%' }}>
127
+ <MessageImage
128
+ {...messageImageProps}
129
+ currentMessage={{ ...currentMessage, image: imageUrl }}
130
+ imageStyle={{
131
+ width: '100%',
132
+ height: '100%',
133
+ borderRadius: 8,
134
+ }}
135
+ />
136
+
137
+ {/* Show upload indicator if needed */}
138
+ {isUploading && (
139
+ <View
140
+ style={{
141
+ position: 'absolute',
142
+ top: 0,
143
+ left: 0,
144
+ right: 0,
145
+ bottom: 0,
146
+ backgroundColor: 'rgba(0,0,0,0.2)',
147
+ justifyContent: 'center',
148
+ alignItems: 'center',
149
+ }}
150
+ >
151
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>
152
+ Uploading...
153
+ </Text>
154
+ </View>
155
+ )}
156
+ </View>
157
+ ) : (
158
+ // For remote images, use CachedImage with loading placeholder
159
+ <CachedImage
160
+ style={[styles.slackImage, { width: '100%', height: '100%' }]}
161
+ cacheKey={`${currentMessage._id}-img-${index}-slack-bubble-imageKey`}
162
+ source={{
163
+ uri: imageUrl,
164
+ expiresIn: 86400,
165
+ }}
166
+ resizeMode={'cover'}
167
+ alt={'image'}
168
+ placeholderContent={
169
+ <View
170
+ style={[
171
+ styles.slackImage,
172
+ {
173
+ width: '100%',
174
+ height: '100%',
175
+ backgroundColor: '#e1e1e1',
176
+ justifyContent: 'center',
177
+ alignItems: 'center',
178
+ borderRadius: 8,
179
+ },
180
+ ]}
181
+ >
182
+ <Text>{isUploading ? 'Uploading...' : 'Loading...'}</Text>
183
+ </View>
184
+ }
185
+ />
186
+ )}
187
+ </View>
188
+ </TouchableHighlight>
189
+ );
190
+ })}
191
+ </View>
192
+ </View>
193
+ );
194
+ }
195
+
196
+ // Fall back to single image rendering if no images array
197
+ if (currentMessage.image) {
61
198
  if (this.props.renderMessageImage) {
62
199
  return this.props.renderMessageImage(messageImageProps);
63
200
  }
64
- const { image, _id } = messageImageProps?.currentMessage;
201
+ const { image, _id } = currentMessage;
202
+
203
+ // Add validation for image URL
204
+ if (!image || typeof image !== 'string') {
205
+ return null;
206
+ }
65
207
 
208
+ // Check if this is a local image (from device camera/library)
209
+ const isLocalImage =
210
+ image.startsWith('file:') ||
211
+ image.startsWith('data:') ||
212
+ image.startsWith('content:') ||
213
+ // Check if the image is a pendingUpload by checking if it doesn't start with http/https
214
+ (!image.startsWith('http://') && !image.startsWith('https://'));
215
+
216
+ // Check uploading state
217
+ const isUploading = currentMessage.isUploading;
218
+
219
+ // Class components can't use hooks like useMemo
220
+ // Directly render the CachedImage instead
66
221
  return (
67
222
  <TouchableHighlight
68
- // underlayColor={'#c0c0c0'}
69
223
  underlayColor={'transparent'}
70
- style={{ width: '100%' }}
71
- onPress={() => this.props.setImageViewer(messageImageProps?.currentMessage, true)}
224
+ style={{ width: '100%', marginVertical: 0 }}
225
+ onPress={() => {
226
+ // Don't allow viewing while uploading
227
+ if (isUploading) return;
228
+ this.props.setImageViewer(currentMessage, true);
229
+ }}
72
230
  >
73
231
  <View
74
232
  style={{
75
- width: windowWidth - (windowWidth - 150),
76
- height: windowHeight - (windowHeight - 100),
233
+ width: windowWidth * 0.6, // 60% of screen width
234
+ height: windowWidth * 0.4, // Maintain aspect ratio
235
+ maxHeight: 200,
236
+ borderRadius: 8,
237
+ overflow: 'hidden',
238
+ borderWidth: 1,
239
+ borderColor: '#e0e0e0',
240
+ backgroundColor: '#f7f7f7',
77
241
  }}
78
242
  >
79
- {/* <MessageImage
80
- {...messageImageProps}
81
- imageStyle={[styles.slackImage, messageImageProps.imageStyle]}
82
- /> */}
83
- <CachedImage
84
- style={[styles.slackImage, { width: '100%', height: '100%' }]}
85
- cacheKey={`${_id}-slack-bubble-imageKey`}
86
- source={{
87
- uri: image,
88
- //headers: `Authorization: Bearer ${token}`,
89
- expiresIn: 86400,
90
- }}
91
- resizeMode={'cover'}
92
- alt={'image'}
93
- />
243
+ {isLocalImage ? (
244
+ // For local images, use direct Image component with no loading state
245
+ <View style={{ width: '100%', height: '100%', position: 'relative' }}>
246
+ <MessageImage
247
+ {...messageImageProps}
248
+ imageStyle={{
249
+ width: '100%',
250
+ height: '100%',
251
+ borderRadius: 8,
252
+ }}
253
+ />
254
+
255
+ {/* Show upload indicator if needed */}
256
+ {isUploading && (
257
+ <View
258
+ style={{
259
+ position: 'absolute',
260
+ top: 0,
261
+ left: 0,
262
+ right: 0,
263
+ bottom: 0,
264
+ backgroundColor: 'rgba(0,0,0,0.2)',
265
+ justifyContent: 'center',
266
+ alignItems: 'center',
267
+ }}
268
+ >
269
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>Uploading...</Text>
270
+ </View>
271
+ )}
272
+ </View>
273
+ ) : (
274
+ // For remote images, use CachedImage with loading placeholder
275
+ <CachedImage
276
+ style={[styles.slackImage, { width: '100%', height: '100%' }]}
277
+ cacheKey={`${_id}-slack-bubble-imageKey`}
278
+ source={{
279
+ uri: image,
280
+ expiresIn: 86400,
281
+ }}
282
+ resizeMode={'cover'}
283
+ alt={'image'}
284
+ placeholderContent={
285
+ <View
286
+ style={[
287
+ styles.slackImage,
288
+ {
289
+ width: '100%',
290
+ height: '100%',
291
+ backgroundColor: '#e1e1e1',
292
+ justifyContent: 'center',
293
+ alignItems: 'center',
294
+ borderRadius: 8,
295
+ },
296
+ ]}
297
+ >
298
+ <Text>{isUploading ? 'Uploading...' : 'Loading...'}</Text>
299
+ </View>
300
+ }
301
+ />
302
+ )}
94
303
  </View>
95
304
  </TouchableHighlight>
96
305
  );
97
306
  }
307
+
98
308
  return null;
99
309
  }
100
310
 
@@ -0,0 +1,61 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Spinner, Text, VStack, Button, ButtonText } from '@admin-layout/gluestack-ui-mobile';
3
+ import colors from 'tailwindcss/colors';
4
+
5
+ interface SmartLoaderProps {
6
+ isLoading: boolean;
7
+ timeoutMs?: number;
8
+ message?: string;
9
+ onRetry?: () => void;
10
+ }
11
+
12
+ /**
13
+ * SmartLoader component that automatically shows a timeout message
14
+ * if loading takes too long, preventing infinite loading states
15
+ */
16
+ export const SmartLoader: React.FC<SmartLoaderProps> = ({
17
+ isLoading,
18
+ timeoutMs = 20000, // Default 20 seconds
19
+ message = 'Taking longer than expected...',
20
+ onRetry,
21
+ }) => {
22
+ const [showTimeout, setShowTimeout] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (!isLoading) {
26
+ setShowTimeout(false);
27
+ return;
28
+ }
29
+
30
+ const timeoutId = setTimeout(() => {
31
+ if (isLoading) {
32
+ setShowTimeout(true);
33
+ }
34
+ }, timeoutMs);
35
+
36
+ return () => clearTimeout(timeoutId);
37
+ }, [isLoading, timeoutMs]);
38
+
39
+ if (!isLoading) return null;
40
+
41
+ return (
42
+ <Box className="flex-1 bg-gray-100 dark:bg-gray-800 items-center justify-center">
43
+ <VStack space="md" className="items-center">
44
+ <Spinner color={colors.blue[500]} size="large" />
45
+
46
+ {showTimeout && (
47
+ <VStack space="sm" className="p-4 items-center">
48
+ <Text className="text-gray-700 dark:text-gray-300 text-center">{message}</Text>
49
+ {onRetry && (
50
+ <Button onPress={onRetry} className="bg-blue-500 mt-3 rounded-full">
51
+ <ButtonText>Try Again</ButtonText>
52
+ </Button>
53
+ )}
54
+ </VStack>
55
+ )}
56
+ </VStack>
57
+ </Box>
58
+ );
59
+ };
60
+
61
+ export default SmartLoader;
@@ -0,0 +1,41 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * Shared SubscriptionHandler for Apollo subscribeToMore
5
+ *
6
+ * @param subscribeToMore - Apollo subscribeToMore function
7
+ * @param document - GraphQL subscription document
8
+ * @param variables - Variables for the subscription
9
+ * @param updateQuery - Apollo updateQuery function
10
+ * @param onError - Optional error handler
11
+ * @param enabled - If false, disables the subscription
12
+ */
13
+ export function SubscriptionHandler({
14
+ subscribeToMore,
15
+ document,
16
+ variables,
17
+ updateQuery,
18
+ onError,
19
+ enabled = true,
20
+ }: {
21
+ subscribeToMore: Function;
22
+ document: any;
23
+ variables: Record<string, any>;
24
+ updateQuery: (prev: any, { subscriptionData }: any) => any;
25
+ onError?: (error: any) => void;
26
+ enabled?: boolean;
27
+ }) {
28
+ useEffect(() => {
29
+ if (!enabled) return;
30
+ const unsubscribe = subscribeToMore({
31
+ document,
32
+ variables,
33
+ updateQuery,
34
+ onError,
35
+ });
36
+ return () => {
37
+ if (unsubscribe && typeof unsubscribe === 'function') unsubscribe();
38
+ };
39
+ }, [subscribeToMore, document, JSON.stringify(variables), updateQuery, onError, enabled]);
40
+ return null;
41
+ }