@planningcenter/chat-react-native 4.3.0-rc.8 → 4.3.0
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_form.js +1 -1
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/hooks/use_debounced_search_term.d.ts +2 -0
- package/build/hooks/use_debounced_search_term.d.ts.map +1 -0
- package/build/hooks/use_debounced_search_term.js +40 -0
- package/build/hooks/use_debounced_search_term.js.map +1 -0
- package/build/hooks/use_search_results.d.ts +19 -0
- package/build/hooks/use_search_results.d.ts.map +1 -0
- package/build/hooks/use_search_results.js +153 -0
- package/build/hooks/use_search_results.js.map +1 -0
- package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts +2 -1
- package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/components/view_more_link_row.js +39 -24
- package/build/screens/conversation_select_recipients/components/view_more_link_row.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +142 -158
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
- package/build/screens/search/components/conversation_result_row.d.ts +8 -0
- package/build/screens/search/components/conversation_result_row.d.ts.map +1 -0
- package/build/screens/search/components/conversation_result_row.js +80 -0
- package/build/screens/search/components/conversation_result_row.js.map +1 -0
- package/build/screens/search/components/conversation_results.d.ts +8 -0
- package/build/screens/search/components/conversation_results.d.ts.map +1 -0
- package/build/screens/search/components/conversation_results.js +77 -0
- package/build/screens/search/components/conversation_results.js.map +1 -0
- package/build/screens/search/components/message_result_row.d.ts +8 -0
- package/build/screens/search/components/message_result_row.d.ts.map +1 -0
- package/build/screens/search/components/message_result_row.js +152 -0
- package/build/screens/search/components/message_result_row.js.map +1 -0
- package/build/screens/search/components/result_row.d.ts +10 -0
- package/build/screens/search/components/result_row.d.ts.map +1 -0
- package/build/screens/search/components/result_row.js +102 -0
- package/build/screens/search/components/result_row.js.map +1 -0
- package/build/screens/search/components/results_section.d.ts +9 -0
- package/build/screens/search/components/results_section.d.ts.map +1 -0
- package/build/screens/search/components/results_section.js +51 -0
- package/build/screens/search/components/results_section.js.map +1 -0
- package/build/screens/search/components/search_blank_states.d.ts +7 -0
- package/build/screens/search/components/search_blank_states.d.ts.map +1 -0
- package/build/screens/search/components/search_blank_states.js +72 -0
- package/build/screens/search/components/search_blank_states.js.map +1 -0
- package/build/screens/search/components/search_results_list.d.ts +8 -0
- package/build/screens/search/components/search_results_list.d.ts.map +1 -0
- package/build/screens/search/components/search_results_list.js +314 -0
- package/build/screens/search/components/search_results_list.js.map +1 -0
- package/build/screens/search/components/snippet.d.ts +8 -0
- package/build/screens/search/components/snippet.d.ts.map +1 -0
- package/build/screens/search/components/snippet.js +68 -0
- package/build/screens/search/components/snippet.js.map +1 -0
- package/build/screens/search/search_screen.d.ts.map +1 -1
- package/build/screens/search/search_screen.js +37 -12
- package/build/screens/search/search_screen.js.map +1 -1
- package/build/utils/client/request_helpers.js +1 -1
- package/build/utils/client/request_helpers.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/utils/client.ts +3 -4
- package/src/components/conversation/__tests__/message_form.test.tsx +83 -0
- package/src/components/conversation/message_form.tsx +1 -1
- package/src/hooks/__tests__/use_debounced_search_term.test.tsx +38 -0
- package/src/hooks/__tests__/use_search_results.test.tsx +88 -0
- package/src/hooks/use_debounced_search_term.ts +20 -0
- package/src/hooks/use_search_results.ts +78 -0
- package/src/screens/conversation_select_recipients/components/view_more_link_row.tsx +12 -3
- package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +3 -8
- package/src/screens/search/__tests__/search_screen.test.tsx +64 -0
- package/src/screens/search/components/__tests__/conversation_results.test.tsx +55 -0
- package/src/screens/search/components/__tests__/message_result_row.test.tsx +65 -0
- package/src/screens/search/components/__tests__/result_row.test.tsx +33 -0
- package/src/screens/search/components/__tests__/search_blank_states.test.tsx +20 -0
- package/src/screens/search/components/__tests__/search_results_list.test.tsx +135 -0
- package/src/screens/search/components/conversation_result_row.tsx +36 -0
- package/src/screens/search/components/conversation_results.tsx +39 -0
- package/src/screens/search/components/message_result_row.tsx +48 -0
- package/src/screens/search/components/result_row.tsx +58 -0
- package/src/screens/search/components/results_section.tsx +34 -0
- package/src/screens/search/components/search_blank_states.tsx +34 -0
- package/src/screens/search/components/search_results_list.tsx +147 -0
- package/src/screens/search/components/snippet.tsx +37 -0
- package/src/screens/search/search_screen.tsx +10 -0
- package/src/utils/client/__tests__/request_helpers.test.ts +40 -0
- package/src/utils/client/request_helpers.ts +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { FlatList, StyleSheet, View } from 'react-native'
|
|
3
|
+
import { Banner, Spinner, Text, TextButton } from '../../../components'
|
|
4
|
+
import { useSearchResults } from '../../../hooks/use_search_results'
|
|
5
|
+
import type {
|
|
6
|
+
ConversationSearchResultResource,
|
|
7
|
+
MessageSearchResultResource,
|
|
8
|
+
SearchResultResource,
|
|
9
|
+
} from '../../../types/resources/search_result'
|
|
10
|
+
import { ConversationResults } from './conversation_results'
|
|
11
|
+
import { MessageResultRow } from './message_result_row'
|
|
12
|
+
import { ResultsSection } from './results_section'
|
|
13
|
+
import { NoResultsBlankState } from './search_blank_states'
|
|
14
|
+
|
|
15
|
+
const SEARCH_ERROR_MESSAGE = "Couldn't search right now. Try again."
|
|
16
|
+
|
|
17
|
+
interface SearchResultsListProps {
|
|
18
|
+
term: string
|
|
19
|
+
onSelect: (result: SearchResultResource) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SearchResultsList({ term, onSelect }: SearchResultsListProps) {
|
|
23
|
+
const {
|
|
24
|
+
results,
|
|
25
|
+
failedTypes,
|
|
26
|
+
isPending,
|
|
27
|
+
isError,
|
|
28
|
+
isFetchNextPageError,
|
|
29
|
+
loadMore,
|
|
30
|
+
hasNextPage,
|
|
31
|
+
isFetchingNextPage,
|
|
32
|
+
} = useSearchResults(term)
|
|
33
|
+
const conversationResults = results.filter(isConversationResult)
|
|
34
|
+
const messageResults = results.filter(isMessageResult)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<FlatList
|
|
38
|
+
data={messageResults}
|
|
39
|
+
keyExtractor={item => item.id}
|
|
40
|
+
keyboardShouldPersistTaps="handled"
|
|
41
|
+
keyboardDismissMode="on-drag"
|
|
42
|
+
contentContainerStyle={styles.content}
|
|
43
|
+
renderItem={({ item }) => <MessageResultRow result={item} onPress={onSelect} />}
|
|
44
|
+
ListHeaderComponent={
|
|
45
|
+
<>
|
|
46
|
+
{conversationResults.length > 0 && (
|
|
47
|
+
<ConversationResults results={conversationResults} onSelect={onSelect} />
|
|
48
|
+
)}
|
|
49
|
+
{failedTypes.includes('conversation') && <SectionFailure type="conversation" />}
|
|
50
|
+
{messageResults.length > 0 && <ResultsSection title="Messages" />}
|
|
51
|
+
</>
|
|
52
|
+
}
|
|
53
|
+
ListEmptyComponent={
|
|
54
|
+
<ListEmpty
|
|
55
|
+
term={term}
|
|
56
|
+
isError={isError}
|
|
57
|
+
isPending={isPending}
|
|
58
|
+
noResults={results.length === 0 && failedTypes.length === 0}
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
ListFooterComponent={
|
|
62
|
+
<>
|
|
63
|
+
{isFetchingNextPage && (
|
|
64
|
+
<View accessibilityLabel="Loading more results" style={styles.loading}>
|
|
65
|
+
<Spinner />
|
|
66
|
+
</View>
|
|
67
|
+
)}
|
|
68
|
+
{isFetchNextPageError && !isFetchingNextPage && (
|
|
69
|
+
<TextButton onPress={loadMore} style={styles.retry}>
|
|
70
|
+
Try again
|
|
71
|
+
</TextButton>
|
|
72
|
+
)}
|
|
73
|
+
{failedTypes.includes('message') && <SectionFailure type="message" />}
|
|
74
|
+
</>
|
|
75
|
+
}
|
|
76
|
+
onEndReached={() => {
|
|
77
|
+
if (hasNextPage && !isFetchingNextPage && !isFetchNextPageError) loadMore()
|
|
78
|
+
}}
|
|
79
|
+
onEndReachedThreshold={0.5}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ListEmptyProps {
|
|
85
|
+
term: string
|
|
86
|
+
isError: boolean
|
|
87
|
+
isPending: boolean
|
|
88
|
+
noResults: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ListEmpty({ term, isError, isPending, noResults }: ListEmptyProps) {
|
|
92
|
+
if (isError) {
|
|
93
|
+
return <Banner appearance="error" description={SEARCH_ERROR_MESSAGE} style={styles.banner} />
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isPending) {
|
|
97
|
+
return (
|
|
98
|
+
<View accessibilityLabel="Loading results" style={styles.loading}>
|
|
99
|
+
<Spinner />
|
|
100
|
+
</View>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (noResults) return <NoResultsBlankState term={term} />
|
|
105
|
+
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function SectionFailure({ type }: { type: SearchResultResource['type'] }) {
|
|
110
|
+
return (
|
|
111
|
+
<Text accessibilityRole="alert" variant="secondary" style={styles.sectionFailure}>
|
|
112
|
+
Couldn’t load {type}s.
|
|
113
|
+
</Text>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isConversationResult(
|
|
118
|
+
result: SearchResultResource
|
|
119
|
+
): result is ConversationSearchResultResource {
|
|
120
|
+
return result.type === 'conversation'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isMessageResult(result: SearchResultResource): result is MessageSearchResultResource {
|
|
124
|
+
return result.type === 'message'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const styles = StyleSheet.create({
|
|
128
|
+
content: {
|
|
129
|
+
flexGrow: 1,
|
|
130
|
+
},
|
|
131
|
+
banner: {
|
|
132
|
+
margin: 16,
|
|
133
|
+
},
|
|
134
|
+
loading: {
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
paddingVertical: 16,
|
|
137
|
+
},
|
|
138
|
+
retry: {
|
|
139
|
+
alignSelf: 'center',
|
|
140
|
+
paddingVertical: 16,
|
|
141
|
+
},
|
|
142
|
+
sectionFailure: {
|
|
143
|
+
fontSize: 14,
|
|
144
|
+
paddingHorizontal: 16,
|
|
145
|
+
paddingVertical: 8,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { StyleSheet } from 'react-native'
|
|
3
|
+
import { Text } from '../../../components/display'
|
|
4
|
+
import { useTheme } from '../../../hooks'
|
|
5
|
+
import type { SnippetSegment } from '../../../types/resources/search_result'
|
|
6
|
+
|
|
7
|
+
interface SnippetProps {
|
|
8
|
+
segments: SnippetSegment[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Snippet({ segments }: SnippetProps) {
|
|
12
|
+
const styles = useStyles()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
{segments.map((segment, index) => (
|
|
17
|
+
<Text key={index} variant="tertiary" style={segment.match && styles.match}>
|
|
18
|
+
{segment.text}
|
|
19
|
+
</Text>
|
|
20
|
+
))}
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function plainSnippetText(segments: SnippetSegment[]) {
|
|
26
|
+
return segments.map(segment => segment.text).join('')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const useStyles = () => {
|
|
30
|
+
const { colors } = useTheme()
|
|
31
|
+
|
|
32
|
+
return StyleSheet.create({
|
|
33
|
+
match: {
|
|
34
|
+
color: colors.interaction,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
NativeStackNavigationOptions,
|
|
4
4
|
NativeStackScreenProps,
|
|
5
5
|
} from '@react-navigation/native-stack'
|
|
6
|
+
import { noop } from 'lodash'
|
|
6
7
|
import React, { useState } from 'react'
|
|
7
8
|
import { StyleSheet, View } from 'react-native'
|
|
8
9
|
import { KeyboardView } from '../../components'
|
|
@@ -10,7 +11,10 @@ import {
|
|
|
10
11
|
composeOptions,
|
|
11
12
|
withMinimalBack,
|
|
12
13
|
} from '../../components/display/platform_modal_header_buttons'
|
|
14
|
+
import { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term'
|
|
15
|
+
import { SearchPromptBlankState } from './components/search_blank_states'
|
|
13
16
|
import { SearchField } from './components/search_field'
|
|
17
|
+
import { SearchResultsList } from './components/search_results_list'
|
|
14
18
|
|
|
15
19
|
export const SearchScreenOptions = ({
|
|
16
20
|
navigation,
|
|
@@ -22,11 +26,17 @@ export const SearchScreenOptions = ({
|
|
|
22
26
|
|
|
23
27
|
export function SearchScreen() {
|
|
24
28
|
const [term, setTerm] = useState('')
|
|
29
|
+
const debouncedTerm = useDebouncedSearchTerm(term)
|
|
25
30
|
|
|
26
31
|
return (
|
|
27
32
|
<KeyboardView>
|
|
28
33
|
<View style={styles.container} testID="search-screen">
|
|
29
34
|
<SearchField term={term} onChange={setTerm} />
|
|
35
|
+
{debouncedTerm.length > 0 ? (
|
|
36
|
+
<SearchResultsList key={debouncedTerm} term={debouncedTerm} onSelect={noop} />
|
|
37
|
+
) : (
|
|
38
|
+
<SearchPromptBlankState />
|
|
39
|
+
)}
|
|
30
40
|
</View>
|
|
31
41
|
</KeyboardView>
|
|
32
42
|
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { instrumentedFetch } from '../instrumented_fetch'
|
|
2
|
+
import { makeRequest } from '../request_helpers'
|
|
3
|
+
|
|
4
|
+
jest.mock('../instrumented_fetch', () => ({ instrumentedFetch: jest.fn() }))
|
|
5
|
+
|
|
6
|
+
const mockFetch = jest.mocked(instrumentedFetch)
|
|
7
|
+
|
|
8
|
+
const fetchedUrl = () => new URL(mockFetch.mock.calls[0][0])
|
|
9
|
+
|
|
10
|
+
const get = (data: Record<string, unknown>) =>
|
|
11
|
+
makeRequest({ action: 'GET', url: 'https://api.example.com/search_results', data, headers: {} })
|
|
12
|
+
|
|
13
|
+
describe('makeRequest', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
mockFetch.mockReset()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('serializes data as snake_case query params', async () => {
|
|
23
|
+
await get({ q: 'potluck', perPage: 20 })
|
|
24
|
+
|
|
25
|
+
const { searchParams } = fetchedUrl()
|
|
26
|
+
expect(searchParams.get('q')).toBe('potluck')
|
|
27
|
+
expect(searchParams.get('per_page')).toBe('20')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it.each(['Pastor Bob', 'café', '#potluck', 'Q&A', '100%'])(
|
|
31
|
+
'sends %p so the server reads it back unchanged',
|
|
32
|
+
async term => {
|
|
33
|
+
await get({ q: term })
|
|
34
|
+
|
|
35
|
+
const { search, searchParams } = fetchedUrl()
|
|
36
|
+
expect(searchParams.get('q')).toBe(term)
|
|
37
|
+
expect(() => decodeURIComponent(search)).not.toThrow()
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
})
|
|
@@ -33,7 +33,7 @@ export const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }: Ma
|
|
|
33
33
|
|
|
34
34
|
const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined
|
|
35
35
|
|
|
36
|
-
return instrumentedFetch(
|
|
36
|
+
return instrumentedFetch(uri.toString(), {
|
|
37
37
|
method: action,
|
|
38
38
|
headers,
|
|
39
39
|
body,
|