@planningcenter/chat-react-native 4.4.1-qa-staging.25 → 4.4.1-qa-staging.27

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 (80) hide show
  1. package/build/components/conversations/conversations.d.ts.map +1 -1
  2. package/build/components/conversations/conversations.js +3 -2
  3. package/build/components/conversations/conversations.js.map +1 -1
  4. package/build/components/display/filter_dropdown.d.ts +6 -1
  5. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  6. package/build/components/display/filter_dropdown.js +57 -35
  7. package/build/components/display/filter_dropdown.js.map +1 -1
  8. package/build/components/display/index.d.ts +1 -0
  9. package/build/components/display/index.d.ts.map +1 -1
  10. package/build/components/display/index.js +1 -0
  11. package/build/components/display/index.js.map +1 -1
  12. package/build/components/display/popover_menu_view.d.ts +8 -0
  13. package/build/components/display/popover_menu_view.d.ts.map +1 -0
  14. package/build/components/display/popover_menu_view.js +482 -0
  15. package/build/components/display/popover_menu_view.js.map +1 -0
  16. package/build/contexts/conversations_context.d.ts.map +1 -1
  17. package/build/contexts/conversations_context.js +20 -17
  18. package/build/contexts/conversations_context.js.map +1 -1
  19. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -1
  20. package/build/hooks/use_clear_conversation_filters.js +2 -1
  21. package/build/hooks/use_clear_conversation_filters.js.map +1 -1
  22. package/build/hooks/use_conversations_cache.d.ts +1 -1
  23. package/build/hooks/use_conversations_cache.d.ts.map +1 -1
  24. package/build/hooks/use_conversations_cache.js +64 -55
  25. package/build/hooks/use_conversations_cache.js.map +1 -1
  26. package/build/hooks/use_conversations_jolt_events.js +2 -2
  27. package/build/hooks/use_conversations_jolt_events.js.map +1 -1
  28. package/build/hooks/use_jolt.d.ts.map +1 -1
  29. package/build/hooks/use_jolt.js +37 -9
  30. package/build/hooks/use_jolt.js.map +1 -1
  31. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  32. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  33. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  34. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  35. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  36. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  37. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
  38. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +251 -185
  39. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
  40. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  41. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  42. package/build/screens/conversations/conversations_screen.js +1 -2
  43. package/build/screens/conversations/conversations_screen.js.map +1 -1
  44. package/build/screens/design_system_screen.d.ts.map +1 -1
  45. package/build/screens/design_system_screen.js +289 -201
  46. package/build/screens/design_system_screen.js.map +1 -1
  47. package/build/utils/native_adapters/menu.d.ts +1 -2
  48. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  49. package/build/utils/native_adapters/menu.js +2 -41
  50. package/build/utils/native_adapters/menu.js.map +1 -1
  51. package/build/utils/request/conversation.d.ts +2 -1
  52. package/build/utils/request/conversation.d.ts.map +1 -1
  53. package/build/utils/request/conversation.js +4 -1
  54. package/build/utils/request/conversation.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +1 -0
  57. package/src/components/conversations/conversations.tsx +12 -2
  58. package/src/components/display/__tests__/filter_dropdown.test.tsx +18 -2
  59. package/src/components/display/__tests__/popover_menu_view.test.tsx +63 -0
  60. package/src/components/display/filter_dropdown.tsx +11 -3
  61. package/src/components/display/index.ts +1 -0
  62. package/src/components/display/popover_menu_view.tsx +239 -0
  63. package/src/contexts/conversations_context.tsx +8 -1
  64. package/src/hooks/__tests__/use_conversations_jolt_events.test.tsx +135 -0
  65. package/src/hooks/__tests__/use_jolt.test.tsx +76 -1
  66. package/src/hooks/use_clear_conversation_filters.ts +1 -0
  67. package/src/hooks/use_conversations_cache.ts +29 -24
  68. package/src/hooks/use_conversations_jolt_events.ts +2 -2
  69. package/src/hooks/use_jolt.ts +26 -2
  70. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  71. package/src/screens/conversation_filters/screen_props.ts +1 -0
  72. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +2 -12
  73. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +74 -7
  74. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +55 -14
  75. package/src/screens/conversations/conversations_screen.tsx +2 -2
  76. package/src/screens/design_system_screen.tsx +34 -1
  77. package/src/utils/__tests__/conversation.test.ts +1 -0
  78. package/src/utils/native_adapters/menu.tsx +4 -18
  79. package/src/utils/request/conversation.ts +4 -0
  80. package/src/utils/native_adapters/__tests__/menu.test.tsx +0 -46
@@ -1,13 +1,13 @@
1
1
  import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
- import React, { useCallback, useMemo } from 'react'
3
- import { Alert, StyleSheet, View } from 'react-native'
2
+ import React, { useCallback, useMemo, useState } from 'react'
3
+ import { Alert, LayoutChangeEvent, StyleSheet, View } from 'react-native'
4
4
  import { FilterDropdown, FilterDropdownOption, TextButton } from '../../../components'
5
5
  import {
6
- useAtFontScaleBreakpoint,
7
6
  useCanBrowseArchivedConversations,
8
7
  useCanCreateConversations,
9
8
  useCurrentPersonCache,
10
9
  useDirectMessagesEligible,
10
+ useFontScale,
11
11
  useHasDirectMessages,
12
12
  useNewConversationEntry,
13
13
  useTheme,
@@ -25,6 +25,8 @@ import { ConversationsScreenProps } from '../conversations_screen'
25
25
  import { ListHeaderActionButton } from './list_header_action_button'
26
26
 
27
27
  const ROW_PADDING_HORIZONTAL = 16
28
+ const HEADING_ROW_GAP = 12
29
+ const DEFAULT_FONT_SCALE = 1
28
30
  const ALL_CONVERSATIONS_LABEL = 'All conversations'
29
31
  const ARCHIVED_LABEL = 'Archived'
30
32
 
@@ -33,6 +35,7 @@ enum FilterTypes {
33
35
  Groups = 'Groups',
34
36
  Teams = 'Teams',
35
37
  DMs = 'DMs',
38
+ Unread = 'Unread',
36
39
  More = 'More',
37
40
  }
38
41
 
@@ -41,6 +44,7 @@ const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, Conversation
41
44
  [FilterTypes.Groups]: { group_source_app_name: 'Groups' },
42
45
  [FilterTypes.Teams]: { group_source_app_name: 'Services' },
43
46
  [FilterTypes.DMs]: { show_direct_messages: true },
47
+ [FilterTypes.Unread]: { show_unread: true },
44
48
  }
45
49
 
46
50
  export const FilterDropdownListHeaderComponent = () => {
@@ -59,6 +63,7 @@ export const FilterDropdownListHeaderComponent = () => {
59
63
  group_source_app_name = '',
60
64
  show_direct_messages,
61
65
  show_archived,
66
+ show_unread,
62
67
  } = route.params || {}
63
68
  const currentPersonCache = useCurrentPersonCache()
64
69
  const { markAllRead, isPending } = useMarkAllRead()
@@ -81,10 +86,12 @@ export const FilterDropdownListHeaderComponent = () => {
81
86
  return FilterTypes.Teams
82
87
  } else if (show_direct_messages) {
83
88
  return FilterTypes.DMs
89
+ } else if (show_unread) {
90
+ return FilterTypes.Unread
84
91
  }
85
92
 
86
93
  return FilterTypes.All
87
- }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
94
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived, show_unread])
88
95
 
89
96
  const options: FilterDropdownOption[] = useMemo(
90
97
  () => [
@@ -92,6 +99,7 @@ export const FilterDropdownListHeaderComponent = () => {
92
99
  ...(canFilterByGroups ? [{ id: FilterTypes.Groups, label: FilterTypes.Groups }] : []),
93
100
  ...(canFilterByTeams ? [{ id: FilterTypes.Teams, label: FilterTypes.Teams }] : []),
94
101
  ...(canFilterByDMs ? [{ id: FilterTypes.DMs, label: FilterTypes.DMs }] : []),
102
+ { id: FilterTypes.Unread, label: FilterTypes.Unread },
95
103
  ],
96
104
  [canFilterByGroups, canFilterByTeams, canFilterByDMs]
97
105
  )
@@ -142,6 +150,7 @@ export const FilterDropdownListHeaderComponent = () => {
142
150
  group_source_app_name: undefined,
143
151
  show_direct_messages: undefined,
144
152
  show_archived: undefined,
153
+ show_unread: undefined,
145
154
  ...params,
146
155
  })
147
156
  },
@@ -170,8 +179,16 @@ export const FilterDropdownListHeaderComponent = () => {
170
179
  group_source_app_name,
171
180
  show_direct_messages,
172
181
  show_archived,
182
+ show_unread,
173
183
  })
174
- }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
184
+ }, [
185
+ navigation,
186
+ chat_group_graph_id,
187
+ group_source_app_name,
188
+ show_direct_messages,
189
+ show_archived,
190
+ show_unread,
191
+ ])
175
192
 
176
193
  const handleMarkAllRead = useCallback(() => {
177
194
  currentPersonCache.update({ unreadCount: 0 })
@@ -190,11 +207,31 @@ export const FilterDropdownListHeaderComponent = () => {
190
207
 
191
208
  const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
192
209
 
193
- const styles = useStyles()
210
+ const stacked = useFontScale() > DEFAULT_FONT_SCALE
211
+ const styles = useStyles({ stacked })
212
+
213
+ // The Android menu host measures the trigger at its intrinsic width, so flex shrinking never
214
+ // truncates the label. Cap it to the room left beside the action buttons instead.
215
+ const [rowWidth, setRowWidth] = useState<number>()
216
+ const [actionsWidth, setActionsWidth] = useState<number>()
217
+ const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
218
+ setRowWidth(event.nativeEvent.layout.width)
219
+ }, [])
220
+ const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
221
+ setActionsWidth(event.nativeEvent.layout.width)
222
+ }, [])
223
+ const triggerMaxWidth =
224
+ !stacked && rowWidth !== undefined && actionsWidth !== undefined
225
+ ? rowWidth - ROW_PADDING_HORIZONTAL * 2 - actionsWidth - HEADING_ROW_GAP
226
+ : undefined
194
227
 
195
228
  return (
196
229
  <View style={styles.listHeader}>
197
- <View style={styles.headingRow} testID="conversations-filter-heading-row">
230
+ <View
231
+ style={styles.headingRow}
232
+ onLayout={handleRowLayout}
233
+ testID="conversations-filter-heading-row"
234
+ >
198
235
  <View style={styles.titleContainer}>
199
236
  <FilterDropdown
200
237
  options={options}
@@ -203,9 +240,14 @@ export const FilterDropdownListHeaderComponent = () => {
203
240
  onMoreFilters={handleOpenConversationFilters}
204
241
  showMoreFilters={showMoreFilters}
205
242
  triggerLabel={triggerLabel}
243
+ maxWidth={triggerMaxWidth}
206
244
  />
207
245
  </View>
208
- <View style={styles.actionsContainer}>
246
+ <View
247
+ style={styles.actionsContainer}
248
+ onLayout={handleActionsLayout}
249
+ testID="conversations-filter-actions"
250
+ >
209
251
  <ListHeaderActionButton
210
252
  name="general.cog"
211
253
  onPress={handleChatSettings}
@@ -250,9 +292,8 @@ export const FilterDropdownListHeaderComponent = () => {
250
292
  )
251
293
  }
252
294
 
253
- const useStyles = () => {
295
+ const useStyles = ({ stacked }: { stacked: boolean }) => {
254
296
  const theme = useTheme()
255
- const atFontScaleBreakpoint = useAtFontScaleBreakpoint(MAX_FONT_SIZE_MULTIPLIER_LANDMARK)
256
297
 
257
298
  return StyleSheet.create({
258
299
  listHeader: {
@@ -262,11 +303,11 @@ const useStyles = () => {
262
303
  gap: 8,
263
304
  },
264
305
  headingRow: {
265
- gap: 12,
266
- flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
267
- alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
306
+ gap: HEADING_ROW_GAP,
307
+ flexDirection: stacked ? 'column' : 'row',
308
+ alignItems: stacked ? 'flex-start' : 'center',
268
309
  justifyContent: 'space-between',
269
- paddingBottom: atFontScaleBreakpoint ? 4 : 0,
310
+ paddingBottom: stacked ? 4 : 0,
270
311
  paddingHorizontal: ROW_PADDING_HORIZONTAL,
271
312
  },
272
313
  titleContainer: {
@@ -8,7 +8,6 @@ import { analyticsEvents, usePublishProductAnalyticsEvent } from '../../hooks/us
8
8
  import { useSearchEnabled } from '../../hooks/use_search_enabled'
9
9
  import { AppName } from '../../types/resources/app_name'
10
10
  import { GraphId } from '../../types/resources/group_resource'
11
- import { Menu } from '../../utils/native_adapters'
12
11
  import { FilterDropdownListHeaderComponent } from './components/filter_dropdown_list_header_component'
13
12
  import { ListHeaderComponent } from './components/list_header_component'
14
13
  import { SearchListHeaderComponent } from './components/search_list_header_component'
@@ -19,6 +18,7 @@ export type ConversationsScreenProps = StaticScreenProps<{
19
18
  group_source_app_name?: AppName
20
19
  show_direct_messages?: boolean
21
20
  show_archived?: boolean
21
+ show_unread?: boolean
22
22
  }>
23
23
 
24
24
  export function ConversationsScreen({ route }: ConversationsScreenProps) {
@@ -40,7 +40,7 @@ const useListHeaderComponent = () => {
40
40
  const filterDropdownEnabled = useFilterDropdownEnabled()
41
41
  const searchEnabled = useSearchEnabled()
42
42
 
43
- if (filterDropdownEnabled && Menu.isConfigured) return FilterDropdownListHeaderComponent
43
+ if (filterDropdownEnabled) return FilterDropdownListHeaderComponent
44
44
  if (searchEnabled) return SearchListHeaderComponent
45
45
 
46
46
  return ListHeaderComponent
@@ -20,6 +20,7 @@ import {
20
20
  IconButton,
21
21
  Image,
22
22
  Person,
23
+ PopoverMenuView,
23
24
  Spinner,
24
25
  Switch,
25
26
  Text,
@@ -223,6 +224,7 @@ function PressablesSection({ isLast }: SectionProps) {
223
224
  const styles = useStyles()
224
225
  const [filterSelected, setFilterSelected] = useState('Groups')
225
226
  const [dropdownSelected, setDropdownSelected] = useState('all')
227
+ const [popoverSelected, setPopoverSelected] = useState('all')
226
228
 
227
229
  return (
228
230
  <CollapsableSection title="Pressables" isLast={isLast}>
@@ -400,7 +402,7 @@ function PressablesSection({ isLast }: SectionProps) {
400
402
  </Group>
401
403
  <Group
402
404
  title="FilterDropdown"
403
- description="Trigger that opens the host's native menu of filter options plus a More filters action. Needs a menu adapter."
405
+ description="Trigger that opens the host's native menu of filter options plus a More filters action. Falls back to PopoverMenuView when the host configures no menu adapter."
404
406
  >
405
407
  <Row style={styles.alignRowLeft}>
406
408
  <FilterDropdown
@@ -414,6 +416,37 @@ function PressablesSection({ isLast }: SectionProps) {
414
416
  />
415
417
  </Row>
416
418
  </Group>
419
+ <Group
420
+ title="PopoverMenuView"
421
+ description="The JS-only menu FilterDropdown falls back to when the host configures no menu adapter. Opens under the trigger, or above it when the trigger is in the lower half of the screen."
422
+ >
423
+ <Row style={styles.alignRowLeft}>
424
+ <PopoverMenuView
425
+ actions={[
426
+ ...filterDropdownOptions.map(option => ({
427
+ id: option.id,
428
+ title: option.label,
429
+ state: option.id === popoverSelected ? ('on' as const) : ('off' as const),
430
+ })),
431
+ {
432
+ id: 'more',
433
+ title: '',
434
+ displayInline: true,
435
+ subactions: [{ id: 'more_filters', title: 'More filters' }],
436
+ },
437
+ ]}
438
+ onPressAction={({ nativeEvent }) =>
439
+ nativeEvent.event === 'more_filters'
440
+ ? Alert.alert('More filters')
441
+ : setPopoverSelected(nativeEvent.event)
442
+ }
443
+ >
444
+ <Text>
445
+ {filterDropdownOptions.find(option => option.id === popoverSelected)?.label} ▾
446
+ </Text>
447
+ </PopoverMenuView>
448
+ </Row>
449
+ </Group>
417
450
  <Group
418
451
  title="IconButton"
419
452
  description="Supports different appearances, sizes, along with loading & disabled states. Use `iconStyle` for custom colors and font sizes. Requires `accessibilityLabel` as icon's don't provide context to screen readers."
@@ -13,6 +13,7 @@ describe('getConversationsRequestArgs', () => {
13
13
  ['chat_group_graph_id', { chat_group_graph_id: groupId }, 'mine_or_not_empty,group'],
14
14
  ['show_direct_messages', { show_direct_messages: true }, 'mine_or_not_empty,direct_message'],
15
15
  ['show_archived', { show_archived: true }, 'mine_or_not_empty,archived'],
16
+ ['show_unread', { show_unread: true }, 'mine_or_not_empty,unread'],
16
17
  ])('%s uses filter: %s', (_label, args, expectedFilter) => {
17
18
  expect(getConversationsRequestArgs(args).data.filter).toBe(expectedFilter)
18
19
  })
@@ -1,5 +1,5 @@
1
- import React, { useEffect } from 'react'
2
- import { StyleProp, View, ViewStyle } from 'react-native'
1
+ import React from 'react'
2
+ import { StyleProp, ViewStyle } from 'react-native'
3
3
 
4
4
  // =================================
5
5
  // ====== Types ====================
@@ -47,24 +47,10 @@ export type MenuView = React.ComponentType<MenuViewProps>
47
47
  // ====== Adapter ==================
48
48
  // =================================
49
49
 
50
- function UnconfiguredMenuView({ children, style, testID }: MenuViewProps) {
51
- useEffect(() => {
52
- console.warn('ChatAdapters.configure() must include a menu adapter before rendering a menu')
53
- }, [])
54
-
55
- return (
56
- <View style={style} testID={testID}>
57
- {children}
58
- </View>
59
- )
60
- }
61
-
62
50
  export class MenuAdapter {
63
- View: MenuView
64
- isConfigured: boolean
51
+ View?: MenuView
65
52
 
66
53
  constructor(methods?: { View: MenuView }) {
67
- this.View = methods?.View ?? UnconfiguredMenuView
68
- this.isConfigured = !!methods?.View
54
+ this.View = methods?.View
69
55
  }
70
56
  }
@@ -7,6 +7,7 @@ export interface ConversationRequestArgs {
7
7
  group_source_app_name?: string
8
8
  show_direct_messages?: boolean
9
9
  show_archived?: boolean
10
+ show_unread?: boolean
10
11
  }
11
12
 
12
13
  export const getConversationsRequestArgs = ({
@@ -15,12 +16,15 @@ export const getConversationsRequestArgs = ({
15
16
  group_source_app_name,
16
17
  show_direct_messages,
17
18
  show_archived,
19
+ show_unread,
18
20
  }: Partial<ConversationRequestArgs> = {}): GetRequest => {
19
21
  let filter: string
20
22
  if (chat_group_graph_id) {
21
23
  filter = 'mine_or_not_empty,group'
22
24
  } else if (show_archived) {
23
25
  filter = 'mine_or_not_empty,archived'
26
+ } else if (show_unread) {
27
+ filter = 'mine_or_not_empty,unread'
24
28
  } else if (show_direct_messages) {
25
29
  filter = 'mine_or_not_empty,direct_message'
26
30
  } else if (group_source_app_name) {
@@ -1,46 +0,0 @@
1
- import { render, screen } from '@testing-library/react-native'
2
- import React from 'react'
3
- import { Text } from 'react-native'
4
- import { MenuAdapter, MenuViewProps } from '../menu'
5
-
6
- const actions = [{ id: 'all', title: 'All' }]
7
-
8
- describe('MenuAdapter', () => {
9
- it('renders the trigger and warns when the host configured no menu', () => {
10
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
11
- const { View, isConfigured } = new MenuAdapter()
12
-
13
- expect(isConfigured).toBe(false)
14
-
15
- render(
16
- <View actions={actions}>
17
- <Text>Trigger</Text>
18
- </View>
19
- )
20
-
21
- expect(screen.getByText('Trigger')).toBeTruthy()
22
- expect(warn).toHaveBeenCalledTimes(1)
23
- warn.mockRestore()
24
- })
25
-
26
- it('renders through the view the host supplies', () => {
27
- const HostMenuView = ({ actions: hostActions, children }: MenuViewProps) => (
28
- <>
29
- {children}
30
- <Text>{hostActions.map(action => action.title).join(',')}</Text>
31
- </>
32
- )
33
- const { View, isConfigured } = new MenuAdapter({ View: HostMenuView })
34
-
35
- expect(isConfigured).toBe(true)
36
-
37
- render(
38
- <View actions={actions}>
39
- <Text>Trigger</Text>
40
- </View>
41
- )
42
-
43
- expect(screen.getByText('Trigger')).toBeTruthy()
44
- expect(screen.getByText('All')).toBeTruthy()
45
- })
46
- })