@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.
- package/CHANGELOG.md +248 -0
- package/lib/compute.js +2 -3
- package/lib/compute.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/module.js.map +1 -1
- package/lib/queries/inboxQueries.js +65 -0
- package/lib/queries/inboxQueries.js.map +1 -0
- package/lib/routes.json +13 -1
- package/lib/screens/inbox/DialogMessages.js +5 -3
- package/lib/screens/inbox/DialogMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreadMessages.js +3 -7
- package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreads.js +57 -18
- package/lib/screens/inbox/DialogThreads.js.map +1 -1
- package/lib/screens/inbox/Inbox.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/index.js +168 -46
- package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
- package/lib/screens/inbox/components/DialogItem.js +169 -0
- package/lib/screens/inbox/components/DialogItem.js.map +1 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
- package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js.map +1 -1
- package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +197 -0
- package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +141 -31
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
- package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
- package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
- package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
- package/lib/screens/inbox/components/ThreadsViewItem.js +66 -44
- package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
- package/lib/screens/inbox/config/config.js +2 -2
- package/lib/screens/inbox/config/config.js.map +1 -1
- package/lib/screens/inbox/containers/ConversationView.js +1205 -432
- package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/Dialogs.js +193 -80
- package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadConversationView.js +724 -207
- package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadsView.js +83 -50
- package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
- package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
- package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +2 -0
- package/src/queries/inboxQueries.ts +299 -0
- package/src/queries/index.d.ts +2 -0
- package/src/queries/index.ts +1 -0
- package/src/screens/inbox/DialogMessages.tsx +2 -1
- package/src/screens/inbox/DialogThreadMessages.tsx +3 -11
- package/src/screens/inbox/DialogThreads.tsx +53 -60
- package/src/screens/inbox/components/Actionsheet.tsx +30 -0
- package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
- package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
- package/src/screens/inbox/components/DialogItem.tsx +306 -0
- package/src/screens/inbox/components/DialogsListItem.tsx +230 -170
- package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
- package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
- package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
- package/src/screens/inbox/components/ServiceDialogsListItem.tsx +330 -171
- package/src/screens/inbox/components/SlackInput.tsx +23 -0
- package/src/screens/inbox/components/SlackMessageContainer/PaymentMessage.tsx +199 -0
- package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
- package/src/screens/inbox/components/SmartLoader.tsx +61 -0
- package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
- package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +41 -24
- package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
- package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
- package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
- package/src/screens/inbox/config/config.ts +2 -2
- package/src/screens/inbox/containers/ConversationView.tsx +2004 -698
- package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
- package/src/screens/inbox/containers/Dialogs.tsx +402 -196
- package/src/screens/inbox/containers/SupportServiceDialogs.tsx +2 -2
- package/src/screens/inbox/containers/ThreadConversationView.tsx +1337 -301
- package/src/screens/inbox/containers/ThreadsView.tsx +105 -186
- package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
- package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
- package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
- package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
- package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
- package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
- package/src/screens/inbox/index.ts +37 -0
- package/src/screens/inbox/machines/threadsMachine.ts +147 -0
- package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
- package/lib/screens/inbox/components/DialogsListItem.js +0 -175
- package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -165
- 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
|
-
|
|
60
|
-
|
|
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 } =
|
|
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={() =>
|
|
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
|
|
76
|
-
height:
|
|
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
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|