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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/build/components/conversations/archived_results_footer.d.ts +6 -0
  2. package/build/components/conversations/archived_results_footer.d.ts.map +1 -0
  3. package/build/components/conversations/archived_results_footer.js +51 -0
  4. package/build/components/conversations/archived_results_footer.js.map +1 -0
  5. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  6. package/build/components/conversations/conversation_actions.js +241 -206
  7. package/build/components/conversations/conversation_actions.js.map +1 -1
  8. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  9. package/build/components/conversations/conversation_preview.js +99 -93
  10. package/build/components/conversations/conversation_preview.js.map +1 -1
  11. package/build/components/conversations/conversations.d.ts.map +1 -1
  12. package/build/components/conversations/conversations.js +65 -40
  13. package/build/components/conversations/conversations.js.map +1 -1
  14. package/build/components/conversations/conversations_blank_state.d.ts.map +1 -1
  15. package/build/components/conversations/conversations_blank_state.js +55 -12
  16. package/build/components/conversations/conversations_blank_state.js.map +1 -1
  17. package/build/components/conversations/filtered_results_footer.d.ts +2 -0
  18. package/build/components/conversations/filtered_results_footer.d.ts.map +1 -0
  19. package/build/components/conversations/filtered_results_footer.js +77 -0
  20. package/build/components/conversations/filtered_results_footer.js.map +1 -0
  21. package/build/components/display/filter_dropdown.d.ts +2 -1
  22. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  23. package/build/components/display/filter_dropdown.js +63 -58
  24. package/build/components/display/filter_dropdown.js.map +1 -1
  25. package/build/components/display/icon_tile.d.ts +12 -0
  26. package/build/components/display/icon_tile.d.ts.map +1 -0
  27. package/build/components/display/icon_tile.js +109 -0
  28. package/build/components/display/icon_tile.js.map +1 -0
  29. package/build/components/display/index.d.ts +1 -0
  30. package/build/components/display/index.d.ts.map +1 -1
  31. package/build/components/display/index.js +1 -0
  32. package/build/components/display/index.js.map +1 -1
  33. package/build/contexts/conversations_context.d.ts.map +1 -1
  34. package/build/contexts/conversations_context.js +21 -18
  35. package/build/contexts/conversations_context.js.map +1 -1
  36. package/build/hooks/index.d.ts +2 -0
  37. package/build/hooks/index.d.ts.map +1 -1
  38. package/build/hooks/index.js +2 -0
  39. package/build/hooks/index.js.map +1 -1
  40. package/build/hooks/use_can_browse_archived_conversations.d.ts +2 -0
  41. package/build/hooks/use_can_browse_archived_conversations.d.ts.map +1 -0
  42. package/build/hooks/use_can_browse_archived_conversations.js +29 -0
  43. package/build/hooks/use_can_browse_archived_conversations.js.map +1 -0
  44. package/build/hooks/use_clear_conversation_filters.d.ts +2 -0
  45. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -0
  46. package/build/hooks/use_clear_conversation_filters.js +25 -0
  47. package/build/hooks/use_clear_conversation_filters.js.map +1 -0
  48. package/build/hooks/use_conversation.d.ts.map +1 -1
  49. package/build/hooks/use_conversation.js +32 -24
  50. package/build/hooks/use_conversation.js.map +1 -1
  51. package/build/hooks/use_current_person.d.ts.map +1 -1
  52. package/build/hooks/use_current_person.js +1 -1
  53. package/build/hooks/use_current_person.js.map +1 -1
  54. package/build/hooks/use_jolt.d.ts.map +1 -1
  55. package/build/hooks/use_jolt.js +29 -8
  56. package/build/hooks/use_jolt.js.map +1 -1
  57. package/build/hooks/use_manual_archiving_enabled.d.ts +2 -0
  58. package/build/hooks/use_manual_archiving_enabled.d.ts.map +1 -0
  59. package/build/hooks/use_manual_archiving_enabled.js +18 -0
  60. package/build/hooks/use_manual_archiving_enabled.js.map +1 -0
  61. package/build/hooks/use_reduce_motion.d.ts.map +1 -1
  62. package/build/hooks/use_reduce_motion.js +2 -1
  63. package/build/hooks/use_reduce_motion.js.map +1 -1
  64. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  65. package/build/screens/conversation_details_screen.js +2 -1
  66. package/build/screens/conversation_details_screen.js.map +1 -1
  67. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
  68. package/build/screens/conversation_filters/components/conversation_filters.js +88 -18
  69. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  70. package/build/screens/conversation_filters/components/rows.d.ts +9 -0
  71. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  72. package/build/screens/conversation_filters/components/rows.js +92 -1
  73. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  74. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  75. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  76. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  77. package/build/screens/conversation_filters/filter_types.d.ts +1 -0
  78. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
  79. package/build/screens/conversation_filters/filter_types.js +1 -0
  80. package/build/screens/conversation_filters/filter_types.js.map +1 -1
  81. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  82. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  83. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  84. package/build/screens/conversation_filters_screen.d.ts +1 -0
  85. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  86. package/build/screens/conversation_filters_screen.js +79 -48
  87. package/build/screens/conversation_filters_screen.js.map +1 -1
  88. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts +2 -0
  89. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -0
  90. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +587 -0
  91. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -0
  92. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  93. package/build/screens/conversations/components/list_header_component.js +95 -90
  94. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  95. package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -1
  96. package/build/screens/conversations/components/search_list_header_component.js +100 -95
  97. package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
  98. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  99. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  100. package/build/screens/conversations/conversations_screen.js +15 -2
  101. package/build/screens/conversations/conversations_screen.js.map +1 -1
  102. package/build/types/resources/person.d.ts +1 -0
  103. package/build/types/resources/person.d.ts.map +1 -1
  104. package/build/types/resources/person.js.map +1 -1
  105. package/build/utils/date.d.ts.map +1 -1
  106. package/build/utils/date.js +5 -10
  107. package/build/utils/date.js.map +1 -1
  108. package/build/utils/native_adapters/menu.d.ts +1 -0
  109. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  110. package/build/utils/native_adapters/menu.js +1 -0
  111. package/build/utils/native_adapters/menu.js.map +1 -1
  112. package/build/utils/request/conversation.d.ts +2 -1
  113. package/build/utils/request/conversation.d.ts.map +1 -1
  114. package/build/utils/request/conversation.js +4 -1
  115. package/build/utils/request/conversation.js.map +1 -1
  116. package/build/vendor/font_awesome_pro_solid_icons.d.ts +1 -0
  117. package/build/vendor/font_awesome_pro_solid_icons.d.ts.map +1 -1
  118. package/build/vendor/font_awesome_pro_solid_icons.js +5 -0
  119. package/build/vendor/font_awesome_pro_solid_icons.js.map +1 -1
  120. package/package.json +3 -3
  121. package/src/__tests__/hooks/use_can_browse_archived_conversations.test.tsx +98 -0
  122. package/src/__tests__/screens/conversation_filters/components/conversation_filters.test.tsx +132 -0
  123. package/src/__tests__/screens/conversation_filters_screen.test.tsx +76 -0
  124. package/src/__tests__/screens/conversation_new/components/services_form.test.tsx +1 -0
  125. package/src/components/conversations/__tests__/conversation_actions.test.tsx +67 -0
  126. package/src/components/conversations/__tests__/conversation_preview.test.tsx +38 -3
  127. package/src/components/conversations/__tests__/conversations.test.tsx +82 -0
  128. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +73 -0
  129. package/src/components/conversations/archived_results_footer.tsx +36 -0
  130. package/src/components/conversations/conversation_actions.tsx +27 -19
  131. package/src/components/conversations/conversation_preview.tsx +30 -10
  132. package/src/components/conversations/conversations.tsx +25 -2
  133. package/src/components/conversations/conversations_blank_state.tsx +21 -0
  134. package/src/components/conversations/filtered_results_footer.tsx +38 -0
  135. package/src/components/display/__tests__/filter_dropdown.test.tsx +9 -1
  136. package/src/components/display/filter_dropdown.tsx +14 -13
  137. package/src/components/display/icon_tile.tsx +54 -0
  138. package/src/components/display/index.ts +1 -0
  139. package/src/contexts/conversations_context.tsx +2 -1
  140. package/src/hooks/__tests__/use_conversation_archive.test.tsx +22 -0
  141. package/src/hooks/__tests__/use_jolt.test.tsx +30 -8
  142. package/src/hooks/__tests__/use_reduce_motion.test.tsx +13 -1
  143. package/src/hooks/index.ts +2 -0
  144. package/src/hooks/use_can_browse_archived_conversations.ts +17 -0
  145. package/src/hooks/use_clear_conversation_filters.ts +16 -0
  146. package/src/hooks/use_conversation.ts +4 -1
  147. package/src/hooks/use_current_person.ts +1 -0
  148. package/src/hooks/use_jolt.ts +33 -8
  149. package/src/hooks/use_manual_archiving_enabled.ts +10 -0
  150. package/src/hooks/use_reduce_motion.ts +2 -1
  151. package/src/screens/conversation_details_screen.tsx +2 -3
  152. package/src/screens/conversation_filters/__tests__/conversation_filters.test.tsx +101 -0
  153. package/src/screens/conversation_filters/components/conversation_filters.tsx +40 -7
  154. package/src/screens/conversation_filters/components/rows.tsx +29 -1
  155. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  156. package/src/screens/conversation_filters/filter_types.ts +1 -0
  157. package/src/screens/conversation_filters/screen_props.ts +1 -0
  158. package/src/screens/conversation_filters_screen.tsx +25 -5
  159. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +52 -0
  160. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +248 -0
  161. package/src/screens/conversations/components/__tests__/list_header_component.test.tsx +21 -4
  162. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +284 -0
  163. package/src/screens/conversations/components/list_header_component.tsx +17 -12
  164. package/src/screens/conversations/components/search_list_header_component.tsx +17 -12
  165. package/src/screens/conversations/conversations_screen.tsx +15 -2
  166. package/src/types/resources/person.ts +1 -0
  167. package/src/utils/__tests__/conversation.test.ts +1 -0
  168. package/src/utils/__tests__/date.test.ts +4 -0
  169. package/src/utils/date.ts +5 -4
  170. package/src/utils/native_adapters/__tests__/menu.test.tsx +6 -2
  171. package/src/utils/native_adapters/menu.tsx +2 -0
  172. package/src/utils/request/conversation.ts +4 -0
  173. package/src/vendor/font_awesome_pro_solid_icons.ts +12 -0
@@ -0,0 +1,132 @@
1
+ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
+ import { render } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { SafeAreaProvider } from 'react-native-safe-area-context'
5
+ import { useFilterDropdownEnabled } from '../../../../hooks/use_filter_dropdown_enabled'
6
+ import { ConversationFilters } from '../../../../screens/conversation_filters/components/conversation_filters'
7
+ import { FilterContext } from '../../../../screens/conversation_filters/context/conversation_filter_context'
8
+ import { FilterTypes } from '../../../../screens/conversation_filters/filter_types'
9
+ import { GroupResource } from '../../../../types/resources/group_resource'
10
+
11
+ jest.mock('react-native-gesture-handler', () => {
12
+ const { FlatList } = jest.requireActual('react-native')
13
+ const ReactActual = jest.requireActual('react')
14
+
15
+ return {
16
+ FlatList: (props: object) =>
17
+ ReactActual.createElement(FlatList, { ...props, initialNumToRender: 50 }),
18
+ }
19
+ })
20
+ jest.mock('@react-navigation/native', () => ({
21
+ ...jest.requireActual('@react-navigation/native'),
22
+ useNavigation: () => ({ navigate: jest.fn(), dispatch: jest.fn(), setParams: jest.fn() }),
23
+ }))
24
+ jest.mock('../../../../hooks/use_api_client', () => ({ useApiClient: () => ({}) }))
25
+ jest.mock('../../../../hooks/use_can_browse_archived_conversations', () => ({
26
+ useCanBrowseArchivedConversations: () => false,
27
+ }))
28
+ jest.mock('../../../../hooks/use_filter_dropdown_enabled', () => ({
29
+ useFilterDropdownEnabled: jest.fn(() => false),
30
+ }))
31
+ jest.mock('../../../../hooks/use_direct_messages_eligible', () => ({
32
+ useDirectMessagesEligible: () => true,
33
+ }))
34
+ jest.mock('../../../../hooks/use_has_direct_messages', () => ({
35
+ useHasDirectMessages: () => true,
36
+ }))
37
+ jest.mock('../../../../hooks/services/use_services_team', () => ({
38
+ useServicesTeamsMap: () => ({}),
39
+ }))
40
+ jest.mock('../../../../screens/conversation_filters/hooks/filters', () => ({
41
+ useGroupsToFilter: () => ({ groups: mockGroups }),
42
+ useTeamsToFilter: () => ({ teams: mockTeams }),
43
+ }))
44
+
45
+ const buildGroup = (id: number, name: string): GroupResource => ({
46
+ id: `Groups-Group-${id}`,
47
+ type: 'Group',
48
+ links: { self: '' },
49
+ name,
50
+ sourceAppName: 'Groups',
51
+ sourceType: 'Group',
52
+ })
53
+
54
+ const buildTeam = (id: number, name: string): GroupResource => ({
55
+ id: `Services-Team-${id}`,
56
+ type: 'Group',
57
+ links: { self: '' },
58
+ name,
59
+ sourceAppName: 'Services',
60
+ sourceType: 'Team',
61
+ })
62
+
63
+ const mockGroups = Array.from({ length: 6 }, (_, i) => buildGroup(i + 1, `Group ${i + 1}`))
64
+ const mockTeams = [buildTeam(1, 'Worship Team')]
65
+
66
+ const mockedFilterDropdownEnabled = jest.mocked(useFilterDropdownEnabled)
67
+
68
+ const safeAreaMetrics = {
69
+ frame: { x: 0, y: 0, width: 390, height: 844 },
70
+ insets: { top: 0, right: 0, bottom: 0, left: 0 },
71
+ }
72
+
73
+ const renderFilters = () =>
74
+ render(
75
+ <SafeAreaProvider initialMetrics={safeAreaMetrics}>
76
+ <ThemeProvider value={DefaultTheme}>
77
+ <FilterContext.Provider
78
+ value={{
79
+ params: {},
80
+ scrollOffset: 0,
81
+ setScrollOffset: jest.fn(),
82
+ setFormSheetHeaderHeight: jest.fn(),
83
+ resetFilter: jest.fn(),
84
+ setGroupFilter: jest.fn(),
85
+ setAppFilter: jest.fn(),
86
+ applyFilters: jest.fn(),
87
+ platformListPaddingBottom: 0,
88
+ }}
89
+ >
90
+ <ConversationFilters />
91
+ </FilterContext.Provider>
92
+ </ThemeProvider>
93
+ </SafeAreaProvider>
94
+ )
95
+
96
+ const generalFilterLabels = [
97
+ 'General Filters',
98
+ FilterTypes.All,
99
+ FilterTypes.Groups,
100
+ FilterTypes.Teams,
101
+ FilterTypes.DirectMessages,
102
+ ]
103
+
104
+ describe('ConversationFilters', () => {
105
+ beforeEach(() => {
106
+ jest.clearAllMocks()
107
+ })
108
+
109
+ it('renders the General Filters section when the filter dropdown is off', () => {
110
+ mockedFilterDropdownEnabled.mockReturnValue(false)
111
+
112
+ const { getByText } = renderFilters()
113
+
114
+ generalFilterLabels.forEach(label => expect(getByText(label)).toBeTruthy())
115
+ expect(getByText('Groups')).toBeTruthy()
116
+ expect(getByText('Teams')).toBeTruthy()
117
+ })
118
+
119
+ it('hides the General Filters section but keeps Groups and Teams when the filter dropdown is on', () => {
120
+ mockedFilterDropdownEnabled.mockReturnValue(true)
121
+
122
+ const { getByText, queryByText, getAllByText } = renderFilters()
123
+
124
+ generalFilterLabels.forEach(label => expect(queryByText(label)).toBeNull())
125
+ expect(getByText('Groups')).toBeTruthy()
126
+ expect(getByText('Teams')).toBeTruthy()
127
+ expect(getByText('Group 1')).toBeTruthy()
128
+ expect(queryByText('Group 6')).toBeNull()
129
+ expect(getByText('Worship Team')).toBeTruthy()
130
+ expect(getAllByText('View more')).toHaveLength(1)
131
+ })
132
+ })
@@ -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
+ })
@@ -9,6 +9,7 @@ import { ServicesForm } from '../../../../screens/conversation_new/components/se
9
9
  jest.mock('react-native-reanimated', () => ({
10
10
  ...jest.requireActual('react-native-reanimated/mock'),
11
11
  LayoutAnimationConfig: ({ children }: { children: React.ReactNode }) => children,
12
+ useReducedMotion: () => false,
12
13
  }))
13
14
  jest.mock('@react-navigation/native', () => ({
14
15
  ...jest.requireActual('@react-navigation/native'),
@@ -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
+ })
@@ -0,0 +1,73 @@
1
+ import { fireEvent, render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { ConversationsBlankState } from '../conversations_blank_state'
4
+
5
+ const mockSetParams = jest.fn()
6
+ const mockUseFilterDropdownEnabled = jest.fn()
7
+
8
+ jest.mock('@react-navigation/native', () => ({
9
+ ...jest.requireActual('@react-navigation/native'),
10
+ useNavigation: () => ({ setParams: mockSetParams }),
11
+ }))
12
+
13
+ jest.mock('../../../hooks/use_filter_dropdown_enabled', () => ({
14
+ useFilterDropdownEnabled: () => mockUseFilterDropdownEnabled(),
15
+ }))
16
+
17
+ describe('ConversationsBlankState', () => {
18
+ beforeEach(() => {
19
+ mockSetParams.mockClear()
20
+ })
21
+
22
+ describe('with the filter dropdown on', () => {
23
+ beforeEach(() => {
24
+ mockUseFilterDropdownEnabled.mockReturnValue(true)
25
+ })
26
+
27
+ it('offers to view all conversations when a filter is applied', () => {
28
+ const view = render(
29
+ <ConversationsBlankState isFilterApplied={true} canCreateConversations={true} />
30
+ )
31
+
32
+ expect(view.getByText('No matches')).toBeTruthy()
33
+ expect(view.queryByText('No conversations yet.')).toBeNull()
34
+
35
+ fireEvent.press(view.getByText('View all conversations'))
36
+
37
+ expect(mockSetParams).toHaveBeenCalledTimes(1)
38
+ expect(mockSetParams.mock.calls[0][0]).toStrictEqual({
39
+ title: undefined,
40
+ chat_group_graph_id: undefined,
41
+ group_source_app_name: undefined,
42
+ show_direct_messages: undefined,
43
+ show_archived: undefined,
44
+ })
45
+ })
46
+
47
+ it('keeps the compose prompt when no filter is applied', () => {
48
+ const view = render(
49
+ <ConversationsBlankState isFilterApplied={false} canCreateConversations={true} />
50
+ )
51
+
52
+ expect(view.getByText('No conversations yet.')).toBeTruthy()
53
+ expect(view.getByText('Tap the compose button to get started.')).toBeTruthy()
54
+ expect(view.queryByText('View all conversations')).toBeNull()
55
+ })
56
+ })
57
+
58
+ describe('with the filter dropdown off', () => {
59
+ beforeEach(() => {
60
+ mockUseFilterDropdownEnabled.mockReturnValue(false)
61
+ })
62
+
63
+ it('keeps the adjust-your-filters copy when a filter is applied', () => {
64
+ const view = render(
65
+ <ConversationsBlankState isFilterApplied={true} canCreateConversations={true} />
66
+ )
67
+
68
+ expect(view.getByText('No conversations yet.')).toBeTruthy()
69
+ expect(view.getByText('Adjust your filters to find conversations.')).toBeTruthy()
70
+ expect(view.queryByText('View all conversations')).toBeNull()
71
+ })
72
+ })
73
+ })
@@ -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: {