@planningcenter/chat-react-native 4.3.0-rc.9 → 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.
Files changed (75) hide show
  1. package/build/components/conversation/message_form.js +1 -1
  2. package/build/components/conversation/message_form.js.map +1 -1
  3. package/build/hooks/use_debounced_search_term.d.ts +2 -0
  4. package/build/hooks/use_debounced_search_term.d.ts.map +1 -0
  5. package/build/hooks/use_debounced_search_term.js +40 -0
  6. package/build/hooks/use_debounced_search_term.js.map +1 -0
  7. package/build/hooks/use_search_results.d.ts +19 -0
  8. package/build/hooks/use_search_results.d.ts.map +1 -0
  9. package/build/hooks/use_search_results.js +153 -0
  10. package/build/hooks/use_search_results.js.map +1 -0
  11. package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts +2 -1
  12. package/build/screens/conversation_select_recipients/components/view_more_link_row.d.ts.map +1 -1
  13. package/build/screens/conversation_select_recipients/components/view_more_link_row.js +39 -24
  14. package/build/screens/conversation_select_recipients/components/view_more_link_row.js.map +1 -1
  15. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
  16. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +142 -158
  17. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
  18. package/build/screens/search/components/conversation_result_row.d.ts +8 -0
  19. package/build/screens/search/components/conversation_result_row.d.ts.map +1 -0
  20. package/build/screens/search/components/conversation_result_row.js +80 -0
  21. package/build/screens/search/components/conversation_result_row.js.map +1 -0
  22. package/build/screens/search/components/conversation_results.d.ts +8 -0
  23. package/build/screens/search/components/conversation_results.d.ts.map +1 -0
  24. package/build/screens/search/components/conversation_results.js +77 -0
  25. package/build/screens/search/components/conversation_results.js.map +1 -0
  26. package/build/screens/search/components/message_result_row.d.ts +8 -0
  27. package/build/screens/search/components/message_result_row.d.ts.map +1 -0
  28. package/build/screens/search/components/message_result_row.js +152 -0
  29. package/build/screens/search/components/message_result_row.js.map +1 -0
  30. package/build/screens/search/components/result_row.d.ts +10 -0
  31. package/build/screens/search/components/result_row.d.ts.map +1 -0
  32. package/build/screens/search/components/result_row.js +102 -0
  33. package/build/screens/search/components/result_row.js.map +1 -0
  34. package/build/screens/search/components/results_section.d.ts +9 -0
  35. package/build/screens/search/components/results_section.d.ts.map +1 -0
  36. package/build/screens/search/components/results_section.js +51 -0
  37. package/build/screens/search/components/results_section.js.map +1 -0
  38. package/build/screens/search/components/search_results_list.d.ts +8 -0
  39. package/build/screens/search/components/search_results_list.d.ts.map +1 -0
  40. package/build/screens/search/components/search_results_list.js +314 -0
  41. package/build/screens/search/components/search_results_list.js.map +1 -0
  42. package/build/screens/search/components/snippet.d.ts +8 -0
  43. package/build/screens/search/components/snippet.d.ts.map +1 -0
  44. package/build/screens/search/components/snippet.js +68 -0
  45. package/build/screens/search/components/snippet.js.map +1 -0
  46. package/build/screens/search/search_screen.d.ts.map +1 -1
  47. package/build/screens/search/search_screen.js +18 -9
  48. package/build/screens/search/search_screen.js.map +1 -1
  49. package/build/utils/client/request_helpers.js +1 -1
  50. package/build/utils/client/request_helpers.js.map +1 -1
  51. package/package.json +3 -3
  52. package/src/__tests__/utils/client.ts +3 -4
  53. package/src/components/conversation/__tests__/message_form.test.tsx +83 -0
  54. package/src/components/conversation/message_form.tsx +1 -1
  55. package/src/hooks/__tests__/use_debounced_search_term.test.tsx +38 -0
  56. package/src/hooks/__tests__/use_search_results.test.tsx +88 -0
  57. package/src/hooks/use_debounced_search_term.ts +20 -0
  58. package/src/hooks/use_search_results.ts +78 -0
  59. package/src/screens/conversation_select_recipients/components/view_more_link_row.tsx +12 -3
  60. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +3 -8
  61. package/src/screens/search/__tests__/search_screen.test.tsx +64 -0
  62. package/src/screens/search/components/__tests__/conversation_results.test.tsx +55 -0
  63. package/src/screens/search/components/__tests__/message_result_row.test.tsx +65 -0
  64. package/src/screens/search/components/__tests__/result_row.test.tsx +33 -0
  65. package/src/screens/search/components/__tests__/search_results_list.test.tsx +135 -0
  66. package/src/screens/search/components/conversation_result_row.tsx +36 -0
  67. package/src/screens/search/components/conversation_results.tsx +39 -0
  68. package/src/screens/search/components/message_result_row.tsx +48 -0
  69. package/src/screens/search/components/result_row.tsx +58 -0
  70. package/src/screens/search/components/results_section.tsx +34 -0
  71. package/src/screens/search/components/search_results_list.tsx +147 -0
  72. package/src/screens/search/components/snippet.tsx +37 -0
  73. package/src/screens/search/search_screen.tsx +9 -1
  74. package/src/utils/client/__tests__/request_helpers.test.ts +40 -0
  75. 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&rsquo;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,8 +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'
13
15
  import { SearchPromptBlankState } from './components/search_blank_states'
14
16
  import { SearchField } from './components/search_field'
17
+ import { SearchResultsList } from './components/search_results_list'
15
18
 
16
19
  export const SearchScreenOptions = ({
17
20
  navigation,
@@ -23,12 +26,17 @@ export const SearchScreenOptions = ({
23
26
 
24
27
  export function SearchScreen() {
25
28
  const [term, setTerm] = useState('')
29
+ const debouncedTerm = useDebouncedSearchTerm(term)
26
30
 
27
31
  return (
28
32
  <KeyboardView>
29
33
  <View style={styles.container} testID="search-screen">
30
34
  <SearchField term={term} onChange={setTerm} />
31
- <SearchPromptBlankState />
35
+ {debouncedTerm.length > 0 ? (
36
+ <SearchResultsList key={debouncedTerm} term={debouncedTerm} onSelect={noop} />
37
+ ) : (
38
+ <SearchPromptBlankState />
39
+ )}
32
40
  </View>
33
41
  </KeyboardView>
34
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(decodeURIComponent(uri.toString()), {
36
+ return instrumentedFetch(uri.toString(), {
37
37
  method: action,
38
38
  headers,
39
39
  body,