@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
@@ -7,10 +7,19 @@ import {
7
7
  useScalableNumberOfLines,
8
8
  } from '../../hooks'
9
9
  import { ConversationPreviewData, ConversationResource } from '../../types'
10
- import { REACTION_EMOJIS, platformFontWeightBold } from '../../utils'
10
+ import { REACTION_EMOJIS, isArchivedConversation, platformFontWeightBold } from '../../utils'
11
11
  import { formatDatePreview } from '../../utils/date'
12
+ import { faBoxArchive } from '../../vendor/font_awesome_pro_solid_icons'
12
13
  import { tokens } from '../../vendor/tapestry/tokens'
13
- import { AvatarGroup, ConversationAvatar, Heading, Text, Badge, type BadgeProps } from '../display'
14
+ import {
15
+ AvatarGroup,
16
+ ConversationAvatar,
17
+ Heading,
18
+ IconTile,
19
+ Text,
20
+ Badge,
21
+ type BadgeProps,
22
+ } from '../display'
14
23
  import { ConversationActions } from './conversation_actions'
15
24
  import { MuteIndicator } from './mute_indicator'
16
25
  import { UnreadCountBadge } from './unread_count_badge'
@@ -42,6 +51,7 @@ export const ConversationPreview = ({
42
51
  unreadReaction,
43
52
  } = conversation
44
53
 
54
+ const archived = isArchivedConversation(conversation)
45
55
  const emptyConversation = !lastMessageCreatedAt
46
56
  const fallbackIconName = emptyConversation ? 'people.noTextMessage' : 'general.person'
47
57
  const hasMetaContent = lastMessageCreatedAt || unreadCount > 0 || muted
@@ -57,25 +67,30 @@ export const ConversationPreview = ({
57
67
  const conversationAccessibilityLabel = getConversationAccessibilityLabel({
58
68
  conversation,
59
69
  showBadges,
70
+ archived,
60
71
  })
61
72
 
62
73
  return (
63
74
  <ConversationActions
64
75
  conversation={conversation}
65
- style={[styles.previewRow, disabled && styles.disabled, style]}
76
+ style={[styles.previewRow, disabled && !archived && styles.disabled, style]}
66
77
  onPress={onPress}
67
78
  accessibilityLabel={conversationAccessibilityLabel}
68
79
  >
69
- <ConversationAvatar
70
- conversation={conversation}
71
- size="lg"
72
- fallbackIconName={fallbackIconName}
73
- />
80
+ {archived ? (
81
+ <IconTile icon={faBoxArchive} size="lg" testID="conversation-archived-icon" />
82
+ ) : (
83
+ <ConversationAvatar
84
+ conversation={conversation}
85
+ size="lg"
86
+ fallbackIconName={fallbackIconName}
87
+ />
88
+ )}
74
89
  <View style={styles.conversationBody}>
75
90
  <Heading
76
91
  numberOfLines={numberOfLinesScaledTitle}
77
92
  variant="h3"
78
- style={[styles.title, isUnread && styles.unreadTitle]}
93
+ style={[styles.title, archived && styles.archivedTitle, isUnread && styles.unreadTitle]}
79
94
  >
80
95
  {title}
81
96
  </Heading>
@@ -193,10 +208,12 @@ function formatConversationPreviewAccessibilityLabel(preview: ConversationPrevie
193
208
  interface GetConversationAccessibilityLabelProps {
194
209
  conversation: ConversationResource
195
210
  showBadges: boolean
211
+ archived: boolean
196
212
  }
197
213
  const getConversationAccessibilityLabel = ({
198
214
  conversation,
199
215
  showBadges,
216
+ archived,
200
217
  }: GetConversationAccessibilityLabelProps) => {
201
218
  const { badges, title, conversationPreview, muted, disabled, unreadCount, unreadReaction } =
202
219
  conversation
@@ -207,7 +224,7 @@ const getConversationAccessibilityLabel = ({
207
224
  const label = {
208
225
  title,
209
226
  badge: hasBadges ? `for ${badges[0]?.pcoResourceType} ${badges[0]?.text || ''}` : '',
210
- disabled: disabled ? 'Disabled' : '',
227
+ disabled: archived ? 'Archived' : disabled ? 'Disabled' : '',
211
228
  muted: muted ? 'Muted' : '',
212
229
  readStatus: hasUnreadMessages
213
230
  ? `${unreadCount} unread messages`
@@ -255,6 +272,9 @@ const useStyles = () => {
255
272
  fontSize: 16,
256
273
  color: colors.textColorDefaultPrimary,
257
274
  },
275
+ archivedTitle: {
276
+ color: colors.textColorDefaultSecondary,
277
+ },
258
278
  unreadTitle: {
259
279
  fontWeight: tokens.fontWeightBold,
260
280
  },
@@ -4,10 +4,14 @@ import { FlatList, StyleSheet, View } from 'react-native'
4
4
  import { useConversationsContext } from '../../contexts/conversations_context'
5
5
  import { useTheme } from '../../hooks'
6
6
  import { useCanCreateConversations } from '../../hooks/use_chat_permissions'
7
+ import { useClearConversationFilters } from '../../hooks/use_clear_conversation_filters'
7
8
  import { useConversationsJoltEvents } from '../../hooks/use_conversations_jolt_events'
9
+ import { useFilterDropdownEnabled } from '../../hooks/use_filter_dropdown_enabled'
8
10
  import { ConversationResource } from '../../types'
11
+ import { ArchivedResultsFooter } from './archived_results_footer'
9
12
  import { ConversationPreview, ConversationPreviewSkeleton } from './conversation_preview'
10
13
  import { ConversationsBlankState } from './conversations_blank_state'
14
+ import { FilteredResultsFooter } from './filtered_results_footer'
11
15
 
12
16
  interface ConversationsProps {
13
17
  ListHeaderComponent?:
@@ -26,11 +30,23 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
26
30
  refetch,
27
31
  isRefetching,
28
32
  isFetched,
29
- args: { chat_group_graph_id, group_source_app_name },
33
+ hasNextPage,
34
+ args: { chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived },
30
35
  } = useConversationsContext()
31
36
  const navigation = useNavigation()
32
37
  const canCreateConversations = useCanCreateConversations()
33
- const isFilterApplied = !!chat_group_graph_id || !!group_source_app_name
38
+ const clearFilters = useClearConversationFilters()
39
+ const filterDropdownEnabled = useFilterDropdownEnabled()
40
+ const isFilterApplied =
41
+ !!chat_group_graph_id || !!group_source_app_name || !!show_direct_messages || !!show_archived
42
+ const showArchivedResultsFooter = !!show_archived && !isLoading
43
+ const showFilteredResultsFooter =
44
+ filterDropdownEnabled &&
45
+ isFilterApplied &&
46
+ !show_archived &&
47
+ !hasNextPage &&
48
+ !isLoading &&
49
+ conversations.length > 0
34
50
 
35
51
  const showBadges = !chat_group_graph_id
36
52
 
@@ -62,6 +78,13 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
62
78
  canCreateConversations={canCreateConversations}
63
79
  />
64
80
  }
81
+ ListFooterComponent={
82
+ showArchivedResultsFooter ? (
83
+ <ArchivedResultsFooter onViewActiveConversations={clearFilters} />
84
+ ) : showFilteredResultsFooter ? (
85
+ <FilteredResultsFooter />
86
+ ) : null
87
+ }
65
88
  renderItem={({ item }) => {
66
89
  if (item.type === 'loading') {
67
90
  return <ConversationPreviewSkeleton />
@@ -1,5 +1,7 @@
1
1
  import React from 'react'
2
2
  import { StyleSheet, View } from 'react-native'
3
+ import { useClearConversationFilters } from '../../hooks/use_clear_conversation_filters'
4
+ import { useFilterDropdownEnabled } from '../../hooks/use_filter_dropdown_enabled'
3
5
  import BlankState from '../primitive/blank_state_primitive'
4
6
 
5
7
  interface ConversationsBlankStateProps {
@@ -11,6 +13,25 @@ export function ConversationsBlankState({
11
13
  isFilterApplied,
12
14
  canCreateConversations,
13
15
  }: ConversationsBlankStateProps) {
16
+ const filterDropdownEnabled = useFilterDropdownEnabled()
17
+ const clearFilters = useClearConversationFilters()
18
+
19
+ if (filterDropdownEnabled && isFilterApplied) {
20
+ return (
21
+ <View style={styles.container}>
22
+ <BlankState.Root>
23
+ <BlankState.Imagery name="general.outlinedTextMessage" />
24
+ <BlankState.Content>
25
+ <BlankState.Heading>No matches</BlankState.Heading>
26
+ </BlankState.Content>
27
+ <BlankState.TextButton onPress={clearFilters}>
28
+ View all conversations
29
+ </BlankState.TextButton>
30
+ </BlankState.Root>
31
+ </View>
32
+ )
33
+ }
34
+
14
35
  return (
15
36
  <View style={styles.container}>
16
37
  <BlankState.Root>
@@ -0,0 +1,38 @@
1
+ import React from 'react'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import { useTheme } from '../../hooks'
4
+ import { useClearConversationFilters } from '../../hooks/use_clear_conversation_filters'
5
+ import { Text, TextButton } from '../display'
6
+
7
+ export function FilteredResultsFooter() {
8
+ const styles = useStyles()
9
+ const clearFilters = useClearConversationFilters()
10
+
11
+ return (
12
+ <View style={styles.container}>
13
+ <Text variant="tertiary" style={styles.text}>
14
+ You&rsquo;re viewing filtered results
15
+ </Text>
16
+ <TextButton variant="tertiary" onPress={clearFilters}>
17
+ View all conversations
18
+ </TextButton>
19
+ </View>
20
+ )
21
+ }
22
+
23
+ const useStyles = () => {
24
+ const { colors } = useTheme()
25
+
26
+ return StyleSheet.create({
27
+ container: {
28
+ alignItems: 'center',
29
+ gap: 8,
30
+ paddingHorizontal: 16,
31
+ paddingVertical: 24,
32
+ },
33
+ text: {
34
+ textAlign: 'center',
35
+ color: colors.textColorDefaultSecondary,
36
+ },
37
+ })
38
+ }
@@ -58,7 +58,7 @@ const options = [
58
58
  { id: 'teams', label: 'Teams' },
59
59
  ]
60
60
 
61
- const renderFilterDropdown = () => {
61
+ const renderFilterDropdown = ({ showMoreFilters }: { showMoreFilters?: boolean } = {}) => {
62
62
  const onSelect = jest.fn()
63
63
  const onMoreFilters = jest.fn()
64
64
 
@@ -69,6 +69,7 @@ const renderFilterDropdown = () => {
69
69
  onSelect={onSelect}
70
70
  onMoreFilters={onMoreFilters}
71
71
  triggerLabel="Groups"
72
+ showMoreFilters={showMoreFilters}
72
73
  />
73
74
  )
74
75
 
@@ -95,6 +96,13 @@ describe('FilterDropdown', () => {
95
96
  expect(onMoreFilters).not.toHaveBeenCalled()
96
97
  })
97
98
 
99
+ it('leaves More filters out when showMoreFilters is false', () => {
100
+ renderFilterDropdown({ showMoreFilters: false })
101
+
102
+ expect(screen.queryByRole('menuitem', { name: 'More filters' })).toBeNull()
103
+ expect(screen.getAllByRole('menuitem')).toHaveLength(options.length)
104
+ })
105
+
98
106
  it('calls onMoreFilters for the More filters action', () => {
99
107
  const { onSelect, onMoreFilters } = renderFilterDropdown()
100
108
 
@@ -34,6 +34,7 @@ export interface FilterDropdownProps {
34
34
  onSelect: (id: string) => void
35
35
  onMoreFilters: () => void
36
36
  triggerLabel: string
37
+ showMoreFilters?: boolean
37
38
  }
38
39
 
39
40
  export function FilterDropdown({
@@ -42,22 +43,22 @@ export function FilterDropdown({
42
43
  onSelect,
43
44
  onMoreFilters,
44
45
  triggerLabel,
46
+ showMoreFilters = true,
45
47
  }: FilterDropdownProps) {
46
48
  const styles = useStyles()
47
49
 
48
- const actions: MenuAction[] = [
49
- ...options.map(option => ({
50
- id: option.id,
51
- title: option.label,
52
- state: option.id === selectedId ? ('on' as const) : ('off' as const),
53
- })),
54
- {
55
- id: MORE_FILTERS_ID,
56
- title: '',
57
- displayInline: true,
58
- subactions: [{ id: MORE_FILTERS_ID, title: MORE_FILTERS_LABEL }],
59
- },
60
- ]
50
+ const optionActions: MenuAction[] = options.map(option => ({
51
+ id: option.id,
52
+ title: option.label,
53
+ state: option.id === selectedId ? ('on' as const) : ('off' as const),
54
+ }))
55
+ const moreFiltersAction: MenuAction = {
56
+ id: MORE_FILTERS_ID,
57
+ title: '',
58
+ displayInline: true,
59
+ subactions: [{ id: MORE_FILTERS_ID, title: MORE_FILTERS_LABEL }],
60
+ }
61
+ const actions = showMoreFilters ? [...optionActions, moreFiltersAction] : optionActions
61
62
 
62
63
  const handlePressAction = ({ nativeEvent }: MenuActionEvent) => {
63
64
  if (nativeEvent.event === MORE_FILTERS_ID) {
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  import { act, renderHook, waitFor } from '@testing-library/react-native'
2
2
  import { AccessibilityInfo, EmitterSubscription } from 'react-native'
3
+ import * as Reanimated from 'react-native-reanimated'
3
4
  import { useReduceMotion } from '../use_reduce_motion'
4
5
 
5
6
  type ReduceMotionListener = (enabled: boolean) => void
@@ -11,6 +12,7 @@ describe('useReduceMotion', () => {
11
12
  beforeEach(() => {
12
13
  listener = undefined
13
14
  remove.mockClear()
15
+ jest.spyOn(Reanimated, 'useReducedMotion').mockReturnValue(false)
14
16
  jest.spyOn(AccessibilityInfo, 'isReduceMotionEnabled').mockResolvedValue(true)
15
17
  const addEventListener: typeof AccessibilityInfo.addEventListener = (_event, handler) => {
16
18
  listener = handler as ReduceMotionListener
@@ -21,7 +23,17 @@ describe('useReduceMotion', () => {
21
23
 
22
24
  afterEach(() => jest.restoreAllMocks())
23
25
 
24
- it('seeds from the system setting', async () => {
26
+ it('seeds synchronously from the launch-time setting', async () => {
27
+ jest.spyOn(Reanimated, 'useReducedMotion').mockReturnValue(true)
28
+ jest.mocked(AccessibilityInfo.isReduceMotionEnabled).mockResolvedValue(false)
29
+
30
+ const { result } = renderHook(() => useReduceMotion())
31
+
32
+ expect(result.current).toBe(true)
33
+ await waitFor(() => expect(result.current).toBe(false))
34
+ })
35
+
36
+ it('updates to the live setting once it resolves', async () => {
25
37
  const { result } = renderHook(() => useReduceMotion())
26
38
 
27
39
  expect(result.current).toBe(false)
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ import { useNavigation } from '@react-navigation/native'
2
+ import { useCallback } from 'react'
3
+
4
+ export function useClearConversationFilters() {
5
+ const navigation = useNavigation()
6
+
7
+ return useCallback(() => {
8
+ navigation.setParams({
9
+ title: undefined,
10
+ chat_group_graph_id: undefined,
11
+ group_source_app_name: undefined,
12
+ show_direct_messages: undefined,
13
+ show_archived: undefined,
14
+ })
15
+ }, [navigation])
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
  },