@planningcenter/chat-react-native 4.4.1-qa-staging.26 → 4.4.1-qa-staging.28

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 (79) 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.map +1 -1
  5. package/build/components/display/filter_dropdown.js +3 -1
  6. package/build/components/display/filter_dropdown.js.map +1 -1
  7. package/build/components/display/index.d.ts +1 -0
  8. package/build/components/display/index.d.ts.map +1 -1
  9. package/build/components/display/index.js +1 -0
  10. package/build/components/display/index.js.map +1 -1
  11. package/build/components/display/popover_menu_view.d.ts +8 -0
  12. package/build/components/display/popover_menu_view.d.ts.map +1 -0
  13. package/build/components/display/popover_menu_view.js +482 -0
  14. package/build/components/display/popover_menu_view.js.map +1 -0
  15. package/build/contexts/conversations_context.d.ts.map +1 -1
  16. package/build/contexts/conversations_context.js +20 -17
  17. package/build/contexts/conversations_context.js.map +1 -1
  18. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -1
  19. package/build/hooks/use_clear_conversation_filters.js +2 -1
  20. package/build/hooks/use_clear_conversation_filters.js.map +1 -1
  21. package/build/hooks/use_conversations_cache.d.ts +1 -1
  22. package/build/hooks/use_conversations_cache.d.ts.map +1 -1
  23. package/build/hooks/use_conversations_cache.js +64 -55
  24. package/build/hooks/use_conversations_cache.js.map +1 -1
  25. package/build/hooks/use_conversations_jolt_events.js +2 -2
  26. package/build/hooks/use_conversations_jolt_events.js.map +1 -1
  27. package/build/hooks/use_jolt.d.ts.map +1 -1
  28. package/build/hooks/use_jolt.js +37 -9
  29. package/build/hooks/use_jolt.js.map +1 -1
  30. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  31. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  32. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  33. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  34. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  35. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  36. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
  37. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +199 -206
  38. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
  39. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  40. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  41. package/build/screens/conversations/conversations_screen.js +1 -2
  42. package/build/screens/conversations/conversations_screen.js.map +1 -1
  43. package/build/screens/design_system_screen.d.ts.map +1 -1
  44. package/build/screens/design_system_screen.js +289 -201
  45. package/build/screens/design_system_screen.js.map +1 -1
  46. package/build/utils/native_adapters/menu.d.ts +1 -2
  47. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  48. package/build/utils/native_adapters/menu.js +2 -41
  49. package/build/utils/native_adapters/menu.js.map +1 -1
  50. package/build/utils/request/conversation.d.ts +2 -1
  51. package/build/utils/request/conversation.d.ts.map +1 -1
  52. package/build/utils/request/conversation.js +4 -1
  53. package/build/utils/request/conversation.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +1 -0
  56. package/src/components/conversations/conversations.tsx +12 -2
  57. package/src/components/display/__tests__/filter_dropdown.test.tsx +18 -2
  58. package/src/components/display/__tests__/popover_menu_view.test.tsx +63 -0
  59. package/src/components/display/filter_dropdown.tsx +4 -2
  60. package/src/components/display/index.ts +1 -0
  61. package/src/components/display/popover_menu_view.tsx +239 -0
  62. package/src/contexts/conversations_context.tsx +8 -1
  63. package/src/hooks/__tests__/use_conversations_jolt_events.test.tsx +135 -0
  64. package/src/hooks/__tests__/use_jolt.test.tsx +76 -1
  65. package/src/hooks/use_clear_conversation_filters.ts +1 -0
  66. package/src/hooks/use_conversations_cache.ts +29 -24
  67. package/src/hooks/use_conversations_jolt_events.ts +2 -2
  68. package/src/hooks/use_jolt.ts +26 -2
  69. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  70. package/src/screens/conversation_filters/screen_props.ts +1 -0
  71. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +2 -12
  72. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +57 -38
  73. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +25 -27
  74. package/src/screens/conversations/conversations_screen.tsx +2 -2
  75. package/src/screens/design_system_screen.tsx +34 -1
  76. package/src/utils/__tests__/conversation.test.ts +1 -0
  77. package/src/utils/native_adapters/menu.tsx +4 -18
  78. package/src/utils/request/conversation.ts +4 -0
  79. package/src/utils/native_adapters/__tests__/menu.test.tsx +0 -46
@@ -7,7 +7,6 @@ import {
7
7
  useCanCreateConversations,
8
8
  useCurrentPersonCache,
9
9
  useDirectMessagesEligible,
10
- useFontScale,
11
10
  useHasDirectMessages,
12
11
  useNewConversationEntry,
13
12
  useTheme,
@@ -26,7 +25,6 @@ import { ListHeaderActionButton } from './list_header_action_button'
26
25
 
27
26
  const ROW_PADDING_HORIZONTAL = 16
28
27
  const HEADING_ROW_GAP = 12
29
- const DEFAULT_FONT_SCALE = 1
30
28
  const ALL_CONVERSATIONS_LABEL = 'All conversations'
31
29
  const ARCHIVED_LABEL = 'Archived'
32
30
 
@@ -35,6 +33,7 @@ enum FilterTypes {
35
33
  Groups = 'Groups',
36
34
  Teams = 'Teams',
37
35
  DMs = 'DMs',
36
+ Unread = 'Unread',
38
37
  More = 'More',
39
38
  }
40
39
 
@@ -43,6 +42,7 @@ const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, Conversation
43
42
  [FilterTypes.Groups]: { group_source_app_name: 'Groups' },
44
43
  [FilterTypes.Teams]: { group_source_app_name: 'Services' },
45
44
  [FilterTypes.DMs]: { show_direct_messages: true },
45
+ [FilterTypes.Unread]: { show_unread: true },
46
46
  }
47
47
 
48
48
  export const FilterDropdownListHeaderComponent = () => {
@@ -61,6 +61,7 @@ export const FilterDropdownListHeaderComponent = () => {
61
61
  group_source_app_name = '',
62
62
  show_direct_messages,
63
63
  show_archived,
64
+ show_unread,
64
65
  } = route.params || {}
65
66
  const currentPersonCache = useCurrentPersonCache()
66
67
  const { markAllRead, isPending } = useMarkAllRead()
@@ -83,10 +84,12 @@ export const FilterDropdownListHeaderComponent = () => {
83
84
  return FilterTypes.Teams
84
85
  } else if (show_direct_messages) {
85
86
  return FilterTypes.DMs
87
+ } else if (show_unread) {
88
+ return FilterTypes.Unread
86
89
  }
87
90
 
88
91
  return FilterTypes.All
89
- }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
92
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived, show_unread])
90
93
 
91
94
  const options: FilterDropdownOption[] = useMemo(
92
95
  () => [
@@ -94,6 +97,7 @@ export const FilterDropdownListHeaderComponent = () => {
94
97
  ...(canFilterByGroups ? [{ id: FilterTypes.Groups, label: FilterTypes.Groups }] : []),
95
98
  ...(canFilterByTeams ? [{ id: FilterTypes.Teams, label: FilterTypes.Teams }] : []),
96
99
  ...(canFilterByDMs ? [{ id: FilterTypes.DMs, label: FilterTypes.DMs }] : []),
100
+ { id: FilterTypes.Unread, label: FilterTypes.Unread },
97
101
  ],
98
102
  [canFilterByGroups, canFilterByTeams, canFilterByDMs]
99
103
  )
@@ -144,6 +148,7 @@ export const FilterDropdownListHeaderComponent = () => {
144
148
  group_source_app_name: undefined,
145
149
  show_direct_messages: undefined,
146
150
  show_archived: undefined,
151
+ show_unread: undefined,
147
152
  ...params,
148
153
  })
149
154
  },
@@ -172,8 +177,16 @@ export const FilterDropdownListHeaderComponent = () => {
172
177
  group_source_app_name,
173
178
  show_direct_messages,
174
179
  show_archived,
180
+ show_unread,
175
181
  })
176
- }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
182
+ }, [
183
+ navigation,
184
+ chat_group_graph_id,
185
+ group_source_app_name,
186
+ show_direct_messages,
187
+ show_archived,
188
+ show_unread,
189
+ ])
177
190
 
178
191
  const handleMarkAllRead = useCallback(() => {
179
192
  currentPersonCache.update({ unreadCount: 0 })
@@ -192,23 +205,13 @@ export const FilterDropdownListHeaderComponent = () => {
192
205
 
193
206
  const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
194
207
 
195
- const stacked = useFontScale() > DEFAULT_FONT_SCALE
196
- const styles = useStyles({ stacked })
208
+ const styles = useStyles()
197
209
 
198
- // The Android menu host measures the trigger at its intrinsic width, so flex shrinking never
199
- // truncates the label. Cap it to the room left beside the action buttons instead.
200
210
  const [rowWidth, setRowWidth] = useState<number>()
201
- const [actionsWidth, setActionsWidth] = useState<number>()
202
211
  const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
203
212
  setRowWidth(event.nativeEvent.layout.width)
204
213
  }, [])
205
- const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
206
- setActionsWidth(event.nativeEvent.layout.width)
207
- }, [])
208
- const triggerMaxWidth =
209
- !stacked && rowWidth !== undefined && actionsWidth !== undefined
210
- ? rowWidth - ROW_PADDING_HORIZONTAL * 2 - actionsWidth - HEADING_ROW_GAP
211
- : undefined
214
+ const triggerMaxWidth = rowWidth !== undefined ? rowWidth - ROW_PADDING_HORIZONTAL * 2 : undefined
212
215
 
213
216
  return (
214
217
  <View style={styles.listHeader}>
@@ -228,11 +231,7 @@ export const FilterDropdownListHeaderComponent = () => {
228
231
  maxWidth={triggerMaxWidth}
229
232
  />
230
233
  </View>
231
- <View
232
- style={styles.actionsContainer}
233
- onLayout={handleActionsLayout}
234
- testID="conversations-filter-actions"
235
- >
234
+ <View style={styles.actionsContainer}>
236
235
  <ListHeaderActionButton
237
236
  name="general.cog"
238
237
  onPress={handleChatSettings}
@@ -277,7 +276,7 @@ export const FilterDropdownListHeaderComponent = () => {
277
276
  )
278
277
  }
279
278
 
280
- const useStyles = ({ stacked }: { stacked: boolean }) => {
279
+ const useStyles = () => {
281
280
  const theme = useTheme()
282
281
 
283
282
  return StyleSheet.create({
@@ -289,15 +288,14 @@ const useStyles = ({ stacked }: { stacked: boolean }) => {
289
288
  },
290
289
  headingRow: {
291
290
  gap: HEADING_ROW_GAP,
292
- flexDirection: stacked ? 'column' : 'row',
293
- alignItems: stacked ? 'flex-start' : 'center',
291
+ flexDirection: 'row',
292
+ flexWrap: 'wrap',
293
+ alignItems: 'center',
294
294
  justifyContent: 'space-between',
295
- paddingBottom: stacked ? 4 : 0,
296
295
  paddingHorizontal: ROW_PADDING_HORIZONTAL,
297
296
  },
298
297
  titleContainer: {
299
- flexShrink: 1,
300
- minWidth: 0,
298
+ flexShrink: 0,
301
299
  },
302
300
  actionsContainer: {
303
301
  flexDirection: 'row',
@@ -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
- })