@planningcenter/chat-react-native 4.4.1-qa-staging.30 → 4.4.1-qa-staging.32

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 (86) hide show
  1. package/build/components/conversation/message_list.d.ts.map +1 -1
  2. package/build/components/conversation/message_list.js +155 -123
  3. package/build/components/conversation/message_list.js.map +1 -1
  4. package/build/components/conversations/conversations.d.ts.map +1 -1
  5. package/build/components/conversations/conversations.js +1 -0
  6. package/build/components/conversations/conversations.js.map +1 -1
  7. package/build/components/display/keyboard_scroll_view.d.ts.map +1 -1
  8. package/build/components/display/keyboard_scroll_view.js +1 -0
  9. package/build/components/display/keyboard_scroll_view.js.map +1 -1
  10. package/build/components/page/page_error_boundary.d.ts +9 -1
  11. package/build/components/page/page_error_boundary.d.ts.map +1 -1
  12. package/build/components/page/page_error_boundary.js +56 -31
  13. package/build/components/page/page_error_boundary.js.map +1 -1
  14. package/build/navigation/index.d.ts.map +1 -1
  15. package/build/navigation/index.js +11 -3
  16. package/build/navigation/index.js.map +1 -1
  17. package/build/navigation/page_error_fallbacks.d.ts +7 -0
  18. package/build/navigation/page_error_fallbacks.d.ts.map +1 -0
  19. package/build/navigation/page_error_fallbacks.js +7 -0
  20. package/build/navigation/page_error_fallbacks.js.map +1 -0
  21. package/build/navigation/screenLayout.d.ts.map +1 -1
  22. package/build/navigation/screenLayout.js +10 -6
  23. package/build/navigation/screenLayout.js.map +1 -1
  24. package/build/screens/age_check/age_check_required_screen.d.ts.map +1 -1
  25. package/build/screens/age_check/age_check_required_screen.js +1 -0
  26. package/build/screens/age_check/age_check_required_screen.js.map +1 -1
  27. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  28. package/build/screens/conversation_details_screen.js +1 -0
  29. package/build/screens/conversation_details_screen.js.map +1 -1
  30. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts.map +1 -1
  31. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js +1 -0
  32. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js.map +1 -1
  33. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
  34. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +332 -291
  35. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
  36. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
  37. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +1 -0
  38. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
  39. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  40. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +1 -0
  41. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  42. package/build/screens/get_help_screen.js +56 -62
  43. package/build/screens/get_help_screen.js.map +1 -1
  44. package/build/screens/group_notification_settings_screen.js +3 -2
  45. package/build/screens/group_notification_settings_screen.js.map +1 -1
  46. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  47. package/build/screens/notification_settings_screen.js +1 -0
  48. package/build/screens/notification_settings_screen.js.map +1 -1
  49. package/build/screens/preferred_app_selection_screen.js +45 -39
  50. package/build/screens/preferred_app_selection_screen.js.map +1 -1
  51. package/build/screens/search/components/search_results_list.d.ts +4 -1
  52. package/build/screens/search/components/search_results_list.d.ts.map +1 -1
  53. package/build/screens/search/components/search_results_list.js +136 -110
  54. package/build/screens/search/components/search_results_list.js.map +1 -1
  55. package/build/screens/search/search_screen.d.ts.map +1 -1
  56. package/build/screens/search/search_screen.js +21 -28
  57. package/build/screens/search/search_screen.js.map +1 -1
  58. package/build/screens/settings_screen.d.ts.map +1 -1
  59. package/build/screens/settings_screen.js +7 -6
  60. package/build/screens/settings_screen.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/components/conversation/__tests__/message_list.test.tsx +4 -0
  63. package/src/components/conversation/message_list.tsx +6 -0
  64. package/src/components/conversations/conversations.tsx +1 -0
  65. package/src/components/display/keyboard_scroll_view.tsx +1 -0
  66. package/src/components/page/__tests__/page_error_boundary.test.tsx +33 -0
  67. package/src/components/page/page_error_boundary.tsx +43 -12
  68. package/src/navigation/index.tsx +11 -2
  69. package/src/navigation/page_error_fallbacks.ts +14 -0
  70. package/src/navigation/screenLayout.tsx +3 -1
  71. package/src/screens/age_check/age_check_required_screen.tsx +5 -1
  72. package/src/screens/conversation_details_screen.tsx +1 -0
  73. package/src/screens/conversation_select_recipients/__tests__/conversation_select_direct_message_recipients_screen.test.tsx +100 -0
  74. package/src/screens/conversation_select_recipients/conversation_new_entry_screen.tsx +4 -1
  75. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +78 -68
  76. package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +1 -0
  77. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +1 -0
  78. package/src/screens/get_help_screen.tsx +4 -4
  79. package/src/screens/group_notification_settings_screen.tsx +3 -3
  80. package/src/screens/notification_settings_screen.tsx +1 -0
  81. package/src/screens/preferred_app_selection_screen.tsx +4 -4
  82. package/src/screens/search/__tests__/search_screen.test.tsx +5 -7
  83. package/src/screens/search/components/__tests__/search_results_list.test.tsx +6 -2
  84. package/src/screens/search/components/search_results_list.tsx +34 -13
  85. package/src/screens/search/search_screen.tsx +8 -18
  86. package/src/screens/settings_screen.tsx +25 -25
@@ -1,11 +1,10 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StackActions, useNavigation } from '@react-navigation/native'
3
- import { RefObject, useRef, useState } from 'react'
3
+ import { ReactElement, RefObject, useRef, useState } from 'react'
4
4
  import { FlatList, Pressable, StyleSheet, TextInput, View } from 'react-native'
5
5
  import { Avatar, Badge, Banner, Icon, KeyboardView, Spinner, Text } from '../../components'
6
6
  import { ActionButton } from '../../components/display/action_button'
7
7
  import { AutoFocusAwareTextInput } from '../../components/display/text_input'
8
- import { ComponentErrorBoundary } from '../../components/page/component_error_boundary'
9
8
  import { useTheme } from '../../hooks'
10
9
  import { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_direct_message_affiliations'
11
10
  import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'
@@ -92,38 +91,31 @@ export function ConversationSelectDirectMessageRecipientsScreen({
92
91
 
93
92
  return (
94
93
  <KeyboardView>
95
- {isError && (
96
- <Banner
97
- appearance="error"
98
- description="There was an issue starting the conversation. Please try again."
99
- style={styles.errorBanner}
100
- />
101
- )}
102
- <ToField
103
- selectedPeople={selectedPeople}
104
- searchTerm={searchTerm}
105
- onSearchChange={setSearchTerm}
106
- onRemove={removePerson}
107
- onClear={() => setSelectedPeople(new Map())}
108
- searchInputRef={searchInputRef}
109
- />
110
94
  <View style={styles.candidateResultsContainer}>
111
- <ComponentErrorBoundary
112
- key={debouncedTerm}
113
- fallback={
114
- <Banner
115
- appearance="error"
116
- description="Couldn't load people right now. Try again."
117
- style={styles.errorBanner}
118
- />
95
+ <CandidateResults
96
+ term={debouncedTerm}
97
+ selectedPeople={selectedPeople}
98
+ onSelect={selectPerson}
99
+ header={
100
+ <>
101
+ {isError && (
102
+ <Banner
103
+ appearance="error"
104
+ description="There was an issue starting the conversation. Please try again."
105
+ style={styles.errorBanner}
106
+ />
107
+ )}
108
+ <ToField
109
+ selectedPeople={selectedPeople}
110
+ searchTerm={searchTerm}
111
+ onSearchChange={setSearchTerm}
112
+ onRemove={removePerson}
113
+ onClear={() => setSelectedPeople(new Map())}
114
+ searchInputRef={searchInputRef}
115
+ />
116
+ </>
119
117
  }
120
- >
121
- <CandidateResults
122
- term={debouncedTerm}
123
- selectedPeople={selectedPeople}
124
- onSelect={selectPerson}
125
- />
126
- </ComponentErrorBoundary>
118
+ />
127
119
  </View>
128
120
  <ActionButton
129
121
  title={buttonText}
@@ -214,53 +206,57 @@ function CandidateResults({
214
206
  term,
215
207
  selectedPeople,
216
208
  onSelect,
209
+ header,
217
210
  }: {
218
211
  term: string
219
212
  selectedPeople: Map<string, DirectMessageCandidateResource>
220
213
  onSelect: (candidate: DirectMessageCandidateResource) => void
214
+ header: ReactElement
221
215
  }) {
222
216
  const styles = useStyles()
223
217
  const { data, isFetching } = useDirectMessageCandidates(term)
224
218
  const { groups, teams, resultsDescription } = useDirectMessageAffiliations()
225
219
 
226
- if (term.length < 1) {
227
- if (selectedPeople.size > 0) return null
220
+ const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))
221
+ const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)
222
+ const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS
223
+ const showsResults = term.length >= 1 && !isAtLimit && unselectedCandidates.length > 0
228
224
 
229
- return (
230
- <View style={styles.emptyState}>
231
- <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon} />
232
- <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
233
- <Text variant="secondary" style={styles.emptyStateSubtitle}>
234
- Start a conversation with people in your groups and teams.
235
- </Text>
236
- </View>
237
- )
238
- }
225
+ const emptyState = (() => {
226
+ if (showsResults) return null
239
227
 
240
- const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS
228
+ if (term.length < 1) {
229
+ if (selectedPeople.size > 0) return null
241
230
 
242
- if (isAtLimit) {
243
- return (
244
- <View style={styles.emptyState}>
245
- <Text variant="secondary" style={styles.emptyStateSubtitle}>
246
- You can add up to {MAX_RECIPIENTS} people to a direct message.
247
- </Text>
248
- </View>
249
- )
250
- }
231
+ return (
232
+ <View style={styles.emptyState}>
233
+ <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon} />
234
+ <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
235
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
236
+ Start a conversation with people in your groups and teams.
237
+ </Text>
238
+ </View>
239
+ )
240
+ }
251
241
 
252
- const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))
253
- const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)
242
+ if (isAtLimit) {
243
+ return (
244
+ <View style={styles.emptyState}>
245
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
246
+ You can add up to {MAX_RECIPIENTS} people to a direct message.
247
+ </Text>
248
+ </View>
249
+ )
250
+ }
254
251
 
255
- if (isFetching && unselectedCandidates.length === 0) {
256
- return (
257
- <View style={styles.emptyState}>
258
- <Spinner />
259
- </View>
260
- )
261
- }
252
+ if (isFetching) {
253
+ return (
254
+ <View style={styles.emptyState}>
255
+ <Spinner />
256
+ </View>
257
+ )
258
+ }
262
259
 
263
- if (!isFetching && unselectedCandidates.length === 0) {
264
260
  return (
265
261
  <View style={styles.emptyState}>
266
262
  <Icon name="general.search" size={32} style={styles.emptyStateIcon} />
@@ -270,13 +266,15 @@ function CandidateResults({
270
266
  </Text>
271
267
  </View>
272
268
  )
273
- }
269
+ })()
274
270
 
275
271
  return (
276
272
  <CandidateList
277
- candidates={unselectedCandidates}
273
+ header={header}
274
+ candidates={showsResults ? unselectedCandidates : []}
278
275
  hasTruncated={hasTruncated}
279
276
  description={resultsDescription}
277
+ emptyState={emptyState}
280
278
  groups={groups}
281
279
  teams={teams}
282
280
  onSelect={onSelect}
@@ -309,27 +307,33 @@ function candidateAffiliationNames(
309
307
  }
310
308
 
311
309
  function CandidateList({
310
+ header,
312
311
  candidates,
313
312
  hasTruncated,
314
313
  description,
314
+ emptyState,
315
315
  groups,
316
316
  teams,
317
317
  onSelect,
318
318
  }: {
319
+ header: ReactElement
319
320
  candidates: DirectMessageCandidateResource[]
320
321
  hasTruncated: boolean
321
322
  description: string | null
323
+ emptyState: ReactElement | null
322
324
  groups: Map<number, string>
323
325
  teams: Map<number, string>
324
326
  onSelect: (candidate: DirectMessageCandidateResource) => void
325
327
  }) {
326
328
  const styles = useStyles()
329
+ const hasCandidates = candidates.length > 0
327
330
  const available = candidates.filter(c => !c.directMessagesDisabled)
328
331
  const dmDisabled = candidates.filter(c => c.directMessagesDisabled)
329
332
 
330
333
  return (
331
334
  <FlatList<DirectMessageCandidateResource>
332
335
  data={available}
336
+ contentInsetAdjustmentBehavior="automatic"
333
337
  keyboardShouldPersistTaps="handled"
334
338
  keyExtractor={item => item.id}
335
339
  renderItem={({ item }) => (
@@ -340,7 +344,13 @@ function CandidateList({
340
344
  onSelect={() => onSelect(item)}
341
345
  />
342
346
  )}
343
- ListHeaderComponent={<ResultsHeading description={description} />}
347
+ ListHeaderComponent={
348
+ <>
349
+ {header}
350
+ {hasCandidates && <ResultsHeading description={description} />}
351
+ </>
352
+ }
353
+ ListEmptyComponent={emptyState}
344
354
  ListFooterComponent={
345
355
  <>
346
356
  {dmDisabled.length > 0 && (
@@ -361,7 +371,7 @@ function CandidateList({
361
371
  ))}
362
372
  </>
363
373
  )}
364
- {hasTruncated && (
374
+ {hasCandidates && hasTruncated && (
365
375
  <View style={styles.truncatedFooter}>
366
376
  <Text variant="secondary" style={styles.truncatedText}>
367
377
  Keep typing to narrow your results.
@@ -60,6 +60,7 @@ export const ConversationSelectGroupRecipientsScreen = ({
60
60
 
61
61
  return (
62
62
  <SectionList<GroupsGroupResource, GroupsSection>
63
+ contentInsetAdjustmentBehavior="automatic"
63
64
  sections={sections}
64
65
  keyExtractor={item => item.id.toString()}
65
66
  stickySectionHeadersEnabled={false}
@@ -32,6 +32,7 @@ export const ConversationSelectTeamsILeadRecipientsScreen = ({
32
32
 
33
33
  return (
34
34
  <FlatList
35
+ contentInsetAdjustmentBehavior="automatic"
35
36
  data={serviceTypes}
36
37
  keyExtractor={item => item.id.toString()}
37
38
  contentContainerStyle={styles.contentContainer}
@@ -1,6 +1,6 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
2
  import { useCallback, useMemo } from 'react'
3
- import { Alert, Linking, StyleSheet, View } from 'react-native'
3
+ import { Alert, Linking, ScrollView, StyleSheet, View } from 'react-native'
4
4
  import { Heading, PressableRow, Text, TextInlineButton } from '../components'
5
5
  import { useApiGet, useTheme } from '../hooks'
6
6
  import { useAppName } from '../hooks/use_app_name'
@@ -56,7 +56,7 @@ export const GetHelpScreen = ({ route }: GetHelpScreenProps) => {
56
56
  }, [navigation])
57
57
 
58
58
  return (
59
- <View style={[styles.container]}>
59
+ <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.container}>
60
60
  {appSpecificHelpUrls.length > 0 && (
61
61
  <Heading variant="h2" style={[styles.heading, styles.headingBottomBorder]}>
62
62
  How-to articles
@@ -90,7 +90,7 @@ export const GetHelpScreen = ({ route }: GetHelpScreenProps) => {
90
90
  to the development team.
91
91
  </Text>
92
92
  </View>
93
- </View>
93
+ </ScrollView>
94
94
  )
95
95
  }
96
96
 
@@ -145,7 +145,7 @@ const useStyles = () => {
145
145
 
146
146
  return StyleSheet.create({
147
147
  container: {
148
- flex: 1,
148
+ flexGrow: 1,
149
149
  },
150
150
  text: {
151
151
  paddingVertical: 4,
@@ -2,7 +2,7 @@ import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
3
  import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
4
4
  import React, { useEffect } from 'react'
5
- import { Platform, StyleSheet, View } from 'react-native'
5
+ import { Platform, ScrollView, StyleSheet, View } from 'react-native'
6
6
  import { Heading, Icon, Text } from '../components'
7
7
  import { useTheme } from '../hooks'
8
8
  import { platformFontWeightBold } from '../utils/styles'
@@ -38,7 +38,7 @@ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSett
38
38
  }
39
39
 
40
40
  return (
41
- <View style={styles.container}>
41
+ <ScrollView contentInsetAdjustmentBehavior="automatic" style={styles.container}>
42
42
  <View style={styles.sectionOuter}>
43
43
  <View style={styles.sectionInner}>
44
44
  <Heading variant="h2" style={styles.sectionHeading}>
@@ -76,7 +76,7 @@ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSett
76
76
  )}
77
77
  </View>
78
78
  </PlatformPressable>
79
- </View>
79
+ </ScrollView>
80
80
  )
81
81
  }
82
82
 
@@ -123,6 +123,7 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
123
123
  return (
124
124
  <View style={styles.listContainer}>
125
125
  <FlatList
126
+ contentInsetAdjustmentBehavior="automatic"
126
127
  data={listData as SectionListData}
127
128
  contentContainerStyle={styles.contentContainer}
128
129
  renderItem={({ item, index }) => {
@@ -2,7 +2,7 @@ import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps } from '@react-navigation/native'
3
3
  import { useMutation } from '@tanstack/react-query'
4
4
  import React, { type PropsWithChildren } from 'react'
5
- import { Image, StyleSheet, View, type ViewStyle } from 'react-native'
5
+ import { Image, ScrollView, StyleSheet, View, type ViewStyle } from 'react-native'
6
6
  import { Heading, Icon, Text } from '../components'
7
7
  import { useApiClient, useTheme } from '../hooks'
8
8
  import { useAppName } from '../hooks/use_app_name'
@@ -45,7 +45,7 @@ export function PreferredAppSelectionScreen({ route }: PreferredAppSelectionScre
45
45
  const sectionTitle = chatType ? chatTypeLabel(chatType.title) : undefined
46
46
 
47
47
  return (
48
- <View style={styles.container}>
48
+ <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.container}>
49
49
  <View style={styles.section}>
50
50
  <Heading variant="h2" style={styles.sectionHeading}>
51
51
  {sectionTitle}
@@ -64,7 +64,7 @@ export function PreferredAppSelectionScreen({ route }: PreferredAppSelectionScre
64
64
  </PressableRow>
65
65
  ))}
66
66
  </View>
67
- </View>
67
+ </ScrollView>
68
68
  )
69
69
  }
70
70
 
@@ -118,7 +118,7 @@ const PressableRow = ({
118
118
 
119
119
  const styles = StyleSheet.create({
120
120
  container: {
121
- flex: 1,
121
+ flexGrow: 1,
122
122
  },
123
123
  section: {
124
124
  paddingTop: 24,
@@ -106,12 +106,13 @@ describe('SearchScreen', () => {
106
106
  fireEvent.changeText(input, ' budget ')
107
107
 
108
108
  expect(view.getByText('Search chat')).toBeTruthy()
109
- expect(mockUseSearchResults).not.toHaveBeenCalled()
109
+ const searchedTerms = () =>
110
+ new Set(mockUseSearchResults.mock.calls.map(([term]) => term).filter(term => term.trim()))
111
+ expect([...searchedTerms()]).toEqual([])
110
112
 
111
113
  act(() => jest.advanceTimersByTime(200))
112
114
 
113
- const searchedTerms = new Set(mockUseSearchResults.mock.calls.map(([term]) => term))
114
- expect([...searchedTerms]).toEqual(['budget'])
115
+ expect([...searchedTerms()]).toEqual(['budget'])
115
116
  expect(view.queryByText('Search chat')).toBeNull()
116
117
  expect(view.getByLabelText('Loading results')).toBeTruthy()
117
118
  })
@@ -121,10 +122,7 @@ describe('SearchScreen', () => {
121
122
 
122
123
  const scrollView = view.UNSAFE_getByType(ScrollView)
123
124
  expect(scrollView.props.keyboardShouldPersistTaps).toBe('handled')
124
- expect(scrollView.props.contentContainerStyle).toMatchObject({
125
- flexGrow: 1,
126
- justifyContent: 'center',
127
- })
125
+ expect(scrollView.props.contentContainerStyle).toMatchObject({ flexGrow: 1 })
128
126
  expect(view.getByText('Search chat')).toBeTruthy()
129
127
  })
130
128
 
@@ -49,9 +49,13 @@ const messageResult = (id: string): MessageSearchResultResource => ({
49
49
  },
50
50
  })
51
51
 
52
+ const slots = { header: <></>, prompt: <></> }
53
+
52
54
  const renderList = (state: Partial<SearchState>, onSettled = jest.fn()) => {
53
55
  mockUseSearchResults.mockReturnValue(searchState(state))
54
- const list = () => <SearchResultsList term="potluck" onSelect={jest.fn()} onSettled={onSettled} />
56
+ const list = () => (
57
+ <SearchResultsList term="potluck" onSelect={jest.fn()} onSettled={onSettled} {...slots} />
58
+ )
55
59
  const view = render(list())
56
60
  const rerenderWith = (next: Partial<SearchState>) => {
57
61
  mockUseSearchResults.mockReturnValue(searchState(next))
@@ -130,7 +134,7 @@ describe('SearchResultsList', () => {
130
134
  const results = [conversationResult('1'), messageResult('1'), messageResult('2')]
131
135
  mockUseSearchResults.mockReturnValue(searchState({ results }))
132
136
  const view = render(
133
- <SearchResultsList term="potluck" onSelect={onSelect} onSettled={jest.fn()} />
137
+ <SearchResultsList term="potluck" onSelect={onSelect} onSettled={jest.fn()} {...slots} />
134
138
  )
135
139
 
136
140
  fireEvent.press(view.getByLabelText(/^Message 2/))
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef } from 'react'
1
+ import React, { ReactElement, useEffect, useRef } from 'react'
2
2
  import { FlatList, StyleSheet, View } from 'react-native'
3
3
  import { Banner, Spinner, Text, TextButton } from '../../../components'
4
4
  import { useSearchResults } from '../../../hooks/use_search_results'
@@ -23,9 +23,17 @@ interface SearchResultsListProps {
23
23
  term: string
24
24
  onSelect: (result: SearchResultResource, position: number) => void
25
25
  onSettled: (term: string, counts: SearchResultCounts) => void
26
+ header: ReactElement
27
+ prompt: ReactElement
26
28
  }
27
29
 
28
- export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsListProps) {
30
+ export function SearchResultsList({
31
+ term,
32
+ onSelect,
33
+ onSettled,
34
+ header,
35
+ prompt,
36
+ }: SearchResultsListProps) {
29
37
  const {
30
38
  results,
31
39
  failedTypes,
@@ -36,9 +44,10 @@ export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsLi
36
44
  hasNextPage,
37
45
  isFetchingNextPage,
38
46
  } = useSearchResults(term)
39
- const conversationResults = results.filter(isConversationResult)
40
- const messageResults = results.filter(isMessageResult)
41
- const settled = !isPending && !isError
47
+ const searching = term.trim().length > 0
48
+ const conversationResults = searching ? results.filter(isConversationResult) : []
49
+ const messageResults = searching ? results.filter(isMessageResult) : []
50
+ const settled = searching && !isPending && !isError
42
51
 
43
52
  const settledTermRef = useRef<string | undefined>(undefined)
44
53
 
@@ -56,6 +65,7 @@ export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsLi
56
65
  <FlatList
57
66
  data={messageResults}
58
67
  keyExtractor={item => item.id}
68
+ contentInsetAdjustmentBehavior="automatic"
59
69
  keyboardShouldPersistTaps="handled"
60
70
  keyboardDismissMode="on-drag"
61
71
  contentContainerStyle={styles.content}
@@ -64,20 +74,27 @@ export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsLi
64
74
  )}
65
75
  ListHeaderComponent={
66
76
  <>
77
+ {header}
67
78
  {conversationResults.length > 0 && (
68
79
  <ConversationResults results={conversationResults} onSelect={onSelect} />
69
80
  )}
70
- {failedTypes.includes('conversation') && <SectionFailure type="conversation" />}
81
+ {searching && failedTypes.includes('conversation') && (
82
+ <SectionFailure type="conversation" />
83
+ )}
71
84
  {messageResults.length > 0 && <ResultsSection title="Messages" />}
72
85
  </>
73
86
  }
74
87
  ListEmptyComponent={
75
- <ListEmpty
76
- term={term}
77
- isError={isError}
78
- isPending={isPending}
79
- noResults={results.length === 0 && failedTypes.length === 0}
80
- />
88
+ searching ? (
89
+ <ListEmpty
90
+ term={term}
91
+ isError={isError}
92
+ isPending={isPending}
93
+ noResults={results.length === 0 && failedTypes.length === 0}
94
+ />
95
+ ) : (
96
+ <View style={styles.prompt}>{prompt}</View>
97
+ )
81
98
  }
82
99
  ListFooterComponent={
83
100
  <>
@@ -91,7 +108,7 @@ export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsLi
91
108
  Try again
92
109
  </TextButton>
93
110
  )}
94
- {failedTypes.includes('message') && <SectionFailure type="message" />}
111
+ {searching && failedTypes.includes('message') && <SectionFailure type="message" />}
95
112
  </>
96
113
  }
97
114
  onEndReached={() => {
@@ -149,6 +166,10 @@ const styles = StyleSheet.create({
149
166
  content: {
150
167
  flexGrow: 1,
151
168
  },
169
+ prompt: {
170
+ flexGrow: 1,
171
+ justifyContent: 'center',
172
+ },
152
173
  banner: {
153
174
  margin: 16,
154
175
  },
@@ -4,7 +4,7 @@ import {
4
4
  NativeStackScreenProps,
5
5
  } from '@react-navigation/native-stack'
6
6
  import React, { useRef, useState } from 'react'
7
- import { Keyboard, ScrollView, StyleSheet, View } from 'react-native'
7
+ import { Keyboard, StyleSheet, View } from 'react-native'
8
8
  import { KeyboardView } from '../../components'
9
9
  import {
10
10
  composeOptions,
@@ -64,19 +64,13 @@ export function SearchScreen() {
64
64
  return (
65
65
  <KeyboardView>
66
66
  <View style={styles.container} testID="search-screen">
67
- <SearchField term={term} onChange={setTerm} />
68
- {debouncedTerm.length > 0 ? (
69
- <SearchResultsList
70
- key={debouncedTerm}
71
- term={debouncedTerm}
72
- onSelect={openResult}
73
- onSettled={publishSearch}
74
- />
75
- ) : (
76
- <ScrollView contentContainerStyle={styles.prompt} keyboardShouldPersistTaps="handled">
77
- <SearchPromptBlankState />
78
- </ScrollView>
79
- )}
67
+ <SearchResultsList
68
+ term={debouncedTerm}
69
+ onSelect={openResult}
70
+ onSettled={publishSearch}
71
+ header={<SearchField term={term} onChange={setTerm} />}
72
+ prompt={<SearchPromptBlankState />}
73
+ />
80
74
  </View>
81
75
  </KeyboardView>
82
76
  )
@@ -86,8 +80,4 @@ const styles = StyleSheet.create({
86
80
  container: {
87
81
  flex: 1,
88
82
  },
89
- prompt: {
90
- flexGrow: 1,
91
- justifyContent: 'center',
92
- },
93
83
  })
@@ -1,7 +1,7 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
3
  import React, { useCallback } from 'react'
4
- import { Platform, StyleSheet, View } from 'react-native'
4
+ import { Platform, ScrollView, StyleSheet, View } from 'react-native'
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
6
6
  import { Heading, Icon, Text, VersionFooter } from '../components'
7
7
  import { Switch } from '../components/display/switch'
@@ -28,29 +28,31 @@ export function SettingsScreen({}: SettingsScreenProps) {
28
28
 
29
29
  return (
30
30
  <View style={styles.container}>
31
- <View style={styles.sectionOuter}>
32
- <View style={styles.sectionHeader}>
33
- <Heading variant="h2">Messages</Heading>
34
- </View>
35
- {directMessagesEligible && (
36
- <View style={styles.settingRow}>
37
- <View style={styles.settingRowText}>
38
- <Text>Allow direct messages</Text>
39
- <Text variant="tertiary">
40
- When off, you can't be added to new direct messages, and any you're already in will
41
- be turned off.
42
- </Text>
43
- </View>
44
- <Switch
45
- value={!directMessagesDisabled}
46
- onValueChange={enabled => setDirectMessagesDisabled(!enabled)}
47
- disabled={isMutating}
48
- />
31
+ <ScrollView contentInsetAdjustmentBehavior="automatic">
32
+ <View style={styles.sectionOuter}>
33
+ <View style={styles.sectionHeader}>
34
+ <Heading variant="h2">Messages</Heading>
49
35
  </View>
50
- )}
51
- <NavRow label="Notification settings" onPress={handleNotificationSettings} />
52
- {appName !== 'services' && <NavRow label="Get help" onPress={handleGetHelp} />}
53
- </View>
36
+ {directMessagesEligible && (
37
+ <View style={styles.settingRow}>
38
+ <View style={styles.settingRowText}>
39
+ <Text>Allow direct messages</Text>
40
+ <Text variant="tertiary">
41
+ When off, you can't be added to new direct messages, and any you're already in
42
+ will be turned off.
43
+ </Text>
44
+ </View>
45
+ <Switch
46
+ value={!directMessagesDisabled}
47
+ onValueChange={enabled => setDirectMessagesDisabled(!enabled)}
48
+ disabled={isMutating}
49
+ />
50
+ </View>
51
+ )}
52
+ <NavRow label="Notification settings" onPress={handleNotificationSettings} />
53
+ {appName !== 'services' && <NavRow label="Get help" onPress={handleGetHelp} />}
54
+ </View>
55
+ </ScrollView>
54
56
  <View style={styles.footer}>
55
57
  <VersionFooter />
56
58
  </View>
@@ -90,8 +92,6 @@ const useStyles = () => {
90
92
  flex: 1,
91
93
  },
92
94
  footer: {
93
- flex: 1,
94
- justifyContent: 'flex-end',
95
95
  paddingBottom: Math.max(insets.bottom, 8),
96
96
  },
97
97
  sectionOuter: {