@planningcenter/chat-react-native 4.5.0-rc.3 → 4.5.0-rc.5
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/display/filter_dropdown.d.ts +16 -0
- package/build/components/display/filter_dropdown.d.ts.map +1 -1
- package/build/components/display/filter_dropdown.js +103 -57
- package/build/components/display/filter_dropdown.js.map +1 -1
- package/build/hooks/use_conversations_actions.d.ts.map +1 -1
- package/build/hooks/use_conversations_actions.js +9 -10
- package/build/hooks/use_conversations_actions.js.map +1 -1
- package/build/hooks/use_conversations_cache.d.ts +2 -0
- package/build/hooks/use_conversations_cache.d.ts.map +1 -1
- package/build/hooks/use_conversations_cache.js +100 -59
- package/build/hooks/use_conversations_cache.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 +159 -124
- package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
- package/package.json +2 -2
- package/src/components/display/filter_dropdown.tsx +50 -18
- package/src/hooks/__tests__/use_conversations_mark_read.test.tsx +33 -0
- package/src/hooks/use_conversations_actions.ts +3 -4
- package/src/hooks/use_conversations_cache.ts +16 -0
- package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +0 -54
- package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +41 -27
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { StyleSheet, View } from 'react-native'
|
|
2
|
+
import { LayoutChangeEvent, StyleSheet, View } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../hooks'
|
|
4
4
|
import {
|
|
5
5
|
MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
|
|
@@ -78,30 +78,62 @@ export function FilterDropdown({
|
|
|
78
78
|
|
|
79
79
|
return (
|
|
80
80
|
<MenuView actions={actions} onPressAction={handlePressAction} style={styles.container}>
|
|
81
|
-
<
|
|
82
|
-
|
|
81
|
+
<FilterDropdownTrigger
|
|
82
|
+
label={triggerLabel}
|
|
83
|
+
maxWidth={maxWidth}
|
|
83
84
|
accessible
|
|
84
85
|
accessibilityRole="button"
|
|
85
86
|
accessibilityHint="Opens filter options"
|
|
86
|
-
|
|
87
|
-
<Text
|
|
88
|
-
style={styles.triggerLabel}
|
|
89
|
-
numberOfLines={1}
|
|
90
|
-
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
91
|
-
>
|
|
92
|
-
{triggerLabel}
|
|
93
|
-
</Text>
|
|
94
|
-
<Icon
|
|
95
|
-
name="general.downChevron"
|
|
96
|
-
style={styles.triggerIcon}
|
|
97
|
-
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
98
|
-
accessibilityElementsHidden
|
|
99
|
-
/>
|
|
100
|
-
</View>
|
|
87
|
+
/>
|
|
101
88
|
</MenuView>
|
|
102
89
|
)
|
|
103
90
|
}
|
|
104
91
|
|
|
92
|
+
// =================================
|
|
93
|
+
// ====== Trigger ==================
|
|
94
|
+
// =================================
|
|
95
|
+
|
|
96
|
+
export interface FilterDropdownTriggerProps {
|
|
97
|
+
label: string
|
|
98
|
+
maxWidth?: number
|
|
99
|
+
accessible?: boolean
|
|
100
|
+
accessibilityRole?: 'button'
|
|
101
|
+
accessibilityHint?: string
|
|
102
|
+
accessibilityElementsHidden?: boolean
|
|
103
|
+
importantForAccessibility?: 'no-hide-descendants'
|
|
104
|
+
onLayout?: (event: LayoutChangeEvent) => void
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The label and chevron the dropdown opens from. Exported so a screen can render a hidden copy
|
|
109
|
+
* to measure how wide a label is at the current font scale.
|
|
110
|
+
*/
|
|
111
|
+
export function FilterDropdownTrigger({
|
|
112
|
+
label,
|
|
113
|
+
maxWidth,
|
|
114
|
+
...viewProps
|
|
115
|
+
}: FilterDropdownTriggerProps) {
|
|
116
|
+
const styles = useStyles()
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View style={[styles.trigger, maxWidth !== undefined && { maxWidth }]} {...viewProps}>
|
|
120
|
+
<Text
|
|
121
|
+
style={styles.triggerLabel}
|
|
122
|
+
numberOfLines={1}
|
|
123
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
124
|
+
>
|
|
125
|
+
{label}
|
|
126
|
+
</Text>
|
|
127
|
+
<Icon
|
|
128
|
+
name="general.downChevron"
|
|
129
|
+
style={styles.triggerIcon}
|
|
130
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
131
|
+
accessibilityElementsHidden
|
|
132
|
+
/>
|
|
133
|
+
</View>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
105
137
|
// =================================
|
|
106
138
|
// ====== Styles ===================
|
|
107
139
|
// =================================
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { act, renderHook } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import { ConversationResource } from '../../types'
|
|
6
|
+
import { getConversationsRequestArgs } from '../../utils/request/conversation'
|
|
7
|
+
import { useConversationsMarkRead } from '../use_conversations_actions'
|
|
8
|
+
import { getRequestQueryKey } from '../use_suspense_api'
|
|
9
|
+
|
|
10
|
+
jest.mock('../use_api_client', () => ({
|
|
11
|
+
useApiClient: () => ({
|
|
12
|
+
chat: { post: jest.fn().mockResolvedValue({}), get: jest.fn().mockResolvedValue({ data: [] }) },
|
|
13
|
+
}),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('useConversationsMarkRead', () => {
|
|
17
|
+
it('marks the conversation read in a list outside the current conversations context', async () => {
|
|
18
|
+
const conversation = { id: 7, unreadCount: 8 } as ConversationResource
|
|
19
|
+
const archivedListKey = getRequestQueryKey(getConversationsRequestArgs({ show_archived: true }))
|
|
20
|
+
const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
|
|
21
|
+
queryClient.setQueryData(archivedListKey, { pages: [{ data: [conversation] }], pageParams: [] })
|
|
22
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
23
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
24
|
+
)
|
|
25
|
+
const { result } = renderHook(() => useConversationsMarkRead({ conversation }), { wrapper })
|
|
26
|
+
|
|
27
|
+
await act(async () => result.current.markRead(true))
|
|
28
|
+
|
|
29
|
+
expect(queryClient.getQueryData(archivedListKey)).toMatchObject({
|
|
30
|
+
pages: [{ data: [{ id: 7, unreadCount: 0 }] }],
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -17,7 +17,7 @@ export const useConversationsMarkRead = ({
|
|
|
17
17
|
const queryClient = useQueryClient()
|
|
18
18
|
const { args } = useConversationsContext()
|
|
19
19
|
const currentPersonCache = useCurrentPersonCache()
|
|
20
|
-
const {
|
|
20
|
+
const { updateInAllLists, invalidateAllLists, fetchUpdate } = useConversationsCache(args)
|
|
21
21
|
const conversationQueryKey = getRequestQueryKey(
|
|
22
22
|
getConversationRequestArgs({ conversation_id: conversation.id })
|
|
23
23
|
)
|
|
@@ -32,8 +32,7 @@ export const useConversationsMarkRead = ({
|
|
|
32
32
|
|
|
33
33
|
const { mutate: handleMarkRead, ...mutation } = useMutation({
|
|
34
34
|
onMutate: async (read: boolean) => {
|
|
35
|
-
|
|
36
|
-
...conversation,
|
|
35
|
+
updateInAllLists(conversation.id, {
|
|
37
36
|
unreadCount: read ? 0 : 1,
|
|
38
37
|
...(read ? { unreadReaction: false } : {}),
|
|
39
38
|
})
|
|
@@ -59,7 +58,7 @@ export const useConversationsMarkRead = ({
|
|
|
59
58
|
},
|
|
60
59
|
onError: () => {
|
|
61
60
|
Alert.alert('Oops', 'Something went wrong updating this conversation, please try again')
|
|
62
|
-
|
|
61
|
+
invalidateAllLists()
|
|
63
62
|
},
|
|
64
63
|
})
|
|
65
64
|
|
|
@@ -67,6 +67,20 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
|
67
67
|
)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const updateInAllLists = (id: number, update: Partial<ConversationResource>) => {
|
|
71
|
+
queryClient.setQueriesData<QueryData>({ queryKey: [conversationsRequestArgs.url] }, prev => {
|
|
72
|
+
if (!prev?.pages) return prev
|
|
73
|
+
|
|
74
|
+
return updateAllRecordsInPagesData({
|
|
75
|
+
data: prev,
|
|
76
|
+
processRecord: record => (record.id === id ? { ...record, ...update } : record),
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const invalidateAllLists = () =>
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: [conversationsRequestArgs.url] })
|
|
83
|
+
|
|
70
84
|
const handleConversationDestroy = ({ id }: { id: number }) => {
|
|
71
85
|
if (!id) return
|
|
72
86
|
|
|
@@ -106,7 +120,9 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
|
106
120
|
fetchSync: fetchAndSync,
|
|
107
121
|
fetchUpdate: fetchAndUpdate,
|
|
108
122
|
invalidate: () => queryClient.invalidateQueries({ queryKey: conversationQueryKey }),
|
|
123
|
+
invalidateAllLists,
|
|
109
124
|
update: updateOrCreate,
|
|
110
125
|
updateAll,
|
|
126
|
+
updateInAllLists,
|
|
111
127
|
}
|
|
112
128
|
}
|
|
@@ -21,15 +21,6 @@ const mockSearchEnabled = jest.fn()
|
|
|
21
21
|
const mockCanBrowseArchived = jest.fn()
|
|
22
22
|
const mockGroupsByGraphId: Record<string, { name: string }> = {}
|
|
23
23
|
|
|
24
|
-
jest.mock('react-native', () => {
|
|
25
|
-
const actual = jest.requireActual('react-native')
|
|
26
|
-
return Object.defineProperty(actual, 'useWindowDimensions', {
|
|
27
|
-
value: jest.fn(() => ({ fontScale: 1 })),
|
|
28
|
-
configurable: true,
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
const mockUseWindowDimensions = jest.requireMock('react-native').useWindowDimensions
|
|
32
|
-
|
|
33
24
|
jest.mock('@react-navigation/native', () => ({
|
|
34
25
|
...jest.requireActual('@react-navigation/native'),
|
|
35
26
|
useNavigation: () => ({ setParams: mockSetParams, navigate: mockNavigate }),
|
|
@@ -130,46 +121,10 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
130
121
|
mockSetParams.mockClear()
|
|
131
122
|
mockSearchEnabled.mockReturnValue(true)
|
|
132
123
|
mockCanBrowseArchived.mockReturnValue(false)
|
|
133
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
|
|
134
124
|
setCapabilities({ groups: true, teams: true })
|
|
135
125
|
setRouteParams({})
|
|
136
126
|
})
|
|
137
127
|
|
|
138
|
-
it('keeps the dropdown and actions on one line at the default font scale', () => {
|
|
139
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
|
|
140
|
-
|
|
141
|
-
render(<FilterDropdownListHeaderComponent />)
|
|
142
|
-
|
|
143
|
-
expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
|
|
144
|
-
flexDirection: 'row',
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('stacks the actions under the dropdown at the first font size above the default', () => {
|
|
149
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1.12 })
|
|
150
|
-
|
|
151
|
-
render(<FilterDropdownListHeaderComponent />)
|
|
152
|
-
|
|
153
|
-
expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
|
|
154
|
-
flexDirection: 'column',
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('caps the trigger to the room beside the actions so a long label truncates', () => {
|
|
159
|
-
render(<FilterDropdownListHeaderComponent />)
|
|
160
|
-
|
|
161
|
-
fireEvent(screen.getByTestId('conversations-filter-heading-row'), 'layout', {
|
|
162
|
-
nativeEvent: { layout: { width: 360 } },
|
|
163
|
-
})
|
|
164
|
-
fireEvent(screen.getByTestId('conversations-filter-actions'), 'layout', {
|
|
165
|
-
nativeEvent: { layout: { width: 160 } },
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
expect(screen.getByRole('button', { name: 'All conversations' })).toHaveStyle({
|
|
169
|
-
maxWidth: 156,
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
128
|
it('offers every filter the person can use, then More filters', () => {
|
|
174
129
|
render(<FilterDropdownListHeaderComponent />)
|
|
175
130
|
|
|
@@ -344,13 +299,4 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
344
299
|
|
|
345
300
|
expect(mockNavigate).toHaveBeenCalledWith('Search')
|
|
346
301
|
})
|
|
347
|
-
|
|
348
|
-
it('renders the search button after the other actions', () => {
|
|
349
|
-
const view = render(<FilterDropdownListHeaderComponent />)
|
|
350
|
-
|
|
351
|
-
const buttons = view.getAllByRole('button').map(button => button.props.accessibilityLabel)
|
|
352
|
-
|
|
353
|
-
expect(buttons.indexOf('Search')).toBeGreaterThan(buttons.indexOf('Chat settings'))
|
|
354
|
-
expect(buttons.indexOf('Search')).toBeGreaterThan(buttons.indexOf('New conversation'))
|
|
355
|
-
})
|
|
356
302
|
})
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
|
|
2
2
|
import React, { useCallback, useMemo, useState } from 'react'
|
|
3
3
|
import { Alert, LayoutChangeEvent, StyleSheet, View } from 'react-native'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FilterDropdown,
|
|
6
|
+
FilterDropdownOption,
|
|
7
|
+
FilterDropdownTrigger,
|
|
8
|
+
TextButton,
|
|
9
|
+
} from '../../../components'
|
|
5
10
|
import {
|
|
6
11
|
useCanBrowseArchivedConversations,
|
|
7
12
|
useCanCreateConversations,
|
|
8
13
|
useCurrentPersonCache,
|
|
9
14
|
useDirectMessagesEligible,
|
|
10
|
-
useFontScale,
|
|
11
15
|
useHasDirectMessages,
|
|
12
16
|
useNewConversationEntry,
|
|
13
17
|
useTheme,
|
|
@@ -26,7 +30,6 @@ import { ListHeaderActionButton } from './list_header_action_button'
|
|
|
26
30
|
|
|
27
31
|
const ROW_PADDING_HORIZONTAL = 16
|
|
28
32
|
const HEADING_ROW_GAP = 12
|
|
29
|
-
const DEFAULT_FONT_SCALE = 1
|
|
30
33
|
const ALL_CONVERSATIONS_LABEL = 'All conversations'
|
|
31
34
|
const ARCHIVED_LABEL = 'Archived'
|
|
32
35
|
|
|
@@ -207,31 +210,43 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
207
210
|
|
|
208
211
|
const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
|
|
209
212
|
|
|
210
|
-
const
|
|
211
|
-
const styles = useStyles({ stacked })
|
|
213
|
+
const styles = useStyles()
|
|
212
214
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
+
// Cap the trigger beside the actions while the widest fixed label fits there, so only longer
|
|
216
|
+
// labels truncate; past that, cap it to the row and let the actions wrap.
|
|
215
217
|
const [rowWidth, setRowWidth] = useState<number>()
|
|
216
218
|
const [actionsWidth, setActionsWidth] = useState<number>()
|
|
219
|
+
const [widestFixedLabelWidth, setWidestFixedLabelWidth] = useState<number>()
|
|
217
220
|
const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
|
|
218
221
|
setRowWidth(event.nativeEvent.layout.width)
|
|
219
222
|
}, [])
|
|
220
223
|
const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
|
|
221
224
|
setActionsWidth(event.nativeEvent.layout.width)
|
|
222
225
|
}, [])
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const handleWidestFixedLabelLayout = useCallback((event: LayoutChangeEvent) => {
|
|
227
|
+
setWidestFixedLabelWidth(event.nativeEvent.layout.width)
|
|
228
|
+
}, [])
|
|
229
|
+
const triggerMaxWidth = useMemo(() => {
|
|
230
|
+
if (rowWidth === undefined) return undefined
|
|
231
|
+
|
|
232
|
+
const rowContentWidth = rowWidth - ROW_PADDING_HORIZONTAL * 2
|
|
233
|
+
if (actionsWidth === undefined || widestFixedLabelWidth === undefined) return rowContentWidth
|
|
234
|
+
|
|
235
|
+
const roomBesideActions = rowContentWidth - actionsWidth - HEADING_ROW_GAP
|
|
236
|
+
return roomBesideActions >= widestFixedLabelWidth ? roomBesideActions : rowContentWidth
|
|
237
|
+
}, [rowWidth, actionsWidth, widestFixedLabelWidth])
|
|
227
238
|
|
|
228
239
|
return (
|
|
229
240
|
<View style={styles.listHeader}>
|
|
230
|
-
<View
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
<View style={styles.widestFixedLabel} pointerEvents="none">
|
|
242
|
+
<FilterDropdownTrigger
|
|
243
|
+
label={ALL_CONVERSATIONS_LABEL}
|
|
244
|
+
onLayout={handleWidestFixedLabelLayout}
|
|
245
|
+
accessibilityElementsHidden
|
|
246
|
+
importantForAccessibility="no-hide-descendants"
|
|
247
|
+
/>
|
|
248
|
+
</View>
|
|
249
|
+
<View style={styles.headingRow} onLayout={handleRowLayout}>
|
|
235
250
|
<View style={styles.titleContainer}>
|
|
236
251
|
<FilterDropdown
|
|
237
252
|
options={options}
|
|
@@ -243,11 +258,7 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
243
258
|
maxWidth={triggerMaxWidth}
|
|
244
259
|
/>
|
|
245
260
|
</View>
|
|
246
|
-
<View
|
|
247
|
-
style={styles.actionsContainer}
|
|
248
|
-
onLayout={handleActionsLayout}
|
|
249
|
-
testID="conversations-filter-actions"
|
|
250
|
-
>
|
|
261
|
+
<View style={styles.actionsContainer} onLayout={handleActionsLayout}>
|
|
251
262
|
<ListHeaderActionButton
|
|
252
263
|
name="general.cog"
|
|
253
264
|
onPress={handleChatSettings}
|
|
@@ -292,7 +303,7 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
292
303
|
)
|
|
293
304
|
}
|
|
294
305
|
|
|
295
|
-
const useStyles = (
|
|
306
|
+
const useStyles = () => {
|
|
296
307
|
const theme = useTheme()
|
|
297
308
|
|
|
298
309
|
return StyleSheet.create({
|
|
@@ -304,15 +315,18 @@ const useStyles = ({ stacked }: { stacked: boolean }) => {
|
|
|
304
315
|
},
|
|
305
316
|
headingRow: {
|
|
306
317
|
gap: HEADING_ROW_GAP,
|
|
307
|
-
flexDirection:
|
|
308
|
-
|
|
318
|
+
flexDirection: 'row',
|
|
319
|
+
flexWrap: 'wrap',
|
|
320
|
+
alignItems: 'center',
|
|
309
321
|
justifyContent: 'space-between',
|
|
310
|
-
paddingBottom: stacked ? 4 : 0,
|
|
311
322
|
paddingHorizontal: ROW_PADDING_HORIZONTAL,
|
|
312
323
|
},
|
|
313
324
|
titleContainer: {
|
|
314
|
-
flexShrink:
|
|
315
|
-
|
|
325
|
+
flexShrink: 0,
|
|
326
|
+
},
|
|
327
|
+
widestFixedLabel: {
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
opacity: 0,
|
|
316
330
|
},
|
|
317
331
|
actionsContainer: {
|
|
318
332
|
flexDirection: 'row',
|