@planningcenter/chat-react-native 3.18.0-rc.1 → 3.18.0-rc.10
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 +28 -17
- 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/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 +42 -20
- 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/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,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]
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { ApiCollection, MessageResource } from '../types'
|
|
2
2
|
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
3
|
-
import {
|
|
4
|
-
deleteRecordInPagesData,
|
|
5
|
-
updateOrCreateRecordInPagesData,
|
|
6
|
-
updateRecordInPagesData,
|
|
7
|
-
} from '../utils'
|
|
3
|
+
import { deleteRecordInPagesData } from '../utils'
|
|
8
4
|
import { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'
|
|
9
5
|
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
|
10
6
|
import { useCurrentPerson } from './use_current_person'
|
|
11
7
|
import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
|
|
12
8
|
import { getRequestQueryKey } from './use_suspense_api'
|
|
13
9
|
import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
|
|
14
|
-
import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
|
|
15
10
|
import { getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
16
11
|
import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
|
|
17
|
-
import { isTemporaryMessageId } from './use_message_create_or_update'
|
|
18
12
|
import { completeMessageCreationTracking } from '../utils/performance_tracking'
|
|
19
13
|
import { useApiClient } from './use_api_client'
|
|
14
|
+
import {
|
|
15
|
+
updateCacheWithMessage,
|
|
16
|
+
updateCacheWithReaction,
|
|
17
|
+
getThreadedMessagesQueryKey,
|
|
18
|
+
} from '../utils/cache/messages_cache'
|
|
20
19
|
|
|
21
20
|
interface Props {
|
|
22
21
|
conversationId: number
|
|
@@ -47,42 +46,17 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
isTemporaryMessageId(existingMessage.id) &&
|
|
62
|
-
existingMessage.text === message.text &&
|
|
63
|
-
existingMessage.mine
|
|
64
|
-
)
|
|
65
|
-
},
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return updateOrCreateRecordInPagesData({
|
|
70
|
-
data: dataAfterTempRemoval,
|
|
71
|
-
record: message,
|
|
72
|
-
processRecord: (record, current) => {
|
|
73
|
-
return { ...current, ...record }
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
} else {
|
|
77
|
-
return updateRecordInPagesData({
|
|
78
|
-
data: prev,
|
|
79
|
-
record: message,
|
|
80
|
-
processRecord: (record, current) => {
|
|
81
|
-
return { ...current, ...record }
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
})
|
|
49
|
+
// Update the main conversation cache
|
|
50
|
+
updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
|
|
51
|
+
|
|
52
|
+
// If message has a reply_root_id, also update the threaded cache
|
|
53
|
+
if (data.reply_root_id) {
|
|
54
|
+
const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
|
|
55
|
+
conversationId,
|
|
56
|
+
data.reply_root_id
|
|
57
|
+
)
|
|
58
|
+
updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
|
|
59
|
+
}
|
|
86
60
|
}
|
|
87
61
|
|
|
88
62
|
const handleMessageDeleted = async (e: MessageDeletedEvent) => {
|
|
@@ -92,50 +66,34 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
92
66
|
currentPersonId: currentPerson.id,
|
|
93
67
|
})
|
|
94
68
|
|
|
69
|
+
// Update the main conversation cache
|
|
95
70
|
queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
|
|
96
71
|
deleteRecordInPagesData({ data: prev, record: message })
|
|
97
72
|
)
|
|
73
|
+
|
|
74
|
+
// If message has a reply_root_id, also update the threaded cache
|
|
75
|
+
if (data.reply_root_id) {
|
|
76
|
+
const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
|
|
77
|
+
conversationId,
|
|
78
|
+
data.reply_root_id
|
|
79
|
+
)
|
|
80
|
+
queryClient.setQueryData<QueryData>(threadedMessagesQueryKey, prev =>
|
|
81
|
+
deleteRecordInPagesData({ data: prev, record: message })
|
|
82
|
+
)
|
|
83
|
+
}
|
|
98
84
|
}
|
|
99
85
|
|
|
100
86
|
const handleReactionJoltEvent = async (e: JoltReactionEvent) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (reactionCount.value === data.value) {
|
|
112
|
-
foundMatch = true
|
|
113
|
-
return transformReactionEventDataToReactionCountResource({
|
|
114
|
-
data,
|
|
115
|
-
oldData: reactionCount,
|
|
116
|
-
event: e.event,
|
|
117
|
-
currentPersonId: currentPerson.id,
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
return reactionCount
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
if (!foundMatch) {
|
|
124
|
-
const newReactionCount = transformReactionEventDataToReactionCountResource({
|
|
125
|
-
data,
|
|
126
|
-
event: e.event,
|
|
127
|
-
currentPersonId: currentPerson.id,
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (newReactionCount?.count) {
|
|
131
|
-
newReactionCounts = [...newReactionCounts, newReactionCount]
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { ...oldMessage, reactionCounts: newReactionCounts }
|
|
136
|
-
},
|
|
137
|
-
})
|
|
138
|
-
)
|
|
87
|
+
// Update the main conversation cache and capture the reply_root_id if present
|
|
88
|
+
updateCacheWithReaction(queryClient, messagesQueryKey, e, currentPerson.id)
|
|
89
|
+
|
|
90
|
+
const replyRootId = e.data.data.reply_root_id
|
|
91
|
+
|
|
92
|
+
// If the message has a reply_root_id, also update the threaded cache
|
|
93
|
+
if (replyRootId) {
|
|
94
|
+
const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, replyRootId)
|
|
95
|
+
updateCacheWithReaction(queryClient, threadedMessagesQueryKey, e, currentPerson.id)
|
|
96
|
+
}
|
|
139
97
|
}
|
|
140
98
|
|
|
141
99
|
const handleTypingEvent = async (e: JoltTypingEvent) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useCallback } from 'react'
|
|
3
|
+
import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
|
|
4
|
+
import { useApiClient } from './use_api_client'
|
|
5
|
+
import type { FeatureResource } from '../types/resources/feature_resource'
|
|
6
|
+
import { ApiCollection } from '../types'
|
|
7
|
+
|
|
8
|
+
export function useFeatures() {
|
|
9
|
+
const apiClient = useApiClient()
|
|
10
|
+
const requestArgs = getFeaturesRequestArgs()
|
|
11
|
+
|
|
12
|
+
const { data } = useSuspenseQuery({
|
|
13
|
+
queryKey: getFeaturesQueryKey(),
|
|
14
|
+
queryFn: () => {
|
|
15
|
+
return apiClient.chat
|
|
16
|
+
.get<ApiCollection<FeatureResource>>(requestArgs)
|
|
17
|
+
.catch(() => stableEmptyFeatures)
|
|
18
|
+
},
|
|
19
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const features = data.data
|
|
23
|
+
|
|
24
|
+
const featureEnabled = useCallback(
|
|
25
|
+
(featureName: string) =>
|
|
26
|
+
features.some(feature => feature.name === featureName && feature.enabled),
|
|
27
|
+
[features]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
features,
|
|
32
|
+
featureEnabled,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const availableFeatures = {
|
|
37
|
+
threaded_replies: 'QA_MOBILE_threaded_replies',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stableEmptyFeatures: ApiCollection<FeatureResource> = {
|
|
41
|
+
data: [],
|
|
42
|
+
links: {},
|
|
43
|
+
meta: {
|
|
44
|
+
count: 0,
|
|
45
|
+
totalCount: 0,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
@@ -14,6 +14,7 @@ import { useCurrentPerson } from './use_current_person'
|
|
|
14
14
|
import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
|
|
15
15
|
import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
|
|
16
16
|
import { startMessageCreationTracking } from '../utils/performance_tracking'
|
|
17
|
+
import { isNewMessage } from '../utils/cache/messages_cache'
|
|
17
18
|
|
|
18
19
|
interface Props {
|
|
19
20
|
conversationId: number
|
|
@@ -93,6 +94,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
93
94
|
text,
|
|
94
95
|
attachments,
|
|
95
96
|
currentPerson,
|
|
97
|
+
replyRootId,
|
|
96
98
|
})
|
|
97
99
|
|
|
98
100
|
return { message: optimisticMessage }
|
|
@@ -103,7 +105,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
103
105
|
|
|
104
106
|
// Add error to the optimistic message from the cache on error
|
|
105
107
|
if (optimisticMessage) {
|
|
106
|
-
const queryKey = getMessagesQueryKey({
|
|
108
|
+
const queryKey = getMessagesQueryKey({
|
|
109
|
+
conversation_id: conversationId,
|
|
110
|
+
reply_root_id: replyRootId,
|
|
111
|
+
})
|
|
107
112
|
chatQueryClient.setQueryData(
|
|
108
113
|
queryKey,
|
|
109
114
|
(data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>
|
|
@@ -123,7 +128,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
123
128
|
const { message: optimisticMessage } = context || {}
|
|
124
129
|
const updatedMessage = result.data
|
|
125
130
|
type QueryData = InfiniteData<ApiCollection<MessageResource>>
|
|
126
|
-
const queryKey = getMessagesQueryKey({
|
|
131
|
+
const queryKey = getMessagesQueryKey({
|
|
132
|
+
conversation_id: conversationId,
|
|
133
|
+
reply_root_id: replyRootId,
|
|
134
|
+
})
|
|
127
135
|
|
|
128
136
|
// First remove the optimistic message if it exists
|
|
129
137
|
if (optimisticMessage) {
|
|
@@ -148,13 +156,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
148
156
|
return mutation
|
|
149
157
|
}
|
|
150
158
|
|
|
151
|
-
export function isTemporaryMessageId(messageId?: string | null): boolean {
|
|
152
|
-
return !!messageId && messageId.endsWith('-temp')
|
|
153
|
-
}
|
|
154
|
-
export function isNewMessage(message?: MessageResource): boolean {
|
|
155
|
-
return !message?.id || isTemporaryMessageId(message.id)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
159
|
/**
|
|
159
160
|
* Generate a random UUID (v4) for idempotent keys.
|
|
160
161
|
* Uses Math.random, which is not cryptographically secure.
|
|
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
|
|
|
2
2
|
import { useMemo } from 'react'
|
|
3
3
|
import { PersonTyping } from './use_typing_status_cache'
|
|
4
4
|
import { useCurrentPerson } from './use_current_person'
|
|
5
|
+
import { useConversationContext } from '../contexts/conversation_context'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Hook for getting people currently typing in a conversation
|
|
@@ -9,9 +10,12 @@ import { useCurrentPerson } from './use_current_person'
|
|
|
9
10
|
* The query function itself doesn't do anything, but we add to the cache
|
|
10
11
|
* from useTypingStatusCache when we receive a typing event.
|
|
11
12
|
*/
|
|
12
|
-
export const useTypingIndicators = (
|
|
13
|
+
export const useTypingIndicators = () => {
|
|
14
|
+
const { conversationId, currentPageReplyRootId } = useConversationContext()
|
|
13
15
|
const cacheKey = useMemo(() => ['conversationTyping', String(conversationId)], [conversationId])
|
|
14
16
|
const currentPerson = useCurrentPerson()
|
|
17
|
+
const isCurrentPerson = (authorId: number) => authorId === currentPerson?.id
|
|
18
|
+
const inMainConversationView = !currentPageReplyRootId
|
|
15
19
|
const now = Date.now()
|
|
16
20
|
const { data } = useQuery({
|
|
17
21
|
queryKey: cacheKey,
|
|
@@ -19,8 +23,16 @@ export const useTypingIndicators = (conversationId: number) => {
|
|
|
19
23
|
initialData: stableArray,
|
|
20
24
|
})
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
return data.filter(({ author_id, expires, reply_root_id }) => {
|
|
27
|
+
if (isCurrentPerson(author_id)) return false
|
|
28
|
+
if (now > expires) return false
|
|
29
|
+
|
|
30
|
+
// If you are in the main conversation view, you will see any message sent
|
|
31
|
+
if (inMainConversationView) return true
|
|
32
|
+
|
|
33
|
+
// If you are in a reply view, you will only see messages sent in this reply thread
|
|
34
|
+
return reply_root_id === currentPageReplyRootId
|
|
35
|
+
})
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
/**
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react'
|
|
11
11
|
import {
|
|
12
|
+
FlatList,
|
|
12
13
|
StyleSheet,
|
|
13
14
|
TextInput,
|
|
14
15
|
View,
|
|
@@ -40,10 +41,11 @@ import {
|
|
|
40
41
|
} from '../hooks/use_conversation'
|
|
41
42
|
import { MemberResource, isDefined } from '../types'
|
|
42
43
|
import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
|
|
43
|
-
import { FlashList } from '@shopify/flash-list'
|
|
44
44
|
import { tokens } from '../vendor/tapestry/tokens'
|
|
45
45
|
import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
|
|
46
46
|
import { GroupResource } from '../types/resources/group_resource'
|
|
47
|
+
import { useFeatures } from '../hooks/use_features'
|
|
48
|
+
import { availableFeatures } from '../hooks/use_features'
|
|
47
49
|
|
|
48
50
|
// =========================================
|
|
49
51
|
// ====== Factory Constants & Types ========
|
|
@@ -93,6 +95,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
93
95
|
const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params)
|
|
94
96
|
const { mutate: saveTitle } = useConversationUpdate(route.params)
|
|
95
97
|
const { mutate: deleteConversation } = useConversationDelete(route.params)
|
|
98
|
+
const { featureEnabled } = useFeatures()
|
|
99
|
+
const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
|
|
96
100
|
|
|
97
101
|
const trimmedTitle = title.trim()
|
|
98
102
|
const emptyTitle = trimmedTitle === '' || title === null
|
|
@@ -250,8 +254,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
250
254
|
{
|
|
251
255
|
type: canUpdate ? SectionTypes.setting : SectionTypes.hidden,
|
|
252
256
|
data: {
|
|
253
|
-
title: 'Freeze conversation',
|
|
254
|
-
subtitle: 'Disables replies for everyone except leaders.',
|
|
257
|
+
title: repliesEnabled ? 'Leader messages only' : 'Freeze conversation',
|
|
258
|
+
subtitle: repliesEnabled ? undefined : 'Disables replies for everyone except leaders.',
|
|
255
259
|
rightItem: (
|
|
256
260
|
<Switch value={repliesDisabled} onChange={() => setRepliesDisabled(!repliesDisabled)} />
|
|
257
261
|
),
|
|
@@ -300,9 +304,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
300
304
|
|
|
301
305
|
return (
|
|
302
306
|
<View style={styles.listContainer}>
|
|
303
|
-
<
|
|
307
|
+
<FlatList
|
|
304
308
|
data={listData as SectionListData}
|
|
305
|
-
estimatedItemSize={52}
|
|
306
309
|
contentContainerStyle={styles.contentContainer}
|
|
307
310
|
renderItem={({ item, index }) => {
|
|
308
311
|
const [isStart, isEnd] = [
|
|
@@ -375,7 +378,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
375
378
|
return null
|
|
376
379
|
}
|
|
377
380
|
}}
|
|
378
|
-
onEndReached={fetchNextPageOfMembers}
|
|
381
|
+
onEndReached={() => fetchNextPageOfMembers()}
|
|
379
382
|
/>
|
|
380
383
|
</View>
|
|
381
384
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { StyleSheet, View } from 'react-native'
|
|
3
|
-
import { FlashList, type FlashListProps } from '@shopify/flash-list'
|
|
2
|
+
import { FlatList, type FlatListProps, StyleSheet, View } from 'react-native'
|
|
4
3
|
import { MemberResource } from '../../../types'
|
|
5
4
|
import { Person, Text } from '../../../components/display'
|
|
6
5
|
import { useTheme } from '../../../hooks'
|
|
@@ -8,7 +7,7 @@ import { useTheme } from '../../../hooks'
|
|
|
8
7
|
interface FormListProps {
|
|
9
8
|
memberData: MemberResource[]
|
|
10
9
|
loadingMore?: boolean
|
|
11
|
-
FormContent?:
|
|
10
|
+
FormContent?: FlatListProps<MemberResource>['ListHeaderComponent']
|
|
12
11
|
listEmptyText?: string
|
|
13
12
|
onEndReached?: () => void
|
|
14
13
|
}
|
|
@@ -23,7 +22,7 @@ export const FormList = ({
|
|
|
23
22
|
const styles = useStyles()
|
|
24
23
|
|
|
25
24
|
return (
|
|
26
|
-
<
|
|
25
|
+
<FlatList
|
|
27
26
|
data={memberData}
|
|
28
27
|
ListHeaderComponent={FormContent}
|
|
29
28
|
renderItem={({ item }) => <Person person={item} style={styles.person} />}
|
|
@@ -31,7 +30,6 @@ export const FormList = ({
|
|
|
31
30
|
loadingMore ? <Text style={styles.loadingMore}>Loading more...</Text> : null
|
|
32
31
|
}
|
|
33
32
|
keyExtractor={item => item.id.toString()}
|
|
34
|
-
estimatedItemSize={45}
|
|
35
33
|
ListEmptyComponent={<ListEmptyText text={listEmptyText || 'No members found'} />}
|
|
36
34
|
onEndReached={onEndReached}
|
|
37
35
|
/>
|