@planningcenter/chat-react-native 4.4.1-qa-staging.17 → 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 (157) 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 +44 -35
  13. package/build/components/conversations/conversations.js.map +1 -1
  14. package/build/components/display/filter_dropdown.d.ts +2 -1
  15. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  16. package/build/components/display/filter_dropdown.js +63 -58
  17. package/build/components/display/filter_dropdown.js.map +1 -1
  18. package/build/components/display/icon_tile.d.ts +12 -0
  19. package/build/components/display/icon_tile.d.ts.map +1 -0
  20. package/build/components/display/icon_tile.js +109 -0
  21. package/build/components/display/icon_tile.js.map +1 -0
  22. package/build/components/display/index.d.ts +1 -0
  23. package/build/components/display/index.d.ts.map +1 -1
  24. package/build/components/display/index.js +1 -0
  25. package/build/components/display/index.js.map +1 -1
  26. package/build/contexts/conversations_context.d.ts.map +1 -1
  27. package/build/contexts/conversations_context.js +21 -18
  28. package/build/contexts/conversations_context.js.map +1 -1
  29. package/build/hooks/index.d.ts +2 -0
  30. package/build/hooks/index.d.ts.map +1 -1
  31. package/build/hooks/index.js +2 -0
  32. package/build/hooks/index.js.map +1 -1
  33. package/build/hooks/use_can_browse_archived_conversations.d.ts +2 -0
  34. package/build/hooks/use_can_browse_archived_conversations.d.ts.map +1 -0
  35. package/build/hooks/use_can_browse_archived_conversations.js +29 -0
  36. package/build/hooks/use_can_browse_archived_conversations.js.map +1 -0
  37. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -1
  38. package/build/hooks/use_clear_conversation_filters.js +2 -1
  39. package/build/hooks/use_clear_conversation_filters.js.map +1 -1
  40. package/build/hooks/use_conversation.d.ts.map +1 -1
  41. package/build/hooks/use_conversation.js +32 -24
  42. package/build/hooks/use_conversation.js.map +1 -1
  43. package/build/hooks/use_current_person.d.ts.map +1 -1
  44. package/build/hooks/use_current_person.js +1 -1
  45. package/build/hooks/use_current_person.js.map +1 -1
  46. package/build/hooks/use_jolt.d.ts.map +1 -1
  47. package/build/hooks/use_jolt.js +29 -8
  48. package/build/hooks/use_jolt.js.map +1 -1
  49. package/build/hooks/use_manual_archiving_enabled.d.ts +2 -0
  50. package/build/hooks/use_manual_archiving_enabled.d.ts.map +1 -0
  51. package/build/hooks/use_manual_archiving_enabled.js +18 -0
  52. package/build/hooks/use_manual_archiving_enabled.js.map +1 -0
  53. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  54. package/build/screens/conversation_details_screen.js +2 -1
  55. package/build/screens/conversation_details_screen.js.map +1 -1
  56. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
  57. package/build/screens/conversation_filters/components/conversation_filters.js +80 -13
  58. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  59. package/build/screens/conversation_filters/components/rows.d.ts +9 -0
  60. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  61. package/build/screens/conversation_filters/components/rows.js +92 -1
  62. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  63. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  64. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  65. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  66. package/build/screens/conversation_filters/filter_types.d.ts +1 -0
  67. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
  68. package/build/screens/conversation_filters/filter_types.js +1 -0
  69. package/build/screens/conversation_filters/filter_types.js.map +1 -1
  70. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  71. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  72. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  73. package/build/screens/conversation_filters_screen.d.ts +1 -0
  74. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  75. package/build/screens/conversation_filters_screen.js +75 -49
  76. package/build/screens/conversation_filters_screen.js.map +1 -1
  77. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts +2 -0
  78. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -0
  79. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +587 -0
  80. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -0
  81. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  82. package/build/screens/conversations/components/list_header_component.js +95 -90
  83. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  84. package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -1
  85. package/build/screens/conversations/components/search_list_header_component.js +100 -95
  86. package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
  87. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  88. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  89. package/build/screens/conversations/conversations_screen.js +15 -2
  90. package/build/screens/conversations/conversations_screen.js.map +1 -1
  91. package/build/types/resources/person.d.ts +1 -0
  92. package/build/types/resources/person.d.ts.map +1 -1
  93. package/build/types/resources/person.js.map +1 -1
  94. package/build/utils/date.d.ts.map +1 -1
  95. package/build/utils/date.js +5 -10
  96. package/build/utils/date.js.map +1 -1
  97. package/build/utils/native_adapters/menu.d.ts +1 -0
  98. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  99. package/build/utils/native_adapters/menu.js +1 -0
  100. package/build/utils/native_adapters/menu.js.map +1 -1
  101. package/build/utils/request/conversation.d.ts +2 -1
  102. package/build/utils/request/conversation.d.ts.map +1 -1
  103. package/build/utils/request/conversation.js +4 -1
  104. package/build/utils/request/conversation.js.map +1 -1
  105. package/build/vendor/font_awesome_pro_solid_icons.d.ts +1 -0
  106. package/build/vendor/font_awesome_pro_solid_icons.d.ts.map +1 -1
  107. package/build/vendor/font_awesome_pro_solid_icons.js +5 -0
  108. package/build/vendor/font_awesome_pro_solid_icons.js.map +1 -1
  109. package/package.json +3 -3
  110. package/src/__tests__/hooks/use_can_browse_archived_conversations.test.tsx +98 -0
  111. package/src/__tests__/screens/conversation_filters/components/conversation_filters.test.tsx +3 -0
  112. package/src/__tests__/screens/conversation_filters_screen.test.tsx +76 -0
  113. package/src/components/conversations/__tests__/conversation_actions.test.tsx +67 -0
  114. package/src/components/conversations/__tests__/conversation_preview.test.tsx +38 -3
  115. package/src/components/conversations/__tests__/conversations.test.tsx +82 -0
  116. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +1 -0
  117. package/src/components/conversations/archived_results_footer.tsx +36 -0
  118. package/src/components/conversations/conversation_actions.tsx +27 -19
  119. package/src/components/conversations/conversation_preview.tsx +30 -10
  120. package/src/components/conversations/conversations.tsx +15 -3
  121. package/src/components/display/__tests__/filter_dropdown.test.tsx +9 -1
  122. package/src/components/display/filter_dropdown.tsx +14 -13
  123. package/src/components/display/icon_tile.tsx +54 -0
  124. package/src/components/display/index.ts +1 -0
  125. package/src/contexts/conversations_context.tsx +2 -1
  126. package/src/hooks/__tests__/use_conversation_archive.test.tsx +22 -0
  127. package/src/hooks/__tests__/use_jolt.test.tsx +30 -8
  128. package/src/hooks/index.ts +2 -0
  129. package/src/hooks/use_can_browse_archived_conversations.ts +17 -0
  130. package/src/hooks/use_clear_conversation_filters.ts +1 -0
  131. package/src/hooks/use_conversation.ts +4 -1
  132. package/src/hooks/use_current_person.ts +1 -0
  133. package/src/hooks/use_jolt.ts +33 -8
  134. package/src/hooks/use_manual_archiving_enabled.ts +10 -0
  135. package/src/screens/conversation_details_screen.tsx +2 -3
  136. package/src/screens/conversation_filters/__tests__/conversation_filters.test.tsx +101 -0
  137. package/src/screens/conversation_filters/components/conversation_filters.tsx +32 -2
  138. package/src/screens/conversation_filters/components/rows.tsx +29 -1
  139. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  140. package/src/screens/conversation_filters/filter_types.ts +1 -0
  141. package/src/screens/conversation_filters/screen_props.ts +1 -0
  142. package/src/screens/conversation_filters_screen.tsx +20 -4
  143. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +52 -0
  144. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +248 -0
  145. package/src/screens/conversations/components/__tests__/list_header_component.test.tsx +21 -4
  146. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +284 -0
  147. package/src/screens/conversations/components/list_header_component.tsx +17 -12
  148. package/src/screens/conversations/components/search_list_header_component.tsx +17 -12
  149. package/src/screens/conversations/conversations_screen.tsx +15 -2
  150. package/src/types/resources/person.ts +1 -0
  151. package/src/utils/__tests__/conversation.test.ts +1 -0
  152. package/src/utils/__tests__/date.test.ts +4 -0
  153. package/src/utils/date.ts +5 -4
  154. package/src/utils/native_adapters/__tests__/menu.test.tsx +6 -2
  155. package/src/utils/native_adapters/menu.tsx +2 -0
  156. package/src/utils/request/conversation.ts +4 -0
  157. package/src/vendor/font_awesome_pro_solid_icons.ts +12 -0
@@ -0,0 +1,54 @@
1
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
3
+ import React from 'react'
4
+ import { StyleSheet, View } from 'react-native'
5
+ import { useFontScale, useTheme } from '../../hooks'
6
+ import { MAX_FONT_SIZE_MULTIPLIER } from '../../utils'
7
+ import AvatarPrimitive, { type AvatarRootProps } from '../primitive/avatar_primitive'
8
+
9
+ const ICON_SIZE_PX = 16
10
+
11
+ interface IconTileProps {
12
+ icon: IconDefinition
13
+ size?: AvatarRootProps['size']
14
+ maxFontSizeMultiplier?: AvatarRootProps['maxFontSizeMultiplier']
15
+ style?: AvatarRootProps['style']
16
+ testID?: string
17
+ }
18
+
19
+ export function IconTile({
20
+ icon,
21
+ size = 'lg',
22
+ maxFontSizeMultiplier = MAX_FONT_SIZE_MULTIPLIER,
23
+ style,
24
+ testID,
25
+ }: IconTileProps) {
26
+ const styles = useStyles()
27
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
28
+
29
+ return (
30
+ <AvatarPrimitive.Root size={size} maxFontSizeMultiplier={maxFontSizeMultiplier} style={style}>
31
+ <AvatarPrimitive.Mask>
32
+ <View style={styles.tile} testID={testID} accessibilityElementsHidden>
33
+ <FontAwesomeIcon icon={icon} size={ICON_SIZE_PX * fontScale} color={styles.icon.color} />
34
+ </View>
35
+ </AvatarPrimitive.Mask>
36
+ </AvatarPrimitive.Root>
37
+ )
38
+ }
39
+
40
+ const useStyles = () => {
41
+ const { colors } = useTheme()
42
+
43
+ return StyleSheet.create({
44
+ tile: {
45
+ flex: 1,
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ backgroundColor: colors.fillColorNeutral070,
49
+ },
50
+ icon: {
51
+ color: colors.iconColorDefaultPrimary,
52
+ },
53
+ })
54
+ }
@@ -4,6 +4,7 @@ export * from './badge'
4
4
  export * from './conversation_avatar'
5
5
  export * from './emoji_avatar'
6
6
  export * from './icon_avatar'
7
+ export * from './icon_tile'
7
8
  export * from './banner_collapsible'
8
9
  export * from './banner'
9
10
  export * from './button'
@@ -45,13 +45,14 @@ export const ConversationsContextProvider = ({
45
45
  opts?: ApiGetQueryOptions
46
46
  }>) => {
47
47
  const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
48
- const { chat_group_graph_id, group_source_app_name, show_direct_messages } = args
48
+ const { chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived } = args
49
49
 
50
50
  const query = useConversations(
51
51
  {
52
52
  chat_group_graph_id,
53
53
  group_source_app_name,
54
54
  show_direct_messages,
55
+ show_archived,
55
56
  },
56
57
  paginatorOptions
57
58
  )
@@ -5,6 +5,7 @@ import { Alert } from 'react-native'
5
5
  import { buildTestQueryClient } from '../../__utils__/query_client'
6
6
  import { getConversationsRequestArgs } from '../../utils/request/conversation'
7
7
  import { getConversationRequestArgs, useConversationArchive } from '../use_conversation'
8
+ import { currentPersonQueryKey } from '../use_current_person'
8
9
  import { getRequestQueryKey } from '../use_suspense_api'
9
10
 
10
11
  const mockPost = jest.fn()
@@ -33,6 +34,9 @@ describe('useConversationArchive', () => {
33
34
  queryClient = buildQueryClient()
34
35
  queryClient.setQueryData(conversationKey, { data: { id: conversation_id } })
35
36
  queryClient.setQueryData(conversationsKey, { data: [] })
37
+ queryClient.setQueryData(currentPersonQueryKey, {
38
+ data: { id: 1, canBrowseArchivedConversations: false },
39
+ })
36
40
  mockPost.mockReset()
37
41
  jest.spyOn(Alert, 'alert').mockImplementation(() => {})
38
42
  })
@@ -58,6 +62,24 @@ describe('useConversationArchive', () => {
58
62
  })
59
63
  expect(isInvalidated(conversationKey)).toBe(true)
60
64
  expect(isInvalidated(conversationsKey)).toBe(true)
65
+ expect(isInvalidated(currentPersonQueryKey)).toBe(false)
66
+ }
67
+ )
68
+
69
+ it.each([
70
+ [true, true],
71
+ [false, false],
72
+ ])(
73
+ 'sets archive access in the person cache to %s after archived=%s',
74
+ async (archived, canBrowse) => {
75
+ mockPost.mockResolvedValue({ data: { id: conversation_id } })
76
+ const { result } = renderHook(() => useConversationArchive({ conversation_id }), { wrapper })
77
+
78
+ await act(() => result.current.mutateAsync(archived))
79
+
80
+ expect(queryClient.getQueryData(currentPersonQueryKey)).toEqual({
81
+ data: { id: 1, canBrowseArchivedConversations: canBrowse },
82
+ })
61
83
  }
62
84
  )
63
85
 
@@ -81,13 +81,15 @@ describe('useJoltForegroundPing', () => {
81
81
  return null
82
82
  }
83
83
 
84
- const renderForegroundPing = (joltClient: Partial<JoltClient>) => {
84
+ const renderForegroundPing = (joltClient: Partial<JoltClient>, { mounts = 1 } = {}) => {
85
85
  const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
86
86
  queryClient.setQueryData(['jolt-client', 'wss://jolt.test', 'a-real-token'], joltClient)
87
87
 
88
- render(
88
+ const result = render(
89
89
  <QueryClientProvider client={queryClient}>
90
- <PingProbe />
90
+ {Array.from({ length: mounts }, (_, index) => (
91
+ <PingProbe key={index} />
92
+ ))}
91
93
  </QueryClientProvider>
92
94
  )
93
95
 
@@ -96,7 +98,7 @@ describe('useJoltForegroundPing', () => {
96
98
  (_state: AppStateStatus) => void,
97
99
  ]
98
100
 
99
- return onChange
101
+ return { onChange, unmount: result.unmount }
100
102
  }
101
103
 
102
104
  beforeEach(() => {
@@ -105,7 +107,7 @@ describe('useJoltForegroundPing', () => {
105
107
 
106
108
  it('pings the jolt client once when the app returns to the foreground', () => {
107
109
  const ping = jest.fn()
108
- const onChange = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
110
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
109
111
 
110
112
  onChange('background')
111
113
  onChange('active')
@@ -113,9 +115,29 @@ describe('useJoltForegroundPing', () => {
113
115
  expect(ping).toHaveBeenCalledTimes(1)
114
116
  })
115
117
 
118
+ it('registers one listener and pings once no matter how many providers mount it', () => {
119
+ const ping = jest.fn()
120
+ const remove = jest.fn()
121
+ addEventListener.mockReturnValueOnce({ remove })
122
+ const { onChange, unmount } = renderForegroundPing(
123
+ { ping, subscriptions: [confirmedSubscription] },
124
+ { mounts: 3 }
125
+ )
126
+
127
+ onChange('background')
128
+ onChange('active')
129
+
130
+ expect(addEventListener).toHaveBeenCalledTimes(1)
131
+ expect(ping).toHaveBeenCalledTimes(1)
132
+
133
+ unmount()
134
+
135
+ expect(remove).toHaveBeenCalledTimes(1)
136
+ })
137
+
116
138
  it('does not ping when there are no confirmed subscriptions', () => {
117
139
  const ping = jest.fn()
118
- const onChange = renderForegroundPing({ ping, subscriptions: [] })
140
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [] })
119
141
 
120
142
  onChange('background')
121
143
  onChange('active')
@@ -125,7 +147,7 @@ describe('useJoltForegroundPing', () => {
125
147
 
126
148
  it('does not ping when the app leaves the foreground', () => {
127
149
  const ping = jest.fn()
128
- const onChange = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
150
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
129
151
 
130
152
  onChange('inactive')
131
153
  onChange('background')
@@ -137,7 +159,7 @@ describe('useJoltForegroundPing', () => {
137
159
  const ping = jest.fn(() => {
138
160
  throw new Error('No WebSocket')
139
161
  })
140
- const onChange = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
162
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
141
163
 
142
164
  onChange('background')
143
165
 
@@ -3,6 +3,7 @@ export { useApiClient } from './use_api_client'
3
3
  export * from './use_api'
4
4
  export * from './use_app_state'
5
5
  export * from './use_async_storage'
6
+ export * from './use_can_browse_archived_conversations'
6
7
  export * from './use_at_font_scale_breakpoint'
7
8
  export * from './use_chat_permissions'
8
9
  export * from './use_create_android_ripple_color'
@@ -16,6 +17,7 @@ export * from './use_groups_groups'
16
17
  export * from './use_groups'
17
18
  export * from './use_selected_chip_background_color'
18
19
  export * from './use_interaction_tinted_background_color'
20
+ export * from './use_manual_archiving_enabled'
19
21
  export * from './use_message_reaction_toggle'
20
22
  export * from './use_new_conversation_entry'
21
23
  export * from './use_organization'
@@ -0,0 +1,17 @@
1
+ import { CurrentPersonResource } from '../types'
2
+ import { FIVE_MINUTES } from '../utils/time'
3
+ import { useApiGet } from './use_api'
4
+ import { currentPersonRequestArgs } from './use_current_person'
5
+ import { useManualArchivingEnabled } from './use_manual_archiving_enabled'
6
+
7
+ export const useCanBrowseArchivedConversations = (): boolean => {
8
+ const manualArchivingEnabled = useManualArchivingEnabled()
9
+ const { data: person, isFetched } = useApiGet<CurrentPersonResource>(currentPersonRequestArgs, {
10
+ enabled: manualArchivingEnabled,
11
+ staleTime: FIVE_MINUTES,
12
+ })
13
+
14
+ if (!manualArchivingEnabled || !isFetched) return false
15
+
16
+ return person?.canBrowseArchivedConversations ?? false
17
+ }
@@ -10,6 +10,7 @@ export function useClearConversationFilters() {
10
10
  chat_group_graph_id: undefined,
11
11
  group_source_app_name: undefined,
12
12
  show_direct_messages: undefined,
13
+ show_archived: undefined,
13
14
  })
14
15
  }, [navigation])
15
16
  }
@@ -6,6 +6,7 @@ import { transformGetToPost } from '../utils/client/request_helpers'
6
6
  import { Haptic } from '../utils/native_adapters'
7
7
  import { getConversationsRequestArgs } from '../utils/request/conversation'
8
8
  import { useApiClient } from './use_api_client'
9
+ import { useCurrentPersonCache } from './use_current_person'
9
10
  import { getRequestQueryKey, SuspenseGetQueryOptions, useSuspenseGet } from './use_suspense_api'
10
11
 
11
12
  export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: number }) => ({
@@ -188,6 +189,7 @@ export const useConversationDelete = ({ conversation_id }: { conversation_id: nu
188
189
  export const useConversationArchive = ({ conversation_id }: { conversation_id: number }) => {
189
190
  const apiClient = useApiClient()
190
191
  const queryClient = useQueryClient()
192
+ const currentPersonCache = useCurrentPersonCache()
191
193
  const conversationQueryKey = getRequestQueryKey(getConversationRequestArgs({ conversation_id }))
192
194
  const { url } = getConversationsRequestArgs()
193
195
 
@@ -201,8 +203,9 @@ export const useConversationArchive = ({ conversation_id }: { conversation_id: n
201
203
  data: { data: { type: 'Conversation', attributes: {} } },
202
204
  })
203
205
  },
204
- onSuccess: () => {
206
+ onSuccess: (_response, archived) => {
205
207
  Haptic.notificationSuccess()
208
+ if (archived) currentPersonCache.update({ canBrowseArchivedConversations: true })
206
209
  queryClient.invalidateQueries({ queryKey: [url] })
207
210
  return queryClient.invalidateQueries({ queryKey: conversationQueryKey })
208
211
  },
@@ -18,6 +18,7 @@ export const currentPersonRequestArgs = {
18
18
  'age_qualification_status',
19
19
  'direct_messages_disabled',
20
20
  'direct_messages_eligible',
21
+ 'can_browse_archived_conversations',
21
22
  'under_18',
22
23
  ],
23
24
  },
@@ -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
+ }
@@ -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,11 +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'
11
12
  import { useFilterDropdownEnabled } from '../../../hooks/use_filter_dropdown_enabled'
13
+ import { faBoxArchive } from '../../../vendor/font_awesome_pro_solid_icons'
12
14
  import { FilterContext, useFilterContext } from '../context/conversation_filter_context'
13
15
  import { FilterTypes } from '../filter_types'
14
16
  import { useGroupsToFilter, useTeamsToFilter } from '../hooks/filters'
@@ -17,6 +19,8 @@ import {
17
19
  FilterRow,
18
20
  GroupRow,
19
21
  GroupRowProps,
22
+ OtherRow,
23
+ OtherRowProps,
20
24
  TeamRow,
21
25
  TeamRowProps,
22
26
  ViewMore,
@@ -33,6 +37,7 @@ enum SectionTypes {
33
37
  header,
34
38
  hidden,
35
39
  more,
40
+ other,
36
41
  teams,
37
42
  }
38
43
 
@@ -42,6 +47,7 @@ type SectionListData = Array<
42
47
  | DataItem<HeaderProps, SectionTypes.header>
43
48
  | DataItem<any, SectionTypes.hidden>
44
49
  | DataItem<ViewMoreRowProps, SectionTypes.more>
50
+ | DataItem<OtherRowProps, SectionTypes.other>
45
51
  | DataItem<TeamRowProps, SectionTypes.teams>
46
52
  >
47
53
 
@@ -58,12 +64,18 @@ interface DataItem<T, TName extends SectionTypes> {
58
64
  export const ConversationFilters = () => {
59
65
  const styles = useStyles()
60
66
  const { setScrollOffset, params } = useContext(FilterContext)
61
- 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
62
73
 
63
74
  const filterDropdownEnabled = useFilterDropdownEnabled()
64
75
  const directMessagesEligible = useDirectMessagesEligible()
65
76
  const hasDirectMessages = useHasDirectMessages()
66
77
  const canFilterByDMs = directMessagesEligible && hasDirectMessages
78
+ const canFilterByArchived = useCanBrowseArchivedConversations()
67
79
 
68
80
  const activeFilter: FilterTypes = useMemo(() => {
69
81
  if (chat_group_graph_id) {
@@ -74,10 +86,12 @@ export const ConversationFilters = () => {
74
86
  return FilterTypes.Teams
75
87
  } else if (show_direct_messages) {
76
88
  return FilterTypes.DirectMessages
89
+ } else if (show_archived) {
90
+ return FilterTypes.Archived
77
91
  }
78
92
 
79
93
  return FilterTypes.All
80
- }, [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])
81
95
 
82
96
  const { groups = [] } = useGroupsToFilter()
83
97
  const { teams = [] } = useTeamsToFilter()
@@ -158,6 +172,20 @@ export const ConversationFilters = () => {
158
172
  type: teamItems.length > 5 ? SectionTypes.more : SectionTypes.hidden,
159
173
  data: { routeName: 'TeamFilters' },
160
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
+ },
161
189
  ]
162
190
 
163
191
  return (
@@ -178,6 +206,8 @@ export const ConversationFilters = () => {
178
206
  return <TeamRow {...item.data} />
179
207
  case SectionTypes.more:
180
208
  return <ViewMore {...item.data} />
209
+ case SectionTypes.other:
210
+ return <OtherRow {...item.data} />
181
211
  default:
182
212
  return null
183
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>