@planningcenter/chat-react-native 4.4.1-qa-staging.16 → 4.4.1-qa-staging.18

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 (173) hide show
  1. package/build/components/conversations/archived_results_footer.d.ts +6 -0
  2. package/build/components/conversations/archived_results_footer.d.ts.map +1 -0
  3. package/build/components/conversations/archived_results_footer.js +51 -0
  4. package/build/components/conversations/archived_results_footer.js.map +1 -0
  5. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  6. package/build/components/conversations/conversation_actions.js +241 -206
  7. package/build/components/conversations/conversation_actions.js.map +1 -1
  8. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  9. package/build/components/conversations/conversation_preview.js +99 -93
  10. package/build/components/conversations/conversation_preview.js.map +1 -1
  11. package/build/components/conversations/conversations.d.ts.map +1 -1
  12. package/build/components/conversations/conversations.js +65 -40
  13. package/build/components/conversations/conversations.js.map +1 -1
  14. package/build/components/conversations/conversations_blank_state.d.ts.map +1 -1
  15. package/build/components/conversations/conversations_blank_state.js +55 -12
  16. package/build/components/conversations/conversations_blank_state.js.map +1 -1
  17. package/build/components/conversations/filtered_results_footer.d.ts +2 -0
  18. package/build/components/conversations/filtered_results_footer.d.ts.map +1 -0
  19. package/build/components/conversations/filtered_results_footer.js +77 -0
  20. package/build/components/conversations/filtered_results_footer.js.map +1 -0
  21. package/build/components/display/filter_dropdown.d.ts +2 -1
  22. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  23. package/build/components/display/filter_dropdown.js +63 -58
  24. package/build/components/display/filter_dropdown.js.map +1 -1
  25. package/build/components/display/icon_tile.d.ts +12 -0
  26. package/build/components/display/icon_tile.d.ts.map +1 -0
  27. package/build/components/display/icon_tile.js +109 -0
  28. package/build/components/display/icon_tile.js.map +1 -0
  29. package/build/components/display/index.d.ts +1 -0
  30. package/build/components/display/index.d.ts.map +1 -1
  31. package/build/components/display/index.js +1 -0
  32. package/build/components/display/index.js.map +1 -1
  33. package/build/contexts/conversations_context.d.ts.map +1 -1
  34. package/build/contexts/conversations_context.js +21 -18
  35. package/build/contexts/conversations_context.js.map +1 -1
  36. package/build/hooks/index.d.ts +2 -0
  37. package/build/hooks/index.d.ts.map +1 -1
  38. package/build/hooks/index.js +2 -0
  39. package/build/hooks/index.js.map +1 -1
  40. package/build/hooks/use_can_browse_archived_conversations.d.ts +2 -0
  41. package/build/hooks/use_can_browse_archived_conversations.d.ts.map +1 -0
  42. package/build/hooks/use_can_browse_archived_conversations.js +29 -0
  43. package/build/hooks/use_can_browse_archived_conversations.js.map +1 -0
  44. package/build/hooks/use_clear_conversation_filters.d.ts +2 -0
  45. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -0
  46. package/build/hooks/use_clear_conversation_filters.js +25 -0
  47. package/build/hooks/use_clear_conversation_filters.js.map +1 -0
  48. package/build/hooks/use_conversation.d.ts.map +1 -1
  49. package/build/hooks/use_conversation.js +32 -24
  50. package/build/hooks/use_conversation.js.map +1 -1
  51. package/build/hooks/use_current_person.d.ts.map +1 -1
  52. package/build/hooks/use_current_person.js +1 -1
  53. package/build/hooks/use_current_person.js.map +1 -1
  54. package/build/hooks/use_jolt.d.ts.map +1 -1
  55. package/build/hooks/use_jolt.js +29 -8
  56. package/build/hooks/use_jolt.js.map +1 -1
  57. package/build/hooks/use_manual_archiving_enabled.d.ts +2 -0
  58. package/build/hooks/use_manual_archiving_enabled.d.ts.map +1 -0
  59. package/build/hooks/use_manual_archiving_enabled.js +18 -0
  60. package/build/hooks/use_manual_archiving_enabled.js.map +1 -0
  61. package/build/hooks/use_reduce_motion.d.ts.map +1 -1
  62. package/build/hooks/use_reduce_motion.js +2 -1
  63. package/build/hooks/use_reduce_motion.js.map +1 -1
  64. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  65. package/build/screens/conversation_details_screen.js +2 -1
  66. package/build/screens/conversation_details_screen.js.map +1 -1
  67. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
  68. package/build/screens/conversation_filters/components/conversation_filters.js +88 -18
  69. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  70. package/build/screens/conversation_filters/components/rows.d.ts +9 -0
  71. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  72. package/build/screens/conversation_filters/components/rows.js +92 -1
  73. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  74. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  75. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  76. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  77. package/build/screens/conversation_filters/filter_types.d.ts +1 -0
  78. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
  79. package/build/screens/conversation_filters/filter_types.js +1 -0
  80. package/build/screens/conversation_filters/filter_types.js.map +1 -1
  81. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  82. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  83. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  84. package/build/screens/conversation_filters_screen.d.ts +1 -0
  85. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  86. package/build/screens/conversation_filters_screen.js +79 -48
  87. package/build/screens/conversation_filters_screen.js.map +1 -1
  88. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts +2 -0
  89. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -0
  90. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +587 -0
  91. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -0
  92. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  93. package/build/screens/conversations/components/list_header_component.js +95 -90
  94. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  95. package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -1
  96. package/build/screens/conversations/components/search_list_header_component.js +100 -95
  97. package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
  98. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  99. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  100. package/build/screens/conversations/conversations_screen.js +15 -2
  101. package/build/screens/conversations/conversations_screen.js.map +1 -1
  102. package/build/types/resources/person.d.ts +1 -0
  103. package/build/types/resources/person.d.ts.map +1 -1
  104. package/build/types/resources/person.js.map +1 -1
  105. package/build/utils/date.d.ts.map +1 -1
  106. package/build/utils/date.js +5 -10
  107. package/build/utils/date.js.map +1 -1
  108. package/build/utils/native_adapters/menu.d.ts +1 -0
  109. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  110. package/build/utils/native_adapters/menu.js +1 -0
  111. package/build/utils/native_adapters/menu.js.map +1 -1
  112. package/build/utils/request/conversation.d.ts +2 -1
  113. package/build/utils/request/conversation.d.ts.map +1 -1
  114. package/build/utils/request/conversation.js +4 -1
  115. package/build/utils/request/conversation.js.map +1 -1
  116. package/build/vendor/font_awesome_pro_solid_icons.d.ts +1 -0
  117. package/build/vendor/font_awesome_pro_solid_icons.d.ts.map +1 -1
  118. package/build/vendor/font_awesome_pro_solid_icons.js +5 -0
  119. package/build/vendor/font_awesome_pro_solid_icons.js.map +1 -1
  120. package/package.json +3 -3
  121. package/src/__tests__/hooks/use_can_browse_archived_conversations.test.tsx +98 -0
  122. package/src/__tests__/screens/conversation_filters/components/conversation_filters.test.tsx +132 -0
  123. package/src/__tests__/screens/conversation_filters_screen.test.tsx +76 -0
  124. package/src/__tests__/screens/conversation_new/components/services_form.test.tsx +1 -0
  125. package/src/components/conversations/__tests__/conversation_actions.test.tsx +67 -0
  126. package/src/components/conversations/__tests__/conversation_preview.test.tsx +38 -3
  127. package/src/components/conversations/__tests__/conversations.test.tsx +82 -0
  128. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +73 -0
  129. package/src/components/conversations/archived_results_footer.tsx +36 -0
  130. package/src/components/conversations/conversation_actions.tsx +27 -19
  131. package/src/components/conversations/conversation_preview.tsx +30 -10
  132. package/src/components/conversations/conversations.tsx +25 -2
  133. package/src/components/conversations/conversations_blank_state.tsx +21 -0
  134. package/src/components/conversations/filtered_results_footer.tsx +38 -0
  135. package/src/components/display/__tests__/filter_dropdown.test.tsx +9 -1
  136. package/src/components/display/filter_dropdown.tsx +14 -13
  137. package/src/components/display/icon_tile.tsx +54 -0
  138. package/src/components/display/index.ts +1 -0
  139. package/src/contexts/conversations_context.tsx +2 -1
  140. package/src/hooks/__tests__/use_conversation_archive.test.tsx +22 -0
  141. package/src/hooks/__tests__/use_jolt.test.tsx +30 -8
  142. package/src/hooks/__tests__/use_reduce_motion.test.tsx +13 -1
  143. package/src/hooks/index.ts +2 -0
  144. package/src/hooks/use_can_browse_archived_conversations.ts +17 -0
  145. package/src/hooks/use_clear_conversation_filters.ts +16 -0
  146. package/src/hooks/use_conversation.ts +4 -1
  147. package/src/hooks/use_current_person.ts +1 -0
  148. package/src/hooks/use_jolt.ts +33 -8
  149. package/src/hooks/use_manual_archiving_enabled.ts +10 -0
  150. package/src/hooks/use_reduce_motion.ts +2 -1
  151. package/src/screens/conversation_details_screen.tsx +2 -3
  152. package/src/screens/conversation_filters/__tests__/conversation_filters.test.tsx +101 -0
  153. package/src/screens/conversation_filters/components/conversation_filters.tsx +40 -7
  154. package/src/screens/conversation_filters/components/rows.tsx +29 -1
  155. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  156. package/src/screens/conversation_filters/filter_types.ts +1 -0
  157. package/src/screens/conversation_filters/screen_props.ts +1 -0
  158. package/src/screens/conversation_filters_screen.tsx +25 -5
  159. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +52 -0
  160. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +248 -0
  161. package/src/screens/conversations/components/__tests__/list_header_component.test.tsx +21 -4
  162. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +284 -0
  163. package/src/screens/conversations/components/list_header_component.tsx +17 -12
  164. package/src/screens/conversations/components/search_list_header_component.tsx +17 -12
  165. package/src/screens/conversations/conversations_screen.tsx +15 -2
  166. package/src/types/resources/person.ts +1 -0
  167. package/src/utils/__tests__/conversation.test.ts +1 -0
  168. package/src/utils/__tests__/date.test.ts +4 -0
  169. package/src/utils/date.ts +5 -4
  170. package/src/utils/native_adapters/__tests__/menu.test.tsx +6 -2
  171. package/src/utils/native_adapters/menu.tsx +2 -0
  172. package/src/utils/request/conversation.ts +4 -0
  173. package/src/vendor/font_awesome_pro_solid_icons.ts +12 -0
@@ -0,0 +1,248 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { Pressable, Text } from 'react-native'
4
+ import {
5
+ AudioAdapter,
6
+ ChatAdapters,
7
+ ClipboardAdapter,
8
+ ImagePickerAdapter,
9
+ MenuAction,
10
+ MenuAdapter,
11
+ MenuViewProps,
12
+ VideoAdapter,
13
+ } from '../../../../utils/native_adapters'
14
+ import { FilterDropdownListHeaderComponent } from '../filter_dropdown_list_header_component'
15
+
16
+ const mockNavigate = jest.fn()
17
+ const mockSetParams = jest.fn()
18
+ const mockRouteParams: Record<string, unknown> = {}
19
+ const mockCanDisplayGroups = jest.fn()
20
+ const mockSearchEnabled = jest.fn()
21
+ const mockCanBrowseArchived = jest.fn()
22
+ const mockGroupsByGraphId: Record<string, { name: string }> = {}
23
+
24
+ jest.mock('@react-navigation/native', () => ({
25
+ ...jest.requireActual('@react-navigation/native'),
26
+ useNavigation: () => ({ setParams: mockSetParams, navigate: mockNavigate }),
27
+ useRoute: () => ({ params: mockRouteParams }),
28
+ }))
29
+
30
+ jest.mock('../../../../hooks/use_api', () => ({
31
+ useApiGet: ({ url }: { url: string }, { enabled }: { enabled: boolean }) => ({
32
+ data: enabled ? mockGroupsByGraphId[url.replace('/me/groups/', '')] : undefined,
33
+ }),
34
+ }))
35
+ jest.mock('../../../../hooks/use_groups', () => ({
36
+ useCanDisplayGroups: (args: { source_type: string }) => mockCanDisplayGroups(args),
37
+ }))
38
+ jest.mock('../../../../hooks/use_direct_messages_eligible', () => ({
39
+ useDirectMessagesEligible: () => true,
40
+ }))
41
+ jest.mock('../../../../hooks/use_has_direct_messages', () => ({
42
+ useHasDirectMessages: () => true,
43
+ }))
44
+ jest.mock('../../../../hooks/use_can_browse_archived_conversations', () => ({
45
+ useCanBrowseArchivedConversations: () => mockCanBrowseArchived(),
46
+ }))
47
+ jest.mock('../../../../hooks/use_chat_permissions', () => ({
48
+ useCanCreateConversations: () => true,
49
+ }))
50
+ jest.mock('../../../../hooks/use_new_conversation_entry', () => ({
51
+ useNewConversationEntry: () => 'select_type',
52
+ }))
53
+ jest.mock('../../../../hooks/use_current_person', () => ({
54
+ useCurrentPersonCache: () => ({ update: jest.fn() }),
55
+ }))
56
+ jest.mock('../../../../hooks/use_conversations_actions', () => ({
57
+ useMarkAllRead: () => ({ markAllRead: jest.fn(), isPending: false }),
58
+ }))
59
+ jest.mock('../../../../hooks/use_new_conversation_from_filter', () => ({
60
+ useNewConversationFromFilter: () => undefined,
61
+ }))
62
+ jest.mock('../../../../hooks/use_search_enabled', () => ({
63
+ useSearchEnabled: () => mockSearchEnabled(),
64
+ }))
65
+
66
+ const FakeMenuView = ({ actions, onPressAction, children }: MenuViewProps) => {
67
+ const flatten = (items: MenuAction[]): MenuAction[] =>
68
+ items.flatMap(item => (item.displayInline && item.subactions ? item.subactions : [item]))
69
+
70
+ return (
71
+ <>
72
+ {children}
73
+ {flatten(actions).map(action => (
74
+ <Pressable
75
+ key={action.id}
76
+ accessibilityRole="menuitem"
77
+ accessibilityLabel={action.title}
78
+ accessibilityState={{ selected: action.state === 'on' }}
79
+ onPress={() => onPressAction?.({ nativeEvent: { event: action.id ?? action.title } })}
80
+ >
81
+ <Text>{action.title}</Text>
82
+ </Pressable>
83
+ ))}
84
+ </>
85
+ )
86
+ }
87
+
88
+ beforeAll(() => {
89
+ ChatAdapters.configure({
90
+ clipboard: new ClipboardAdapter({
91
+ getStringAsync: async () => '',
92
+ setStringAsync: async () => {},
93
+ }),
94
+ audio: new AudioAdapter({ useAudio: () => ({}) as any }),
95
+ video: new VideoAdapter({ Player: (() => null) as any }),
96
+ imagePicker: new ImagePickerAdapter({
97
+ openCameraAsync: async () => ({ canceled: true, assets: null }),
98
+ openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
99
+ }),
100
+ menu: new MenuAdapter({ View: FakeMenuView }),
101
+ })
102
+ })
103
+
104
+ const setCapabilities = ({ groups, teams }: { groups: boolean; teams: boolean }) => {
105
+ mockCanDisplayGroups.mockImplementation(({ source_type }: { source_type: string }) =>
106
+ source_type === 'Group' ? groups : teams
107
+ )
108
+ }
109
+
110
+ const setRouteParams = (params: Record<string, unknown>) => {
111
+ Object.keys(mockRouteParams).forEach(key => delete mockRouteParams[key])
112
+ Object.assign(mockRouteParams, params)
113
+ }
114
+
115
+ const menuItemNames = () =>
116
+ screen.getAllByRole('menuitem').map(item => item.props.accessibilityLabel)
117
+
118
+ describe('FilterDropdownListHeaderComponent', () => {
119
+ beforeEach(() => {
120
+ mockNavigate.mockClear()
121
+ mockSetParams.mockClear()
122
+ mockSearchEnabled.mockReturnValue(true)
123
+ mockCanBrowseArchived.mockReturnValue(false)
124
+ setCapabilities({ groups: true, teams: true })
125
+ setRouteParams({})
126
+ })
127
+
128
+ it('offers every filter the person can use, then More filters', () => {
129
+ render(<FilterDropdownListHeaderComponent />)
130
+
131
+ expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'More filters'])
132
+ expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
133
+ })
134
+
135
+ it('leaves out Teams and More filters when only DMs are filterable', () => {
136
+ setCapabilities({ groups: false, teams: false })
137
+
138
+ render(<FilterDropdownListHeaderComponent />)
139
+
140
+ expect(menuItemNames()).toEqual(['All', 'DMs'])
141
+ })
142
+
143
+ it('selecting Groups filters to group conversations', () => {
144
+ render(<FilterDropdownListHeaderComponent />)
145
+
146
+ fireEvent.press(screen.getByRole('menuitem', { name: 'Groups' }))
147
+
148
+ expect(mockSetParams).toHaveBeenCalledWith(
149
+ expect.objectContaining({ group_source_app_name: 'Groups', show_direct_messages: undefined })
150
+ )
151
+ })
152
+
153
+ it('labels the trigger with the active filter and checks it', () => {
154
+ setRouteParams({ group_source_app_name: 'Services' })
155
+
156
+ render(<FilterDropdownListHeaderComponent />)
157
+
158
+ expect(screen.getByRole('button', { name: 'Teams' })).toBeTruthy()
159
+ expect(screen.getByRole('menuitem', { name: 'Teams' }).props.accessibilityState.selected).toBe(
160
+ true
161
+ )
162
+ })
163
+
164
+ it("labels the trigger with the group's name when filtered to one group from More filters", () => {
165
+ mockGroupsByGraphId['Group/1'] = { name: 'Youth Group' }
166
+ setRouteParams({ chat_group_graph_id: 'Group/1' })
167
+
168
+ render(<FilterDropdownListHeaderComponent />)
169
+
170
+ expect(screen.getByRole('button', { name: 'Youth Group' })).toBeTruthy()
171
+ })
172
+
173
+ it('falls back to the route title while the group is still loading', () => {
174
+ delete mockGroupsByGraphId['Group/2']
175
+ setRouteParams({ chat_group_graph_id: 'Group/2', title: 'Youth Group' })
176
+
177
+ render(<FilterDropdownListHeaderComponent />)
178
+
179
+ expect(screen.getByRole('button', { name: 'Youth Group' })).toBeTruthy()
180
+ })
181
+
182
+ it('offers More filters when archived conversations are the only extra filter', () => {
183
+ setCapabilities({ groups: false, teams: false })
184
+ mockCanBrowseArchived.mockReturnValue(true)
185
+
186
+ render(<FilterDropdownListHeaderComponent />)
187
+
188
+ expect(menuItemNames()).toEqual(['All', 'DMs', 'More filters'])
189
+ })
190
+
191
+ it('labels the archived list and carries it into More filters', () => {
192
+ setRouteParams({ show_archived: true })
193
+
194
+ render(<FilterDropdownListHeaderComponent />)
195
+
196
+ expect(screen.getByRole('button', { name: 'Archived' })).toBeTruthy()
197
+
198
+ fireEvent.press(screen.getByRole('menuitem', { name: 'More filters' }))
199
+
200
+ expect(mockNavigate).toHaveBeenCalledWith(
201
+ 'ConversationFilters',
202
+ expect.objectContaining({ show_archived: true })
203
+ )
204
+ })
205
+
206
+ it('leaves the archived list when another filter is picked', () => {
207
+ setRouteParams({ show_archived: true })
208
+
209
+ render(<FilterDropdownListHeaderComponent />)
210
+
211
+ fireEvent.press(screen.getByRole('menuitem', { name: 'Groups' }))
212
+
213
+ expect(mockSetParams).toHaveBeenCalledWith(
214
+ expect.objectContaining({ group_source_app_name: 'Groups', show_archived: undefined })
215
+ )
216
+ })
217
+
218
+ it('More filters opens the filters sheet', () => {
219
+ render(<FilterDropdownListHeaderComponent />)
220
+
221
+ fireEvent.press(screen.getByRole('menuitem', { name: 'More filters' }))
222
+
223
+ expect(mockNavigate).toHaveBeenCalledWith('ConversationFilters', expect.any(Object))
224
+ })
225
+
226
+ it('shows the search button only when search is enabled', () => {
227
+ mockSearchEnabled.mockReturnValue(false)
228
+ const view = render(<FilterDropdownListHeaderComponent />)
229
+
230
+ expect(view.queryByTestId('conversations-search-button')).toBeNull()
231
+
232
+ mockSearchEnabled.mockReturnValue(true)
233
+ view.rerender(<FilterDropdownListHeaderComponent />)
234
+
235
+ fireEvent.press(view.getByTestId('conversations-search-button'))
236
+
237
+ expect(mockNavigate).toHaveBeenCalledWith('Search')
238
+ })
239
+
240
+ it('renders the search button after the other actions', () => {
241
+ const view = render(<FilterDropdownListHeaderComponent />)
242
+
243
+ const buttons = view.getAllByRole('button').map(button => button.props.accessibilityLabel)
244
+
245
+ expect(buttons.indexOf('Search')).toBeGreaterThan(buttons.indexOf('Chat settings'))
246
+ expect(buttons.indexOf('Search')).toBeGreaterThan(buttons.indexOf('New conversation'))
247
+ })
248
+ })
@@ -3,13 +3,12 @@ import React from 'react'
3
3
  import { ListHeaderComponent } from '../list_header_component'
4
4
 
5
5
  const mockSetParams = jest.fn()
6
+ let mockParams: Record<string, unknown> = {}
6
7
 
7
8
  jest.mock('@react-navigation/native', () => ({
8
9
  ...jest.requireActual('@react-navigation/native'),
9
10
  useNavigation: () => ({ setParams: mockSetParams, navigate: jest.fn() }),
10
- useRoute: () => ({
11
- params: { chat_group_graph_id: 'Groups/Group/1', title: 'Waco Family Group' },
12
- }),
11
+ useRoute: () => ({ params: mockParams }),
13
12
  }))
14
13
 
15
14
  jest.mock('../../../../hooks/use_groups', () => ({
@@ -45,10 +44,14 @@ const clearedFilter = {
45
44
  chat_group_graph_id: undefined,
46
45
  group_source_app_name: undefined,
47
46
  show_direct_messages: undefined,
47
+ show_archived: undefined,
48
48
  }
49
49
 
50
50
  describe('ListHeaderComponent filter toggles', () => {
51
- beforeEach(() => mockSetParams.mockClear())
51
+ beforeEach(() => {
52
+ mockSetParams.mockClear()
53
+ mockParams = { chat_group_graph_id: 'Groups/Group/1', title: 'Waco Family Group' }
54
+ })
52
55
 
53
56
  it.each([
54
57
  ['Show all conversations', clearedFilter],
@@ -64,3 +67,17 @@ describe('ListHeaderComponent filter toggles', () => {
64
67
  expect(mockSetParams.mock.calls[0][0]).toStrictEqual(expected)
65
68
  })
66
69
  })
70
+
71
+ describe('ListHeaderComponent mark all read', () => {
72
+ it('shows Mark all read on the active list', () => {
73
+ mockParams = {}
74
+
75
+ expect(render(<ListHeaderComponent />).queryByText('Mark all read')).not.toBeNull()
76
+ })
77
+
78
+ it('hides Mark all read on the archived list', () => {
79
+ mockParams = { show_archived: true }
80
+
81
+ expect(render(<ListHeaderComponent />).queryByText('Mark all read')).toBeNull()
82
+ })
83
+ })
@@ -0,0 +1,284 @@
1
+ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
+ import React, { useCallback, useMemo } from 'react'
3
+ import { Alert, StyleSheet, View } from 'react-native'
4
+ import { FilterDropdown, FilterDropdownOption, TextButton } from '../../../components'
5
+ import {
6
+ useAtFontScaleBreakpoint,
7
+ useCanBrowseArchivedConversations,
8
+ useCanCreateConversations,
9
+ useCurrentPersonCache,
10
+ useDirectMessagesEligible,
11
+ useHasDirectMessages,
12
+ useNewConversationEntry,
13
+ useTheme,
14
+ } from '../../../hooks'
15
+ import { useApiGet } from '../../../hooks/use_api'
16
+ import { useMarkAllRead } from '../../../hooks/use_conversations_actions'
17
+ import { useCanDisplayGroups } from '../../../hooks/use_groups'
18
+ import { useNewConversationFromFilter } from '../../../hooks/use_new_conversation_from_filter'
19
+ import { useSearchEnabled } from '../../../hooks/use_search_enabled'
20
+ import { GraphId, GroupResource } from '../../../types/resources/group_resource'
21
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../../utils'
22
+ import { Haptic } from '../../../utils/native_adapters'
23
+ import { ConversationFiltersParams } from '../../conversation_filters/screen_props'
24
+ import { ConversationsScreenProps } from '../conversations_screen'
25
+ import { ListHeaderActionButton } from './list_header_action_button'
26
+
27
+ const ROW_PADDING_HORIZONTAL = 16
28
+ const ALL_CONVERSATIONS_LABEL = 'All conversations'
29
+ const ARCHIVED_LABEL = 'Archived'
30
+
31
+ enum FilterTypes {
32
+ All = 'All',
33
+ Groups = 'Groups',
34
+ Teams = 'Teams',
35
+ DMs = 'DMs',
36
+ More = 'More',
37
+ }
38
+
39
+ const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, ConversationFiltersParams> = {
40
+ [FilterTypes.All]: {},
41
+ [FilterTypes.Groups]: { group_source_app_name: 'Groups' },
42
+ [FilterTypes.Teams]: { group_source_app_name: 'Services' },
43
+ [FilterTypes.DMs]: { show_direct_messages: true },
44
+ }
45
+
46
+ export const FilterDropdownListHeaderComponent = () => {
47
+ const navigation = useNavigation()
48
+ const canFilterByTeams = useCanDisplayGroups({ source_app_name: 'Services', source_type: 'Team' })
49
+ const canFilterByGroups = useCanDisplayGroups({ source_app_name: 'Groups', source_type: 'Group' })
50
+ const directMessagesEligible = useDirectMessagesEligible()
51
+ const hasDirectMessages = useHasDirectMessages()
52
+ const canFilterByDMs = directMessagesEligible && hasDirectMessages
53
+ const canBrowseArchived = useCanBrowseArchivedConversations()
54
+ const searchEnabled = useSearchEnabled()
55
+ const route = useRoute<RouteProp<ConversationsScreenProps['route']>>()
56
+ const {
57
+ title,
58
+ chat_group_graph_id,
59
+ group_source_app_name = '',
60
+ show_direct_messages,
61
+ show_archived,
62
+ } = route.params || {}
63
+ const currentPersonCache = useCurrentPersonCache()
64
+ const { markAllRead, isPending } = useMarkAllRead()
65
+ const canCreateConversations = useCanCreateConversations()
66
+ const entryMode = useNewConversationEntry()
67
+ const newConversationFromFilter = useNewConversationFromFilter(
68
+ chat_group_graph_id as GraphId | undefined
69
+ )
70
+ const { data: group } = useApiGet<GroupResource>(
71
+ { url: `/me/groups/${chat_group_graph_id}`, data: { fields: { Group: [] } }, app: 'chat' },
72
+ { enabled: !!chat_group_graph_id }
73
+ )
74
+
75
+ const active: FilterTypes = useMemo(() => {
76
+ if (chat_group_graph_id || show_archived) {
77
+ return FilterTypes.More
78
+ } else if (/groups/i.test(group_source_app_name)) {
79
+ return FilterTypes.Groups
80
+ } else if (/services/i.test(group_source_app_name)) {
81
+ return FilterTypes.Teams
82
+ } else if (show_direct_messages) {
83
+ return FilterTypes.DMs
84
+ }
85
+
86
+ return FilterTypes.All
87
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
88
+
89
+ const options: FilterDropdownOption[] = useMemo(
90
+ () => [
91
+ { id: FilterTypes.All, label: FilterTypes.All },
92
+ ...(canFilterByGroups ? [{ id: FilterTypes.Groups, label: FilterTypes.Groups }] : []),
93
+ ...(canFilterByTeams ? [{ id: FilterTypes.Teams, label: FilterTypes.Teams }] : []),
94
+ ...(canFilterByDMs ? [{ id: FilterTypes.DMs, label: FilterTypes.DMs }] : []),
95
+ ],
96
+ [canFilterByGroups, canFilterByTeams, canFilterByDMs]
97
+ )
98
+
99
+ const triggerLabel = useMemo(() => {
100
+ if (chat_group_graph_id) return group?.name || title || ALL_CONVERSATIONS_LABEL
101
+ if (show_archived) return ARCHIVED_LABEL
102
+ if (active === FilterTypes.All) return ALL_CONVERSATIONS_LABEL
103
+
104
+ return active
105
+ }, [chat_group_graph_id, group?.name, title, show_archived, active])
106
+
107
+ const handleNewConversationNavigation = useCallback(() => {
108
+ if (newConversationFromFilter || entryMode === 'select_type') {
109
+ return navigation.navigate('ConversationSelectType', {
110
+ ...route.params,
111
+ newConversationFromFilter,
112
+ })
113
+ }
114
+
115
+ if (entryMode === 'groups') {
116
+ return navigation.navigate('New', {
117
+ screen: 'ConversationSelectGroupRecipients',
118
+ params: { ...route.params },
119
+ })
120
+ }
121
+
122
+ if (entryMode === 'teams') {
123
+ return navigation.navigate('New', {
124
+ screen: 'ConversationSelectTeamsILeadRecipients',
125
+ params: { ...route.params },
126
+ })
127
+ }
128
+
129
+ if (entryMode === 'direct_message') {
130
+ return navigation.navigate('New', {
131
+ screen: 'ConversationSelectDirectMessageRecipients',
132
+ params: { ...route.params },
133
+ })
134
+ }
135
+ }, [navigation, route.params, entryMode, newConversationFromFilter])
136
+
137
+ const handleSetFilter = useCallback(
138
+ (params: ConversationFiltersParams) => {
139
+ navigation.setParams({
140
+ title: undefined,
141
+ chat_group_graph_id: undefined,
142
+ group_source_app_name: undefined,
143
+ show_direct_messages: undefined,
144
+ show_archived: undefined,
145
+ ...params,
146
+ })
147
+ },
148
+ [navigation]
149
+ )
150
+
151
+ const handleSelectFilter = useCallback(
152
+ (id: string) => {
153
+ const params = FILTER_PARAMS[id as keyof typeof FILTER_PARAMS]
154
+ if (params) handleSetFilter(params)
155
+ },
156
+ [handleSetFilter]
157
+ )
158
+
159
+ const handleSearch = useCallback(() => {
160
+ navigation.navigate('Search')
161
+ }, [navigation])
162
+
163
+ const handleChatSettings = useCallback(() => {
164
+ navigation.navigate('ChatSettings', {})
165
+ }, [navigation])
166
+
167
+ const handleOpenConversationFilters = useCallback(() => {
168
+ navigation.navigate('ConversationFilters', {
169
+ chat_group_graph_id,
170
+ group_source_app_name,
171
+ show_direct_messages,
172
+ show_archived,
173
+ })
174
+ }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
175
+
176
+ const handleMarkAllRead = useCallback(() => {
177
+ currentPersonCache.update({ unreadCount: 0 })
178
+ Haptic.notificationSuccess()
179
+ markAllRead()
180
+ }, [currentPersonCache, markAllRead])
181
+
182
+ const alertMarkAllRead = useCallback(() => {
183
+ if (active === FilterTypes.All) return handleMarkAllRead()
184
+
185
+ Alert.alert('Mark all read', 'This includes conversations not shown by the current filter.', [
186
+ { text: 'Cancel' },
187
+ { text: 'OK', onPress: () => handleMarkAllRead() },
188
+ ])
189
+ }, [active, handleMarkAllRead])
190
+
191
+ const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
192
+
193
+ const styles = useStyles()
194
+
195
+ return (
196
+ <View style={styles.listHeader}>
197
+ <View style={styles.headingRow}>
198
+ <View style={styles.titleContainer}>
199
+ <FilterDropdown
200
+ options={options}
201
+ selectedId={active}
202
+ onSelect={handleSelectFilter}
203
+ onMoreFilters={handleOpenConversationFilters}
204
+ showMoreFilters={showMoreFilters}
205
+ triggerLabel={triggerLabel}
206
+ />
207
+ </View>
208
+ <View style={styles.actionsContainer}>
209
+ <ListHeaderActionButton
210
+ name="general.cog"
211
+ onPress={handleChatSettings}
212
+ accessibilityLabel="Chat settings"
213
+ testID="conversations-chat-settings-button"
214
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
215
+ />
216
+ {canCreateConversations && (
217
+ <ListHeaderActionButton
218
+ name="churchCenter.signups"
219
+ onPress={handleNewConversationNavigation}
220
+ accessibilityLabel="New conversation"
221
+ testID="conversations-new-conversation-button"
222
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
223
+ />
224
+ )}
225
+ {searchEnabled && (
226
+ <ListHeaderActionButton
227
+ name="general.search"
228
+ onPress={handleSearch}
229
+ accessibilityLabel="Search"
230
+ testID="conversations-search-button"
231
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
232
+ />
233
+ )}
234
+ </View>
235
+ </View>
236
+ <View style={styles.markAllReadRow}>
237
+ <TextButton
238
+ onPress={alertMarkAllRead}
239
+ disabled={isPending}
240
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
241
+ accessibilityShowsLargeContentViewer
242
+ accessibilityLargeContentTitle="Mark all read"
243
+ >
244
+ Mark all read
245
+ </TextButton>
246
+ </View>
247
+ </View>
248
+ )
249
+ }
250
+
251
+ const useStyles = () => {
252
+ const theme = useTheme()
253
+ const atFontScaleBreakpoint = useAtFontScaleBreakpoint()
254
+
255
+ return StyleSheet.create({
256
+ listHeader: {
257
+ borderBottomWidth: 1,
258
+ borderBottomColor: theme.colors.fillColorNeutral050Base,
259
+ paddingBottom: 16,
260
+ gap: 8,
261
+ },
262
+ headingRow: {
263
+ gap: 12,
264
+ flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
265
+ alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
266
+ justifyContent: 'space-between',
267
+ paddingBottom: atFontScaleBreakpoint ? 4 : 0,
268
+ paddingHorizontal: ROW_PADDING_HORIZONTAL,
269
+ },
270
+ titleContainer: {
271
+ flexShrink: 1,
272
+ minWidth: 0,
273
+ },
274
+ actionsContainer: {
275
+ flexDirection: 'row',
276
+ gap: 8,
277
+ alignItems: 'center',
278
+ },
279
+ markAllReadRow: {
280
+ alignItems: 'flex-start',
281
+ paddingHorizontal: ROW_PADDING_HORIZONTAL,
282
+ },
283
+ })
284
+ }
@@ -44,6 +44,7 @@ export const ListHeaderComponent = () => {
44
44
  chat_group_graph_id,
45
45
  group_source_app_name = '',
46
46
  show_direct_messages,
47
+ show_archived,
47
48
  } = route.params || {}
48
49
  const currentPersonCache = useCurrentPersonCache()
49
50
  const { markAllRead, isPending } = useMarkAllRead()
@@ -54,7 +55,7 @@ export const ListHeaderComponent = () => {
54
55
  )
55
56
 
56
57
  const active: FilterTypes = useMemo(() => {
57
- if (chat_group_graph_id) {
58
+ if (chat_group_graph_id || show_archived) {
58
59
  return FilterTypes.More
59
60
  } else if (/groups/i.test(group_source_app_name)) {
60
61
  return FilterTypes.Groups
@@ -65,7 +66,7 @@ export const ListHeaderComponent = () => {
65
66
  }
66
67
 
67
68
  return FilterTypes.All
68
- }, [chat_group_graph_id, group_source_app_name, show_direct_messages])
69
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
69
70
 
70
71
  const handleNewConversationNavigation = useCallback(() => {
71
72
  if (newConversationFromFilter || entryMode === 'select_type') {
@@ -104,6 +105,7 @@ export const ListHeaderComponent = () => {
104
105
  chat_group_graph_id: undefined,
105
106
  group_source_app_name: undefined,
106
107
  show_direct_messages: undefined,
108
+ show_archived: undefined,
107
109
  ...params,
108
110
  })
109
111
  },
@@ -119,8 +121,9 @@ export const ListHeaderComponent = () => {
119
121
  chat_group_graph_id,
120
122
  group_source_app_name,
121
123
  show_direct_messages,
124
+ show_archived,
122
125
  })
123
- }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages])
126
+ }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
124
127
 
125
128
  const handleMarkAllRead = useCallback(() => {
126
129
  currentPersonCache.update({ unreadCount: 0 })
@@ -161,15 +164,17 @@ export const ListHeaderComponent = () => {
161
164
  <Heading numberOfLines={1} variant="h2">
162
165
  Conversations
163
166
  </Heading>
164
- <TextButton
165
- onPress={alertMarkAllRead}
166
- disabled={isPending}
167
- maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
168
- accessibilityShowsLargeContentViewer
169
- accessibilityLargeContentTitle="Mark all read"
170
- >
171
- Mark all read
172
- </TextButton>
167
+ {!show_archived && (
168
+ <TextButton
169
+ onPress={alertMarkAllRead}
170
+ disabled={isPending}
171
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
172
+ accessibilityShowsLargeContentViewer
173
+ accessibilityLargeContentTitle="Mark all read"
174
+ >
175
+ Mark all read
176
+ </TextButton>
177
+ )}
173
178
  </View>
174
179
  <View style={styles.actionsContainer}>
175
180
  <ListHeaderActionButton