@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
@@ -140,19 +140,44 @@ const pingSubscribedJoltClients = (queryClient: QueryClient) => {
140
140
  })
141
141
  }
142
142
 
143
+ const foregroundPingListeners = new Map<QueryClient, { mounts: number; remove: () => void }>()
144
+
145
+ const subscribeToForegroundPing = (queryClient: QueryClient) => {
146
+ const existing = foregroundPingListeners.get(queryClient)
147
+ if (existing) {
148
+ existing.mounts += 1
149
+ return
150
+ }
151
+
152
+ let previousState: AppStateStatus = AppState.currentState
153
+ const subscription = AppState.addEventListener('change', nextState => {
154
+ if (nextState === 'active' && previousState !== 'active') {
155
+ pingSubscribedJoltClients(queryClient)
156
+ }
157
+ previousState = nextState
158
+ })
159
+
160
+ foregroundPingListeners.set(queryClient, { mounts: 1, remove: () => subscription.remove() })
161
+ }
162
+
163
+ const unsubscribeFromForegroundPing = (queryClient: QueryClient) => {
164
+ const existing = foregroundPingListeners.get(queryClient)
165
+ if (!existing) return
166
+
167
+ existing.mounts -= 1
168
+ if (existing.mounts > 0) return
169
+
170
+ existing.remove()
171
+ foregroundPingListeners.delete(queryClient)
172
+ }
173
+
143
174
  export const useJoltForegroundPing = () => {
144
175
  const queryClient = useQueryClient()
145
176
 
146
177
  useEffect(() => {
147
- let previousState: AppStateStatus = AppState.currentState
148
- const subscription = AppState.addEventListener('change', nextState => {
149
- if (nextState === 'active' && previousState !== 'active') {
150
- pingSubscribedJoltClients(queryClient)
151
- }
152
- previousState = nextState
153
- })
178
+ subscribeToForegroundPing(queryClient)
154
179
 
155
- return () => subscription.remove()
180
+ return () => unsubscribeFromForegroundPing(queryClient)
156
181
  }, [queryClient])
157
182
  }
158
183
 
@@ -0,0 +1,10 @@
1
+ import { availableFeatures, useFeatures } from './use_features'
2
+
3
+ export function useManualArchivingEnabled() {
4
+ const { featureEnabled } = useFeatures()
5
+
6
+ return (
7
+ featureEnabled(availableFeatures.manual_archiving) ||
8
+ featureEnabled(availableFeatures.manual_archiving_rollout)
9
+ )
10
+ }
@@ -1,8 +1,9 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { AccessibilityInfo } from 'react-native'
3
+ import { useReducedMotion } from 'react-native-reanimated'
3
4
 
4
5
  export const useReduceMotion = () => {
5
- const [reduceMotion, setReduceMotion] = useState(false)
6
+ const [reduceMotion, setReduceMotion] = useState(useReducedMotion())
6
7
 
7
8
  useEffect(() => {
8
9
  AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
@@ -50,6 +50,7 @@ import {
50
50
  useConversationUpdate,
51
51
  } from '../hooks/use_conversation'
52
52
  import { availableFeatures, useFeatures } from '../hooks/use_features'
53
+ import { useManualArchivingEnabled } from '../hooks/use_manual_archiving_enabled'
53
54
  import { usePreviewAvatarDiameter } from '../hooks/use_preview_avatar_diameter'
54
55
  import { useUnarchiveConversationAction } from '../hooks/use_unarchive_conversation_action'
55
56
  import { type ConversationResource, MemberResource, isDefined } from '../types'
@@ -113,9 +114,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
113
114
  const { featureEnabled } = useFeatures()
114
115
  const granularNotificationsEnabled = featureEnabled(availableFeatures.granular_notifications_ui)
115
116
  const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars)
116
- const manualArchivingEnabled =
117
- featureEnabled(availableFeatures.manual_archiving) ||
118
- featureEnabled(availableFeatures.manual_archiving_rollout)
117
+ const manualArchivingEnabled = useManualArchivingEnabled()
119
118
  const showGenderFilter =
120
119
  featureEnabled(availableFeatures.gender_specific_conversations) && !!conversation.genderOption
121
120
  const genderLabel = conversation.genderOption ? genderDisplayLabel(conversation.genderOption) : ''
@@ -0,0 +1,101 @@
1
+ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
+ import { fireEvent, render, within } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { ConversationFilters } from '../components/conversation_filters'
5
+ import { FilterContext } from '../context/conversation_filter_context'
6
+ import { ConversationFiltersParams } from '../screen_props'
7
+
8
+ jest.mock('react-native-gesture-handler', () => {
9
+ const { createElement } = jest.requireActual('react')
10
+ const { FlatList } = jest.requireActual('react-native')
11
+
12
+ return {
13
+ FlatList: (props: object) => createElement(FlatList, { ...props, initialNumToRender: 50 }),
14
+ }
15
+ })
16
+
17
+ let mockCanBrowseArchived = false
18
+ jest.mock('../../../hooks/use_can_browse_archived_conversations', () => ({
19
+ useCanBrowseArchivedConversations: () => mockCanBrowseArchived,
20
+ }))
21
+ jest.mock('../../../hooks/use_direct_messages_eligible', () => ({
22
+ useDirectMessagesEligible: () => false,
23
+ }))
24
+ jest.mock('../../../hooks/use_filter_dropdown_enabled', () => ({
25
+ useFilterDropdownEnabled: () => false,
26
+ }))
27
+ jest.mock('../../../hooks/use_has_direct_messages', () => ({
28
+ useHasDirectMessages: () => false,
29
+ }))
30
+ jest.mock('../hooks/filters', () => ({
31
+ useGroupsToFilter: () => ({ groups: [] }),
32
+ useTeamsToFilter: () => ({ teams: [] }),
33
+ }))
34
+
35
+ const mockSetAppFilter = jest.fn()
36
+
37
+ const renderFilters = (params: ConversationFiltersParams = {}) =>
38
+ render(
39
+ <ThemeProvider value={DefaultTheme}>
40
+ <FilterContext.Provider
41
+ value={{
42
+ params,
43
+ scrollOffset: 0,
44
+ setScrollOffset: jest.fn(),
45
+ setFormSheetHeaderHeight: jest.fn(),
46
+ resetFilter: jest.fn(),
47
+ setGroupFilter: jest.fn(),
48
+ setAppFilter: mockSetAppFilter,
49
+ applyFilters: jest.fn(),
50
+ platformListPaddingBottom: 0,
51
+ }}
52
+ >
53
+ <ConversationFilters />
54
+ </FilterContext.Provider>
55
+ </ThemeProvider>
56
+ )
57
+
58
+ describe('ConversationFilters archived option', () => {
59
+ beforeEach(() => {
60
+ mockSetAppFilter.mockClear()
61
+ mockCanBrowseArchived = false
62
+ })
63
+
64
+ it('hides the Other section when the person cannot browse archived conversations', () => {
65
+ const view = renderFilters()
66
+
67
+ expect(view.queryByText('Other')).toBeNull()
68
+ expect(view.queryByText('Archived conversations')).toBeNull()
69
+ })
70
+
71
+ it('offers Archived under Other when the person can browse archived conversations', () => {
72
+ mockCanBrowseArchived = true
73
+
74
+ const view = renderFilters()
75
+
76
+ expect(view.getByText('Other')).toBeTruthy()
77
+ expect(view.getByText('Archived conversations')).toBeTruthy()
78
+ expect(view.getByText('Archived for everybody')).toBeTruthy()
79
+ expect(view.queryByRole('button', { selected: true })).toBeNull()
80
+ })
81
+
82
+ it('selecting Archived sets the archived filter param', () => {
83
+ mockCanBrowseArchived = true
84
+
85
+ const view = renderFilters()
86
+
87
+ fireEvent.press(view.getByText('Archived conversations'))
88
+
89
+ expect(mockSetAppFilter).toHaveBeenCalledTimes(1)
90
+ expect(mockSetAppFilter).toHaveBeenCalledWith({ show_archived: true })
91
+ })
92
+
93
+ it('preselects Archived when it is the active filter', () => {
94
+ mockCanBrowseArchived = true
95
+
96
+ const view = renderFilters({ show_archived: true })
97
+
98
+ const selected = view.getByRole('button', { selected: true })
99
+ expect(within(selected).getByText('Archived conversations')).toBeTruthy()
100
+ })
101
+ })
@@ -4,10 +4,13 @@ import { FlatList } from 'react-native-gesture-handler'
4
4
  import { Heading } from '../../../components'
5
5
  import {
6
6
  useAtFontScaleBreakpoint,
7
+ useCanBrowseArchivedConversations,
7
8
  useDirectMessagesEligible,
8
9
  useHasDirectMessages,
9
10
  useTheme,
10
11
  } from '../../../hooks'
12
+ import { useFilterDropdownEnabled } from '../../../hooks/use_filter_dropdown_enabled'
13
+ import { faBoxArchive } from '../../../vendor/font_awesome_pro_solid_icons'
11
14
  import { FilterContext, useFilterContext } from '../context/conversation_filter_context'
12
15
  import { FilterTypes } from '../filter_types'
13
16
  import { useGroupsToFilter, useTeamsToFilter } from '../hooks/filters'
@@ -16,6 +19,8 @@ import {
16
19
  FilterRow,
17
20
  GroupRow,
18
21
  GroupRowProps,
22
+ OtherRow,
23
+ OtherRowProps,
19
24
  TeamRow,
20
25
  TeamRowProps,
21
26
  ViewMore,
@@ -32,6 +37,7 @@ enum SectionTypes {
32
37
  header,
33
38
  hidden,
34
39
  more,
40
+ other,
35
41
  teams,
36
42
  }
37
43
 
@@ -41,6 +47,7 @@ type SectionListData = Array<
41
47
  | DataItem<HeaderProps, SectionTypes.header>
42
48
  | DataItem<any, SectionTypes.hidden>
43
49
  | DataItem<ViewMoreRowProps, SectionTypes.more>
50
+ | DataItem<OtherRowProps, SectionTypes.other>
44
51
  | DataItem<TeamRowProps, SectionTypes.teams>
45
52
  >
46
53
 
@@ -57,11 +64,18 @@ interface DataItem<T, TName extends SectionTypes> {
57
64
  export const ConversationFilters = () => {
58
65
  const styles = useStyles()
59
66
  const { setScrollOffset, params } = useContext(FilterContext)
60
- const { chat_group_graph_id, group_source_app_name = '', show_direct_messages } = params
67
+ const {
68
+ chat_group_graph_id,
69
+ group_source_app_name = '',
70
+ show_direct_messages,
71
+ show_archived,
72
+ } = params
61
73
 
74
+ const filterDropdownEnabled = useFilterDropdownEnabled()
62
75
  const directMessagesEligible = useDirectMessagesEligible()
63
76
  const hasDirectMessages = useHasDirectMessages()
64
77
  const canFilterByDMs = directMessagesEligible && hasDirectMessages
78
+ const canFilterByArchived = useCanBrowseArchivedConversations()
65
79
 
66
80
  const activeFilter: FilterTypes = useMemo(() => {
67
81
  if (chat_group_graph_id) {
@@ -72,10 +86,12 @@ export const ConversationFilters = () => {
72
86
  return FilterTypes.Teams
73
87
  } else if (show_direct_messages) {
74
88
  return FilterTypes.DirectMessages
89
+ } else if (show_archived) {
90
+ return FilterTypes.Archived
75
91
  }
76
92
 
77
93
  return FilterTypes.All
78
- }, [chat_group_graph_id, group_source_app_name, show_direct_messages])
94
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
79
95
 
80
96
  const { groups = [] } = useGroupsToFilter()
81
97
  const { teams = [] } = useTeamsToFilter()
@@ -101,8 +117,9 @@ export const ConversationFilters = () => {
101
117
  const groupItemData = selectActiveWithFirst(5)(groupItems)
102
118
  const teamItemData = selectActiveWithFirst(5)(teamItems)
103
119
 
104
- const hideAppFilters = groupItems.length < 1 || teamItems.length < 1
105
- const showGeneralFilters = !hideAppFilters || canFilterByDMs
120
+ const showAppFilters = !filterDropdownEnabled && groupItems.length > 0 && teamItems.length > 0
121
+ const showDMFilter = !filterDropdownEnabled && canFilterByDMs
122
+ const showGeneralFilters = showAppFilters || showDMFilter
106
123
 
107
124
  const listData: SectionListData = [
108
125
  {
@@ -114,7 +131,7 @@ export const ConversationFilters = () => {
114
131
  data: { filter: FilterTypes.All, isActive: activeFilter === FilterTypes.All },
115
132
  },
116
133
  {
117
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.filter,
134
+ type: showAppFilters ? SectionTypes.filter : SectionTypes.hidden,
118
135
  data: {
119
136
  filter: FilterTypes.Groups,
120
137
  group_source_app_name: 'groups',
@@ -122,7 +139,7 @@ export const ConversationFilters = () => {
122
139
  },
123
140
  },
124
141
  {
125
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.filter,
142
+ type: showAppFilters ? SectionTypes.filter : SectionTypes.hidden,
126
143
  data: {
127
144
  filter: FilterTypes.Teams,
128
145
  group_source_app_name: 'services',
@@ -130,7 +147,7 @@ export const ConversationFilters = () => {
130
147
  },
131
148
  },
132
149
  {
133
- type: canFilterByDMs ? SectionTypes.filter : SectionTypes.hidden,
150
+ type: showDMFilter ? SectionTypes.filter : SectionTypes.hidden,
134
151
  data: {
135
152
  filter: FilterTypes.DirectMessages,
136
153
  show_direct_messages: true,
@@ -155,6 +172,20 @@ export const ConversationFilters = () => {
155
172
  type: teamItems.length > 5 ? SectionTypes.more : SectionTypes.hidden,
156
173
  data: { routeName: 'TeamFilters' },
157
174
  },
175
+ {
176
+ type: canFilterByArchived ? SectionTypes.header : SectionTypes.hidden,
177
+ data: { title: 'Other', style: styles.extraHeaderScalableSpace },
178
+ },
179
+ {
180
+ type: canFilterByArchived ? SectionTypes.other : SectionTypes.hidden,
181
+ data: {
182
+ filter: FilterTypes.Archived,
183
+ description: 'Archived for everybody',
184
+ icon: faBoxArchive,
185
+ show_archived: true,
186
+ isActive: activeFilter === FilterTypes.Archived,
187
+ },
188
+ },
158
189
  ]
159
190
 
160
191
  return (
@@ -175,6 +206,8 @@ export const ConversationFilters = () => {
175
206
  return <TeamRow {...item.data} />
176
207
  case SectionTypes.more:
177
208
  return <ViewMore {...item.data} />
209
+ case SectionTypes.other:
210
+ return <OtherRow {...item.data} />
178
211
  default:
179
212
  return null
180
213
  }
@@ -1,8 +1,9 @@
1
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
1
2
  import { PlatformPressable } from '@react-navigation/elements'
2
3
  import { NavigationProp, useNavigation } from '@react-navigation/native'
3
4
  import React, { PropsWithChildren, useContext } from 'react'
4
5
  import { StyleSheet, View, ViewStyle } from 'react-native'
5
- import { Heading, Icon, Image, Text, TextButton } from '../../../components'
6
+ import { Heading, Icon, IconTile, Image, Text, TextButton } from '../../../components'
6
7
  import {
7
8
  useAtFontScaleBreakpoint,
8
9
  useFontScale,
@@ -115,6 +116,33 @@ export const TeamRow = ({ team }: TeamRowProps) => {
115
116
  )
116
117
  }
117
118
 
119
+ export type OtherRowProps = {
120
+ show_archived?: boolean
121
+ isActive: boolean
122
+ filter: FilterTypes
123
+ description: string
124
+ icon: IconDefinition
125
+ }
126
+
127
+ export const OtherRow = ({ show_archived, isActive, filter, description, icon }: OtherRowProps) => {
128
+ const styles = useRowStyles()
129
+ const { setAppFilter } = useContext(FilterContext)
130
+
131
+ return (
132
+ <PressableRow onPress={() => setAppFilter({ show_archived })} isActive={isActive}>
133
+ <IconTile icon={icon} size="lg" />
134
+ <View style={styles.rowTextContainer}>
135
+ <Heading variant="h3" style={styles.rowTitle}>
136
+ {filter}
137
+ </Heading>
138
+ <Text variant="footnote" style={styles.rowSubtitle}>
139
+ {description}
140
+ </Text>
141
+ </View>
142
+ </PressableRow>
143
+ )
144
+ }
145
+
118
146
  export type ViewMoreRowProps = { routeName: keyof ConversationFilterStackParamList }
119
147
 
120
148
  export const ViewMore = ({ routeName }: ViewMoreRowProps) => {
@@ -22,6 +22,7 @@ export const FilterContext = createContext<{
22
22
  chat_group_graph_id: undefined,
23
23
  group_source_app_name: undefined,
24
24
  show_direct_messages: undefined,
25
+ show_archived: undefined,
25
26
  },
26
27
  scrollOffset: 0,
27
28
  setScrollOffset: () => {},
@@ -57,6 +58,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
57
58
  chat_group_graph_id: undefined,
58
59
  group_source_app_name: undefined,
59
60
  show_direct_messages: undefined,
61
+ show_archived: undefined,
60
62
  })
61
63
  )
62
64
  },
@@ -65,6 +67,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
65
67
  chat_group_graph_id: params.chat_group_graph_id,
66
68
  group_source_app_name: undefined,
67
69
  show_direct_messages: undefined,
70
+ show_archived: undefined,
68
71
  })
69
72
  },
70
73
  setAppFilter: (params: Omit<ConversationFiltersParams, 'chat_group_graph_id'>) => {
@@ -72,6 +75,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
72
75
  chat_group_graph_id: undefined,
73
76
  group_source_app_name: params.group_source_app_name,
74
77
  show_direct_messages: params.show_direct_messages,
78
+ show_archived: params.show_archived,
75
79
  })
76
80
  },
77
81
  applyFilters: (params: ConversationFiltersParams) => {
@@ -3,5 +3,6 @@ export enum FilterTypes {
3
3
  DirectMessages = 'Direct messages',
4
4
  Groups = 'All my groups',
5
5
  Teams = 'All my teams and plans',
6
+ Archived = 'Archived conversations',
6
7
  More = 'More',
7
8
  }
@@ -5,6 +5,7 @@ export type ConversationFiltersParams = {
5
5
  chat_group_graph_id?: GraphId
6
6
  group_source_app_name?: string
7
7
  show_direct_messages?: boolean
8
+ show_archived?: boolean
8
9
  }
9
10
 
10
11
  export type ConversationFiltersScreenProps = StaticScreenProps<ConversationFiltersParams>
@@ -1,11 +1,13 @@
1
- import { useHeaderHeight } from '@react-navigation/elements'
1
+ import { HeaderBackButton, useHeaderHeight } from '@react-navigation/elements'
2
2
  import * as ReactNavigationNative from '@react-navigation/native'
3
- import { RouteProp, useRoute } from '@react-navigation/native'
3
+ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
4
4
  import { createNativeStackNavigator } from '@react-navigation/native-stack'
5
5
  import React, { useCallback } from 'react'
6
6
  import { LayoutChangeEvent, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
7
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
8
8
  import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
9
+ import { useTheme } from '../hooks'
10
+ import { useFilterDropdownEnabled } from '../hooks/use_filter_dropdown_enabled'
9
11
  import { ConversationFilters } from './conversation_filters/components/conversation_filters'
10
12
  import {
11
13
  FilterProvider,
@@ -86,16 +88,19 @@ export const ConversationFiltersScreen = (_props: ConversationFiltersScreenProps
86
88
  return (
87
89
  <FilterProvider>
88
90
  <FormSheet.Root contentStyle={styles.contentContainer} showAndroidSheetGrabber={false}>
89
- <FormSheetHeaderWithContext />
91
+ <ConversationFiltersHeader />
90
92
  <Filters />
91
93
  </FormSheet.Root>
92
94
  </FilterProvider>
93
95
  )
94
96
  }
95
97
 
96
- const FormSheetHeaderWithContext = () => {
98
+ export const ConversationFiltersHeader = () => {
97
99
  const { resetFilter, applyFilters, setFormSheetHeaderHeight } = useFilterContext()
100
+ const navigation = useNavigation()
98
101
  const route = useRoute<RouteProp<ConversationFiltersScreenProps['route']>>()
102
+ const filterDropdownEnabled = useFilterDropdownEnabled()
103
+ const { colors } = useTheme()
99
104
  const styles = useStyles()
100
105
 
101
106
  const handleFormSheetHeaderLayout = useCallback(
@@ -108,7 +113,18 @@ const FormSheetHeaderWithContext = () => {
108
113
  return (
109
114
  <View onLayout={handleFormSheetHeaderLayout}>
110
115
  <FormSheet.Header style={styles.header}>
111
- <FormSheet.HeaderTitle>Filters</FormSheet.HeaderTitle>
116
+ {Platform.OS === 'android' && (
117
+ <HeaderBackButton
118
+ displayMode="minimal"
119
+ accessibilityLabel="Back"
120
+ tintColor={colors.iconColorDefaultPrimary}
121
+ onPress={navigation.goBack}
122
+ style={styles.backButton}
123
+ />
124
+ )}
125
+ <FormSheet.HeaderTitle>
126
+ {filterDropdownEnabled ? 'More filters' : 'Filters'}
127
+ </FormSheet.HeaderTitle>
112
128
  <FormSheet.HeaderActions>
113
129
  <FormSheet.HeaderTextButton onPress={resetFilter}>Reset</FormSheet.HeaderTextButton>
114
130
  <FormSheet.HeaderButton title="Apply" onPress={() => applyFilters(route.params)} />
@@ -141,5 +157,9 @@ const useStyles = () => {
141
157
  // separator the way the non-scrolling reaction sheet does.
142
158
  borderBottomWidth: 0,
143
159
  },
160
+ backButton: {
161
+ marginLeft: -3,
162
+ marginRight: 8,
163
+ },
144
164
  })
145
165
  }
@@ -2,6 +2,14 @@ import { QueryClientProvider } from '@tanstack/react-query'
2
2
  import { render } from '@testing-library/react-native'
3
3
  import React, { Suspense } from 'react'
4
4
  import { buildTestQueryClient } from '../../../__utils__/query_client'
5
+ import {
6
+ AudioAdapter,
7
+ ChatAdapters,
8
+ ClipboardAdapter,
9
+ ImagePickerAdapter,
10
+ MenuAdapter,
11
+ VideoAdapter,
12
+ } from '../../../utils/native_adapters'
5
13
  import { ConversationsScreen } from '../conversations_screen'
6
14
 
7
15
  const mockGet = jest.fn()
@@ -28,6 +36,26 @@ jest.mock('../components/search_list_header_component', () => {
28
36
  const { Text } = require('react-native')
29
37
  return { SearchListHeaderComponent: () => <Text>Search header</Text> }
30
38
  })
39
+ jest.mock('../components/filter_dropdown_list_header_component', () => {
40
+ const { Text } = require('react-native')
41
+ return { FilterDropdownListHeaderComponent: () => <Text>Filter dropdown header</Text> }
42
+ })
43
+
44
+ const configureAdapters = ({ menu }: { menu: boolean }) => {
45
+ ChatAdapters.configure({
46
+ clipboard: new ClipboardAdapter({
47
+ getStringAsync: async () => '',
48
+ setStringAsync: async () => {},
49
+ }),
50
+ audio: new AudioAdapter({ useAudio: () => ({}) as any }),
51
+ video: new VideoAdapter({ Player: (() => null) as any }),
52
+ imagePicker: new ImagePickerAdapter({
53
+ openCameraAsync: async () => ({ canceled: true, assets: null }),
54
+ openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
55
+ }),
56
+ ...(menu ? { menu: new MenuAdapter({ View: () => null }) } : {}),
57
+ })
58
+ }
31
59
 
32
60
  const renderWithFeatures = (enabledFeatures: string[]) => {
33
61
  mockGet.mockResolvedValue({
@@ -51,6 +79,10 @@ const renderWithFeatures = (enabledFeatures: string[]) => {
51
79
  }
52
80
 
53
81
  describe('ConversationsScreen header gating', () => {
82
+ beforeEach(() => {
83
+ configureAdapters({ menu: true })
84
+ })
85
+
54
86
  it('shows the search header with only the QA flag on', async () => {
55
87
  const view = renderWithFeatures(['QA_MOBILE_search'])
56
88
 
@@ -63,6 +95,26 @@ describe('ConversationsScreen header gating', () => {
63
95
  expect(await view.findByText('Search header')).toBeTruthy()
64
96
  })
65
97
 
98
+ it('shows the filter dropdown header over the search header when its flag is on', async () => {
99
+ const view = renderWithFeatures(['QA_MOBILE_filter_dropdown', 'ROLLOUT_MOBILE_search'])
100
+
101
+ expect(await view.findByText('Filter dropdown header')).toBeTruthy()
102
+ })
103
+
104
+ it('shows the filter dropdown header with only its rollout flag on', async () => {
105
+ const view = renderWithFeatures(['ROLLOUT_MOBILE_filter_dropdown'])
106
+
107
+ expect(await view.findByText('Filter dropdown header')).toBeTruthy()
108
+ })
109
+
110
+ it('falls back to the search header when the host has no menu adapter', async () => {
111
+ configureAdapters({ menu: false })
112
+
113
+ const view = renderWithFeatures(['QA_MOBILE_filter_dropdown', 'ROLLOUT_MOBILE_search'])
114
+
115
+ expect(await view.findByText('Search header')).toBeTruthy()
116
+ })
117
+
66
118
  it('shows the default header with neither flag on', async () => {
67
119
  const view = renderWithFeatures(['ROLLOUT_WEB_search'])
68
120