@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,76 @@
1
+ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { Platform } from 'react-native'
5
+ import { SafeAreaProvider } from 'react-native-safe-area-context'
6
+ import { FilterContext } from '../../screens/conversation_filters/context/conversation_filter_context'
7
+ import { ConversationFiltersHeader } from '../../screens/conversation_filters_screen'
8
+
9
+ const mockGoBack = jest.fn()
10
+
11
+ jest.mock('@react-navigation/elements', () => ({
12
+ ...jest.requireActual('@react-navigation/elements'),
13
+ useHeaderHeight: () => 0,
14
+ }))
15
+ jest.mock('@react-navigation/native', () => ({
16
+ ...jest.requireActual('@react-navigation/native'),
17
+ useNavigation: () => ({ goBack: mockGoBack }),
18
+ useRoute: () => ({ params: {} }),
19
+ }))
20
+ jest.mock('../../hooks/use_filter_dropdown_enabled', () => ({
21
+ useFilterDropdownEnabled: () => true,
22
+ }))
23
+
24
+ const safeAreaMetrics = {
25
+ frame: { x: 0, y: 0, width: 390, height: 844 },
26
+ insets: { top: 0, right: 0, bottom: 0, left: 0 },
27
+ }
28
+
29
+ const renderHeader = () =>
30
+ render(
31
+ <SafeAreaProvider initialMetrics={safeAreaMetrics}>
32
+ <ThemeProvider value={DefaultTheme}>
33
+ <FilterContext.Provider
34
+ value={{
35
+ params: {},
36
+ scrollOffset: 0,
37
+ setScrollOffset: jest.fn(),
38
+ setFormSheetHeaderHeight: jest.fn(),
39
+ resetFilter: jest.fn(),
40
+ setGroupFilter: jest.fn(),
41
+ setAppFilter: jest.fn(),
42
+ applyFilters: jest.fn(),
43
+ platformListPaddingBottom: 0,
44
+ }}
45
+ >
46
+ <ConversationFiltersHeader />
47
+ </FilterContext.Provider>
48
+ </ThemeProvider>
49
+ </SafeAreaProvider>
50
+ )
51
+
52
+ describe('ConversationFiltersHeader', () => {
53
+ afterEach(() => {
54
+ jest.restoreAllMocks()
55
+ mockGoBack.mockClear()
56
+ })
57
+
58
+ it('shows a back button that dismisses the sheet on Android', async () => {
59
+ jest.replaceProperty(Platform, 'OS', 'android')
60
+
61
+ renderHeader()
62
+
63
+ fireEvent.press(screen.getByLabelText('Back'))
64
+
65
+ await waitFor(() => expect(mockGoBack).toHaveBeenCalledTimes(1))
66
+ })
67
+
68
+ it('does not show a back button on iOS', () => {
69
+ jest.replaceProperty(Platform, 'OS', 'ios')
70
+
71
+ renderHeader()
72
+
73
+ expect(screen.getByText('More filters')).toBeTruthy()
74
+ expect(screen.queryByLabelText('Back')).toBeNull()
75
+ })
76
+ })
@@ -0,0 +1,67 @@
1
+ import { render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { Text } from 'react-native'
4
+ import type { ConversationResource } from '../../../types'
5
+ import { ConversationActions } from '../conversation_actions'
6
+
7
+ jest.mock('react-native-gesture-handler/ReanimatedSwipeable', () => {
8
+ const { View: MockView } = jest.requireActual('react-native')
9
+ return {
10
+ __esModule: true,
11
+ default: ({
12
+ children,
13
+ renderLeftActions,
14
+ }: {
15
+ children: React.ReactNode
16
+ renderLeftActions: () => React.ReactNode
17
+ }) => (
18
+ <MockView>
19
+ {renderLeftActions()}
20
+ {children}
21
+ </MockView>
22
+ ),
23
+ }
24
+ })
25
+ jest.mock('../../../hooks/use_conversations_actions', () => ({
26
+ useConversationsMute: () => ({ muted: false, setMuted: jest.fn(), isPending: false }),
27
+ useConversationsMarkRead: () => ({ read: false, markRead: jest.fn(), isPending: false }),
28
+ }))
29
+
30
+ const createdAt = '2026-10-01T12:00:00Z'
31
+
32
+ const conversation = (overrides: Partial<ConversationResource>): ConversationResource => ({
33
+ type: 'Conversation',
34
+ id: 7,
35
+ title: 'Youth Group',
36
+ conversationPreview: null,
37
+ createdAt,
38
+ lastMessageCreatedAt: createdAt,
39
+ muted: false,
40
+ repliesDisabled: false,
41
+ unreadCount: 1,
42
+ unreadReaction: false,
43
+ updatedAt: createdAt,
44
+ ...overrides,
45
+ })
46
+
47
+ const renderActions = (overrides: Partial<ConversationResource>) =>
48
+ render(
49
+ <ConversationActions conversation={conversation(overrides)} onPress={jest.fn()}>
50
+ <Text>Youth Group</Text>
51
+ </ConversationActions>
52
+ )
53
+
54
+ describe('ConversationActions', () => {
55
+ it('offers mark read but not mute on an archived conversation', () => {
56
+ const view = renderActions({ disabled: true, disabledReason: 'archived' })
57
+
58
+ expect(view.getByText('Mark read')).toBeTruthy()
59
+ expect(view.queryByText('Silence')).toBeNull()
60
+ })
61
+
62
+ it('offers mute on a conversation disabled for another reason', () => {
63
+ const view = renderActions({ disabled: true, disabledReason: 'safety_check' })
64
+
65
+ expect(view.getByText('Silence')).toBeTruthy()
66
+ })
67
+ })
@@ -19,7 +19,10 @@ jest.mock('../conversation_actions', () => ({
19
19
 
20
20
  const createdAt = '2026-03-04T15:30:00Z'
21
21
 
22
- const conversation = (conversationPreview: ConversationPreviewData): ConversationResource => ({
22
+ const conversation = (
23
+ conversationPreview: ConversationPreviewData,
24
+ overrides: Partial<ConversationResource> = {}
25
+ ): ConversationResource => ({
23
26
  type: 'Conversation',
24
27
  id: 7,
25
28
  title: 'Worship Team',
@@ -31,10 +34,24 @@ const conversation = (conversationPreview: ConversationPreviewData): Conversatio
31
34
  unreadCount: 0,
32
35
  unreadReaction: false,
33
36
  updatedAt: createdAt,
37
+ ...overrides,
34
38
  })
35
39
 
36
- const renderPreview = (preview: ConversationPreviewData) =>
37
- render(<ConversationPreview conversation={conversation(preview)} onPress={jest.fn()} />)
40
+ const messagePreview: ConversationPreviewData = {
41
+ kind: 'message',
42
+ authorName: 'Ada Lovelace',
43
+ value: null,
44
+ text: 'See you Sunday',
45
+ createdAt,
46
+ }
47
+
48
+ const renderPreview = (
49
+ preview: ConversationPreviewData,
50
+ overrides: Partial<ConversationResource> = {}
51
+ ) =>
52
+ render(
53
+ <ConversationPreview conversation={conversation(preview, overrides)} onPress={jest.fn()} />
54
+ )
38
55
 
39
56
  describe('ConversationPreview', () => {
40
57
  it('prefixes message previews with the author', () => {
@@ -67,4 +84,22 @@ describe('ConversationPreview', () => {
67
84
  )
68
85
  ).toBeTruthy()
69
86
  })
87
+
88
+ it('marks an archived conversation with an archive icon instead of the disabled label', () => {
89
+ const view = renderPreview(messagePreview, { disabled: true, disabledReason: 'archived' })
90
+
91
+ expect(
92
+ view.getByTestId('conversation-archived-icon', { includeHiddenElements: true })
93
+ ).toBeTruthy()
94
+ expect(view.getByLabelText(/^Select Worship Team, Archived,/)).toBeTruthy()
95
+ })
96
+
97
+ it('keeps the disabled label for a conversation disabled for another reason', () => {
98
+ const view = renderPreview(messagePreview, { disabled: true, disabledReason: 'safety_check' })
99
+
100
+ expect(
101
+ view.queryByTestId('conversation-archived-icon', { includeHiddenElements: true })
102
+ ).toBeNull()
103
+ expect(view.getByLabelText(/^Select Worship Team, Disabled,/)).toBeTruthy()
104
+ })
70
105
  })
@@ -0,0 +1,82 @@
1
+ import { fireEvent, render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { ConversationFiltersParams } from '../../../screens/conversation_filters/screen_props'
4
+ import { Conversations } from '../conversations'
5
+
6
+ const mockSetParams = jest.fn()
7
+ let mockArgs: ConversationFiltersParams = {}
8
+ let mockFilterDropdownEnabled = false
9
+
10
+ jest.mock('@react-navigation/native', () => ({
11
+ ...jest.requireActual('@react-navigation/native'),
12
+ useNavigation: () => ({ setParams: mockSetParams, navigate: jest.fn() }),
13
+ }))
14
+ jest.mock('../../../contexts/conversations_context', () => ({
15
+ useConversationsContext: () => ({
16
+ conversations: [{ id: 1, title: 'Youth Group' }],
17
+ isLoading: false,
18
+ isFetched: true,
19
+ isRefetching: false,
20
+ isError: false,
21
+ error: null,
22
+ fetchNextPage: jest.fn(),
23
+ refetch: jest.fn(),
24
+ args: mockArgs,
25
+ }),
26
+ }))
27
+ jest.mock('../../../hooks/use_chat_permissions', () => ({
28
+ useCanCreateConversations: () => false,
29
+ }))
30
+ jest.mock('../../../hooks/use_filter_dropdown_enabled', () => ({
31
+ useFilterDropdownEnabled: () => mockFilterDropdownEnabled,
32
+ }))
33
+ jest.mock('../../../hooks/use_conversations_jolt_events', () => ({
34
+ useConversationsJoltEvents: () => {},
35
+ }))
36
+ jest.mock('../conversation_preview', () => ({
37
+ ConversationPreview: () => null,
38
+ ConversationPreviewSkeleton: () => null,
39
+ }))
40
+ jest.mock('../conversations_blank_state', () => ({
41
+ ConversationsBlankState: () => null,
42
+ }))
43
+
44
+ describe('Conversations archived footer', () => {
45
+ beforeEach(() => {
46
+ mockSetParams.mockClear()
47
+ mockFilterDropdownEnabled = false
48
+ })
49
+
50
+ it('ends the archived list with a way back to active conversations', () => {
51
+ mockArgs = { show_archived: true }
52
+ const view = render(<Conversations />)
53
+
54
+ expect(view.getByText('You’re viewing archived results.')).toBeTruthy()
55
+
56
+ fireEvent.press(view.getByLabelText('View active conversations'))
57
+
58
+ expect(mockSetParams).toHaveBeenCalledWith({
59
+ title: undefined,
60
+ chat_group_graph_id: undefined,
61
+ group_source_app_name: undefined,
62
+ show_direct_messages: undefined,
63
+ show_archived: undefined,
64
+ })
65
+ })
66
+
67
+ it('shows the archived footer instead of the filtered one when the dropdown is on', () => {
68
+ mockArgs = { show_archived: true }
69
+ mockFilterDropdownEnabled = true
70
+ const view = render(<Conversations />)
71
+
72
+ expect(view.getByLabelText('View active conversations')).toBeTruthy()
73
+ expect(view.queryByText('You’re viewing filtered results')).toBeNull()
74
+ })
75
+
76
+ it('leaves the active list without the archived footer', () => {
77
+ mockArgs = {}
78
+ const view = render(<Conversations />)
79
+
80
+ expect(view.queryByLabelText('View active conversations')).toBeNull()
81
+ })
82
+ })
@@ -40,6 +40,7 @@ describe('ConversationsBlankState', () => {
40
40
  chat_group_graph_id: undefined,
41
41
  group_source_app_name: undefined,
42
42
  show_direct_messages: undefined,
43
+ show_archived: undefined,
43
44
  })
44
45
  })
45
46
 
@@ -0,0 +1,36 @@
1
+ import React from 'react'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import { Text, TextInlineButton } from '../display'
4
+
5
+ interface ArchivedResultsFooterProps {
6
+ onViewActiveConversations: () => void
7
+ }
8
+
9
+ export function ArchivedResultsFooter({ onViewActiveConversations }: ArchivedResultsFooterProps) {
10
+ return (
11
+ <View style={styles.container}>
12
+ <Text variant="tertiary" style={styles.text}>
13
+ You&rsquo;re viewing archived results.
14
+ </Text>
15
+ <TextInlineButton
16
+ variant="tertiary"
17
+ onPress={onViewActiveConversations}
18
+ accessibilityLabel="View active conversations"
19
+ >
20
+ View active conversations
21
+ </TextInlineButton>
22
+ </View>
23
+ )
24
+ }
25
+
26
+ const styles = StyleSheet.create({
27
+ container: {
28
+ alignItems: 'center',
29
+ gap: 4,
30
+ paddingVertical: 24,
31
+ paddingHorizontal: 16,
32
+ },
33
+ text: {
34
+ textAlign: 'center',
35
+ },
36
+ })
@@ -18,7 +18,7 @@ import {
18
18
  useConversationsMute,
19
19
  } from '../../hooks/use_conversations_actions'
20
20
  import { ConversationResource } from '../../types'
21
- import { platformPressedOpacityStyle } from '../../utils'
21
+ import { isArchivedConversation, platformPressedOpacityStyle } from '../../utils'
22
22
  import { tokens } from '../../vendor/tapestry/tokens'
23
23
  import { SwipeableToggleButton } from './swipeable_toggle_button'
24
24
 
@@ -53,6 +53,7 @@ export function ConversationActions({
53
53
  })
54
54
 
55
55
  const isConversationEmpty = conversation.lastMessageCreatedAt === null
56
+ const canMute = !isArchivedConversation(conversation)
56
57
 
57
58
  const handleSwipeableClose = useCallback(() => {
58
59
  setDisabled(false)
@@ -88,13 +89,16 @@ export function ConversationActions({
88
89
  ]
89
90
  : []
90
91
 
91
- const accessibilityActions = [
92
- {
93
- name: muted ? 'unmute' : 'mute',
94
- label: muted ? 'Enable' : 'Silence',
95
- },
96
- ...readAccessibilityAction,
97
- ]
92
+ const muteAccessibilityAction = canMute
93
+ ? [
94
+ {
95
+ name: muted ? 'unmute' : 'mute',
96
+ label: muted ? 'Enable' : 'Silence',
97
+ },
98
+ ]
99
+ : []
100
+
101
+ const accessibilityActions = [...muteAccessibilityAction, ...readAccessibilityAction]
98
102
 
99
103
  const handleAccessibilityAction = useCallback(
100
104
  (event: AccessibilityActionEvent) => {
@@ -136,6 +140,7 @@ export function ConversationActions({
136
140
  renderLeftActions={() => (
137
141
  <LeftActions
138
142
  isConversationEmpty={isConversationEmpty}
143
+ canMute={canMute}
139
144
  handleMute={handleMute}
140
145
  handleLatestMessageUnread={handleLatestMessageUnread}
141
146
  muted={muted}
@@ -163,6 +168,7 @@ export function ConversationActions({
163
168
 
164
169
  interface LeftActionsProps {
165
170
  isConversationEmpty: boolean
171
+ canMute: boolean
166
172
  handleMute: () => void
167
173
  handleLatestMessageUnread: () => void
168
174
  muted: boolean
@@ -173,6 +179,7 @@ interface LeftActionsProps {
173
179
 
174
180
  function LeftActions({
175
181
  isConversationEmpty,
182
+ canMute,
176
183
  handleMute,
177
184
  handleLatestMessageUnread,
178
185
  muted,
@@ -180,7 +187,7 @@ function LeftActions({
180
187
  isMutedPending,
181
188
  isMarkReadPending,
182
189
  }: LeftActionsProps) {
183
- const styles = useStyles()
190
+ const styles = useStyles({ actionButtonCount: canMute ? 2 : 1 })
184
191
 
185
192
  const muteToggleContent = {
186
193
  true: { iconName: 'general.bell', label: 'Enable' },
@@ -202,21 +209,22 @@ function LeftActions({
202
209
  toggleContent={latestMessageUnreadToggleContent}
203
210
  backgroundColor={tokens.fillColorInteractionSwipeDefault}
204
211
  />
205
- <SwipeableToggleButton
206
- loading={isMutedPending}
207
- toggled={muted}
208
- onPress={handleMute}
209
- toggleContent={muteToggleContent}
210
- backgroundColor={tokens.fillColorInteractionSwipeSecondary}
211
- />
212
+ {canMute && (
213
+ <SwipeableToggleButton
214
+ loading={isMutedPending}
215
+ toggled={muted}
216
+ onPress={handleMute}
217
+ toggleContent={muteToggleContent}
218
+ backgroundColor={tokens.fillColorInteractionSwipeSecondary}
219
+ />
220
+ )}
212
221
  </View>
213
222
  )
214
223
  }
215
224
 
216
225
  const ACTION_BUTTON_WIDTH = 120
217
- const ACTION_BUTTON_COUNT = 2
218
226
 
219
- const useStyles = () => {
227
+ const useStyles = ({ actionButtonCount = 2 }: { actionButtonCount?: number } = {}) => {
220
228
  const { colors } = useTheme()
221
229
  return StyleSheet.create({
222
230
  swipeableChildContainer: {
@@ -225,7 +233,7 @@ const useStyles = () => {
225
233
  actionButtonContainer: {
226
234
  flexDirection: 'row',
227
235
  alignItems: 'center',
228
- width: ACTION_BUTTON_WIDTH * ACTION_BUTTON_COUNT,
236
+ width: ACTION_BUTTON_WIDTH * actionButtonCount,
229
237
  alignContent: 'stretch',
230
238
  },
231
239
  swipeableContainer: {
@@ -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,9 +4,11 @@ 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'
8
9
  import { useFilterDropdownEnabled } from '../../hooks/use_filter_dropdown_enabled'
9
10
  import { ConversationResource } from '../../types'
11
+ import { ArchivedResultsFooter } from './archived_results_footer'
10
12
  import { ConversationPreview, ConversationPreviewSkeleton } from './conversation_preview'
11
13
  import { ConversationsBlankState } from './conversations_blank_state'
12
14
  import { FilteredResultsFooter } from './filtered_results_footer'
@@ -29,15 +31,19 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
29
31
  isRefetching,
30
32
  isFetched,
31
33
  hasNextPage,
32
- args: { chat_group_graph_id, group_source_app_name, show_direct_messages },
34
+ args: { chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived },
33
35
  } = useConversationsContext()
34
36
  const navigation = useNavigation()
35
37
  const canCreateConversations = useCanCreateConversations()
38
+ const clearFilters = useClearConversationFilters()
36
39
  const filterDropdownEnabled = useFilterDropdownEnabled()
37
- const isFilterApplied = !!chat_group_graph_id || !!group_source_app_name || !!show_direct_messages
40
+ const isFilterApplied =
41
+ !!chat_group_graph_id || !!group_source_app_name || !!show_direct_messages || !!show_archived
42
+ const showArchivedResultsFooter = !!show_archived && !isLoading
38
43
  const showFilteredResultsFooter =
39
44
  filterDropdownEnabled &&
40
45
  isFilterApplied &&
46
+ !show_archived &&
41
47
  !hasNextPage &&
42
48
  !isLoading &&
43
49
  conversations.length > 0
@@ -72,7 +78,13 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
72
78
  canCreateConversations={canCreateConversations}
73
79
  />
74
80
  }
75
- ListFooterComponent={showFilteredResultsFooter ? <FilteredResultsFooter /> : null}
81
+ ListFooterComponent={
82
+ showArchivedResultsFooter ? (
83
+ <ArchivedResultsFooter onViewActiveConversations={clearFilters} />
84
+ ) : showFilteredResultsFooter ? (
85
+ <FilteredResultsFooter />
86
+ ) : null
87
+ }
76
88
  renderItem={({ item }) => {
77
89
  if (item.type === 'loading') {
78
90
  return <ConversationPreviewSkeleton />
@@ -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) {