@planningcenter/chat-react-native 3.16.0-rc.2 → 3.16.0-rc.4
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/attachments/giphy_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/giphy_attachment.js +1 -11
- package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
- 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 +23 -23
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts +2 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +38 -16
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/reply_connectors.js +1 -1
- package/build/components/conversation/reply_connectors.js.map +1 -1
- package/build/components/conversation/shadow_message.d.ts +8 -0
- package/build/components/conversation/shadow_message.d.ts.map +1 -0
- package/build/components/conversation/shadow_message.js +161 -0
- package/build/components/conversation/shadow_message.js.map +1 -0
- package/build/components/display/icon.d.ts +3 -1
- package/build/components/display/icon.d.ts.map +1 -1
- package/build/components/display/icon.js +2 -0
- package/build/components/display/icon.js.map +1 -1
- package/build/components/primitive/avatar_primitive.d.ts +1 -0
- package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
- package/build/components/primitive/avatar_primitive.js +4 -0
- package/build/components/primitive/avatar_primitive.js.map +1 -1
- package/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_animated_message_background_color.d.ts +8 -0
- package/build/hooks/use_animated_message_background_color.d.ts.map +1 -0
- package/build/hooks/use_animated_message_background_color.js +29 -0
- package/build/hooks/use_animated_message_background_color.js.map +1 -0
- package/build/hooks/use_conversation_messages.d.ts +6 -3
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +32 -25
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts +2 -1
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +6 -2
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/hooks/use_suspense_api.d.ts +3 -1
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +1 -0
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/navigation/index.d.ts +6 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +6 -0
- package/build/navigation/index.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 +7 -5
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/message_actions_screen.d.ts +1 -0
- package/build/screens/message_actions_screen.d.ts.map +1 -1
- package/build/screens/message_actions_screen.js +16 -3
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/types/jolt_events/message_events.d.ts +2 -0
- package/build/types/jolt_events/message_events.d.ts.map +1 -1
- package/build/types/jolt_events/message_events.js.map +1 -1
- package/build/types/resources/message.d.ts +2 -0
- package/build/types/resources/message.d.ts.map +1 -1
- package/build/types/resources/message.js.map +1 -1
- package/build/utils/assert_keys_are_numbers.d.ts +2 -0
- package/build/utils/assert_keys_are_numbers.d.ts.map +1 -0
- package/build/utils/assert_keys_are_numbers.js +12 -0
- package/build/utils/assert_keys_are_numbers.js.map +1 -0
- package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
- package/build/utils/cache/optimistically_create_message.js +2 -0
- package/build/utils/cache/optimistically_create_message.js.map +1 -1
- package/build/utils/index.d.ts +2 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +2 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js +2 -0
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
- package/build/utils/replies_local_feature_flag.d.ts +2 -0
- package/build/utils/replies_local_feature_flag.d.ts.map +1 -0
- package/build/utils/replies_local_feature_flag.js +3 -0
- package/build/utils/replies_local_feature_flag.js.map +1 -0
- package/package.json +2 -2
- package/src/components/conversation/attachments/giphy_attachment.tsx +1 -14
- package/src/components/conversation/message.tsx +40 -36
- package/src/components/conversation/message_form.tsx +85 -15
- package/src/components/conversation/reply_connectors.tsx +1 -1
- package/src/components/conversation/shadow_message.tsx +274 -0
- package/src/components/display/icon.tsx +3 -0
- package/src/components/primitive/avatar_primitive.tsx +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use_animated_message_background_color.ts +44 -0
- package/src/hooks/use_conversation_messages.ts +45 -25
- package/src/hooks/use_message_create_or_update.ts +7 -2
- package/src/hooks/use_suspense_api.ts +3 -0
- package/src/navigation/index.tsx +6 -0
- package/src/screens/conversation_screen.tsx +9 -4
- package/src/screens/message_actions_screen.tsx +27 -1
- package/src/types/jolt_events/message_events.ts +2 -0
- package/src/types/resources/message.ts +2 -0
- package/src/utils/assert_keys_are_numbers.ts +13 -0
- package/src/utils/cache/optimistically_create_message.ts +2 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +2 -0
- package/src/utils/replies_local_feature_flag.ts +2 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
3
|
+
import { Avatar, Icon, IconProps, Image, Text } from '../display'
|
|
4
|
+
import {
|
|
5
|
+
useAnimatedMessageBackgroundColor,
|
|
6
|
+
useFontScale,
|
|
7
|
+
useScalableNumberOfLines,
|
|
8
|
+
useTheme,
|
|
9
|
+
} from '../../hooks'
|
|
10
|
+
import { MessageResource } from '../../types'
|
|
11
|
+
import {
|
|
12
|
+
DenormalizedAttachmentResource,
|
|
13
|
+
DenormalizedGiphyAttachmentResource,
|
|
14
|
+
DenormalizedExpandedLinkAttachmentResource,
|
|
15
|
+
DenormalizedMessageAttachmentResource,
|
|
16
|
+
} from '../../types/resources/denormalized_attachment_resource'
|
|
17
|
+
import {
|
|
18
|
+
CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
19
|
+
MAX_FONT_SIZE_MULTIPLIER,
|
|
20
|
+
MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
|
|
21
|
+
platformFontWeightMedium,
|
|
22
|
+
} from '../../utils/styles'
|
|
23
|
+
import Animated from 'react-native-reanimated'
|
|
24
|
+
import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
|
|
25
|
+
import { assertKeysAreNumbers } from '../../utils'
|
|
26
|
+
import { useNavigation } from '@react-navigation/native'
|
|
27
|
+
|
|
28
|
+
interface ShadowMessageProps extends MessageResource {
|
|
29
|
+
conversation_id: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProps) {
|
|
33
|
+
const { text } = message
|
|
34
|
+
const styles = useStyles(message)
|
|
35
|
+
const { colors } = useTheme()
|
|
36
|
+
const navigation = useNavigation()
|
|
37
|
+
|
|
38
|
+
const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
|
|
39
|
+
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
40
|
+
useAnimatedMessageBackgroundColor()
|
|
41
|
+
const scalableNumberOfLines = useScalableNumberOfLines(2)
|
|
42
|
+
|
|
43
|
+
const handleNavigateToReplies = () => {
|
|
44
|
+
navigation.navigate('ConversationReply', {
|
|
45
|
+
conversation_id,
|
|
46
|
+
reply_root_id: message.id,
|
|
47
|
+
// TODO: Add a way to pass the reply root author's name
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Pressable
|
|
53
|
+
android_ripple={{ color: colors.androidRippleNeutral }}
|
|
54
|
+
onPress={handleNavigateToReplies}
|
|
55
|
+
onPressIn={handleMessagePressIn}
|
|
56
|
+
onPressOut={handleMessagePressOut}
|
|
57
|
+
accessibilityHint="Navigate to reply screen for this message"
|
|
58
|
+
accessibilityRole="link"
|
|
59
|
+
>
|
|
60
|
+
<Animated.View style={[styles.message, animatedBackgroundColor]}>
|
|
61
|
+
{!message.mine && (
|
|
62
|
+
<View>
|
|
63
|
+
<View style={styles.avatarWrapper}>
|
|
64
|
+
<Avatar
|
|
65
|
+
size="xs"
|
|
66
|
+
sourceUri={message.author.avatar}
|
|
67
|
+
style={styles.avatar}
|
|
68
|
+
maxFontSizeMultiplier={1}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
<TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
|
|
72
|
+
</View>
|
|
73
|
+
)}
|
|
74
|
+
<View style={styles.messageContent}>
|
|
75
|
+
<View
|
|
76
|
+
style={styles.messageBubble}
|
|
77
|
+
onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
|
|
78
|
+
>
|
|
79
|
+
<MessageAttachmentImagery attachments={message.attachments} />
|
|
80
|
+
{text && (
|
|
81
|
+
<Text
|
|
82
|
+
variant="footnote"
|
|
83
|
+
style={styles.messageText}
|
|
84
|
+
numberOfLines={scalableNumberOfLines}
|
|
85
|
+
>
|
|
86
|
+
{text}
|
|
87
|
+
</Text>
|
|
88
|
+
)}
|
|
89
|
+
</View>
|
|
90
|
+
<View style={styles.messageMeta}>
|
|
91
|
+
<Text variant="footnote" style={styles.replyCountText}>
|
|
92
|
+
{message.replyCount} replies
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
</View>
|
|
96
|
+
{message.mine && (
|
|
97
|
+
<MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
|
|
98
|
+
)}
|
|
99
|
+
</Animated.View>
|
|
100
|
+
</Pressable>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function MessageAttachmentImagery({
|
|
105
|
+
attachments,
|
|
106
|
+
}: {
|
|
107
|
+
attachments: DenormalizedAttachmentResource[]
|
|
108
|
+
}) {
|
|
109
|
+
if (!attachments || attachments.length === 0) return null
|
|
110
|
+
|
|
111
|
+
const attachment = attachments[0]
|
|
112
|
+
|
|
113
|
+
if (attachment.type === 'giphy') {
|
|
114
|
+
return <GiphyImage attachment={attachment} />
|
|
115
|
+
}
|
|
116
|
+
if (attachment.type === 'ExpandedLink') {
|
|
117
|
+
return <ExpandedLinkImage attachment={attachment} />
|
|
118
|
+
}
|
|
119
|
+
if (attachment.type === 'MessageAttachment') {
|
|
120
|
+
const contentType = attachment.attributes?.contentType
|
|
121
|
+
const basicType = contentType?.split('/')[0]
|
|
122
|
+
|
|
123
|
+
switch (basicType) {
|
|
124
|
+
case 'image':
|
|
125
|
+
return <MessageAttachmentImage attachment={attachment} />
|
|
126
|
+
case 'video':
|
|
127
|
+
return <MessageAttachmentIcon iconName="general.outlinedVideoFile" />
|
|
128
|
+
case 'audio':
|
|
129
|
+
return <MessageAttachmentIcon iconName="general.outlinedMusicFile" />
|
|
130
|
+
case 'application':
|
|
131
|
+
return <MessageAttachmentIcon iconName="general.outlinedGenericFile" />
|
|
132
|
+
default:
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function GiphyImage({ attachment }: { attachment: DenormalizedGiphyAttachmentResource }) {
|
|
141
|
+
const { title, giphy } = attachment
|
|
142
|
+
const { url } = giphy.fixedWidth
|
|
143
|
+
const { width, height } = assertKeysAreNumbers(giphy.fixedWidth)
|
|
144
|
+
const styles = useStyles({ imageWidth: width, imageHeight: height })
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Image
|
|
148
|
+
source={{ uri: url }}
|
|
149
|
+
wrapperStyle={styles.imageWrapper}
|
|
150
|
+
style={styles.image}
|
|
151
|
+
alt={title}
|
|
152
|
+
loaderSize={16}
|
|
153
|
+
/>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ExpandedLinkImage({
|
|
158
|
+
attachment,
|
|
159
|
+
}: {
|
|
160
|
+
attachment: DenormalizedExpandedLinkAttachmentResource
|
|
161
|
+
}) {
|
|
162
|
+
const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes
|
|
163
|
+
const styles = useStyles({ imageWidth, imageHeight })
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Image
|
|
167
|
+
source={{ uri: imageUrl }}
|
|
168
|
+
wrapperStyle={styles.imageWrapper}
|
|
169
|
+
style={styles.image}
|
|
170
|
+
alt={title}
|
|
171
|
+
loaderSize={16}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function MessageAttachmentImage({
|
|
177
|
+
attachment,
|
|
178
|
+
}: {
|
|
179
|
+
attachment: DenormalizedMessageAttachmentResource
|
|
180
|
+
}) {
|
|
181
|
+
const { url, urlMedium, filename, metadata = {} } = attachment.attributes
|
|
182
|
+
const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Image
|
|
186
|
+
source={{ uri: urlMedium || url }}
|
|
187
|
+
style={styles.image}
|
|
188
|
+
wrapperStyle={styles.imageWrapper}
|
|
189
|
+
alt={filename}
|
|
190
|
+
loaderSize={16}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {
|
|
196
|
+
const styles = useStyles()
|
|
197
|
+
return (
|
|
198
|
+
<Icon
|
|
199
|
+
name={iconName}
|
|
200
|
+
style={styles.attachmentIcon}
|
|
201
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}
|
|
202
|
+
/>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface StylesProps {
|
|
207
|
+
imageWidth?: number
|
|
208
|
+
imageHeight?: number
|
|
209
|
+
mine?: boolean
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}) => {
|
|
213
|
+
const { colors } = useTheme()
|
|
214
|
+
const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
|
|
215
|
+
const { width } = useWindowDimensions()
|
|
216
|
+
const tabletWidth = width >= 744 // Smallest iPad Mini's width
|
|
217
|
+
|
|
218
|
+
return StyleSheet.create({
|
|
219
|
+
message: {
|
|
220
|
+
gap: 8,
|
|
221
|
+
flexDirection: mine ? 'row-reverse' : 'row',
|
|
222
|
+
paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
223
|
+
},
|
|
224
|
+
messageContent: {
|
|
225
|
+
flex: 1,
|
|
226
|
+
gap: 4,
|
|
227
|
+
marginBottom: 12,
|
|
228
|
+
},
|
|
229
|
+
avatarWrapper: {
|
|
230
|
+
width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
|
|
231
|
+
alignItems: 'center',
|
|
232
|
+
},
|
|
233
|
+
avatar: {
|
|
234
|
+
marginBottom: 8,
|
|
235
|
+
opacity: 0.5,
|
|
236
|
+
},
|
|
237
|
+
messageBubble: {
|
|
238
|
+
flexDirection: 'row',
|
|
239
|
+
alignSelf: mine ? 'flex-end' : 'flex-start',
|
|
240
|
+
alignItems: 'center',
|
|
241
|
+
gap: 8,
|
|
242
|
+
borderColor: colors.borderColorDefaultBase,
|
|
243
|
+
borderWidth: 1,
|
|
244
|
+
borderRadius: 8,
|
|
245
|
+
maxWidth: tabletWidth ? 360 : '80%',
|
|
246
|
+
paddingVertical: 6,
|
|
247
|
+
paddingHorizontal: 8,
|
|
248
|
+
},
|
|
249
|
+
messageText: {
|
|
250
|
+
color: colors.textColorDefaultPlaceholder,
|
|
251
|
+
flexShrink: 1,
|
|
252
|
+
},
|
|
253
|
+
messageMeta: {
|
|
254
|
+
flexDirection: 'row',
|
|
255
|
+
justifyContent: mine ? 'flex-end' : 'flex-start',
|
|
256
|
+
},
|
|
257
|
+
replyCountText: {
|
|
258
|
+
color: colors.interaction,
|
|
259
|
+
fontWeight: platformFontWeightMedium,
|
|
260
|
+
},
|
|
261
|
+
imageWrapper: {
|
|
262
|
+
width: 32 * fontScale,
|
|
263
|
+
aspectRatio: imageWidth / imageHeight,
|
|
264
|
+
opacity: 0.5,
|
|
265
|
+
},
|
|
266
|
+
image: {
|
|
267
|
+
borderRadius: 4,
|
|
268
|
+
},
|
|
269
|
+
attachmentIcon: {
|
|
270
|
+
color: colors.iconColorDefaultDim,
|
|
271
|
+
fontSize: 16,
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
}
|
|
@@ -17,6 +17,7 @@ import * as logomark from '@planningcenter/icons/paths/logomark'
|
|
|
17
17
|
import * as people from '@planningcenter/icons/paths/people'
|
|
18
18
|
import * as services from '@planningcenter/icons/paths/services'
|
|
19
19
|
import * as publishing from '@planningcenter/icons/paths/publishing'
|
|
20
|
+
import * as registrations from '@planningcenter/icons/paths/registrations'
|
|
20
21
|
|
|
21
22
|
// =================================
|
|
22
23
|
// ====== Constants ================
|
|
@@ -37,6 +38,7 @@ const ICONS = {
|
|
|
37
38
|
people,
|
|
38
39
|
services,
|
|
39
40
|
publishing,
|
|
41
|
+
registrations,
|
|
40
42
|
} as const
|
|
41
43
|
|
|
42
44
|
export type IconStyle = ViewStyle & {
|
|
@@ -61,6 +63,7 @@ export type IconString =
|
|
|
61
63
|
| `people.${IconName<'people'>}`
|
|
62
64
|
| `services.${IconName<'services'>}`
|
|
63
65
|
| `publishing.${IconName<'publishing'>}`
|
|
66
|
+
| `registrations.${IconName<'registrations'>}`
|
|
64
67
|
|
|
65
68
|
// =================================
|
|
66
69
|
// ====== Component ================
|
|
@@ -45,6 +45,7 @@ export type {
|
|
|
45
45
|
// =================================
|
|
46
46
|
|
|
47
47
|
const AVATAR_SIZES = {
|
|
48
|
+
xs: 'xs',
|
|
48
49
|
sm: 'sm',
|
|
49
50
|
md: 'md',
|
|
50
51
|
lg: 'lg',
|
|
@@ -60,18 +61,21 @@ type AvatarSize = (typeof AVATAR_SIZES)[keyof typeof AVATAR_SIZES]
|
|
|
60
61
|
type AvatarPresenceType = (typeof AVATAR_PRESENCE_TYPES)[keyof typeof AVATAR_PRESENCE_TYPES]
|
|
61
62
|
|
|
62
63
|
const AVATAR_PX: Record<AvatarSize, number> = {
|
|
64
|
+
[AVATAR_SIZES.xs]: 20,
|
|
63
65
|
[AVATAR_SIZES.sm]: 24,
|
|
64
66
|
[AVATAR_SIZES.md]: 32,
|
|
65
67
|
[AVATAR_SIZES.lg]: 40,
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
|
|
71
|
+
[AVATAR_SIZES.xs]: 8,
|
|
69
72
|
[AVATAR_SIZES.sm]: 10,
|
|
70
73
|
[AVATAR_SIZES.md]: 12,
|
|
71
74
|
[AVATAR_SIZES.lg]: 14,
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const AVATAR_FALLBACK_ICON_PX: Record<AvatarSize, number> = {
|
|
78
|
+
[AVATAR_SIZES.xs]: 10,
|
|
75
79
|
[AVATAR_SIZES.sm]: 12,
|
|
76
80
|
[AVATAR_SIZES.md]: 16,
|
|
77
81
|
[AVATAR_SIZES.lg]: 20,
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
2
|
+
import {
|
|
3
|
+
useSharedValue,
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
withTiming,
|
|
6
|
+
interpolateColor,
|
|
7
|
+
Easing,
|
|
8
|
+
} from 'react-native-reanimated'
|
|
9
|
+
import { useTheme } from '../hooks'
|
|
10
|
+
|
|
11
|
+
export function useAnimatedMessageBackgroundColor() {
|
|
12
|
+
const { colors } = useTheme()
|
|
13
|
+
const bgFadeProgress = useSharedValue(0)
|
|
14
|
+
|
|
15
|
+
const pressedColor = Platform.select({
|
|
16
|
+
ios: colors.fillColorNeutral050Base,
|
|
17
|
+
default: 'transparent',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const animatedBackgroundColor = useAnimatedStyle(() => {
|
|
21
|
+
const backgroundColor = interpolateColor(
|
|
22
|
+
bgFadeProgress.value,
|
|
23
|
+
[0, 1],
|
|
24
|
+
['transparent', pressedColor]
|
|
25
|
+
)
|
|
26
|
+
return {
|
|
27
|
+
backgroundColor,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const handleMessagePressIn = () => {
|
|
32
|
+
bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleMessagePressOut = () => {
|
|
36
|
+
bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
animatedBackgroundColor,
|
|
41
|
+
handleMessagePressIn,
|
|
42
|
+
handleMessagePressOut,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
} from './use_suspense_api'
|
|
8
8
|
|
|
9
9
|
export const useConversationMessages = (
|
|
10
|
-
{ conversation_id }: { conversation_id: number },
|
|
10
|
+
{ conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string },
|
|
11
11
|
opts?: SuspensePaginatorOptions
|
|
12
12
|
) => {
|
|
13
13
|
const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
|
|
14
|
-
getMessagesRequestArgs({ conversation_id }),
|
|
14
|
+
getMessagesRequestArgs({ conversation_id, reply_root_id }),
|
|
15
15
|
opts
|
|
16
16
|
)
|
|
17
|
-
const queryKey = getMessagesQueryKey({ conversation_id })
|
|
17
|
+
const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
|
|
18
18
|
const messages = useMemo(
|
|
19
19
|
() =>
|
|
20
20
|
data
|
|
@@ -28,29 +28,49 @@ export const useConversationMessages = (
|
|
|
28
28
|
return { messages, refetch, isRefetching, fetchNextPage, queryKey }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export const getMessagesRequestArgs = ({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
export const getMessagesRequestArgs = ({
|
|
32
|
+
conversation_id,
|
|
33
|
+
reply_root_id,
|
|
34
|
+
}: {
|
|
35
|
+
conversation_id: number
|
|
36
|
+
reply_root_id?: string
|
|
37
|
+
}) => {
|
|
38
|
+
const url = reply_root_id
|
|
39
|
+
? `/me/conversations/${conversation_id}/messages/${reply_root_id}/replies`
|
|
40
|
+
: `/me/conversations/${conversation_id}/messages`
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
url,
|
|
44
|
+
data: {
|
|
45
|
+
perPage: 25,
|
|
46
|
+
fields: {
|
|
47
|
+
Message: [
|
|
48
|
+
'text',
|
|
49
|
+
'text_edited_at',
|
|
50
|
+
'mine',
|
|
51
|
+
'attachments',
|
|
52
|
+
'created_at',
|
|
53
|
+
'deleted_at',
|
|
54
|
+
'author',
|
|
55
|
+
'reaction_counts',
|
|
56
|
+
'reply_count',
|
|
57
|
+
'reply_root',
|
|
58
|
+
],
|
|
59
|
+
Person: ['name', 'avatar'],
|
|
60
|
+
ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
|
|
61
|
+
},
|
|
62
|
+
include: ['author', 'reaction_counts'],
|
|
48
63
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
52
66
|
|
|
53
|
-
export const getMessagesQueryKey = ({
|
|
54
|
-
|
|
67
|
+
export const getMessagesQueryKey = ({
|
|
68
|
+
conversation_id,
|
|
69
|
+
reply_root_id,
|
|
70
|
+
}: {
|
|
71
|
+
conversation_id: number
|
|
72
|
+
reply_root_id?: string
|
|
73
|
+
}) => {
|
|
74
|
+
const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
|
|
55
75
|
return getRequestQueryKey(requestArgs)
|
|
56
76
|
}
|
|
@@ -18,9 +18,10 @@ import { startMessageCreationTracking } from '../utils/performance_tracking'
|
|
|
18
18
|
interface Props {
|
|
19
19
|
conversationId: number
|
|
20
20
|
message?: MessageResource
|
|
21
|
+
replyRootId?: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
|
|
24
|
+
export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
|
|
24
25
|
const messageId = message?.id || null
|
|
25
26
|
const isEditing = !isNewMessage(message)
|
|
26
27
|
const apiClient = useApiClient()
|
|
@@ -38,7 +39,11 @@ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
|
|
|
38
39
|
const fieldsWithValueJoined = Object.fromEntries(
|
|
39
40
|
Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
|
|
40
41
|
)
|
|
41
|
-
let attributes: any = {
|
|
42
|
+
let attributes: any = {
|
|
43
|
+
text,
|
|
44
|
+
...(attachments ? { attachments } : {}),
|
|
45
|
+
...(replyRootId ? { reply_root: { id: replyRootId } } : {}),
|
|
46
|
+
}
|
|
42
47
|
if (!isEditing) {
|
|
43
48
|
const idempotentKey = insecureUUID()
|
|
44
49
|
attributes.idempotent_key = idempotentKey
|
|
@@ -14,6 +14,7 @@ import { ResponseError } from '../utils/response_error'
|
|
|
14
14
|
|
|
15
15
|
interface SuspenseGetOptions extends GetRequest {
|
|
16
16
|
app?: App
|
|
17
|
+
reply_root_id?: string
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export type SuspenseGetQueryOptions<T extends ResourceObject | ResourceObject[]> = Omit<
|
|
@@ -114,10 +115,12 @@ export type RequestQueryKey = [
|
|
|
114
115
|
SuspenseGetOptions['data'],
|
|
115
116
|
SuspenseGetOptions['headers'],
|
|
116
117
|
SuspenseGetOptions['app'],
|
|
118
|
+
SuspenseGetOptions['reply_root_id'],
|
|
117
119
|
]
|
|
118
120
|
export const getRequestQueryKey = (args: SuspenseGetOptions): RequestQueryKey => [
|
|
119
121
|
args.url,
|
|
120
122
|
args.data,
|
|
121
123
|
args.headers,
|
|
122
124
|
args.app || 'chat',
|
|
125
|
+
args.reply_root_id,
|
|
123
126
|
]
|
package/src/navigation/index.tsx
CHANGED
|
@@ -184,6 +184,12 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
184
184
|
),
|
|
185
185
|
}),
|
|
186
186
|
},
|
|
187
|
+
ConversationReply: {
|
|
188
|
+
screen: ConversationScreen,
|
|
189
|
+
options: {
|
|
190
|
+
title: 'Reply', // TODO: Get root reply author
|
|
191
|
+
},
|
|
192
|
+
},
|
|
187
193
|
TeamConversation: {
|
|
188
194
|
screen: TeamConversationScreen,
|
|
189
195
|
options: {
|
|
@@ -35,10 +35,10 @@ import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
|
|
|
35
35
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
36
36
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
37
37
|
|
|
38
|
-
export const REPLIES_FEATURE_ENABLED = false
|
|
39
|
-
|
|
40
38
|
export type ConversationRouteProps = {
|
|
41
39
|
conversation_id: number
|
|
40
|
+
reply_root_id?: string
|
|
41
|
+
replyRootAuthor?: string
|
|
42
42
|
chat_group_graph_id?: string
|
|
43
43
|
clear_input?: boolean
|
|
44
44
|
editing_message_id?: number | null
|
|
@@ -53,10 +53,11 @@ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
|
53
53
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
54
54
|
const styles = useStyles()
|
|
55
55
|
const navigation = useNavigation()
|
|
56
|
-
const { conversation_id, editing_message_id } = route.params
|
|
56
|
+
const { conversation_id, editing_message_id, reply_root_id } = route.params
|
|
57
57
|
const { data: conversation } = useConversation(route.params)
|
|
58
58
|
const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
|
|
59
59
|
conversation_id,
|
|
60
|
+
reply_root_id,
|
|
60
61
|
})
|
|
61
62
|
useConversationJoltEvents({ conversationId: conversation_id })
|
|
62
63
|
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
@@ -86,12 +87,14 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
86
87
|
}, [])
|
|
87
88
|
|
|
88
89
|
useEffect(() => {
|
|
90
|
+
if (reply_root_id) return
|
|
91
|
+
|
|
89
92
|
navigation.setParams({
|
|
90
93
|
title: title,
|
|
91
94
|
badge: badges?.[0],
|
|
92
95
|
deleted: conversation?.deleted,
|
|
93
96
|
})
|
|
94
|
-
}, [navigation, title, badges, conversation?.deleted])
|
|
97
|
+
}, [navigation, title, badges, conversation?.deleted, reply_root_id])
|
|
95
98
|
|
|
96
99
|
if (!conversation || conversation.deleted) {
|
|
97
100
|
return (
|
|
@@ -139,6 +142,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
139
142
|
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
|
|
140
143
|
conversation_id={conversation_id}
|
|
141
144
|
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
145
|
+
inReplyScreen={!!reply_root_id}
|
|
142
146
|
/>
|
|
143
147
|
)
|
|
144
148
|
}}
|
|
@@ -152,6 +156,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
152
156
|
{canReply ? (
|
|
153
157
|
<MessageForm.Root
|
|
154
158
|
conversation={conversation}
|
|
159
|
+
replyRootId={reply_root_id}
|
|
155
160
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
156
161
|
// We use a separate key so that it remounts component when switching between new
|
|
157
162
|
// and edit message. This simplifies internal state handling.
|
|
@@ -14,6 +14,7 @@ import { useMessageReactionToggle } from '../hooks/use_message_reaction_toggle'
|
|
|
14
14
|
import { ReactionCountResource } from '../types/resources/reaction'
|
|
15
15
|
import { Clipboard, Haptic } from '../utils/native_adapters'
|
|
16
16
|
import { MessageResource } from '../types'
|
|
17
|
+
import { REPLIES_FEATURE_ENABLED } from '../utils'
|
|
17
18
|
|
|
18
19
|
export const MessageActionsScreenOptions = getFormSheetScreenOptions({
|
|
19
20
|
sheetAllowedDetents: [0.5],
|
|
@@ -24,10 +25,11 @@ export type MessageActionsScreenProps = StaticScreenProps<{
|
|
|
24
25
|
message_id: string
|
|
25
26
|
conversation_id: number
|
|
26
27
|
canDeleteNonAuthoredMessages?: boolean
|
|
28
|
+
inReplyScreen?: boolean
|
|
27
29
|
}>
|
|
28
30
|
|
|
29
31
|
export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
|
|
30
|
-
const { conversation_id, message_id, canDeleteNonAuthoredMessages } = route.params
|
|
32
|
+
const { conversation_id, message_id, canDeleteNonAuthoredMessages, inReplyScreen } = route.params
|
|
31
33
|
|
|
32
34
|
const { messages, refetch } = useConversationMessages(
|
|
33
35
|
{ conversation_id },
|
|
@@ -43,6 +45,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
|
|
|
43
45
|
conversation_id={conversation_id}
|
|
44
46
|
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages || false}
|
|
45
47
|
refetchMessages={refetch}
|
|
48
|
+
inReplyScreen={inReplyScreen}
|
|
46
49
|
/>
|
|
47
50
|
)
|
|
48
51
|
}
|
|
@@ -52,11 +55,13 @@ function MessageActionsScreenContent({
|
|
|
52
55
|
conversation_id,
|
|
53
56
|
canDeleteNonAuthoredMessages,
|
|
54
57
|
refetchMessages,
|
|
58
|
+
inReplyScreen,
|
|
55
59
|
}: {
|
|
56
60
|
message: MessageResource
|
|
57
61
|
conversation_id: number
|
|
58
62
|
canDeleteNonAuthoredMessages: boolean
|
|
59
63
|
refetchMessages: () => void
|
|
64
|
+
inReplyScreen?: boolean
|
|
60
65
|
}) {
|
|
61
66
|
const navigation = useNavigation()
|
|
62
67
|
const apiClient = useApiClient()
|
|
@@ -83,6 +88,18 @@ function MessageActionsScreenContent({
|
|
|
83
88
|
return ''
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
const handleReplyPress = useCallback(() => {
|
|
92
|
+
navigation.goBack()
|
|
93
|
+
// Waits for the modal to be dismissed before pushing the reply screen
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
navigation.navigate('ConversationReply', {
|
|
96
|
+
conversation_id,
|
|
97
|
+
reply_root_id: message.replyRootId || message.id,
|
|
98
|
+
// TODO: Update title param with reply root author
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
}, [navigation, conversation_id, message.id, message.replyRootId])
|
|
102
|
+
|
|
86
103
|
const handleCopyPress = () => {
|
|
87
104
|
Clipboard.setStringAsync(message?.text || attachmentForCopy() || '')
|
|
88
105
|
navigation.goBack()
|
|
@@ -163,6 +180,15 @@ function MessageActionsScreenContent({
|
|
|
163
180
|
))}
|
|
164
181
|
</View>
|
|
165
182
|
<View style={styles.actions}>
|
|
183
|
+
{REPLIES_FEATURE_ENABLED && !inReplyScreen && (
|
|
184
|
+
<FormSheet.Action
|
|
185
|
+
onPress={handleReplyPress}
|
|
186
|
+
title="Reply to message"
|
|
187
|
+
iconName="registrations.undo"
|
|
188
|
+
accessibilityHint="Navigates to the reply screen"
|
|
189
|
+
accessibilityRole="link"
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
166
192
|
<FormSheet.Action
|
|
167
193
|
onPress={handleCopyPress}
|
|
168
194
|
title="Copy text"
|
|
@@ -15,6 +15,8 @@ export interface MessageResource {
|
|
|
15
15
|
attachments: DenormalizedAttachmentResource[]
|
|
16
16
|
author: PersonResource
|
|
17
17
|
reactionCounts: ReactionCountResource[]
|
|
18
|
+
replyCount: number
|
|
19
|
+
replyRootId?: string | null
|
|
18
20
|
|
|
19
21
|
// Custom Local Properties we set for rendering
|
|
20
22
|
renderAuthor?: boolean
|