@planningcenter/chat-react-native 3.18.0-rc.0 → 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/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/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
|
@@ -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
|
/>
|
|
@@ -14,9 +14,9 @@ import { FlatList, Platform, StyleSheet, View } from 'react-native'
|
|
|
14
14
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
15
15
|
import { Badge, Icon, Text } from '../components'
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '../components/conversation/
|
|
17
|
+
LeaderMessagesDisabledBanner,
|
|
18
|
+
MemberMessagesDisabledBanner,
|
|
19
|
+
} from '../components/conversation/messages_disabled_banners'
|
|
20
20
|
import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
|
|
21
21
|
import BlankState from '../components/primitive/blank_state_primitive'
|
|
22
22
|
import { Message } from '../components/conversation/message'
|
|
@@ -35,7 +35,8 @@ 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
|
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
|
|
38
|
-
import {
|
|
38
|
+
import { availableFeatures, useFeatures } from '../hooks/use_features'
|
|
39
|
+
import { ConversationContextProvider } from '../contexts/conversation_context'
|
|
39
40
|
|
|
40
41
|
export type ConversationRouteProps = {
|
|
41
42
|
conversation_id: number
|
|
@@ -53,6 +54,19 @@ export type ConversationRouteProps = {
|
|
|
53
54
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
54
55
|
|
|
55
56
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
57
|
+
const { conversation_id, reply_root_id } = route.params
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ConversationContextProvider
|
|
61
|
+
conversationId={conversation_id}
|
|
62
|
+
currentPageReplyRootId={reply_root_id ?? null}
|
|
63
|
+
>
|
|
64
|
+
<ConversationScreenContent route={route} />
|
|
65
|
+
</ConversationContextProvider>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
56
70
|
const styles = useStyles()
|
|
57
71
|
const navigation = useNavigation()
|
|
58
72
|
const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
|
|
@@ -66,7 +80,13 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
66
80
|
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
67
81
|
useEnsureConversationsRouteExists()
|
|
68
82
|
useMarkLatestMessageRead({ conversation, messages })
|
|
69
|
-
const
|
|
83
|
+
const { featureEnabled } = useFeatures()
|
|
84
|
+
const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
|
|
85
|
+
const messagesWithSeparators = groupMessages({
|
|
86
|
+
ms: messages,
|
|
87
|
+
inReplyScreen: !!reply_root_id,
|
|
88
|
+
repliesEnabled,
|
|
89
|
+
})
|
|
70
90
|
const noMessages = messagesWithSeparators.length === 0
|
|
71
91
|
|
|
72
92
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
@@ -164,6 +184,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
164
184
|
conversation_id={conversation_id}
|
|
165
185
|
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
166
186
|
inReplyScreen={!!reply_root_id}
|
|
187
|
+
repliesEnabled={repliesEnabled}
|
|
167
188
|
/>
|
|
168
189
|
)
|
|
169
190
|
}}
|
|
@@ -172,8 +193,8 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
172
193
|
/>
|
|
173
194
|
)}
|
|
174
195
|
<JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
|
|
175
|
-
{!noMessages && <TypingIndicator
|
|
176
|
-
{showLeaderDisabledReplyBanner && <
|
|
196
|
+
{!noMessages && <TypingIndicator />}
|
|
197
|
+
{showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
|
|
177
198
|
{canReply ? (
|
|
178
199
|
<MessageForm.Root
|
|
179
200
|
replyRootAuthorFirstName={replyRootAuthorFirstName}
|
|
@@ -194,7 +215,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
194
215
|
<MessageForm.SubmitButton />
|
|
195
216
|
</MessageForm.Root>
|
|
196
217
|
) : (
|
|
197
|
-
<
|
|
218
|
+
<MemberMessagesDisabledBanner />
|
|
198
219
|
)}
|
|
199
220
|
</KeyboardView>
|
|
200
221
|
</View>
|
|
@@ -252,9 +273,14 @@ type ReplyShadowMessage = {
|
|
|
252
273
|
interface GroupMessagesProps {
|
|
253
274
|
ms: MessageResource[]
|
|
254
275
|
inReplyScreen?: boolean
|
|
276
|
+
repliesEnabled?: boolean
|
|
255
277
|
}
|
|
256
278
|
|
|
257
|
-
export const groupMessages = ({
|
|
279
|
+
export const groupMessages = ({
|
|
280
|
+
ms,
|
|
281
|
+
inReplyScreen,
|
|
282
|
+
repliesEnabled = false,
|
|
283
|
+
}: GroupMessagesProps) => {
|
|
258
284
|
let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
|
|
259
285
|
let encounteredOneOfMyMessages = false
|
|
260
286
|
|
|
@@ -284,6 +310,24 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
|
284
310
|
message.replyRootId &&
|
|
285
311
|
!threadRoot &&
|
|
286
312
|
(prevMessageDifferentThread || prevMessageIsDateSeparator)
|
|
313
|
+
const lastInGroup =
|
|
314
|
+
!nextMessage ||
|
|
315
|
+
nextMessageDifferentAuthor ||
|
|
316
|
+
nextMessageMoreThan5Minutes ||
|
|
317
|
+
nextMessageDifferentThread ||
|
|
318
|
+
nextMessageIsDateSeparator
|
|
319
|
+
const renderAuthor =
|
|
320
|
+
!message.mine &&
|
|
321
|
+
(!prevMessage ||
|
|
322
|
+
prevMessageDifferentAuthor ||
|
|
323
|
+
prevMessageMoreThan5Minutes ||
|
|
324
|
+
prevMessageDifferentThread ||
|
|
325
|
+
prevMessageIsDateSeparator)
|
|
326
|
+
const nextIsReplyShadowMessage =
|
|
327
|
+
repliesEnabled &&
|
|
328
|
+
nextMessageInThread &&
|
|
329
|
+
!nextMessageThreadRoot &&
|
|
330
|
+
(nextMessageDifferentThread || nextMessageIsDateSeparator)
|
|
287
331
|
|
|
288
332
|
if (message.mine && !encounteredOneOfMyMessages) {
|
|
289
333
|
encounteredOneOfMyMessages = true
|
|
@@ -291,17 +335,12 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
|
291
335
|
} else {
|
|
292
336
|
message.myLatestInConversation = false
|
|
293
337
|
}
|
|
294
|
-
message.lastInGroup =
|
|
295
|
-
message.renderAuthor =
|
|
296
|
-
!message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
|
|
338
|
+
message.lastInGroup = lastInGroup
|
|
339
|
+
message.renderAuthor = renderAuthor
|
|
297
340
|
message.threadPosition = null
|
|
298
341
|
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
299
342
|
message.isReplyShadowMessage = false
|
|
300
|
-
message.nextIsReplyShadowMessage =
|
|
301
|
-
REPLIES_FEATURE_ENABLED &&
|
|
302
|
-
nextMessageInThread &&
|
|
303
|
-
!nextMessageThreadRoot &&
|
|
304
|
-
(nextMessageDifferentThread || nextMessageIsDateSeparator)
|
|
343
|
+
message.nextIsReplyShadowMessage = nextIsReplyShadowMessage
|
|
305
344
|
|
|
306
345
|
if (!inReplyScreen && inThread) {
|
|
307
346
|
message.prevIsMyReply = prevMessage?.mine
|
|
@@ -314,13 +353,12 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
|
314
353
|
message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
|
|
315
354
|
else if (firstInThread) message.threadPosition = 'first'
|
|
316
355
|
else if (lastInThread) message.threadPosition = 'last'
|
|
317
|
-
else
|
|
318
|
-
message.threadPosition = 'center'
|
|
356
|
+
else message.threadPosition = 'center'
|
|
319
357
|
}
|
|
320
358
|
|
|
321
359
|
enrichedMessages.push(message)
|
|
322
360
|
|
|
323
|
-
if (insertReplyShadowMessage &&
|
|
361
|
+
if (insertReplyShadowMessage && repliesEnabled) {
|
|
324
362
|
enrichedMessages.push({
|
|
325
363
|
type: 'ReplyShadowMessage',
|
|
326
364
|
id: `${message.id}-${message.replyRootId}`,
|
package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { StyleSheet, View } from 'react-native'
|
|
4
|
-
import { FlashList } from '@shopify/flash-list'
|
|
3
|
+
import { FlatList, StyleSheet, View } from 'react-native'
|
|
5
4
|
import { Heading } from '../../components'
|
|
6
5
|
import { GroupsGroupResource } from '../../types'
|
|
7
6
|
import { useGroupsGroups } from '../../hooks/use_groups_groups'
|
|
@@ -31,10 +30,9 @@ export const ConversationSelectGroupRecipientsScreen = ({
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
return (
|
|
34
|
-
<
|
|
33
|
+
<FlatList
|
|
35
34
|
data={groupsWithCreatePermission}
|
|
36
35
|
keyExtractor={item => item.id.toString()}
|
|
37
|
-
estimatedItemSize={65}
|
|
38
36
|
contentContainerStyle={styles.contentContainer}
|
|
39
37
|
ListHeaderComponent={
|
|
40
38
|
<View style={styles.sectionHeader}>
|