@planningcenter/chat-react-native 4.4.1-qa-1036.2 → 4.4.1-qa-staging.41
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_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +155 -123
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +1 -0
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/components/display/filter_dropdown.d.ts +1 -0
- package/build/components/display/filter_dropdown.d.ts.map +1 -1
- package/build/components/display/filter_dropdown.js.map +1 -1
- package/build/components/display/keyboard_scroll_view.d.ts.map +1 -1
- package/build/components/display/keyboard_scroll_view.js +1 -0
- package/build/components/display/keyboard_scroll_view.js.map +1 -1
- package/build/components/page/page_error_boundary.d.ts +9 -1
- package/build/components/page/page_error_boundary.d.ts.map +1 -1
- package/build/components/page/page_error_boundary.js +56 -31
- package/build/components/page/page_error_boundary.js.map +1 -1
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +11 -3
- package/build/navigation/index.js.map +1 -1
- package/build/navigation/page_error_fallbacks.d.ts +7 -0
- package/build/navigation/page_error_fallbacks.d.ts.map +1 -0
- package/build/navigation/page_error_fallbacks.js +7 -0
- package/build/navigation/page_error_fallbacks.js.map +1 -0
- package/build/navigation/screenLayout.d.ts.map +1 -1
- package/build/navigation/screenLayout.js +21 -13
- package/build/navigation/screenLayout.js.map +1 -1
- package/build/screens/age_check/age_check_required_screen.d.ts.map +1 -1
- package/build/screens/age_check/age_check_required_screen.js +1 -0
- package/build/screens/age_check/age_check_required_screen.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +1 -0
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js +1 -0
- package/build/screens/conversation_select_recipients/conversation_new_entry_screen.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 +332 -291
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +1 -0
- package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +1 -0
- package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
- package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
- package/build/screens/conversations/components/filter_dropdown_list_header_component.js +22 -7
- package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
- package/build/screens/get_help_screen.js +56 -62
- package/build/screens/get_help_screen.js.map +1 -1
- package/build/screens/group_notification_settings_screen.js +3 -2
- package/build/screens/group_notification_settings_screen.js.map +1 -1
- package/build/screens/notification_settings_screen.d.ts.map +1 -1
- package/build/screens/notification_settings_screen.js +1 -0
- package/build/screens/notification_settings_screen.js.map +1 -1
- package/build/screens/preferred_app_selection_screen.js +45 -39
- package/build/screens/preferred_app_selection_screen.js.map +1 -1
- package/build/screens/search/components/search_results_list.d.ts +4 -1
- package/build/screens/search/components/search_results_list.d.ts.map +1 -1
- package/build/screens/search/components/search_results_list.js +136 -110
- package/build/screens/search/components/search_results_list.js.map +1 -1
- package/build/screens/search/search_screen.d.ts.map +1 -1
- package/build/screens/search/search_screen.js +21 -28
- package/build/screens/search/search_screen.js.map +1 -1
- package/build/screens/settings_screen.d.ts.map +1 -1
- package/build/screens/settings_screen.js +7 -6
- package/build/screens/settings_screen.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/navigation/screen_layout.test.tsx +55 -0
- package/src/components/conversation/__tests__/message_list.test.tsx +4 -0
- package/src/components/conversation/message_list.tsx +6 -0
- package/src/components/conversations/conversations.tsx +1 -0
- package/src/components/display/filter_dropdown.tsx +1 -0
- package/src/components/display/keyboard_scroll_view.tsx +1 -0
- package/src/components/page/__tests__/page_error_boundary.test.tsx +33 -0
- package/src/components/page/page_error_boundary.tsx +43 -12
- package/src/navigation/index.tsx +11 -2
- package/src/navigation/page_error_fallbacks.ts +14 -0
- package/src/navigation/screenLayout.tsx +6 -3
- package/src/screens/age_check/age_check_required_screen.tsx +5 -1
- package/src/screens/conversation_details_screen.tsx +1 -0
- package/src/screens/conversation_select_recipients/__tests__/conversation_select_direct_message_recipients_screen.test.tsx +100 -0
- package/src/screens/conversation_select_recipients/conversation_new_entry_screen.tsx +4 -1
- package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +78 -68
- package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +1 -0
- package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +1 -0
- package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +59 -1
- package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +24 -9
- package/src/screens/get_help_screen.tsx +4 -4
- package/src/screens/group_notification_settings_screen.tsx +3 -3
- package/src/screens/notification_settings_screen.tsx +1 -0
- package/src/screens/preferred_app_selection_screen.tsx +4 -4
- package/src/screens/search/__tests__/search_screen.test.tsx +5 -7
- package/src/screens/search/components/__tests__/search_results_list.test.tsx +6 -2
- package/src/screens/search/components/search_results_list.tsx +34 -13
- package/src/screens/search/search_screen.tsx +8 -18
- 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
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
228
|
+
if (term.length < 1) {
|
|
229
|
+
if (selectedPeople.size > 0) return null
|
|
241
230
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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={
|
|
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.
|
package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx
CHANGED
|
@@ -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}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react-native'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { Pressable, Text } from 'react-native'
|
|
3
|
+
import { Pressable, StyleSheet, Text } from 'react-native'
|
|
4
4
|
import {
|
|
5
5
|
AudioAdapter,
|
|
6
6
|
ChatAdapters,
|
|
@@ -115,6 +115,25 @@ const setRouteParams = (params: Record<string, unknown>) => {
|
|
|
115
115
|
const menuItemNames = () =>
|
|
116
116
|
screen.getAllByRole('menuitem').map(item => item.props.accessibilityLabel)
|
|
117
117
|
|
|
118
|
+
const ROW_WIDTH = 360
|
|
119
|
+
const ROW_CONTENT_WIDTH = ROW_WIDTH - 16 * 2
|
|
120
|
+
const HEADING_ROW_GAP = 12
|
|
121
|
+
const WIDEST_LABEL_WIDTH = 150
|
|
122
|
+
|
|
123
|
+
const layOut = (testID: string, width: number) =>
|
|
124
|
+
fireEvent(screen.getByTestId(testID, { includeHiddenElements: true }), 'layout', {
|
|
125
|
+
nativeEvent: { layout: { width } },
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const layOutHeader = ({ actionsWidth }: { actionsWidth: number }) => {
|
|
129
|
+
layOut('conversations-filter-heading-row', ROW_WIDTH)
|
|
130
|
+
layOut('conversations-filter-widest-label', WIDEST_LABEL_WIDTH)
|
|
131
|
+
layOut('conversations-filter-heading-actions', actionsWidth)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const triggerMaxWidth = (): number =>
|
|
135
|
+
StyleSheet.flatten(screen.getByRole('button', { name: 'All conversations' }).props.style).maxWidth
|
|
136
|
+
|
|
118
137
|
describe('FilterDropdownListHeaderComponent', () => {
|
|
119
138
|
beforeEach(() => {
|
|
120
139
|
mockNavigate.mockClear()
|
|
@@ -286,6 +305,45 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
286
305
|
)
|
|
287
306
|
})
|
|
288
307
|
|
|
308
|
+
it('caps the trigger short of the room beside the actions so a full label never wraps them', () => {
|
|
309
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
310
|
+
|
|
311
|
+
layOutHeader({ actionsWidth: 120.4 })
|
|
312
|
+
|
|
313
|
+
expect(triggerMaxWidth()).toBeGreaterThanOrEqual(WIDEST_LABEL_WIDTH)
|
|
314
|
+
expect(triggerMaxWidth() + HEADING_ROW_GAP + 120.4).toBeLessThan(ROW_CONTENT_WIDTH)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('holds the cap when the actions remeasure by less than a point', () => {
|
|
318
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
319
|
+
|
|
320
|
+
layOutHeader({ actionsWidth: 120.4 })
|
|
321
|
+
const capBefore = triggerMaxWidth()
|
|
322
|
+
|
|
323
|
+
layOut('conversations-filter-heading-actions', 120.9)
|
|
324
|
+
|
|
325
|
+
expect(triggerMaxWidth()).toBe(capBefore)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('recomputes the cap when the actions grow by a button', () => {
|
|
329
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
330
|
+
|
|
331
|
+
layOutHeader({ actionsWidth: 120.4 })
|
|
332
|
+
const capBefore = triggerMaxWidth()
|
|
333
|
+
|
|
334
|
+
layOut('conversations-filter-heading-actions', 160)
|
|
335
|
+
|
|
336
|
+
expect(triggerMaxWidth()).toBeLessThan(capBefore)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('caps the trigger to the row once All conversations no longer fits beside the actions', () => {
|
|
340
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
341
|
+
|
|
342
|
+
layOutHeader({ actionsWidth: 200 })
|
|
343
|
+
|
|
344
|
+
expect(triggerMaxWidth()).toBe(ROW_CONTENT_WIDTH)
|
|
345
|
+
})
|
|
346
|
+
|
|
289
347
|
it('shows the search button only when search is enabled', () => {
|
|
290
348
|
mockSearchEnabled.mockReturnValue(false)
|
|
291
349
|
const view = render(<FilterDropdownListHeaderComponent />)
|
|
@@ -30,6 +30,8 @@ import { ListHeaderActionButton } from './list_header_action_button'
|
|
|
30
30
|
|
|
31
31
|
const ROW_PADDING_HORIZONTAL = 16
|
|
32
32
|
const HEADING_ROW_GAP = 12
|
|
33
|
+
const TRIGGER_CAP_SLACK = 2
|
|
34
|
+
const LAYOUT_JITTER = 1
|
|
33
35
|
const ALL_CONVERSATIONS_LABEL = 'All conversations'
|
|
34
36
|
const ARCHIVED_LABEL = 'Archived'
|
|
35
37
|
|
|
@@ -42,6 +44,9 @@ enum FilterTypes {
|
|
|
42
44
|
More = 'More',
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
const holdSubPointJitter = (previous: number | undefined, next: number) =>
|
|
48
|
+
previous !== undefined && Math.abs(next - previous) < LAYOUT_JITTER ? previous : next
|
|
49
|
+
|
|
45
50
|
const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, ConversationFiltersParams> = {
|
|
46
51
|
[FilterTypes.All]: {},
|
|
47
52
|
[FilterTypes.Groups]: { group_source_app_name: 'Groups' },
|
|
@@ -217,14 +222,14 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
217
222
|
const [rowWidth, setRowWidth] = useState<number>()
|
|
218
223
|
const [actionsWidth, setActionsWidth] = useState<number>()
|
|
219
224
|
const [widestFixedLabelWidth, setWidestFixedLabelWidth] = useState<number>()
|
|
220
|
-
const handleRowLayout = useCallback((
|
|
221
|
-
setRowWidth(
|
|
225
|
+
const handleRowLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
|
226
|
+
setRowWidth(previous => holdSubPointJitter(previous, nativeEvent.layout.width))
|
|
222
227
|
}, [])
|
|
223
|
-
const handleActionsLayout = useCallback((
|
|
224
|
-
setActionsWidth(
|
|
228
|
+
const handleActionsLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
|
229
|
+
setActionsWidth(previous => holdSubPointJitter(previous, nativeEvent.layout.width))
|
|
225
230
|
}, [])
|
|
226
|
-
const handleWidestFixedLabelLayout = useCallback((
|
|
227
|
-
setWidestFixedLabelWidth(
|
|
231
|
+
const handleWidestFixedLabelLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
|
232
|
+
setWidestFixedLabelWidth(previous => holdSubPointJitter(previous, nativeEvent.layout.width))
|
|
228
233
|
}, [])
|
|
229
234
|
const triggerMaxWidth = useMemo(() => {
|
|
230
235
|
if (rowWidth === undefined) return undefined
|
|
@@ -232,7 +237,8 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
232
237
|
const rowContentWidth = rowWidth - ROW_PADDING_HORIZONTAL * 2
|
|
233
238
|
if (actionsWidth === undefined || widestFixedLabelWidth === undefined) return rowContentWidth
|
|
234
239
|
|
|
235
|
-
const roomBesideActions =
|
|
240
|
+
const roomBesideActions =
|
|
241
|
+
Math.floor(rowContentWidth - actionsWidth - HEADING_ROW_GAP) - TRIGGER_CAP_SLACK
|
|
236
242
|
return roomBesideActions >= widestFixedLabelWidth ? roomBesideActions : rowContentWidth
|
|
237
243
|
}, [rowWidth, actionsWidth, widestFixedLabelWidth])
|
|
238
244
|
|
|
@@ -242,11 +248,16 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
242
248
|
<FilterDropdownTrigger
|
|
243
249
|
label={ALL_CONVERSATIONS_LABEL}
|
|
244
250
|
onLayout={handleWidestFixedLabelLayout}
|
|
251
|
+
testID="conversations-filter-widest-label"
|
|
245
252
|
accessibilityElementsHidden
|
|
246
253
|
importantForAccessibility="no-hide-descendants"
|
|
247
254
|
/>
|
|
248
255
|
</View>
|
|
249
|
-
<View
|
|
256
|
+
<View
|
|
257
|
+
style={styles.headingRow}
|
|
258
|
+
onLayout={handleRowLayout}
|
|
259
|
+
testID="conversations-filter-heading-row"
|
|
260
|
+
>
|
|
250
261
|
<View style={styles.titleContainer}>
|
|
251
262
|
<FilterDropdown
|
|
252
263
|
options={options}
|
|
@@ -258,7 +269,11 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
258
269
|
maxWidth={triggerMaxWidth}
|
|
259
270
|
/>
|
|
260
271
|
</View>
|
|
261
|
-
<View
|
|
272
|
+
<View
|
|
273
|
+
style={styles.actionsContainer}
|
|
274
|
+
onLayout={handleActionsLayout}
|
|
275
|
+
testID="conversations-filter-heading-actions"
|
|
276
|
+
>
|
|
262
277
|
<ListHeaderActionButton
|
|
263
278
|
name="general.cog"
|
|
264
279
|
onPress={handleChatSettings}
|
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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/))
|