@planningcenter/chat-react-native 3.16.2-rc.0 → 3.17.0-rc.1
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.map +1 -1
- package/build/components/conversation/message.js +16 -6
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/reply_connectors.d.ts +1 -0
- package/build/components/conversation/reply_connectors.d.ts.map +1 -1
- package/build/components/conversation/reply_connectors.js +57 -23
- package/build/components/conversation/reply_connectors.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +12 -0
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -0
- package/build/components/conversation/{shadow_message.js → reply_shadow_message.js} +35 -5
- package/build/components/conversation/reply_shadow_message.js.map +1 -0
- package/build/hooks/use_conversation_message.d.ts +11 -0
- package/build/hooks/use_conversation_message.d.ts.map +1 -0
- package/build/hooks/use_conversation_message.js +8 -0
- package/build/hooks/use_conversation_message.js.map +1 -0
- package/build/hooks/use_conversation_messages.d.ts +1 -20
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +2 -33
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts +1 -1
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +1 -1
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/hooks/use_message_reaction_toggle.js +1 -1
- package/build/hooks/use_message_reaction_toggle.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +14 -2
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +49 -2
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/types/resources/message.d.ts +6 -0
- package/build/types/resources/message.d.ts.map +1 -1
- package/build/types/resources/message.js.map +1 -1
- package/build/utils/cache/optimistically_create_message.js +1 -1
- package/build/utils/cache/optimistically_create_message.js.map +1 -1
- package/build/utils/cache/optimistically_update_message.js +1 -1
- package/build/utils/cache/optimistically_update_message.js.map +1 -1
- package/build/utils/pluralize.js +1 -1
- package/build/utils/pluralize.js.map +1 -1
- package/build/utils/request/get_message.d.ts +20 -0
- package/build/utils/request/get_message.d.ts.map +1 -0
- package/build/utils/request/get_message.js +18 -0
- package/build/utils/request/get_message.js.map +1 -0
- package/build/utils/request/get_messages.d.ts +20 -0
- package/build/utils/request/get_messages.d.ts.map +1 -0
- package/build/utils/request/get_messages.js +20 -0
- package/build/utils/request/get_messages.js.map +1 -0
- package/build/utils/request/messages_data_options.d.ts +7 -0
- package/build/utils/request/messages_data_options.d.ts.map +1 -0
- package/build/utils/request/messages_data_options.js +18 -0
- package/build/utils/request/messages_data_options.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/utils/pluralize.tsx +3 -0
- package/src/components/conversation/message.tsx +19 -6
- package/src/components/conversation/message_form.tsx +2 -2
- package/src/components/conversation/reply_connectors.tsx +62 -31
- package/src/components/conversation/{shadow_message.tsx → reply_shadow_message.tsx} +54 -5
- package/src/hooks/use_conversation_message.ts +20 -0
- package/src/hooks/use_conversation_messages.ts +3 -53
- package/src/hooks/use_conversation_messages_jolt_events.ts +1 -1
- package/src/hooks/use_message_create_or_update.ts +2 -2
- package/src/hooks/use_message_reaction_toggle.ts +1 -1
- package/src/screens/conversation_screen.tsx +77 -4
- package/src/types/resources/message.ts +6 -0
- package/src/utils/cache/optimistically_create_message.ts +1 -1
- package/src/utils/cache/optimistically_update_message.ts +1 -1
- package/src/utils/pluralize.ts +1 -1
- package/src/utils/request/get_message.ts +32 -0
- package/src/utils/request/get_messages.ts +34 -0
- package/src/utils/request/messages_data_options.ts +18 -0
- package/build/components/conversation/shadow_message.d.ts +0 -8
- package/build/components/conversation/shadow_message.d.ts.map +0 -1
- package/build/components/conversation/shadow_message.js.map +0 -1
- package/build/utils/request/messages.d.ts +0 -15
- package/build/utils/request/messages.d.ts.map +0 -1
- package/build/utils/request/messages.js +0 -22
- package/build/utils/request/messages.js.map +0 -1
- package/src/utils/request/messages.ts +0 -21
|
@@ -10,6 +10,8 @@ import { REPLIES_FEATURE_ENABLED } from '../../utils'
|
|
|
10
10
|
|
|
11
11
|
const MY_REPLY_CONNECTOR_WIDTH = 38
|
|
12
12
|
const CONNECTOR_BORDER_WIDTH = 4
|
|
13
|
+
const OFFSET_CONNECTOR_CONTAINER = CONNECTOR_BORDER_WIDTH / 2 // Accounts for the border width ensuring the connectors are centered under the avatar
|
|
14
|
+
export const AVATAR_CONNECTOR_SPACING = 8
|
|
13
15
|
|
|
14
16
|
interface ReplyConnectorProps {
|
|
15
17
|
message: MessageResource
|
|
@@ -18,40 +20,47 @@ interface ReplyConnectorProps {
|
|
|
18
20
|
|
|
19
21
|
export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConnectorProps) {
|
|
20
22
|
const styles = useStyles()
|
|
23
|
+
const { nextRendersAuthor, threadPosition, renderAuthor, isReplyShadowMessage } = message
|
|
21
24
|
|
|
22
25
|
if (!REPLIES_FEATURE_ENABLED) return null
|
|
23
|
-
|
|
24
|
-
// TODO: Need to remove, just adding this to remove the lint warning about message being unused
|
|
25
|
-
console.log('message', message)
|
|
26
|
+
if (messageBubbleHeight === 0) return null // Prevents UI shifting
|
|
26
27
|
|
|
27
28
|
const connectorMap: Record<string, React.ReactNode | null> = {
|
|
28
29
|
shortTailCurve: <ShortTailCurveConnector height={messageBubbleHeight / 2} />,
|
|
30
|
+
shortHeadCurve: <ShortHeadCurveConnector height={messageBubbleHeight / 2} />,
|
|
29
31
|
verticalLine: <VerticalLineConnector />,
|
|
30
32
|
default: null,
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
// TODO: Currently using fake message attributes. Will need to refactor once we have the message reply logic in place.
|
|
34
35
|
const getConnectorKey = () => {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
if (threadPosition === 'first' && !renderAuthor) return 'shortHeadCurve'
|
|
37
|
+
if (threadPosition === 'last' && !renderAuthor) return 'shortTailCurve'
|
|
38
|
+
if (
|
|
39
|
+
(threadPosition === 'first' && renderAuthor) ||
|
|
40
|
+
threadPosition === 'center' ||
|
|
41
|
+
isReplyShadowMessage
|
|
42
|
+
)
|
|
43
|
+
return 'verticalLine'
|
|
37
44
|
return 'default'
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const ConnectorComponent = connectorMap[getConnectorKey()]
|
|
41
|
-
|
|
42
48
|
if (!ConnectorComponent) return null
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
const spacerStyle = nextRendersAuthor ? styles.theirReplyConnectorSpacer : null
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View style={[styles.theirReplyConnectorContainer, spacerStyle]}>{ConnectorComponent}</View>
|
|
54
|
+
)
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
// TODO: Refactor how we choose the correct connector once we have the logic in place
|
|
48
57
|
export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnectorProps) {
|
|
49
58
|
const styles = useStyles()
|
|
59
|
+
const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
|
|
60
|
+
message
|
|
50
61
|
|
|
51
62
|
if (!REPLIES_FEATURE_ENABLED) return null
|
|
52
|
-
|
|
53
|
-
// TODO: Need to remove, just adding this to remove the lint warning about message being unused
|
|
54
|
-
console.log('message', message)
|
|
63
|
+
if (messageBubbleHeight === 0) return null // Prevents UI shifting
|
|
55
64
|
|
|
56
65
|
const connectorMap: Record<string, React.ReactNode | null> = {
|
|
57
66
|
longTailCurve: (
|
|
@@ -67,24 +76,25 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
|
|
|
67
76
|
/>
|
|
68
77
|
),
|
|
69
78
|
verticalLine: <VerticalLineConnector style={styles.myReplyConnectorPosition} />,
|
|
70
|
-
swirl: <SwirlConnector style={styles.myReplyConnectorPosition} />,
|
|
79
|
+
swirl: <SwirlConnector style={styles.myReplyConnectorPosition} height={messageBubbleHeight} />,
|
|
71
80
|
default: null,
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
// TODO: Currently using fake message attributes. Will need to refactor once we have the message reply logic in place.
|
|
75
83
|
const getConnectorKey = () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
if (threadPosition === 'first' || isReplyShadowMessage) return 'longHeadCurve'
|
|
85
|
+
if (threadPosition === 'center' && prevIsMyReply && nextIsMyReply) return 'verticalLine'
|
|
86
|
+
if (threadPosition === 'center' && (!prevIsMyReply || !nextIsMyReply)) return 'swirl'
|
|
87
|
+
if (threadPosition === 'last') return 'longTailCurve'
|
|
88
|
+
|
|
80
89
|
return 'default'
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
const ConnectorComponent = connectorMap[getConnectorKey()]
|
|
84
|
-
|
|
85
93
|
if (!ConnectorComponent) return null
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
const spacerStyle = nextRendersAuthor ? styles.myReplyConnectorSpacer : null
|
|
96
|
+
|
|
97
|
+
return <View style={[styles.myReplyConnectorContainer, spacerStyle]}>{ConnectorComponent}</View>
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
function VerticalLineConnector({ style }: { style?: ViewStyle }) {
|
|
@@ -102,26 +112,33 @@ function LongTailCurveConnector({ height, style }: { height: number; style?: Vie
|
|
|
102
112
|
return <View style={[styles.longTailCurveConnector, { height }, style]} />
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
function ShortHeadCurveConnector({ height }: { height: number }) {
|
|
116
|
+
const styles = useStyles()
|
|
117
|
+
return <View style={[styles.shortHeadCurveConnector, { marginTop: height }]} />
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
function ShortTailCurveConnector({ height }: { height: number }) {
|
|
106
121
|
const styles = useStyles()
|
|
107
122
|
return <View style={[styles.shortTailCurveConnector, { height }]} />
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
function SwirlConnector({ style }: { style?: ViewStyle }) {
|
|
125
|
+
function SwirlConnector({ style, height }: { style?: ViewStyle; height: number }) {
|
|
111
126
|
const styles = useStyles()
|
|
112
127
|
const { colors } = useTheme()
|
|
113
|
-
const borderColor = colors.borderColorDefaultBase
|
|
114
128
|
|
|
115
129
|
return (
|
|
116
130
|
<View style={[styles.swirlConnectorContainer, style]}>
|
|
117
|
-
<View style={
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
<View style={{ height }}>
|
|
132
|
+
<View style={styles.swirlConnectorVerticalLineHead} />
|
|
133
|
+
<Svg width={27} height={34} fill="none">
|
|
134
|
+
<Path
|
|
135
|
+
stroke={colors.borderColorDefaultBase}
|
|
136
|
+
strokeWidth={CONNECTOR_BORDER_WIDTH}
|
|
137
|
+
d="M2.07 34c0-6.142 1.201-12.283 3.343-17m0 0c2.305-5.075 5.699-8.5 9.857-8.5 4.86 0 8.8 3.806 8.8 8.5s-3.94 8.5-8.8 8.5c-4.158 0-7.552-3.425-9.857-8.5zm0 0C3.271 12.283 2.07 6.142 2.07 0"
|
|
138
|
+
/>
|
|
139
|
+
</Svg>
|
|
140
|
+
<View style={styles.swirlConnectorVerticalLineTail} />
|
|
141
|
+
</View>
|
|
125
142
|
<View style={styles.swirlConnectorVerticalLineTail} />
|
|
126
143
|
</View>
|
|
127
144
|
)
|
|
@@ -134,19 +151,26 @@ const useStyles = () => {
|
|
|
134
151
|
return StyleSheet.create({
|
|
135
152
|
theirReplyConnectorContainer: {
|
|
136
153
|
width: '50%',
|
|
154
|
+
marginRight: OFFSET_CONNECTOR_CONTAINER,
|
|
137
155
|
alignSelf: 'flex-end',
|
|
138
156
|
flex: 1,
|
|
139
157
|
},
|
|
158
|
+
theirReplyConnectorSpacer: {
|
|
159
|
+
paddingBottom: AVATAR_CONNECTOR_SPACING,
|
|
160
|
+
},
|
|
140
161
|
myReplyConnectorContainer: {
|
|
141
162
|
width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
|
|
142
163
|
position: 'absolute',
|
|
143
|
-
left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
164
|
+
left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL - OFFSET_CONNECTOR_CONTAINER,
|
|
144
165
|
top: 0,
|
|
145
166
|
bottom: 0,
|
|
146
167
|
},
|
|
147
168
|
myReplyConnectorPosition: {
|
|
148
169
|
left: '50%',
|
|
149
170
|
},
|
|
171
|
+
myReplyConnectorSpacer: {
|
|
172
|
+
bottom: AVATAR_CONNECTOR_SPACING,
|
|
173
|
+
},
|
|
150
174
|
verticalLineConnector: {
|
|
151
175
|
flex: 1,
|
|
152
176
|
borderLeftWidth: CONNECTOR_BORDER_WIDTH,
|
|
@@ -167,6 +191,13 @@ const useStyles = () => {
|
|
|
167
191
|
borderBottomWidth: CONNECTOR_BORDER_WIDTH,
|
|
168
192
|
borderBottomLeftRadius: 16,
|
|
169
193
|
},
|
|
194
|
+
shortHeadCurveConnector: {
|
|
195
|
+
borderColor: borderColor,
|
|
196
|
+
borderLeftWidth: CONNECTOR_BORDER_WIDTH,
|
|
197
|
+
borderTopWidth: CONNECTOR_BORDER_WIDTH,
|
|
198
|
+
borderTopLeftRadius: 16,
|
|
199
|
+
flex: 1,
|
|
200
|
+
},
|
|
170
201
|
shortTailCurveConnector: {
|
|
171
202
|
borderColor: borderColor,
|
|
172
203
|
borderLeftWidth: CONNECTOR_BORDER_WIDTH,
|
|
@@ -22,14 +22,45 @@ import {
|
|
|
22
22
|
} from '../../utils/styles'
|
|
23
23
|
import Animated from 'react-native-reanimated'
|
|
24
24
|
import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
|
|
25
|
-
import { assertKeysAreNumbers } from '../../utils'
|
|
25
|
+
import { assertKeysAreNumbers, pluralize } from '../../utils'
|
|
26
26
|
import { useNavigation } from '@react-navigation/native'
|
|
27
|
+
import { useConversationMessage } from '../../hooks/use_conversation_message'
|
|
27
28
|
|
|
28
|
-
interface
|
|
29
|
+
interface ReplyShadowMessageProps extends MessageResource {
|
|
30
|
+
messageId: string
|
|
29
31
|
conversation_id: number
|
|
32
|
+
inReplyScreen?: boolean
|
|
33
|
+
isReplyShadowMessage: boolean
|
|
34
|
+
nextRendersAuthor: boolean
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
export function
|
|
37
|
+
export function ReplyShadowMessage({
|
|
38
|
+
conversation_id,
|
|
39
|
+
inReplyScreen,
|
|
40
|
+
messageId,
|
|
41
|
+
isReplyShadowMessage,
|
|
42
|
+
nextRendersAuthor,
|
|
43
|
+
}: ReplyShadowMessageProps) {
|
|
44
|
+
const { message, isError, isLoading } = useConversationMessage({ conversation_id, messageId })
|
|
45
|
+
|
|
46
|
+
if (inReplyScreen) return null
|
|
47
|
+
if (isLoading) return <ShadowMessageFallback text="Loading..." />
|
|
48
|
+
if (isError || !message) return <ShadowMessageFallback text="Message not found" />
|
|
49
|
+
|
|
50
|
+
const enrichedMessage = {
|
|
51
|
+
...message,
|
|
52
|
+
isReplyShadowMessage,
|
|
53
|
+
nextRendersAuthor,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return <ShadowMessageContent conversation_id={conversation_id} {...enrichedMessage} />
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ShadowMessageContentProps extends MessageResource {
|
|
60
|
+
conversation_id: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) {
|
|
33
64
|
const { text } = message
|
|
34
65
|
const styles = useStyles(message)
|
|
35
66
|
const { colors } = useTheme()
|
|
@@ -39,11 +70,12 @@ export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProp
|
|
|
39
70
|
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
40
71
|
useAnimatedMessageBackgroundColor()
|
|
41
72
|
const scalableNumberOfLines = useScalableNumberOfLines(2)
|
|
73
|
+
const replyCountText = pluralize(message.replyCount, 'reply')
|
|
42
74
|
|
|
43
75
|
const handleNavigateToReplies = () => {
|
|
44
76
|
navigation.navigate('ConversationReply', {
|
|
45
77
|
conversation_id,
|
|
46
|
-
reply_root_id: message.
|
|
78
|
+
reply_root_id: message.replyRootId,
|
|
47
79
|
// TODO: Add a way to pass the reply root author's name
|
|
48
80
|
})
|
|
49
81
|
}
|
|
@@ -89,7 +121,7 @@ export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProp
|
|
|
89
121
|
</View>
|
|
90
122
|
<View style={styles.messageMeta}>
|
|
91
123
|
<Text variant="footnote" style={styles.replyCountText}>
|
|
92
|
-
{
|
|
124
|
+
{replyCountText}
|
|
93
125
|
</Text>
|
|
94
126
|
</View>
|
|
95
127
|
</View>
|
|
@@ -203,6 +235,23 @@ function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {
|
|
|
203
235
|
)
|
|
204
236
|
}
|
|
205
237
|
|
|
238
|
+
// TODO: The mine prop is not used yet, but in the future we will be adding a way to find the `mine` value for this fallback.
|
|
239
|
+
function ShadowMessageFallback({ text, mine }: { text: string; mine?: boolean }) {
|
|
240
|
+
const styles = useStyles({ mine })
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<View style={styles.message}>
|
|
244
|
+
<View style={styles.messageContent}>
|
|
245
|
+
<View style={styles.messageBubble}>
|
|
246
|
+
<Text variant="footnote" style={styles.messageText}>
|
|
247
|
+
{text}
|
|
248
|
+
</Text>
|
|
249
|
+
</View>
|
|
250
|
+
</View>
|
|
251
|
+
</View>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
206
255
|
interface StylesProps {
|
|
207
256
|
imageWidth?: number
|
|
208
257
|
imageHeight?: number
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MessageResource } from '../types'
|
|
2
|
+
import { useApiGet } from './use_api'
|
|
3
|
+
import { getMessageRequestArgs, getMessageQueryKey } from '../utils/request/get_message'
|
|
4
|
+
|
|
5
|
+
export const useConversationMessage = ({
|
|
6
|
+
conversation_id,
|
|
7
|
+
messageId,
|
|
8
|
+
}: {
|
|
9
|
+
conversation_id: number
|
|
10
|
+
messageId: string
|
|
11
|
+
}) => {
|
|
12
|
+
const {
|
|
13
|
+
data: message,
|
|
14
|
+
isError,
|
|
15
|
+
isLoading,
|
|
16
|
+
} = useApiGet<MessageResource>(getMessageRequestArgs({ conversation_id, messageId }))
|
|
17
|
+
const queryKey = getMessageQueryKey({ conversation_id, messageId })
|
|
18
|
+
|
|
19
|
+
return { message, isError, isLoading, queryKey }
|
|
20
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { MessageResource } from '../types'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
SuspensePaginatorOptions,
|
|
6
|
-
useSuspensePaginator,
|
|
7
|
-
} from './use_suspense_api'
|
|
3
|
+
import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
|
|
4
|
+
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
8
5
|
|
|
9
6
|
export const useConversationMessages = (
|
|
10
|
-
{ conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string },
|
|
7
|
+
{ conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
|
|
11
8
|
opts?: SuspensePaginatorOptions
|
|
12
9
|
) => {
|
|
13
10
|
const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
|
|
@@ -27,50 +24,3 @@ export const useConversationMessages = (
|
|
|
27
24
|
|
|
28
25
|
return { messages, refetch, isRefetching, fetchNextPage, queryKey }
|
|
29
26
|
}
|
|
30
|
-
|
|
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'],
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
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 })
|
|
75
|
-
return getRequestQueryKey(requestArgs)
|
|
76
|
-
}
|
|
@@ -12,7 +12,7 @@ import { transformMessageEventDataToMessageResource } from '../utils/jolt/transf
|
|
|
12
12
|
import { getRequestQueryKey } from './use_suspense_api'
|
|
13
13
|
import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
|
|
14
14
|
import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
|
|
15
|
-
import { getMessagesRequestArgs } from '../utils/request/
|
|
15
|
+
import { getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
16
16
|
import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
|
|
17
17
|
import { isTemporaryMessageId } from './use_message_create_or_update'
|
|
18
18
|
import { completeMessageCreationTracking } from '../utils/performance_tracking'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InfiniteData, useMutation } from '@tanstack/react-query'
|
|
2
|
-
import { getMessagesQueryKey, getMessagesRequestArgs } from '
|
|
2
|
+
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
3
3
|
import { useApiClient } from './use_api_client'
|
|
4
4
|
import { ApiCollection, ApiResource, MessageResource } from '../types'
|
|
5
5
|
import { chatQueryClient } from '../contexts/api_provider'
|
|
@@ -18,7 +18,7 @@ import { startMessageCreationTracking } from '../utils/performance_tracking'
|
|
|
18
18
|
interface Props {
|
|
19
19
|
conversationId: number
|
|
20
20
|
message?: MessageResource
|
|
21
|
-
replyRootId?: string
|
|
21
|
+
replyRootId?: string | null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
|
|
@@ -2,7 +2,7 @@ import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query
|
|
|
2
2
|
import { useCallback } from 'react'
|
|
3
3
|
import { Alert } from 'react-native'
|
|
4
4
|
import { useApiClient } from './use_api_client'
|
|
5
|
-
import { getMessagesQueryKey, getMessagesRequestArgs } from '
|
|
5
|
+
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
6
6
|
import { ApiCollection, ApiResource, MessageResource } from '../types'
|
|
7
7
|
import { ReactionCountResource } from '../types/resources/reaction'
|
|
8
8
|
import { updateRecordInPagesData } from '../utils'
|
|
@@ -34,10 +34,12 @@ import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
|
34
34
|
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
|
+
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
|
|
38
|
+
import { REPLIES_FEATURE_ENABLED } from '../utils'
|
|
37
39
|
|
|
38
40
|
export type ConversationRouteProps = {
|
|
39
41
|
conversation_id: number
|
|
40
|
-
reply_root_id?: string
|
|
42
|
+
reply_root_id?: string | null
|
|
41
43
|
replyRootAuthor?: string
|
|
42
44
|
chat_group_graph_id?: string
|
|
43
45
|
clear_input?: boolean
|
|
@@ -63,7 +65,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
63
65
|
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
64
66
|
useEnsureConversationsRouteExists()
|
|
65
67
|
useMarkLatestMessageRead({ conversation, messages })
|
|
66
|
-
const messagesWithSeparators = groupMessages(messages)
|
|
68
|
+
const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen: !!reply_root_id })
|
|
67
69
|
const noMessages = messagesWithSeparators.length === 0
|
|
68
70
|
|
|
69
71
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
@@ -136,6 +138,16 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
136
138
|
return <InlineDateSeparator {...item} />
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
if (item.type === 'ReplyShadowMessage') {
|
|
142
|
+
return (
|
|
143
|
+
<ReplyShadowMessage
|
|
144
|
+
{...item}
|
|
145
|
+
conversation_id={conversation_id}
|
|
146
|
+
inReplyScreen={!!reply_root_id}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
return (
|
|
140
152
|
<Message
|
|
141
153
|
{...item}
|
|
@@ -219,14 +231,33 @@ const useDateSeparatorStyles = () => {
|
|
|
219
231
|
})
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
type ReplyShadowMessage = {
|
|
235
|
+
type: 'ReplyShadowMessage'
|
|
236
|
+
id: string
|
|
237
|
+
messageId: string
|
|
238
|
+
isReplyShadowMessage: boolean
|
|
239
|
+
nextRendersAuthor: boolean
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface GroupMessagesProps {
|
|
243
|
+
ms: MessageResource[]
|
|
244
|
+
inReplyScreen?: boolean
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
248
|
+
let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
|
|
224
249
|
let encounteredOneOfMyMessages = false
|
|
225
250
|
|
|
226
251
|
ms.forEach((message, i) => {
|
|
227
252
|
const prevMessage = ms[i + 1]
|
|
228
253
|
const nextMessage = ms[i - 1]
|
|
229
254
|
const date = moment(message.createdAt).format('YYYY-MM-DD')
|
|
255
|
+
const inThread = message.replyRootId !== null
|
|
256
|
+
const nextMessageInThread = nextMessage?.replyRootId !== null
|
|
257
|
+
const threadRoot = message.replyRootId === message.id
|
|
258
|
+
const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
|
|
259
|
+
const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
|
|
260
|
+
const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
|
|
230
261
|
const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
|
|
231
262
|
const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id
|
|
232
263
|
const prevMessageMoreThan5Minutes =
|
|
@@ -235,6 +266,14 @@ export const groupMessages = (ms: MessageResource[]) => {
|
|
|
235
266
|
const nextMessageMoreThan5Minutes =
|
|
236
267
|
nextMessage &&
|
|
237
268
|
new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
|
|
269
|
+
const prevMessageIsDateSeparator =
|
|
270
|
+
prevMessage && date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')
|
|
271
|
+
const nextMessageIsDateSeparator =
|
|
272
|
+
nextMessage && date !== moment(nextMessage.createdAt).format('YYYY-MM-DD')
|
|
273
|
+
const insertReplyShadowMessage =
|
|
274
|
+
message.replyRootId &&
|
|
275
|
+
!threadRoot &&
|
|
276
|
+
(prevMessageDifferentThread || prevMessageIsDateSeparator)
|
|
238
277
|
|
|
239
278
|
if (message.mine && !encounteredOneOfMyMessages) {
|
|
240
279
|
encounteredOneOfMyMessages = true
|
|
@@ -245,8 +284,42 @@ export const groupMessages = (ms: MessageResource[]) => {
|
|
|
245
284
|
message.lastInGroup = !nextMessage || nextMessageDifferentAuthor || nextMessageMoreThan5Minutes
|
|
246
285
|
message.renderAuthor =
|
|
247
286
|
!message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
|
|
287
|
+
message.threadPosition = null
|
|
288
|
+
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
289
|
+
message.isReplyShadowMessage = false
|
|
290
|
+
message.nextIsReplyShadowMessage =
|
|
291
|
+
REPLIES_FEATURE_ENABLED &&
|
|
292
|
+
nextMessageInThread &&
|
|
293
|
+
!nextMessageThreadRoot &&
|
|
294
|
+
(nextMessageDifferentThread || nextMessageIsDateSeparator)
|
|
295
|
+
|
|
296
|
+
if (!inReplyScreen && inThread) {
|
|
297
|
+
message.prevIsMyReply = prevMessage?.mine
|
|
298
|
+
message.nextIsMyReply = nextMessage?.mine
|
|
299
|
+
|
|
300
|
+
const firstInThread = threadRoot
|
|
301
|
+
const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator
|
|
302
|
+
|
|
303
|
+
if (firstInThread && lastInThread)
|
|
304
|
+
message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
|
|
305
|
+
else if (firstInThread) message.threadPosition = 'first'
|
|
306
|
+
else if (lastInThread) message.threadPosition = 'last'
|
|
307
|
+
else if (!prevMessageDifferentThread && !nextMessageDifferentThread)
|
|
308
|
+
message.threadPosition = 'center'
|
|
309
|
+
}
|
|
248
310
|
|
|
249
311
|
enrichedMessages.push(message)
|
|
312
|
+
|
|
313
|
+
if (insertReplyShadowMessage && REPLIES_FEATURE_ENABLED) {
|
|
314
|
+
enrichedMessages.push({
|
|
315
|
+
type: 'ReplyShadowMessage',
|
|
316
|
+
id: `${message.id}-${message.replyRootId}`,
|
|
317
|
+
messageId: message.replyRootId!,
|
|
318
|
+
isReplyShadowMessage: true,
|
|
319
|
+
nextRendersAuthor: message?.renderAuthor,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
250
323
|
if (!prevMessage || date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')) {
|
|
251
324
|
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
|
|
252
325
|
}
|
|
@@ -23,6 +23,12 @@ export interface MessageResource {
|
|
|
23
23
|
renderTime?: boolean
|
|
24
24
|
myLatestInConversation?: boolean
|
|
25
25
|
lastInGroup?: boolean
|
|
26
|
+
threadPosition?: 'first' | 'center' | 'last' | null
|
|
27
|
+
prevIsMyReply?: boolean
|
|
28
|
+
nextIsMyReply?: boolean
|
|
29
|
+
nextRendersAuthor?: boolean
|
|
30
|
+
isReplyShadowMessage?: boolean
|
|
31
|
+
nextIsReplyShadowMessage?: boolean
|
|
26
32
|
|
|
27
33
|
// Properties for mutation state
|
|
28
34
|
pending?: boolean // Indicates if the message is optimistically created and pending server response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InfiniteData } from '@tanstack/react-query'
|
|
2
2
|
import { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'
|
|
3
|
-
import { getMessagesQueryKey } from '../../
|
|
3
|
+
import { getMessagesQueryKey } from '../../utils/request/get_messages'
|
|
4
4
|
import { chatQueryClient } from '../../contexts/api_provider'
|
|
5
5
|
import { updateOrCreateRecordInPagesData } from './page_mutations'
|
|
6
6
|
import { convertAttachmentsForCreate } from '../convert_attachments_for_create'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InfiniteData } from '@tanstack/react-query'
|
|
2
2
|
import { ApiCollection, MessageResource } from '../../types'
|
|
3
|
-
import { getMessagesQueryKey } from '../../
|
|
3
|
+
import { getMessagesQueryKey } from '../../utils/request/get_messages'
|
|
4
4
|
import { chatQueryClient } from '../../contexts/api_provider'
|
|
5
5
|
import { updateOrCreateRecordInPagesData } from './page_mutations'
|
|
6
6
|
|
package/src/utils/pluralize.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const irregularInflections: Record<string, string> = { person: 'people' }
|
|
1
|
+
const irregularInflections: Record<string, string> = { person: 'people', reply: 'replies' }
|
|
2
2
|
|
|
3
3
|
export function pluralize(count: number, singularWord: string, includeCount = true) {
|
|
4
4
|
const plural = count !== 1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getRequestQueryKey } from '../../hooks/use_suspense_api'
|
|
2
|
+
import { getMessageFields, getMessagesInclude } from './messages_data_options'
|
|
3
|
+
|
|
4
|
+
export const getMessageRequestArgs = ({
|
|
5
|
+
conversation_id,
|
|
6
|
+
messageId,
|
|
7
|
+
}: {
|
|
8
|
+
conversation_id: number
|
|
9
|
+
messageId: string
|
|
10
|
+
}) => {
|
|
11
|
+
const url = `/me/conversations/${conversation_id}/messages/${messageId}`
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
url,
|
|
15
|
+
data: {
|
|
16
|
+
perPage: 25,
|
|
17
|
+
fields: getMessageFields,
|
|
18
|
+
include: getMessagesInclude,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getMessageQueryKey = ({
|
|
24
|
+
conversation_id,
|
|
25
|
+
messageId,
|
|
26
|
+
}: {
|
|
27
|
+
conversation_id: number
|
|
28
|
+
messageId: string
|
|
29
|
+
}) => {
|
|
30
|
+
const requestArgs = getMessageRequestArgs({ conversation_id, messageId })
|
|
31
|
+
return getRequestQueryKey(requestArgs)
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getRequestQueryKey } from '../../hooks/use_suspense_api'
|
|
2
|
+
import { getMessageFields, getMessagesInclude } from './messages_data_options'
|
|
3
|
+
|
|
4
|
+
export const getMessagesRequestArgs = ({
|
|
5
|
+
conversation_id,
|
|
6
|
+
reply_root_id,
|
|
7
|
+
}: {
|
|
8
|
+
conversation_id: number
|
|
9
|
+
reply_root_id?: string | null
|
|
10
|
+
}) => {
|
|
11
|
+
const url = reply_root_id
|
|
12
|
+
? `/me/conversations/${conversation_id}/messages/${reply_root_id}/replies`
|
|
13
|
+
: `/me/conversations/${conversation_id}/messages`
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
url,
|
|
17
|
+
data: {
|
|
18
|
+
perPage: 25,
|
|
19
|
+
fields: getMessageFields,
|
|
20
|
+
include: getMessagesInclude,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getMessagesQueryKey = ({
|
|
26
|
+
conversation_id,
|
|
27
|
+
reply_root_id,
|
|
28
|
+
}: {
|
|
29
|
+
conversation_id: number
|
|
30
|
+
reply_root_id?: string | null
|
|
31
|
+
}) => {
|
|
32
|
+
const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
|
|
33
|
+
return getRequestQueryKey(requestArgs)
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const getMessageFields = {
|
|
2
|
+
Message: [
|
|
3
|
+
'text',
|
|
4
|
+
'text_edited_at',
|
|
5
|
+
'mine',
|
|
6
|
+
'attachments',
|
|
7
|
+
'created_at',
|
|
8
|
+
'deleted_at',
|
|
9
|
+
'author',
|
|
10
|
+
'reaction_counts',
|
|
11
|
+
'reply_count',
|
|
12
|
+
'reply_root',
|
|
13
|
+
],
|
|
14
|
+
Person: ['name', 'avatar'],
|
|
15
|
+
ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getMessagesInclude = ['author', 'reaction_counts']
|