@planningcenter/chat-react-native 3.18.0-rc.1 → 3.18.0-rc.11
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/build/components/conversation/message.d.ts +2 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +53 -26
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +7 -6
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/messages_disabled_banners.d.ts +3 -0
- package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -0
- package/build/components/conversation/messages_disabled_banners.js +53 -0
- package/build/components/conversation/messages_disabled_banners.js.map +1 -0
- package/build/components/conversation/reply_connectors.d.ts.map +1 -1
- package/build/components/conversation/reply_connectors.js +0 -5
- package/build/components/conversation/reply_connectors.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js +8 -3
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/components/conversation/typing_indicator.d.ts +1 -5
- package/build/components/conversation/typing_indicator.d.ts.map +1 -1
- package/build/components/conversation/typing_indicator.js +2 -2
- package/build/components/conversation/typing_indicator.js.map +1 -1
- package/build/components/conversations/conversation_actions.d.ts.map +1 -1
- package/build/components/conversations/conversation_actions.js +1 -2
- package/build/components/conversations/conversation_actions.js.map +1 -1
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +2 -3
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/contexts/conversation_context.d.ts +13 -0
- package/build/contexts/conversation_context.d.ts.map +1 -0
- package/build/contexts/conversation_context.js +14 -0
- package/build/contexts/conversation_context.js.map +1 -0
- package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
- package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
- package/build/hooks/use_broadcast_typing_status.js +7 -3
- package/build/hooks/use_broadcast_typing_status.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +2 -1
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_features.d.ts +9 -0
- package/build/hooks/use_features.d.ts.map +1 -0
- package/build/hooks/use_features.js +35 -0
- package/build/hooks/use_features.js.map +1 -0
- package/build/hooks/use_message_create_or_update.d.ts +0 -2
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +10 -8
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/hooks/use_typing_indicators.d.ts +1 -1
- package/build/hooks/use_typing_indicators.d.ts.map +1 -1
- package/build/hooks/use_typing_indicators.js +16 -3
- package/build/hooks/use_typing_indicators.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +9 -6
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_new/components/form_list.d.ts +2 -2
- package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
- package/build/screens/conversation_new/components/form_list.js +2 -3
- package/build/screens/conversation_new/components/form_list.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +2 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +41 -18
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +2 -3
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +2 -3
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
- package/build/screens/message_actions_screen.js +4 -2
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/types/jolt_events/reaction_events.d.ts +1 -0
- package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
- package/build/types/jolt_events/reaction_events.js.map +1 -1
- package/build/types/jolt_events/typing_events.d.ts +1 -0
- package/build/types/jolt_events/typing_events.d.ts.map +1 -1
- package/build/types/jolt_events/typing_events.js.map +1 -1
- package/build/types/resources/feature_resource.d.ts +7 -0
- package/build/types/resources/feature_resource.d.ts.map +1 -0
- package/build/types/resources/feature_resource.js +2 -0
- package/build/types/resources/feature_resource.js.map +1 -0
- package/build/utils/cache/messages_cache.d.ts +9 -0
- package/build/utils/cache/messages_cache.d.ts.map +1 -0
- package/build/utils/cache/messages_cache.js +89 -0
- package/build/utils/cache/messages_cache.js.map +1 -0
- package/build/utils/cache/optimistically_create_message.d.ts +2 -1
- package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
- package/build/utils/cache/optimistically_create_message.js +6 -3
- package/build/utils/cache/optimistically_create_message.js.map +1 -1
- package/build/utils/index.d.ts +0 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +0 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/request/get_features.d.ts +11 -0
- package/build/utils/request/get_features.d.ts.map +1 -0
- package/build/utils/request/get_features.js +18 -0
- package/build/utils/request/get_features.js.map +1 -0
- package/package.json +2 -3
- package/src/components/conversation/message.tsx +80 -29
- package/src/components/conversation/message_form.tsx +6 -11
- package/src/components/conversation/messages_disabled_banners.tsx +69 -0
- package/src/components/conversation/reply_connectors.tsx +0 -3
- package/src/components/conversation/reply_shadow_message.tsx +9 -2
- package/src/components/conversation/typing_indicator.tsx +2 -6
- package/src/components/conversations/conversation_actions.tsx +1 -1
- package/src/components/conversations/conversations.tsx +7 -9
- package/src/contexts/conversation_context.tsx +34 -0
- package/src/hooks/use_broadcast_typing_status.ts +7 -3
- package/src/hooks/use_conversation_messages.ts +3 -1
- package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
- package/src/hooks/use_features.ts +47 -0
- package/src/hooks/use_message_create_or_update.ts +10 -9
- package/src/hooks/use_typing_indicators.ts +15 -3
- package/src/screens/conversation_details_screen.tsx +9 -6
- package/src/screens/conversation_new/components/form_list.tsx +3 -5
- package/src/screens/conversation_screen.tsx +58 -20
- package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +2 -4
- package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +2 -4
- package/src/screens/message_actions_screen.tsx +4 -2
- package/src/types/jolt_events/reaction_events.ts +1 -0
- package/src/types/jolt_events/typing_events.ts +1 -0
- package/src/types/resources/feature_resource.ts +6 -0
- package/src/utils/cache/messages_cache.ts +113 -0
- package/src/utils/cache/optimistically_create_message.ts +7 -2
- package/src/utils/index.ts +0 -1
- package/src/utils/request/get_features.ts +20 -0
- package/build/components/conversation/disabled_replies_banners.d.ts +0 -3
- package/build/components/conversation/disabled_replies_banners.d.ts.map +0 -1
- package/build/components/conversation/disabled_replies_banners.js +0 -41
- package/build/components/conversation/disabled_replies_banners.js.map +0 -1
- package/build/utils/replies_local_feature_flag.d.ts +0 -2
- package/build/utils/replies_local_feature_flag.d.ts.map +0 -1
- package/build/utils/replies_local_feature_flag.js +0 -3
- package/build/utils/replies_local_feature_flag.js.map +0 -1
- package/src/components/conversation/disabled_replies_banners.tsx +0 -58
- package/src/utils/replies_local_feature_flag.ts +0 -2
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
2
|
import React, { useEffect } from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AccessibilityActionEvent,
|
|
5
|
+
Pressable,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native'
|
|
4
10
|
import { MessageReaction } from '../../components/conversation/message_reaction'
|
|
5
11
|
import { Avatar, Icon, Spinner, Text, TextButton, TextInlineButton } from '../../components/display'
|
|
6
12
|
import {
|
|
@@ -22,11 +28,13 @@ import {
|
|
|
22
28
|
import Animated from 'react-native-reanimated'
|
|
23
29
|
import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
|
|
24
30
|
import { MessageReadReceipts } from './message_read_receipts'
|
|
25
|
-
import {
|
|
31
|
+
import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
|
|
26
32
|
import { Haptic } from '../../utils/native_adapters'
|
|
27
33
|
import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from './reply_connectors'
|
|
28
|
-
import { pluralize
|
|
34
|
+
import { pluralize } from '../../utils'
|
|
29
35
|
import { useConversationMessage } from '../../hooks/use_conversation_message'
|
|
36
|
+
import { some } from 'lodash'
|
|
37
|
+
import { isNewMessage } from '../../utils/cache/messages_cache'
|
|
30
38
|
|
|
31
39
|
/** Message
|
|
32
40
|
* Component for display of a message within a conversation list
|
|
@@ -36,6 +44,7 @@ interface MessageProps extends MessageResource {
|
|
|
36
44
|
conversation_id: number
|
|
37
45
|
latestReadMessageSortKey?: string
|
|
38
46
|
inReplyScreen?: boolean
|
|
47
|
+
repliesEnabled?: boolean
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export function Message({
|
|
@@ -43,9 +52,10 @@ export function Message({
|
|
|
43
52
|
conversation_id,
|
|
44
53
|
latestReadMessageSortKey,
|
|
45
54
|
inReplyScreen,
|
|
55
|
+
repliesEnabled,
|
|
46
56
|
...message
|
|
47
57
|
}: MessageProps) {
|
|
48
|
-
const { text, reactionCounts, pending, error } = message
|
|
58
|
+
const { text, reactionCounts, pending, error, attachments, author } = message
|
|
49
59
|
const styles = useMessageStyles(message)
|
|
50
60
|
const navigation = useNavigation()
|
|
51
61
|
const { colors } = useTheme()
|
|
@@ -67,22 +77,21 @@ export function Message({
|
|
|
67
77
|
})
|
|
68
78
|
|
|
69
79
|
const metaProps = {
|
|
70
|
-
authorName:
|
|
80
|
+
authorName: author.name,
|
|
71
81
|
createdAt: message.createdAt,
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
const renderAuthor = (!message.mine && message.renderAuthor) || false
|
|
75
85
|
const showReplyCountButton =
|
|
76
|
-
!inReplyScreen && message.replyRootId === message.id &&
|
|
86
|
+
!inReplyScreen && message.replyRootId === message.id && repliesEnabled
|
|
77
87
|
const isReplyRootMessage = message.replyRootId === message.id
|
|
78
88
|
const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
|
|
89
|
+
const replyToReplyRootMessage = repliesEnabled && !isReplyRootMessage && !!message.replyRootId
|
|
79
90
|
|
|
80
|
-
const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
|
|
81
91
|
const replyCountText = pluralize(message.replyCount, 'reply')
|
|
82
92
|
const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
|
|
83
|
-
const replyRootAuthorName = message.replyRootId
|
|
84
|
-
|
|
85
|
-
: message.author.name
|
|
93
|
+
const replyRootAuthorName = message.replyRootId ? replyRootMessage?.author.name : author.name
|
|
94
|
+
const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
|
|
86
95
|
|
|
87
96
|
const messageBottomMargin =
|
|
88
97
|
message.lastInGroup || message.nextIsReplyShadowMessage
|
|
@@ -91,6 +100,11 @@ export function Message({
|
|
|
91
100
|
? 8
|
|
92
101
|
: 4
|
|
93
102
|
|
|
103
|
+
const messageIsReplyLabel = replyToReplyRootMessage ? 'Reply' : ''
|
|
104
|
+
const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
|
|
105
|
+
const replyCountLabel = isReplyRootMessage ? replyCountText : ''
|
|
106
|
+
const accessibilityLabel = `${author?.name || ''} ${messageIsReplyLabel} ${attachmentLabel} ${messageText || ''} ${timestamp} ${replyCountLabel}`
|
|
107
|
+
|
|
94
108
|
useEffect(() => {
|
|
95
109
|
if (pending) {
|
|
96
110
|
const timer = setTimeout(() => {
|
|
@@ -115,6 +129,10 @@ export function Message({
|
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
const handleMessagePress = () => {
|
|
133
|
+
setShowMessageMetaToggle(!showMessageMetaToggle)
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
const handleMessageLongPress = () => {
|
|
119
137
|
if (!isPersisted) return
|
|
120
138
|
|
|
@@ -127,6 +145,7 @@ export function Message({
|
|
|
127
145
|
reply_root_author_name: replyRootAuthorName,
|
|
128
146
|
})
|
|
129
147
|
}
|
|
148
|
+
|
|
130
149
|
const handleReactionLongPress = (reaction: ReactionCountResource) => {
|
|
131
150
|
Haptic.impactLight()
|
|
132
151
|
navigation.navigate('Reactions', {
|
|
@@ -152,18 +171,36 @@ export function Message({
|
|
|
152
171
|
navigation.navigate('ConversationReply', {
|
|
153
172
|
conversation_id,
|
|
154
173
|
reply_root_id: message.id,
|
|
155
|
-
reply_root_author_name:
|
|
174
|
+
reply_root_author_name: author.name,
|
|
156
175
|
})
|
|
157
176
|
}
|
|
158
177
|
|
|
178
|
+
const accessibilityActions = [
|
|
179
|
+
{
|
|
180
|
+
name: 'navigateToReplies',
|
|
181
|
+
label: 'Navigate to replies',
|
|
182
|
+
},
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const handleAccessibilityAction = (event: AccessibilityActionEvent) => {
|
|
186
|
+
if (event.nativeEvent.actionName === 'navigateToReplies') {
|
|
187
|
+
handleNavigateToReplyPress()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
return (
|
|
160
192
|
<Pressable
|
|
161
193
|
onLongPress={handleMessageLongPress}
|
|
162
|
-
onPress={
|
|
194
|
+
onPress={handleMessagePress}
|
|
163
195
|
onPressIn={handleMessagePressIn}
|
|
164
196
|
onPressOut={handleMessagePressOut}
|
|
165
197
|
android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
|
|
198
|
+
accessibilityRole="button"
|
|
199
|
+
accessibilityLabel={accessibilityLabel}
|
|
166
200
|
accessibilityHint="Long press to view message actions like reacting and copying."
|
|
201
|
+
accessibilityActions={isReplyRootMessage ? accessibilityActions : undefined}
|
|
202
|
+
onAccessibilityAction={isReplyRootMessage ? handleAccessibilityAction : undefined}
|
|
203
|
+
disabled={isDeletedReplyRootMessage}
|
|
167
204
|
>
|
|
168
205
|
<Animated.View style={[styles.message, animatedBackgroundColor]}>
|
|
169
206
|
{!message.mine && (
|
|
@@ -171,7 +208,7 @@ export function Message({
|
|
|
171
208
|
{renderAuthor ? (
|
|
172
209
|
<Avatar
|
|
173
210
|
size={'md'}
|
|
174
|
-
sourceUri={
|
|
211
|
+
sourceUri={author.avatar}
|
|
175
212
|
style={styles.avatar}
|
|
176
213
|
maxFontSizeMultiplier={1}
|
|
177
214
|
minFontSizeMultiplier={1}
|
|
@@ -181,31 +218,41 @@ export function Message({
|
|
|
181
218
|
) : (
|
|
182
219
|
<View style={styles.avatarPlaceholder} />
|
|
183
220
|
)}
|
|
184
|
-
|
|
221
|
+
{repliesEnabled && (
|
|
222
|
+
<TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
|
|
223
|
+
)}
|
|
185
224
|
</View>
|
|
186
225
|
)}
|
|
187
226
|
<View style={[styles.messageContent, { marginBottom: messageBottomMargin }]}>
|
|
188
|
-
{renderAuthor && (
|
|
227
|
+
{renderAuthor && !isDeletedReplyRootMessage && (
|
|
189
228
|
<Text variant="footnote" style={styles.authorName}>
|
|
190
|
-
{
|
|
229
|
+
{author.name}
|
|
191
230
|
</Text>
|
|
192
231
|
)}
|
|
193
232
|
<View
|
|
194
233
|
style={styles.messageBubble}
|
|
195
234
|
onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
|
|
196
235
|
>
|
|
197
|
-
|
|
198
|
-
<MessageAttachments
|
|
199
|
-
attachments={message.attachments}
|
|
200
|
-
metaProps={metaProps}
|
|
201
|
-
onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
|
|
202
|
-
onMessageLongPress={handleMessageLongPress}
|
|
203
|
-
/>
|
|
204
|
-
</ErrorBoundary>
|
|
205
|
-
{messageText && (
|
|
236
|
+
{isDeletedReplyRootMessage ? (
|
|
206
237
|
<View style={styles.messageText}>
|
|
207
|
-
<
|
|
238
|
+
<Text style={styles.replyRootDeletedText}>{messageText}</Text>
|
|
208
239
|
</View>
|
|
240
|
+
) : (
|
|
241
|
+
<>
|
|
242
|
+
<ErrorBoundary>
|
|
243
|
+
<MessageAttachments
|
|
244
|
+
attachments={attachments}
|
|
245
|
+
metaProps={metaProps}
|
|
246
|
+
onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
|
|
247
|
+
onMessageLongPress={handleMessageLongPress}
|
|
248
|
+
/>
|
|
249
|
+
</ErrorBoundary>
|
|
250
|
+
{text && (
|
|
251
|
+
<View style={styles.messageText}>
|
|
252
|
+
<MessageMarkdown text={text} />
|
|
253
|
+
</View>
|
|
254
|
+
)}
|
|
255
|
+
</>
|
|
209
256
|
)}
|
|
210
257
|
</View>
|
|
211
258
|
{showReplyCountButton && (
|
|
@@ -219,7 +266,7 @@ export function Message({
|
|
|
219
266
|
{replyCountText}
|
|
220
267
|
</TextButton>
|
|
221
268
|
)}
|
|
222
|
-
{hasReactions && (
|
|
269
|
+
{hasReactions && !isDeletedReplyRootMessage && (
|
|
223
270
|
<View style={styles.messageReactions}>
|
|
224
271
|
{reactionCounts.map(reaction => (
|
|
225
272
|
<MessageReaction
|
|
@@ -232,7 +279,7 @@ export function Message({
|
|
|
232
279
|
))}
|
|
233
280
|
</View>
|
|
234
281
|
)}
|
|
235
|
-
{showMessageMeta && (
|
|
282
|
+
{showMessageMeta && !isDeletedReplyRootMessage && (
|
|
236
283
|
<View style={styles.messageMeta}>
|
|
237
284
|
{message.mine && !pending && !error && (
|
|
238
285
|
<MessageReadReceipts
|
|
@@ -282,7 +329,7 @@ export function Message({
|
|
|
282
329
|
</View>
|
|
283
330
|
)}
|
|
284
331
|
</View>
|
|
285
|
-
{message.mine && (
|
|
332
|
+
{repliesEnabled && message.mine && (
|
|
286
333
|
<MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
|
|
287
334
|
)}
|
|
288
335
|
</Animated.View>
|
|
@@ -360,5 +407,9 @@ const useMessageStyles = ({ mine }: MessageResource) => {
|
|
|
360
407
|
errorText: {
|
|
361
408
|
color: colors.statusErrorText,
|
|
362
409
|
},
|
|
410
|
+
replyRootDeletedText: {
|
|
411
|
+
color: colors.textColorDefaultSecondary,
|
|
412
|
+
fontStyle: 'italic',
|
|
413
|
+
},
|
|
363
414
|
})
|
|
364
415
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RouteProp,
|
|
3
|
-
useNavigation,
|
|
4
|
-
useTheme as useNavigationTheme,
|
|
5
|
-
useRoute,
|
|
6
|
-
} from '@react-navigation/native'
|
|
1
|
+
import { useNavigation, useTheme as useNavigationTheme, useRoute } from '@react-navigation/native'
|
|
7
2
|
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
|
8
3
|
import {
|
|
9
4
|
Platform,
|
|
@@ -30,10 +25,10 @@ import { ChatContext } from '../../contexts/chat_context'
|
|
|
30
25
|
import { Haptic, ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
|
|
31
26
|
import {
|
|
32
27
|
MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
|
|
33
|
-
REPLIES_FEATURE_ENABLED,
|
|
34
28
|
platformFontWeightMedium,
|
|
35
29
|
platformPressedOpacityStyle,
|
|
36
30
|
} from '../../utils'
|
|
31
|
+
import { availableFeatures, useFeatures } from '../../hooks/use_features'
|
|
37
32
|
import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
|
|
38
33
|
import { useMessageDraft } from '../../hooks/use_message_draft'
|
|
39
34
|
import {
|
|
@@ -319,9 +314,7 @@ function MessageFormInput() {
|
|
|
319
314
|
React.useContext(MessageFormContext)
|
|
320
315
|
const attachmentError = attachmentUploader?.errorMessage
|
|
321
316
|
|
|
322
|
-
const
|
|
323
|
-
const conversationId = route.params.conversation_id
|
|
324
|
-
const broadcastTypingStatus = useBroadcastTypingStatus(conversationId)
|
|
317
|
+
const broadcastTypingStatus = useBroadcastTypingStatus()
|
|
325
318
|
|
|
326
319
|
const handleTextChange = (newText: string) => {
|
|
327
320
|
setText(newText)
|
|
@@ -566,9 +559,11 @@ function EditingIndicator() {
|
|
|
566
559
|
function ReplyIndicator() {
|
|
567
560
|
const { replyRootId, replyRootAuthorFirstName } = React.useContext(MessageFormContext)
|
|
568
561
|
const navigation = useNavigation()
|
|
562
|
+
const { featureEnabled } = useFeatures()
|
|
563
|
+
const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
|
|
569
564
|
const title = replyRootAuthorFirstName ? `Reply to ${replyRootAuthorFirstName}` : 'Reply'
|
|
570
565
|
|
|
571
|
-
if (!
|
|
566
|
+
if (!repliesEnabled || !replyRootId) return null
|
|
572
567
|
|
|
573
568
|
return (
|
|
574
569
|
<FormIndicatorRow
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewStyle } from 'react-native'
|
|
2
|
+
import { useTheme } from '../../hooks'
|
|
3
|
+
import { Text } from '../display'
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
5
|
+
import { useFeatures } from '../../hooks/use_features'
|
|
6
|
+
import { availableFeatures } from '../../hooks/use_features'
|
|
7
|
+
|
|
8
|
+
export const LeaderMessagesDisabledBanner = () => {
|
|
9
|
+
const { featureEnabled } = useFeatures()
|
|
10
|
+
const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
|
|
11
|
+
|
|
12
|
+
const description = repliesEnabled
|
|
13
|
+
? 'Only leaders can send messages in this conversation.'
|
|
14
|
+
: 'Replies are frozen for everyone else.'
|
|
15
|
+
|
|
16
|
+
return <MessagesDisabledBanner description={description} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MemberMessagesDisabledBanner = () => {
|
|
20
|
+
const styles = useStyles()
|
|
21
|
+
const { featureEnabled } = useFeatures()
|
|
22
|
+
const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
|
|
23
|
+
|
|
24
|
+
const description = repliesEnabled
|
|
25
|
+
? 'Only leaders can send messages, but you can still add reactions.'
|
|
26
|
+
: 'Replies have been disabled by a leader, but you can still add reactions.'
|
|
27
|
+
|
|
28
|
+
return <MessagesDisabledBanner description={description} style={styles.memberBanner} />
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MessagesDisabledBannerProps {
|
|
32
|
+
description: string
|
|
33
|
+
style?: ViewStyle
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MessagesDisabledBanner = ({ description, style }: MessagesDisabledBannerProps) => {
|
|
37
|
+
const styles = useStyles()
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={[styles.baseBanner, style]}>
|
|
41
|
+
<Text style={styles.text} variant="tertiary">
|
|
42
|
+
{description}
|
|
43
|
+
</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const useStyles = () => {
|
|
49
|
+
const { colors } = useTheme()
|
|
50
|
+
const { bottom } = useSafeAreaInsets()
|
|
51
|
+
|
|
52
|
+
return StyleSheet.create({
|
|
53
|
+
baseBanner: {
|
|
54
|
+
paddingHorizontal: 16,
|
|
55
|
+
paddingVertical: 4,
|
|
56
|
+
backgroundColor: colors.statusNeutralComposedBackground,
|
|
57
|
+
borderTopWidth: 1,
|
|
58
|
+
borderTopColor: colors.borderColorDefaultBase,
|
|
59
|
+
},
|
|
60
|
+
text: {
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
},
|
|
63
|
+
memberBanner: {
|
|
64
|
+
paddingTop: 16,
|
|
65
|
+
paddingBottom: 16 + bottom,
|
|
66
|
+
marginBottom: -bottom,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
7
7
|
MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
|
|
8
8
|
} from '../../utils/styles'
|
|
9
|
-
import { REPLIES_FEATURE_ENABLED } from '../../utils'
|
|
10
9
|
|
|
11
10
|
const MY_REPLY_CONNECTOR_WIDTH = 38
|
|
12
11
|
const CONNECTOR_BORDER_WIDTH = 4
|
|
@@ -22,7 +21,6 @@ export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConne
|
|
|
22
21
|
const styles = useStyles()
|
|
23
22
|
const { nextRendersAuthor, threadPosition, renderAuthor, isReplyShadowMessage } = message
|
|
24
23
|
|
|
25
|
-
if (!REPLIES_FEATURE_ENABLED) return null
|
|
26
24
|
if (messageBubbleHeight === 0) return null // Prevents UI shifting
|
|
27
25
|
|
|
28
26
|
const connectorMap: Record<string, React.ReactNode | null> = {
|
|
@@ -59,7 +57,6 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
|
|
|
59
57
|
const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
|
|
60
58
|
message
|
|
61
59
|
|
|
62
|
-
if (!REPLIES_FEATURE_ENABLED) return null
|
|
63
60
|
if (messageBubbleHeight === 0) return null // Prevents UI shifting
|
|
64
61
|
|
|
65
62
|
const connectorMap: Record<string, React.ReactNode | null> = {
|
|
@@ -25,6 +25,8 @@ import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
|
|
|
25
25
|
import { assertKeysAreNumbers, pluralize } from '../../utils'
|
|
26
26
|
import { useNavigation } from '@react-navigation/native'
|
|
27
27
|
import { useConversationMessage } from '../../hooks/use_conversation_message'
|
|
28
|
+
import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
|
|
29
|
+
import { some } from 'lodash'
|
|
28
30
|
|
|
29
31
|
interface ReplyShadowMessageProps extends MessageResource {
|
|
30
32
|
messageId: string
|
|
@@ -61,10 +63,11 @@ interface ShadowMessageContentProps extends MessageResource {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) {
|
|
64
|
-
const { text, deletedAt } = message
|
|
66
|
+
const { text, deletedAt, author, attachments } = message
|
|
65
67
|
const styles = useStyles(message)
|
|
66
68
|
const { colors } = useTheme()
|
|
67
69
|
const navigation = useNavigation()
|
|
70
|
+
const timestamp = useLiveRelativeTime(message.createdAt)
|
|
68
71
|
|
|
69
72
|
const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
|
|
70
73
|
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
@@ -82,12 +85,16 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
82
85
|
})
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
|
|
89
|
+
const accessibilityLabel = `${author?.name || ''} Reply Preview ${attachmentLabel} ${messageText || ''} ${timestamp} ${replyCountText}`
|
|
90
|
+
|
|
85
91
|
return (
|
|
86
92
|
<Pressable
|
|
87
93
|
android_ripple={{ color: colors.androidRippleNeutral }}
|
|
88
94
|
onPress={handleNavigateToReplies}
|
|
89
95
|
onPressIn={handleMessagePressIn}
|
|
90
96
|
onPressOut={handleMessagePressOut}
|
|
97
|
+
accessibilityLabel={accessibilityLabel}
|
|
91
98
|
accessibilityHint="Navigate to reply screen for this message"
|
|
92
99
|
accessibilityRole="link"
|
|
93
100
|
>
|
|
@@ -112,7 +119,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
112
119
|
style={styles.messageBubble}
|
|
113
120
|
onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
|
|
114
121
|
>
|
|
115
|
-
<MessageAttachmentImagery attachments={
|
|
122
|
+
<MessageAttachmentImagery attachments={attachments} />
|
|
116
123
|
{text && (
|
|
117
124
|
<Text
|
|
118
125
|
variant="footnote"
|
|
@@ -41,16 +41,12 @@ const TypingDots = () => {
|
|
|
41
41
|
)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
interface TypingIndicatorProps {
|
|
45
|
-
conversationId: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
44
|
/**
|
|
49
45
|
* Component to display typing indicators in a conversation
|
|
50
46
|
* Shows "X is typing..." with animated dots
|
|
51
47
|
*/
|
|
52
|
-
export const TypingIndicator = (
|
|
53
|
-
const typingPeople = useTypingIndicators(
|
|
48
|
+
export const TypingIndicator = () => {
|
|
49
|
+
const typingPeople = useTypingIndicators()
|
|
54
50
|
const styles = useStyles()
|
|
55
51
|
const enabled = typingPeople.length > 0
|
|
56
52
|
|
|
@@ -2,6 +2,7 @@ import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'reac
|
|
|
2
2
|
import {
|
|
3
3
|
AccessibilityActionEvent,
|
|
4
4
|
Platform,
|
|
5
|
+
Pressable,
|
|
5
6
|
StyleProp,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
View,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
import ReanimatedSwipeable, {
|
|
11
12
|
SwipeableMethods,
|
|
12
13
|
} from 'react-native-gesture-handler/ReanimatedSwipeable'
|
|
13
|
-
import { Pressable } from 'react-native-gesture-handler'
|
|
14
14
|
import { useConversationsContext } from '../../contexts/conversations_context'
|
|
15
15
|
import { useTheme, useCreateAndroidRippleColor } from '../../hooks'
|
|
16
16
|
import {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
|
-
import { FlashList } from '@shopify/flash-list'
|
|
3
2
|
import React, { useMemo } from 'react'
|
|
4
|
-
import { StyleSheet, View } from 'react-native'
|
|
3
|
+
import { FlatList, StyleSheet, View } from 'react-native'
|
|
5
4
|
import { useConversationsContext } from '../../contexts/conversations_context'
|
|
6
5
|
import { useTheme } from '../../hooks'
|
|
7
6
|
import { ConversationResource } from '../../types'
|
|
@@ -35,7 +34,7 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
|
|
|
35
34
|
|
|
36
35
|
const showBadges = !chat_group_graph_id
|
|
37
36
|
|
|
38
|
-
const data:
|
|
37
|
+
const data: FlatListItem[] = useMemo(() => {
|
|
39
38
|
if (isLoading) {
|
|
40
39
|
return loadingPlaceholder
|
|
41
40
|
}
|
|
@@ -54,9 +53,8 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
|
|
|
54
53
|
|
|
55
54
|
return (
|
|
56
55
|
<View style={styles.container}>
|
|
57
|
-
<
|
|
56
|
+
<FlatList
|
|
58
57
|
data={data}
|
|
59
|
-
estimatedItemSize={97}
|
|
60
58
|
keyExtractor={item => item.id.toString()}
|
|
61
59
|
contentContainerStyle={styles.contentContainer}
|
|
62
60
|
onRefresh={refetch}
|
|
@@ -114,18 +112,18 @@ const useStyles = () => {
|
|
|
114
112
|
})
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
interface
|
|
115
|
+
interface FlatListLoadingItem {
|
|
118
116
|
type: 'loading'
|
|
119
117
|
id: string
|
|
120
118
|
}
|
|
121
|
-
interface
|
|
119
|
+
interface FlatListConversationItem {
|
|
122
120
|
type: 'conversation'
|
|
123
121
|
resource: ConversationResource
|
|
124
122
|
id: number
|
|
125
123
|
}
|
|
126
|
-
type
|
|
124
|
+
type FlatListItem = FlatListLoadingItem | FlatListConversationItem
|
|
127
125
|
|
|
128
|
-
const loadingPlaceholder:
|
|
126
|
+
const loadingPlaceholder: FlatListItem[] = Array.from({ length: 5 }, (_, i) => ({
|
|
129
127
|
type: 'loading',
|
|
130
128
|
id: `loading${i}`,
|
|
131
129
|
}))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ConversationContextValue {
|
|
4
|
+
conversationId: number
|
|
5
|
+
currentPageReplyRootId: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ConversationContextProviderProps extends PropsWithChildren {
|
|
9
|
+
conversationId: number
|
|
10
|
+
currentPageReplyRootId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ConversationContext = createContext<ConversationContextValue>({
|
|
14
|
+
conversationId: 0,
|
|
15
|
+
currentPageReplyRootId: null,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const ConversationContextProvider = ({
|
|
19
|
+
children,
|
|
20
|
+
conversationId,
|
|
21
|
+
currentPageReplyRootId,
|
|
22
|
+
}: ConversationContextProviderProps) => {
|
|
23
|
+
const value = useMemo(
|
|
24
|
+
() => ({
|
|
25
|
+
conversationId,
|
|
26
|
+
currentPageReplyRootId,
|
|
27
|
+
}),
|
|
28
|
+
[conversationId, currentPageReplyRootId]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return <ConversationContext.Provider value={value}>{children}</ConversationContext.Provider>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useConversationContext = () => useContext(ConversationContext)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'react'
|
|
2
2
|
import { useApiClient } from './use_api_client'
|
|
3
|
+
import { useConversationContext } from '../contexts/conversation_context'
|
|
3
4
|
|
|
4
5
|
const THROTTLE_INTERVAL = 3000 // 3 seconds
|
|
5
6
|
|
|
@@ -10,7 +11,8 @@ const THROTTLE_INTERVAL = 3000 // 3 seconds
|
|
|
10
11
|
* after receiving a typing event. This is how we can show a steady typing indicator even
|
|
11
12
|
* if the user types once every 2.9 seconds.
|
|
12
13
|
*/
|
|
13
|
-
export const useBroadcastTypingStatus = (
|
|
14
|
+
export const useBroadcastTypingStatus = () => {
|
|
15
|
+
const { conversationId, currentPageReplyRootId } = useConversationContext()
|
|
14
16
|
const apiClient = useApiClient()
|
|
15
17
|
const lastBroadcastTime = useRef<number>(0)
|
|
16
18
|
|
|
@@ -26,12 +28,14 @@ export const useBroadcastTypingStatus = (conversationId: string | number) => {
|
|
|
26
28
|
apiClient.chat
|
|
27
29
|
.post({
|
|
28
30
|
url: `/me/conversations/${conversationId}/broadcast_typing_status`,
|
|
29
|
-
data: {
|
|
31
|
+
data: {
|
|
32
|
+
data: { type: 'TypingStatus', attributes: { reply_root_id: currentPageReplyRootId } },
|
|
33
|
+
},
|
|
30
34
|
})
|
|
31
35
|
.catch(error => {
|
|
32
36
|
console.error('Failed to broadcast typing status:', error)
|
|
33
37
|
})
|
|
34
|
-
}, [apiClient, conversationId])
|
|
38
|
+
}, [apiClient.chat, conversationId, currentPageReplyRootId])
|
|
35
39
|
|
|
36
40
|
return broadcastTypingStatus
|
|
37
41
|
}
|
|
@@ -16,7 +16,9 @@ export const useConversationMessages = (
|
|
|
16
16
|
() =>
|
|
17
17
|
data
|
|
18
18
|
.filter(
|
|
19
|
-
message =>
|
|
19
|
+
message =>
|
|
20
|
+
(!message.deletedAt || message.replyRootId) &&
|
|
21
|
+
(message.attachments?.length || message.text?.length)
|
|
20
22
|
)
|
|
21
23
|
.sort((a, b) => -a.id.localeCompare(b.id)),
|
|
22
24
|
[data]
|