@planningcenter/chat-react-native 4.2.0-rc.0 → 4.2.0-rc.2
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 +3 -2
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +23 -22
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_list.d.ts +21 -0
- package/build/components/conversation/message_list.d.ts.map +1 -0
- package/build/components/conversation/message_list.js +273 -0
- package/build/components/conversation/message_list.js.map +1 -0
- package/build/components/conversation/message_read_receipts.d.ts +2 -4
- package/build/components/conversation/message_read_receipts.d.ts.map +1 -1
- package/build/components/conversation/message_read_receipts.js +2 -3
- package/build/components/conversation/message_read_receipts.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +2 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js +47 -46
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +12 -1
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +40 -28
- 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 +6 -1
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_features.d.ts +16 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +43 -47
- package/build/hooks/use_features.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +184 -365
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversations/components/search_list_header_component.d.ts +2 -0
- package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -0
- package/build/screens/conversations/components/search_list_header_component.js +496 -0
- package/build/screens/conversations/components/search_list_header_component.js.map +1 -0
- package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
- package/build/screens/conversations/conversations_screen.js +28 -16
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/types/resources/message.d.ts +1 -0
- package/build/types/resources/message.d.ts.map +1 -1
- package/build/types/resources/message.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +1 -0
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +10 -1
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/client/transform_response.d.ts.map +1 -1
- package/build/utils/client/transform_response.js +9 -5
- package/build/utils/client/transform_response.js.map +1 -1
- package/build/utils/group_messages.d.ts +1 -0
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +10 -1
- package/build/utils/group_messages.js.map +1 -1
- package/build/utils/replace_equal_by_id.d.ts +6 -0
- package/build/utils/replace_equal_by_id.d.ts.map +1 -0
- package/build/utils/replace_equal_by_id.js +41 -0
- package/build/utils/replace_equal_by_id.js.map +1 -0
- package/build/utils/request/messages_data_options.d.ts.map +1 -1
- package/build/utils/request/messages_data_options.js +1 -1
- package/build/utils/request/messages_data_options.js.map +1 -1
- package/build/utils/time.d.ts +1 -0
- package/build/utils/time.d.ts.map +1 -1
- package/build/utils/time.js +1 -0
- package/build/utils/time.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/hooks/use_features.test.ts +61 -0
- package/src/components/conversation/__tests__/message_list.test.tsx +277 -0
- package/src/components/conversation/__tests__/reply_shadow_message.test.tsx +119 -0
- package/src/components/conversation/message.tsx +15 -18
- package/src/components/conversation/message_list.tsx +157 -0
- package/src/components/conversation/message_read_receipts.tsx +3 -5
- package/src/components/conversation/reply_shadow_message.tsx +9 -7
- package/src/contexts/api_provider.tsx +4 -0
- package/src/hooks/use_conversation_messages.ts +5 -1
- package/src/hooks/use_conversation_messages_jolt_events.ts +7 -0
- package/src/hooks/use_features.ts +26 -14
- package/src/screens/conversation_screen.tsx +11 -104
- package/src/screens/conversations/components/search_list_header_component.tsx +291 -0
- package/src/screens/conversations/conversations_screen.tsx +6 -1
- package/src/types/resources/message.ts +1 -0
- package/src/utils/__tests__/group_messages.test.ts +48 -0
- package/src/utils/__tests__/replace_equal_by_id.test.ts +73 -0
- package/src/utils/cache/__tests__/messages_cache_reply_roots.test.ts +70 -0
- package/src/utils/cache/messages_cache.ts +21 -1
- package/src/utils/client/__tests__/transform_response.test.ts +62 -0
- package/src/utils/client/transform_response.ts +7 -5
- package/src/utils/group_messages.ts +18 -1
- package/src/utils/replace_equal_by_id.ts +42 -0
- package/src/utils/request/messages_data_options.ts +1 -1
- package/src/utils/time.ts +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
2
|
import { some } from 'lodash'
|
|
3
|
-
import React from 'react'
|
|
3
|
+
import React, { memo } from 'react'
|
|
4
4
|
import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
5
5
|
import Animated from 'react-native-reanimated'
|
|
6
6
|
import {
|
|
@@ -37,18 +37,20 @@ interface ReplyShadowMessageProps extends MessageResource {
|
|
|
37
37
|
nextRendersAuthor: boolean
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
40
|
+
export const ReplyShadowMessage = memo(function ReplyShadowMessageRow({
|
|
41
41
|
conversation_id,
|
|
42
42
|
inReplyScreen,
|
|
43
43
|
messageId,
|
|
44
|
+
replyRoot,
|
|
44
45
|
isReplyShadowMessage,
|
|
45
46
|
nextRendersAuthor,
|
|
46
47
|
}: ReplyShadowMessageProps) {
|
|
47
|
-
const
|
|
48
|
+
const fetched = useConversationMessage({ conversation_id, messageId, enabled: !replyRoot })
|
|
49
|
+
const message = replyRoot ?? fetched.message
|
|
48
50
|
|
|
49
51
|
if (inReplyScreen) return null
|
|
50
|
-
if (isLoading) return <ShadowMessageFallback text="Loading..." />
|
|
51
|
-
if (
|
|
52
|
+
if (!message && fetched.isLoading) return <ShadowMessageFallback text="Loading..." />
|
|
53
|
+
if (!message) return <ShadowMessageFallback text="Message deleted" />
|
|
52
54
|
|
|
53
55
|
const enrichedMessage = {
|
|
54
56
|
...message,
|
|
@@ -57,7 +59,7 @@ export function ReplyShadowMessage({
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
return <ShadowMessageContent conversation_id={conversation_id} {...enrichedMessage} />
|
|
60
|
-
}
|
|
62
|
+
})
|
|
61
63
|
|
|
62
64
|
interface ShadowMessageContentProps extends MessageResource {
|
|
63
65
|
conversation_id: number
|
|
@@ -82,7 +84,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
82
84
|
const handleNavigateToReplies = () => {
|
|
83
85
|
navigation.navigate('ConversationReply', {
|
|
84
86
|
conversation_id,
|
|
85
|
-
reply_root_id: message.replyRootId,
|
|
87
|
+
reply_root_id: message.replyRootId ?? message.id,
|
|
86
88
|
reply_root_author_name: message.author.name,
|
|
87
89
|
})
|
|
88
90
|
}
|
|
@@ -10,6 +10,7 @@ import { ViewProps } from 'react-native'
|
|
|
10
10
|
import { ApiClient, useApiClient } from '../hooks/use_api_client'
|
|
11
11
|
import { useAppState } from '../hooks/use_app_state'
|
|
12
12
|
import { appGrantsRequestArgs } from '../hooks/use_chat_permissions'
|
|
13
|
+
import { featuresQueryOptions } from '../hooks/use_features'
|
|
13
14
|
import { getRequestQueryKey, RequestQueryKey } from '../hooks/use_suspense_api'
|
|
14
15
|
import { emitAuthRefresh } from '../utils/auth_events'
|
|
15
16
|
import { ResponseError } from '../utils/response_error'
|
|
@@ -81,10 +82,13 @@ export function ApiProvider({ children }: ViewProps) {
|
|
|
81
82
|
// This needs to live in the provider so that it can access the api client
|
|
82
83
|
// and the chat query client
|
|
83
84
|
const PrefetchQueries = () => {
|
|
85
|
+
const client = useApiClient()
|
|
86
|
+
|
|
84
87
|
usePrefetchQuery({
|
|
85
88
|
queryKey: getRequestQueryKey(appGrantsRequestArgs),
|
|
86
89
|
queryFn: defaultQueryFn,
|
|
87
90
|
})
|
|
91
|
+
usePrefetchQuery(featuresQueryOptions(client))
|
|
88
92
|
|
|
89
93
|
return null
|
|
90
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { MessageResource } from '../types'
|
|
3
|
+
import { replaceEqualById } from '../utils/replace_equal_by_id'
|
|
3
4
|
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
4
5
|
import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
|
|
5
6
|
|
|
@@ -10,7 +11,10 @@ export const useConversationMessages = (
|
|
|
10
11
|
const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
|
|
11
12
|
useSuspensePaginator<MessageResource>(
|
|
12
13
|
getMessagesRequestArgs({ conversation_id, reply_root_id }),
|
|
13
|
-
|
|
14
|
+
{
|
|
15
|
+
structuralSharing: replaceEqualById,
|
|
16
|
+
...opts,
|
|
17
|
+
}
|
|
14
18
|
)
|
|
15
19
|
const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
|
|
16
20
|
const messages = useMemo(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
updateCacheWithMessage,
|
|
12
12
|
updateCacheWithIndividualMessage,
|
|
13
13
|
updateCacheWithReaction,
|
|
14
|
+
updateCacheReplyRoots,
|
|
14
15
|
getThreadedMessagesQueryKey,
|
|
15
16
|
} from '../utils/cache/messages_cache'
|
|
16
17
|
import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
|
|
@@ -44,6 +45,7 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
44
45
|
data,
|
|
45
46
|
currentPersonId: currentPerson.id,
|
|
46
47
|
})
|
|
48
|
+
const isReplyRootUpdate = e.event === 'message.updated' && data.reply_root_id === data.sort_key
|
|
47
49
|
|
|
48
50
|
if (e.event === 'message.created' && data.author_id) {
|
|
49
51
|
removeAllTypingEventsByAuthorId(data.author_id)
|
|
@@ -62,6 +64,11 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
62
64
|
data.reply_root_id
|
|
63
65
|
)
|
|
64
66
|
updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
|
|
67
|
+
|
|
68
|
+
if (isReplyRootUpdate) {
|
|
69
|
+
updateCacheReplyRoots(queryClient, messagesQueryKey, message)
|
|
70
|
+
updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, message)
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
// Update individual message query cache so reply shadow messages update immediately
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
1
|
+
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
|
|
2
2
|
import { useCallback } from 'react'
|
|
3
3
|
import { ApiCollection } from '../types'
|
|
4
4
|
import type { FeatureResource } from '../types/resources/feature_resource'
|
|
5
5
|
import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
|
|
6
|
-
import { FIVE_MINUTES } from '../utils/time'
|
|
7
|
-
import { useApiClient } from './use_api_client'
|
|
6
|
+
import { FIVE_MINUTES, ONE_MINUTE } from '../utils/time'
|
|
7
|
+
import { ApiClient, useApiClient } from './use_api_client'
|
|
8
8
|
|
|
9
9
|
type FeatureName = (typeof availableFeatures)[keyof typeof availableFeatures]
|
|
10
10
|
|
|
11
|
+
type FeaturesCollection = ApiCollection<FeatureResource> & {
|
|
12
|
+
failure?: { status?: number }
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export function useFeatures() {
|
|
12
16
|
const apiClient = useApiClient()
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const { data } = useSuspenseQuery({
|
|
16
|
-
queryKey: getFeaturesQueryKey(),
|
|
17
|
-
queryFn: () => {
|
|
18
|
-
return apiClient.chat
|
|
19
|
-
.get<ApiCollection<FeatureResource>>(requestArgs)
|
|
20
|
-
.catch(() => stableEmptyFeatures)
|
|
21
|
-
},
|
|
22
|
-
staleTime: FIVE_MINUTES,
|
|
23
|
-
})
|
|
17
|
+
const { data } = useSuspenseQuery(featuresQueryOptions(apiClient))
|
|
24
18
|
|
|
25
19
|
const features = data.data
|
|
26
20
|
|
|
@@ -36,6 +30,23 @@ export function useFeatures() {
|
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
|
|
33
|
+
export const featuresQueryOptions = (apiClient: ApiClient) =>
|
|
34
|
+
queryOptions({
|
|
35
|
+
queryKey: getFeaturesQueryKey(),
|
|
36
|
+
queryFn: (): Promise<FeaturesCollection> => {
|
|
37
|
+
return apiClient.chat
|
|
38
|
+
.get<ApiCollection<FeatureResource>>(getFeaturesRequestArgs())
|
|
39
|
+
.catch(error => ({ ...stableEmptyFeatures, failure: { status: error?.status } }))
|
|
40
|
+
},
|
|
41
|
+
staleTime: query => {
|
|
42
|
+
const failure = query.state.data?.failure
|
|
43
|
+
if (!failure) return FIVE_MINUTES
|
|
44
|
+
|
|
45
|
+
return failure.status === 429 ? ONE_MINUTE : 0
|
|
46
|
+
},
|
|
47
|
+
gcTime: Infinity,
|
|
48
|
+
})
|
|
49
|
+
|
|
39
50
|
export const availableFeatures = {
|
|
40
51
|
gender_specific_conversations: 'ROLLOUT_gender_specific_conversations',
|
|
41
52
|
message_reporting: 'ROLLOUT_MOBILE_message_reporting',
|
|
@@ -44,6 +55,7 @@ export const availableFeatures = {
|
|
|
44
55
|
conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
|
|
45
56
|
video_moderation: 'ROLLOUT_MOBILE_video_moderation',
|
|
46
57
|
dm_search_clear_fix: 'ROLLOUT_MOBILE_dm_search_clear_fix',
|
|
58
|
+
search: 'ROLLOUT_search',
|
|
47
59
|
} as const satisfies Record<string, `ROLLOUT_${string}`>
|
|
48
60
|
|
|
49
61
|
const stableEmptyFeatures: ApiCollection<FeatureResource> = {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { date as formatDate } from '@planningcenter/datetime-fmt'
|
|
2
1
|
import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
|
|
3
2
|
import {
|
|
4
3
|
CommonActions,
|
|
@@ -11,24 +10,21 @@ import {
|
|
|
11
10
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
12
11
|
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
13
12
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
14
|
-
import { Badge, Icon
|
|
13
|
+
import { Badge, Icon } from '../components'
|
|
15
14
|
import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
|
|
16
15
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
17
|
-
import { Message } from '../components/conversation/message'
|
|
18
16
|
import { MessageForm } from '../components/conversation/message_form'
|
|
17
|
+
import { MessageList } from '../components/conversation/message_list'
|
|
19
18
|
import {
|
|
20
19
|
ConversationDisabledBanner,
|
|
21
20
|
LeaderMessagesDisabledBanner,
|
|
22
21
|
MemberMessagesDisabledBanner,
|
|
23
22
|
} from '../components/conversation/messages_disabled_banners'
|
|
24
|
-
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
|
|
25
|
-
import { SystemMessage } from '../components/conversation/system_message'
|
|
26
23
|
import { TypingIndicator } from '../components/conversation/typing_indicator'
|
|
27
24
|
import { KeyboardView } from '../components/display/keyboard_view'
|
|
28
25
|
import { useHeaderSlotReserve } from '../components/display/platform_modal_header_buttons'
|
|
29
26
|
import BlankState from '../components/primitive/blank_state_primitive'
|
|
30
27
|
import { ConversationContextProvider } from '../contexts/conversation_context'
|
|
31
|
-
import { useTheme } from '../hooks'
|
|
32
28
|
import { useMessageAvailableMembersAction } from '../hooks/direct_messages/use_message_available_members_action'
|
|
33
29
|
import { useConversation } from '../hooks/use_conversation'
|
|
34
30
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
@@ -45,10 +41,6 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
|
|
|
45
41
|
import { ConversationResource } from '../types/resources/conversation'
|
|
46
42
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
47
43
|
import { MessageResource } from '../types/resources/message'
|
|
48
|
-
import { getRelativeDateStatus } from '../utils/date'
|
|
49
|
-
import { groupMessages, type DateSeparator } from '../utils/group_messages'
|
|
50
|
-
import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
|
|
51
|
-
import { isSystemMessage } from '../utils/system_messages'
|
|
52
44
|
|
|
53
45
|
export type ConversationRouteProps = {
|
|
54
46
|
conversation_id: number
|
|
@@ -101,16 +93,11 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
101
93
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
102
94
|
useEnsureConversationsRouteExists()
|
|
103
95
|
useMarkLatestMessageRead({ conversation, messages })
|
|
104
|
-
const
|
|
105
|
-
ms: messages,
|
|
106
|
-
inReplyScreen: !!reply_root_id,
|
|
107
|
-
})
|
|
108
|
-
const noMessages = messagesWithSeparators.length === 0
|
|
96
|
+
const noMessages = messages.length === 0
|
|
109
97
|
|
|
110
98
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
111
99
|
const canReply = memberAbility?.canReply
|
|
112
100
|
const showLeaderDisabledReplyBanner = canReply && repliesDisabled
|
|
113
|
-
const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
|
|
114
101
|
const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
|
|
115
102
|
const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
|
|
116
103
|
const replyHeaderTitle = replyRootAuthorFirstName
|
|
@@ -181,54 +168,19 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
181
168
|
<ConversationStartBanner conversation={conversation} hasMessages={false} />
|
|
182
169
|
</View>
|
|
183
170
|
) : (
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
<MessageList
|
|
172
|
+
listRef={listRef}
|
|
173
|
+
conversation={conversation}
|
|
174
|
+
conversationId={conversation_id}
|
|
175
|
+
messages={messages}
|
|
176
|
+
replyRootId={reply_root_id}
|
|
177
|
+
hasNextPage={hasNextPage}
|
|
178
|
+
onEndReached={() => fetchNextPage()}
|
|
190
179
|
onScroll={trackScroll}
|
|
191
180
|
onLayout={onLayout}
|
|
192
181
|
onContentSizeChange={onContentSizeChange}
|
|
193
182
|
onScrollEndDrag={onScrollEndDrag}
|
|
194
183
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
195
|
-
scrollEventThrottle={10}
|
|
196
|
-
ListFooterComponent={
|
|
197
|
-
hasNextPage ? null : (
|
|
198
|
-
<ConversationStartBanner conversation={conversation} hasMessages={true} />
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
renderItem={({ item }) => {
|
|
202
|
-
if (item.type === 'DateSeparator') {
|
|
203
|
-
return <InlineDateSeparator {...item} />
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (item.type === 'ReplyShadowMessage') {
|
|
207
|
-
return (
|
|
208
|
-
<ReplyShadowMessage
|
|
209
|
-
{...(item as any)}
|
|
210
|
-
conversation_id={conversation_id}
|
|
211
|
-
inReplyScreen={!!reply_root_id}
|
|
212
|
-
/>
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (isSystemMessage(item)) {
|
|
217
|
-
return <SystemMessage message={item} conversationId={conversation_id} />
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return (
|
|
221
|
-
<Message
|
|
222
|
-
{...item}
|
|
223
|
-
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
|
|
224
|
-
conversation_id={conversation_id}
|
|
225
|
-
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
226
|
-
inReplyScreen={!!reply_root_id}
|
|
227
|
-
/>
|
|
228
|
-
)
|
|
229
|
-
}}
|
|
230
|
-
onEndReached={() => fetchNextPage()}
|
|
231
|
-
ListHeaderComponent={<View style={styles.listHeader} />}
|
|
232
184
|
/>
|
|
233
185
|
)}
|
|
234
186
|
<JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
|
|
@@ -295,44 +247,6 @@ function ConversationBottomBar({
|
|
|
295
247
|
)
|
|
296
248
|
}
|
|
297
249
|
|
|
298
|
-
function InlineDateSeparator({ date }: DateSeparator) {
|
|
299
|
-
const styles = useDateSeparatorStyles()
|
|
300
|
-
const { isThisYear } = getRelativeDateStatus(date)
|
|
301
|
-
const showYear = !isThisYear
|
|
302
|
-
const dateStamp = formatDate(date, { style: 'long', year: showYear })
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<View style={styles.container}>
|
|
306
|
-
<View style={styles.separator} />
|
|
307
|
-
<Text variant="footnote" style={styles.dateText}>
|
|
308
|
-
{dateStamp}
|
|
309
|
-
</Text>
|
|
310
|
-
<View style={styles.separator} />
|
|
311
|
-
</View>
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const useDateSeparatorStyles = () => {
|
|
316
|
-
const theme = useTheme()
|
|
317
|
-
return StyleSheet.create({
|
|
318
|
-
container: {
|
|
319
|
-
alignItems: 'center',
|
|
320
|
-
flexDirection: 'row',
|
|
321
|
-
paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
322
|
-
paddingVertical: 16,
|
|
323
|
-
},
|
|
324
|
-
separator: {
|
|
325
|
-
flex: 1,
|
|
326
|
-
height: 1,
|
|
327
|
-
borderTopWidth: 1,
|
|
328
|
-
borderTopColor: theme.colors.borderColorDefaultBase,
|
|
329
|
-
},
|
|
330
|
-
dateText: {
|
|
331
|
-
paddingHorizontal: 8,
|
|
332
|
-
},
|
|
333
|
-
})
|
|
334
|
-
}
|
|
335
|
-
|
|
336
250
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
337
251
|
conversation_id: number
|
|
338
252
|
badge?: ConversationBadgeResource
|
|
@@ -429,13 +343,6 @@ const useStyles = () => {
|
|
|
429
343
|
flex: 1,
|
|
430
344
|
justifyContent: 'center',
|
|
431
345
|
},
|
|
432
|
-
listContainer: {
|
|
433
|
-
paddingVertical: 12,
|
|
434
|
-
},
|
|
435
|
-
listHeader: {
|
|
436
|
-
// Just whitespace to provide space where the typing indicator can be
|
|
437
|
-
height: 16,
|
|
438
|
-
},
|
|
439
346
|
})
|
|
440
347
|
}
|
|
441
348
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
|
|
2
|
+
import React, { useCallback, useMemo } from 'react'
|
|
3
|
+
import { Alert, ScrollView, StyleSheet, View } from 'react-native'
|
|
4
|
+
import { Heading, TextButton, ToggleButton } from '../../../components'
|
|
5
|
+
import {
|
|
6
|
+
useAtFontScaleBreakpoint,
|
|
7
|
+
useCanCreateConversations,
|
|
8
|
+
useCurrentPersonCache,
|
|
9
|
+
useDirectMessagesEligible,
|
|
10
|
+
useHasDirectMessages,
|
|
11
|
+
useNewConversationEntry,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from '../../../hooks'
|
|
14
|
+
import { useMarkAllRead } from '../../../hooks/use_conversations_actions'
|
|
15
|
+
import { useCanDisplayGroups } from '../../../hooks/use_groups'
|
|
16
|
+
import { useNewConversationFromFilter } from '../../../hooks/use_new_conversation_from_filter'
|
|
17
|
+
import { GraphId } from '../../../types/resources/group_resource'
|
|
18
|
+
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../../utils'
|
|
19
|
+
import { Haptic } from '../../../utils/native_adapters'
|
|
20
|
+
import { ConversationFiltersParams } from '../../conversation_filters/screen_props'
|
|
21
|
+
import { ConversationsScreenProps } from '../conversations_screen'
|
|
22
|
+
import { ChatGroupBadge } from './chat_group_badge'
|
|
23
|
+
import { ListHeaderActionButton } from './list_header_action_button'
|
|
24
|
+
|
|
25
|
+
const ROW_PADDING_HORIZONTAL = 16
|
|
26
|
+
|
|
27
|
+
enum FilterTypes {
|
|
28
|
+
All = 'All',
|
|
29
|
+
Groups = 'Groups',
|
|
30
|
+
Teams = 'Teams',
|
|
31
|
+
DMs = 'DMs',
|
|
32
|
+
More = 'More',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SearchListHeaderComponent = () => {
|
|
36
|
+
const navigation = useNavigation()
|
|
37
|
+
const canFilterByTeams = useCanDisplayGroups({ source_app_name: 'Services', source_type: 'Team' })
|
|
38
|
+
const canFilterByGroups = useCanDisplayGroups({ source_app_name: 'Groups', source_type: 'Group' })
|
|
39
|
+
const directMessagesEligible = useDirectMessagesEligible()
|
|
40
|
+
const hasDirectMessages = useHasDirectMessages()
|
|
41
|
+
const canFilterByDMs = directMessagesEligible && hasDirectMessages
|
|
42
|
+
const route = useRoute<RouteProp<ConversationsScreenProps['route']>>()
|
|
43
|
+
const {
|
|
44
|
+
chat_group_graph_id,
|
|
45
|
+
group_source_app_name = '',
|
|
46
|
+
show_direct_messages,
|
|
47
|
+
} = route.params || {}
|
|
48
|
+
const currentPersonCache = useCurrentPersonCache()
|
|
49
|
+
const { markAllRead, isPending } = useMarkAllRead()
|
|
50
|
+
const canCreateConversations = useCanCreateConversations()
|
|
51
|
+
const entryMode = useNewConversationEntry()
|
|
52
|
+
const newConversationFromFilter = useNewConversationFromFilter(
|
|
53
|
+
chat_group_graph_id as GraphId | undefined
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const active: FilterTypes = useMemo(() => {
|
|
57
|
+
if (chat_group_graph_id) {
|
|
58
|
+
return FilterTypes.More
|
|
59
|
+
} else if (/groups/i.test(group_source_app_name)) {
|
|
60
|
+
return FilterTypes.Groups
|
|
61
|
+
} else if (/services/i.test(group_source_app_name)) {
|
|
62
|
+
return FilterTypes.Teams
|
|
63
|
+
} else if (show_direct_messages) {
|
|
64
|
+
return FilterTypes.DMs
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return FilterTypes.All
|
|
68
|
+
}, [chat_group_graph_id, group_source_app_name, show_direct_messages])
|
|
69
|
+
|
|
70
|
+
const handleNewConversationNavigation = useCallback(() => {
|
|
71
|
+
if (newConversationFromFilter || entryMode === 'select_type') {
|
|
72
|
+
return navigation.navigate('ConversationSelectType', {
|
|
73
|
+
...route.params,
|
|
74
|
+
newConversationFromFilter,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (entryMode === 'groups') {
|
|
79
|
+
return navigation.navigate('New', {
|
|
80
|
+
screen: 'ConversationSelectGroupRecipients',
|
|
81
|
+
params: { ...route.params },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (entryMode === 'teams') {
|
|
86
|
+
return navigation.navigate('New', {
|
|
87
|
+
screen: 'ConversationSelectTeamsILeadRecipients',
|
|
88
|
+
params: { ...route.params },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (entryMode === 'direct_message') {
|
|
93
|
+
return navigation.navigate('New', {
|
|
94
|
+
screen: 'ConversationSelectDirectMessageRecipients',
|
|
95
|
+
params: { ...route.params },
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}, [navigation, route.params, entryMode, newConversationFromFilter])
|
|
99
|
+
|
|
100
|
+
const handleSetFilter = useCallback(
|
|
101
|
+
(params: ConversationFiltersParams) => {
|
|
102
|
+
navigation.setParams({
|
|
103
|
+
title: undefined,
|
|
104
|
+
chat_group_graph_id: undefined,
|
|
105
|
+
group_source_app_name: undefined,
|
|
106
|
+
show_direct_messages: undefined,
|
|
107
|
+
...params,
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
[navigation]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const handleChatSettings = useCallback(() => {
|
|
114
|
+
navigation.navigate('ChatSettings', {})
|
|
115
|
+
}, [navigation])
|
|
116
|
+
|
|
117
|
+
const handleOpenConversationFilters = useCallback(() => {
|
|
118
|
+
navigation.navigate('ConversationFilters', {
|
|
119
|
+
chat_group_graph_id,
|
|
120
|
+
group_source_app_name,
|
|
121
|
+
show_direct_messages,
|
|
122
|
+
})
|
|
123
|
+
}, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages])
|
|
124
|
+
|
|
125
|
+
const handleMarkAllRead = useCallback(() => {
|
|
126
|
+
currentPersonCache.update({ unreadCount: 0 })
|
|
127
|
+
Haptic.notificationSuccess()
|
|
128
|
+
markAllRead()
|
|
129
|
+
}, [currentPersonCache, markAllRead])
|
|
130
|
+
|
|
131
|
+
const alertMarkAllRead = useCallback(() => {
|
|
132
|
+
if (active === FilterTypes.All) return handleMarkAllRead()
|
|
133
|
+
|
|
134
|
+
Alert.alert('Mark all read', 'This includes conversations not shown by the current filter.', [
|
|
135
|
+
{ text: 'Cancel' },
|
|
136
|
+
{ text: 'OK', onPress: () => handleMarkAllRead() },
|
|
137
|
+
])
|
|
138
|
+
}, [active, handleMarkAllRead])
|
|
139
|
+
|
|
140
|
+
const hasSomeSourceFilterCapability = canFilterByTeams || canFilterByGroups
|
|
141
|
+
const hasMultiSourceFilterCapabilities = canFilterByTeams && canFilterByGroups
|
|
142
|
+
const showFilterRow = hasMultiSourceFilterCapabilities || canFilterByDMs
|
|
143
|
+
const shouldShowHeaderFiltersButton = hasSomeSourceFilterCapability && !showFilterRow
|
|
144
|
+
const moreFiltersButtonA11yLabel = shouldShowHeaderFiltersButton
|
|
145
|
+
? 'View filters'
|
|
146
|
+
: 'View more filters'
|
|
147
|
+
|
|
148
|
+
const moreFiltersButtonProps = {
|
|
149
|
+
onPress: handleOpenConversationFilters,
|
|
150
|
+
active: active === FilterTypes.More,
|
|
151
|
+
iconNameRight: 'general.threeReducingHorizontalBars' as const,
|
|
152
|
+
accessibilityLabel: moreFiltersButtonA11yLabel,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const styles = useStyles({ shouldShowHeaderFiltersButton })
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<View style={styles.listHeader}>
|
|
159
|
+
<View style={styles.headingRow}>
|
|
160
|
+
<View style={styles.titleContainer}>
|
|
161
|
+
<Heading numberOfLines={1} variant="h2">
|
|
162
|
+
Conversations
|
|
163
|
+
</Heading>
|
|
164
|
+
<TextButton
|
|
165
|
+
onPress={alertMarkAllRead}
|
|
166
|
+
disabled={isPending}
|
|
167
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
168
|
+
accessibilityShowsLargeContentViewer
|
|
169
|
+
accessibilityLargeContentTitle="Mark all read"
|
|
170
|
+
>
|
|
171
|
+
Mark all read
|
|
172
|
+
</TextButton>
|
|
173
|
+
</View>
|
|
174
|
+
<View style={styles.actionsContainer}>
|
|
175
|
+
<ListHeaderActionButton
|
|
176
|
+
name="general.cog"
|
|
177
|
+
onPress={handleChatSettings}
|
|
178
|
+
accessibilityLabel="Chat settings"
|
|
179
|
+
testID="conversations-chat-settings-button"
|
|
180
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
181
|
+
/>
|
|
182
|
+
{canCreateConversations && (
|
|
183
|
+
<ListHeaderActionButton
|
|
184
|
+
name="churchCenter.signups"
|
|
185
|
+
onPress={handleNewConversationNavigation}
|
|
186
|
+
accessibilityLabel="New conversation"
|
|
187
|
+
testID="conversations-new-conversation-button"
|
|
188
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
{shouldShowHeaderFiltersButton && <ToggleButton {...moreFiltersButtonProps} />}
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
{showFilterRow && (
|
|
195
|
+
<ScrollView
|
|
196
|
+
horizontal
|
|
197
|
+
showsHorizontalScrollIndicator={false}
|
|
198
|
+
contentContainerStyle={styles.filterRow}
|
|
199
|
+
accessibilityRole="toolbar"
|
|
200
|
+
>
|
|
201
|
+
<ToggleButton
|
|
202
|
+
title={FilterTypes.All}
|
|
203
|
+
active={active === FilterTypes.All}
|
|
204
|
+
onPress={() => handleSetFilter({})}
|
|
205
|
+
accessibilityLabel="Show all conversations"
|
|
206
|
+
style={styles.filterButton}
|
|
207
|
+
/>
|
|
208
|
+
{canFilterByGroups && (
|
|
209
|
+
<ToggleButton
|
|
210
|
+
title={FilterTypes.Groups}
|
|
211
|
+
active={active === FilterTypes.Groups}
|
|
212
|
+
onPress={() => handleSetFilter({ group_source_app_name: 'Groups' })}
|
|
213
|
+
accessibilityLabel="Filter to group conversations"
|
|
214
|
+
style={styles.filterButton}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
{canFilterByTeams && (
|
|
218
|
+
<ToggleButton
|
|
219
|
+
title={FilterTypes.Teams}
|
|
220
|
+
active={active === FilterTypes.Teams}
|
|
221
|
+
onPress={() => handleSetFilter({ group_source_app_name: 'Services' })}
|
|
222
|
+
accessibilityLabel="Filter to team conversations"
|
|
223
|
+
style={styles.filterButton}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
{canFilterByDMs && (
|
|
227
|
+
<ToggleButton
|
|
228
|
+
title={FilterTypes.DMs}
|
|
229
|
+
active={active === FilterTypes.DMs}
|
|
230
|
+
onPress={() => handleSetFilter({ show_direct_messages: true })}
|
|
231
|
+
accessibilityLabel="Filter to direct message conversations"
|
|
232
|
+
style={styles.filterButton}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
<ToggleButton {...moreFiltersButtonProps} />
|
|
236
|
+
</ScrollView>
|
|
237
|
+
)}
|
|
238
|
+
<ChatGroupBadge rowPaddingHorizontal={ROW_PADDING_HORIZONTAL} />
|
|
239
|
+
</View>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const useStyles = ({
|
|
244
|
+
shouldShowHeaderFiltersButton,
|
|
245
|
+
}: {
|
|
246
|
+
shouldShowHeaderFiltersButton: boolean
|
|
247
|
+
}) => {
|
|
248
|
+
const theme = useTheme()
|
|
249
|
+
const atFontScaleBreakpoint = useAtFontScaleBreakpoint(
|
|
250
|
+
shouldShowHeaderFiltersButton ? 1.2 : undefined
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return StyleSheet.create({
|
|
254
|
+
listHeader: {
|
|
255
|
+
borderBottomWidth: 1,
|
|
256
|
+
borderBottomColor: theme.colors.statusErrorBorder,
|
|
257
|
+
paddingBottom: 16,
|
|
258
|
+
gap: 16,
|
|
259
|
+
},
|
|
260
|
+
headingRow: {
|
|
261
|
+
gap: 12,
|
|
262
|
+
flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
|
|
263
|
+
alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
|
|
264
|
+
justifyContent: 'space-between',
|
|
265
|
+
paddingBottom: atFontScaleBreakpoint ? 4 : 0,
|
|
266
|
+
paddingHorizontal: ROW_PADDING_HORIZONTAL,
|
|
267
|
+
},
|
|
268
|
+
titleContainer: {
|
|
269
|
+
gap: 8,
|
|
270
|
+
},
|
|
271
|
+
actionsContainer: {
|
|
272
|
+
flexDirection: 'row',
|
|
273
|
+
gap: 8,
|
|
274
|
+
alignItems: 'center',
|
|
275
|
+
},
|
|
276
|
+
markAllReadRow: {
|
|
277
|
+
paddingBottom: 4,
|
|
278
|
+
},
|
|
279
|
+
filterRow: {
|
|
280
|
+
flexDirection: 'row',
|
|
281
|
+
justifyContent: 'flex-start',
|
|
282
|
+
gap: 8,
|
|
283
|
+
paddingHorizontal: ROW_PADDING_HORIZONTAL,
|
|
284
|
+
flexGrow: 1,
|
|
285
|
+
},
|
|
286
|
+
filterButton: {
|
|
287
|
+
flex: 1,
|
|
288
|
+
minWidth: 65,
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
}
|