@planningcenter/chat-react-native 4.4.1-qa-staging.12 → 4.4.1-qa-staging.14
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.
- package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
- package/build/components/conversation/jump_to_bottom_button.js +42 -40
- package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
- package/build/components/conversation/message.d.ts +2 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +3 -1
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +35 -31
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/components/conversation/message_reaction.d.ts +2 -1
- package/build/components/conversation/message_reaction.d.ts.map +1 -1
- package/build/components/conversation/message_reaction.js +67 -60
- package/build/components/conversation/message_reaction.js.map +1 -1
- package/build/components/conversation/messages_disabled_banners.d.ts +7 -5
- package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
- package/build/components/conversation/messages_disabled_banners.js +15 -14
- package/build/components/conversation/messages_disabled_banners.js.map +1 -1
- package/build/components/conversation/system_message.d.ts +2 -1
- package/build/components/conversation/system_message.d.ts.map +1 -1
- package/build/components/conversation/system_message.js +87 -65
- package/build/components/conversation/system_message.js.map +1 -1
- package/build/components/display/action_button.d.ts.map +1 -1
- package/build/components/display/action_button.js +18 -16
- package/build/components/display/action_button.js.map +1 -1
- package/build/components/display/filter_dropdown.d.ts +13 -0
- package/build/components/display/filter_dropdown.d.ts.map +1 -0
- package/build/components/display/filter_dropdown.js +188 -0
- package/build/components/display/filter_dropdown.js.map +1 -0
- package/build/components/display/index.d.ts +1 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +1 -0
- package/build/components/display/index.js.map +1 -1
- package/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_animated_message_background_color.d.ts.map +1 -1
- package/build/hooks/use_animated_message_background_color.js +5 -3
- package/build/hooks/use_animated_message_background_color.js.map +1 -1
- package/build/hooks/use_conversation.d.ts +73 -1
- package/build/hooks/use_conversation.d.ts.map +1 -1
- package/build/hooks/use_conversation.js +122 -3
- package/build/hooks/use_conversation.js.map +1 -1
- package/build/hooks/use_reduce_motion.d.ts +2 -0
- package/build/hooks/use_reduce_motion.d.ts.map +1 -0
- package/build/hooks/use_reduce_motion.js +27 -0
- package/build/hooks/use_reduce_motion.js.map +1 -0
- package/build/hooks/use_unarchive_conversation_action.d.ts +7 -0
- package/build/hooks/use_unarchive_conversation_action.d.ts.map +1 -0
- package/build/hooks/use_unarchive_conversation_action.js +52 -0
- package/build/hooks/use_unarchive_conversation_action.js.map +1 -0
- package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
- package/build/screens/attachment_actions/attachment_actions_screen.js +84 -61
- package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +487 -393
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +12 -9
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/design_system_screen.d.ts.map +1 -1
- package/build/screens/design_system_screen.js +275 -215
- package/build/screens/design_system_screen.js.map +1 -1
- package/build/screens/message_actions_screen.d.ts.map +1 -1
- package/build/screens/message_actions_screen.js +112 -92
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/types/resources/member_ability.d.ts +2 -0
- package/build/types/resources/member_ability.d.ts.map +1 -1
- package/build/types/resources/member_ability.js.map +1 -1
- package/build/utils/archived_conversation.d.ts +5 -0
- package/build/utils/archived_conversation.d.ts.map +1 -0
- package/build/utils/archived_conversation.js +5 -0
- package/build/utils/archived_conversation.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/native_adapters/configuration.d.ts +3 -0
- package/build/utils/native_adapters/configuration.d.ts.map +1 -1
- package/build/utils/native_adapters/configuration.js +3 -0
- package/build/utils/native_adapters/configuration.js.map +1 -1
- package/build/utils/native_adapters/index.d.ts +1 -0
- package/build/utils/native_adapters/index.d.ts.map +1 -1
- package/build/utils/native_adapters/index.js +1 -0
- package/build/utils/native_adapters/index.js.map +1 -1
- package/build/utils/native_adapters/menu.d.ts +40 -0
- package/build/utils/native_adapters/menu.d.ts.map +1 -0
- package/build/utils/native_adapters/menu.js +55 -0
- package/build/utils/native_adapters/menu.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/screens/message_actions_screen.test.tsx +65 -2
- package/src/components/conversation/__tests__/message_list.test.tsx +68 -2
- package/src/components/conversation/__tests__/system_message.test.tsx +3 -1
- package/src/components/conversation/jump_to_bottom_button.tsx +4 -3
- package/src/components/conversation/message.tsx +3 -0
- package/src/components/conversation/message_list.tsx +9 -1
- package/src/components/conversation/message_reaction.tsx +7 -2
- package/src/components/conversation/messages_disabled_banners.tsx +17 -18
- package/src/components/conversation/system_message.tsx +24 -10
- package/src/components/display/__tests__/filter_dropdown.test.tsx +106 -0
- package/src/components/display/action_button.tsx +4 -3
- package/src/components/display/filter_dropdown.tsx +123 -0
- package/src/components/display/index.ts +1 -0
- package/src/hooks/__tests__/use_conversation_archive.test.tsx +130 -0
- package/src/hooks/__tests__/use_conversation_jolt_events.test.tsx +74 -0
- package/src/hooks/__tests__/use_reduce_motion.test.tsx +49 -0
- package/src/hooks/__tests__/use_unarchive_conversation_action.test.tsx +67 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use_animated_message_background_color.ts +6 -3
- package/src/hooks/use_conversation.ts +41 -3
- package/src/hooks/use_reduce_motion.ts +18 -0
- package/src/hooks/use_unarchive_conversation_action.ts +24 -0
- package/src/screens/__tests__/conversation_details_screen.test.tsx +289 -0
- package/src/screens/attachment_actions/__tests__/attachment_actions_screen.test.tsx +72 -0
- package/src/screens/attachment_actions/attachment_actions_screen.tsx +3 -1
- package/src/screens/conversation_details_screen.tsx +78 -14
- package/src/screens/conversation_screen.tsx +3 -1
- package/src/screens/design_system_screen.tsx +26 -0
- package/src/screens/message_actions_screen.tsx +29 -20
- package/src/types/resources/member_ability.ts +2 -0
- package/src/utils/archived_conversation.ts +13 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/native_adapters/__tests__/menu.test.tsx +42 -0
- package/src/utils/native_adapters/configuration.ts +5 -0
- package/src/utils/native_adapters/index.ts +1 -0
- package/src/utils/native_adapters/menu.tsx +68 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
+
import { Alert } from 'react-native'
|
|
3
4
|
import { ApiResource, ConversationResource } from '../types'
|
|
4
5
|
import { transformGetToPost } from '../utils/client/request_helpers'
|
|
5
6
|
import { Haptic } from '../utils/native_adapters'
|
|
6
7
|
import { getConversationsRequestArgs } from '../utils/request/conversation'
|
|
7
8
|
import { useApiClient } from './use_api_client'
|
|
8
|
-
import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
|
|
9
|
+
import { getRequestQueryKey, SuspenseGetQueryOptions, useSuspenseGet } from './use_suspense_api'
|
|
9
10
|
|
|
10
11
|
export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: number }) => ({
|
|
11
12
|
url: `/me/conversations/${conversation_id}`,
|
|
@@ -49,6 +50,8 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
|
|
|
49
50
|
'can_reply',
|
|
50
51
|
'can_delete_non_authored_messages',
|
|
51
52
|
'can_message_available_members',
|
|
53
|
+
'can_archive',
|
|
54
|
+
'can_unarchive',
|
|
52
55
|
],
|
|
53
56
|
ConversationMembership: [
|
|
54
57
|
'last_read_message_sort_key',
|
|
@@ -70,10 +73,13 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
|
|
|
70
73
|
},
|
|
71
74
|
})
|
|
72
75
|
|
|
73
|
-
export const useConversation = (
|
|
76
|
+
export const useConversation = (
|
|
77
|
+
{ conversation_id }: { conversation_id: number },
|
|
78
|
+
options?: SuspenseGetQueryOptions<ConversationResource>
|
|
79
|
+
) => {
|
|
74
80
|
const args = getConversationRequestArgs({ conversation_id })
|
|
75
81
|
|
|
76
|
-
return useSuspenseGet<ConversationResource>(args)
|
|
82
|
+
return useSuspenseGet<ConversationResource>(args, options)
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export const useConversationMute = ({ conversation_id }: { conversation_id: number }) => {
|
|
@@ -179,6 +185,38 @@ export const useConversationDelete = ({ conversation_id }: { conversation_id: nu
|
|
|
179
185
|
})
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
export const useConversationArchive = ({ conversation_id }: { conversation_id: number }) => {
|
|
189
|
+
const apiClient = useApiClient()
|
|
190
|
+
const queryClient = useQueryClient()
|
|
191
|
+
const conversationQueryKey = getRequestQueryKey(getConversationRequestArgs({ conversation_id }))
|
|
192
|
+
const { url } = getConversationsRequestArgs()
|
|
193
|
+
|
|
194
|
+
const { mutate: setArchived, ...mutation } = useMutation({
|
|
195
|
+
mutationKey: ['archiveConversation'],
|
|
196
|
+
mutationFn: async (archived: boolean) => {
|
|
197
|
+
const action = archived ? 'archive' : 'unarchive'
|
|
198
|
+
|
|
199
|
+
return apiClient.chat.post({
|
|
200
|
+
url: `/me/conversations/${conversation_id}/${action}`,
|
|
201
|
+
data: { data: { type: 'Conversation', attributes: {} } },
|
|
202
|
+
})
|
|
203
|
+
},
|
|
204
|
+
onSuccess: () => {
|
|
205
|
+
Haptic.notificationSuccess()
|
|
206
|
+
queryClient.invalidateQueries({ queryKey: [url] })
|
|
207
|
+
return queryClient.invalidateQueries({ queryKey: conversationQueryKey })
|
|
208
|
+
},
|
|
209
|
+
onError: (_error, archived) => {
|
|
210
|
+
Alert.alert(
|
|
211
|
+
'Oops',
|
|
212
|
+
`Couldn't ${archived ? 'archive' : 'unarchive'} this conversation. Please try again.`
|
|
213
|
+
)
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return { setArchived, ...mutation }
|
|
218
|
+
}
|
|
219
|
+
|
|
182
220
|
export const useConversationDisableReplies = ({ conversation_id }: { conversation_id: number }) => {
|
|
183
221
|
const apiClient = useApiClient()
|
|
184
222
|
const queryClient = useQueryClient()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { AccessibilityInfo } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export const useReduceMotion = () => {
|
|
5
|
+
const [reduceMotion, setReduceMotion] = useState(false)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
|
9
|
+
|
|
10
|
+
const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion)
|
|
11
|
+
|
|
12
|
+
return () => {
|
|
13
|
+
subscription.remove()
|
|
14
|
+
}
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
return reduceMotion
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ConversationResource } from '../types'
|
|
2
|
+
import { isArchivedConversation } from '../utils/archived_conversation'
|
|
3
|
+
import { useConversationArchive } from './use_conversation'
|
|
4
|
+
import { availableFeatures, useFeatures } from './use_features'
|
|
5
|
+
|
|
6
|
+
export function useUnarchiveConversationAction(conversation: ConversationResource) {
|
|
7
|
+
const { featureEnabled } = useFeatures()
|
|
8
|
+
const { setArchived, isPending } = useConversationArchive({ conversation_id: conversation.id })
|
|
9
|
+
const manualArchivingEnabled =
|
|
10
|
+
featureEnabled(availableFeatures.manual_archiving) ||
|
|
11
|
+
featureEnabled(availableFeatures.manual_archiving_rollout)
|
|
12
|
+
const canUnarchive =
|
|
13
|
+
manualArchivingEnabled &&
|
|
14
|
+
isArchivedConversation(conversation) &&
|
|
15
|
+
!!conversation.memberAbility?.canUnarchive
|
|
16
|
+
|
|
17
|
+
if (!canUnarchive) return undefined
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
label: 'Unarchive',
|
|
21
|
+
onPress: () => setArchived(false),
|
|
22
|
+
loading: isPending,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
|
2
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { Alert } from 'react-native'
|
|
6
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
7
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
8
|
+
import type { ConversationResource } from '../../types'
|
|
9
|
+
import { ConversationDetailsScreen } from '../conversation_details_screen'
|
|
10
|
+
|
|
11
|
+
const baseConversation = {
|
|
12
|
+
type: 'Conversation',
|
|
13
|
+
id: 7,
|
|
14
|
+
title: 'Youth Group',
|
|
15
|
+
muted: false,
|
|
16
|
+
repliesDisabled: false,
|
|
17
|
+
unreadCount: 0,
|
|
18
|
+
unreadReaction: false,
|
|
19
|
+
conversationPreview: null,
|
|
20
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
21
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
22
|
+
badges: [],
|
|
23
|
+
groups: [],
|
|
24
|
+
disabled: false,
|
|
25
|
+
disabledReason: null,
|
|
26
|
+
disabledMessage: null,
|
|
27
|
+
memberAbility: {
|
|
28
|
+
type: 'MemberAbility',
|
|
29
|
+
canUpdate: true,
|
|
30
|
+
canDelete: true,
|
|
31
|
+
canDeleteNonAuthoredMessages: true,
|
|
32
|
+
leader: true,
|
|
33
|
+
canReply: true,
|
|
34
|
+
canMessageAvailableMembers: false,
|
|
35
|
+
},
|
|
36
|
+
conversationMembership: { notificationLevelDescription: 'All messages' },
|
|
37
|
+
} as unknown as ConversationResource
|
|
38
|
+
|
|
39
|
+
const archivedConversation = {
|
|
40
|
+
...baseConversation,
|
|
41
|
+
disabled: true,
|
|
42
|
+
disabledReason: 'archived',
|
|
43
|
+
disabledMessage: 'This conversation is archived. Only you and other leaders can unarchive.',
|
|
44
|
+
memberAbility: { ...baseConversation.memberAbility, canUpdate: false },
|
|
45
|
+
} as ConversationResource
|
|
46
|
+
|
|
47
|
+
const mockNavigation = {
|
|
48
|
+
navigate: jest.fn(),
|
|
49
|
+
dispatch: jest.fn(),
|
|
50
|
+
goBack: jest.fn(),
|
|
51
|
+
setOptions: jest.fn(),
|
|
52
|
+
}
|
|
53
|
+
jest.mock('@react-navigation/native', () => ({
|
|
54
|
+
...jest.requireActual('@react-navigation/native'),
|
|
55
|
+
useNavigation: () => mockNavigation,
|
|
56
|
+
}))
|
|
57
|
+
jest.mock('../../hooks/use_api_client', () => ({ useApiClient: () => ({}) }))
|
|
58
|
+
|
|
59
|
+
let mockConversation: ConversationResource
|
|
60
|
+
const mockArchive = jest.fn()
|
|
61
|
+
const mockSaveTitle = jest.fn()
|
|
62
|
+
jest.mock('../../hooks/use_conversation', () => ({
|
|
63
|
+
useConversation: () => ({ data: mockConversation }),
|
|
64
|
+
useConversationArchive: () => ({ setArchived: mockArchive, isPending: false }),
|
|
65
|
+
useConversationDelete: () => ({ mutate: jest.fn() }),
|
|
66
|
+
useConversationDisableReplies: () => ({ repliesDisabled: false, setRepliesDisabled: jest.fn() }),
|
|
67
|
+
useConversationMute: () => ({ muted: false, setMuted: jest.fn() }),
|
|
68
|
+
useConversationUpdate: () => ({ mutate: mockSaveTitle }),
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
let mockEnabledFeatures: string[] = []
|
|
72
|
+
jest.mock('../../hooks/use_features', () => ({
|
|
73
|
+
...jest.requireActual('../../hooks/use_features'),
|
|
74
|
+
useFeatures: () => ({
|
|
75
|
+
features: [],
|
|
76
|
+
featureEnabled: (name: string) => mockEnabledFeatures.includes(name),
|
|
77
|
+
}),
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
jest.mock('../../hooks/use_suspense_api', () => ({
|
|
81
|
+
...jest.requireActual('../../hooks/use_suspense_api'),
|
|
82
|
+
useSuspensePaginator: () => ({ data: [], fetchNextPage: jest.fn(), isFetchingNextPage: false }),
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
let mockPressDone = () => {}
|
|
86
|
+
jest.mock('../../components/display/platform_modal_header_buttons', () => ({
|
|
87
|
+
...jest.requireActual('../../components/display/platform_modal_header_buttons'),
|
|
88
|
+
useHeaderRight: ({ onPress }: { onPress: () => void }) => {
|
|
89
|
+
mockPressDone = onPress
|
|
90
|
+
},
|
|
91
|
+
}))
|
|
92
|
+
jest.mock('../../components/display/conversation_avatar', () => ({
|
|
93
|
+
ConversationAvatar: () => null,
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
const buildConversation = (canArchive: boolean) =>
|
|
97
|
+
({
|
|
98
|
+
type: 'Conversation',
|
|
99
|
+
id: 7,
|
|
100
|
+
title: 'Youth Group',
|
|
101
|
+
memberAbility: { type: 'MemberAbility', canArchive, canDelete: false },
|
|
102
|
+
groups: [],
|
|
103
|
+
badges: [],
|
|
104
|
+
}) as unknown as ConversationResource
|
|
105
|
+
|
|
106
|
+
const safeAreaMetrics = {
|
|
107
|
+
frame: { x: 0, y: 0, width: 390, height: 844 },
|
|
108
|
+
insets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const renderScreen = () =>
|
|
112
|
+
render(
|
|
113
|
+
<QueryClientProvider client={buildTestQueryClient()}>
|
|
114
|
+
<SafeAreaProvider initialMetrics={safeAreaMetrics}>
|
|
115
|
+
<ThemeProvider value={DefaultTheme}>
|
|
116
|
+
<ConversationDetailsScreen route={{ params: { conversation_id: 7 } }} />
|
|
117
|
+
</ThemeProvider>
|
|
118
|
+
</SafeAreaProvider>
|
|
119
|
+
</QueryClientProvider>
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const alertButton = (text: string) => {
|
|
123
|
+
const buttons = jest.mocked(Alert.alert).mock.calls[0][2] ?? []
|
|
124
|
+
return buttons.find(button => button.text === text)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe('ConversationDetailsScreen', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
mockConversation = baseConversation
|
|
130
|
+
mockEnabledFeatures = []
|
|
131
|
+
jest.clearAllMocks()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('for an active conversation', () => {
|
|
135
|
+
it('offers mute and the leader settings', () => {
|
|
136
|
+
mockEnabledFeatures = ['ROLLOUT_custom_conversation_avatars']
|
|
137
|
+
const { getByText } = renderScreen()
|
|
138
|
+
|
|
139
|
+
expect(getByText('Mute')).toBeTruthy()
|
|
140
|
+
expect(getByText('Only leaders can message')).toBeTruthy()
|
|
141
|
+
expect(getByText('Update avatar')).toBeTruthy()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('offers notification levels when granular notifications are on', () => {
|
|
145
|
+
mockEnabledFeatures = ['ROLLOUT_granular_notification_preferences_ui']
|
|
146
|
+
const { getByText } = renderScreen()
|
|
147
|
+
|
|
148
|
+
expect(getByText('Notifications')).toBeTruthy()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('saves the title on done', () => {
|
|
152
|
+
renderScreen()
|
|
153
|
+
mockPressDone()
|
|
154
|
+
|
|
155
|
+
expect(mockSaveTitle).toHaveBeenCalledWith({ title: 'Youth Group' })
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('for an archived conversation', () => {
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
mockConversation = archivedConversation
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('replaces mute and the leader settings with the archived notice', () => {
|
|
165
|
+
mockEnabledFeatures = ['ROLLOUT_custom_conversation_avatars']
|
|
166
|
+
const { getByText, queryByText } = renderScreen()
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
getByText('This conversation is archived. Only you and other leaders can unarchive.')
|
|
170
|
+
).toBeTruthy()
|
|
171
|
+
expect(queryByText('Mute')).toBeNull()
|
|
172
|
+
expect(queryByText('Only leaders can message')).toBeNull()
|
|
173
|
+
expect(queryByText('Update avatar')).toBeNull()
|
|
174
|
+
expect(getByText('Delete conversation…')).toBeTruthy()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('falls back to a default notice when the server sends no message', () => {
|
|
178
|
+
mockConversation = { ...archivedConversation, disabledMessage: null }
|
|
179
|
+
const { getByText } = renderScreen()
|
|
180
|
+
|
|
181
|
+
expect(getByText('This conversation is archived.')).toBeTruthy()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('hides notification levels even when granular notifications are on', () => {
|
|
185
|
+
mockEnabledFeatures = ['ROLLOUT_granular_notification_preferences_ui']
|
|
186
|
+
const { queryByText } = renderScreen()
|
|
187
|
+
|
|
188
|
+
expect(queryByText('Notifications')).toBeNull()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('locks the title', () => {
|
|
192
|
+
const { getByDisplayValue } = renderScreen()
|
|
193
|
+
|
|
194
|
+
expect(getByDisplayValue('Youth Group').props.editable).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does not save the title on done', () => {
|
|
198
|
+
renderScreen()
|
|
199
|
+
mockPressDone()
|
|
200
|
+
|
|
201
|
+
expect(mockSaveTitle).not.toHaveBeenCalled()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('unarchives from the notice for someone who can unarchive', () => {
|
|
205
|
+
mockEnabledFeatures = ['ROLLOUT_MOBILE_manual_archiving']
|
|
206
|
+
mockConversation = {
|
|
207
|
+
...archivedConversation,
|
|
208
|
+
memberAbility: { ...archivedConversation.memberAbility!, canUnarchive: true },
|
|
209
|
+
}
|
|
210
|
+
const { getByText } = renderScreen()
|
|
211
|
+
|
|
212
|
+
fireEvent.press(getByText('Unarchive'))
|
|
213
|
+
|
|
214
|
+
expect(mockArchive).toHaveBeenCalledWith(false)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('ConversationDetailsScreen archive', () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
jest.clearAllMocks()
|
|
222
|
+
jest.spyOn(Alert, 'alert').mockImplementation(() => {})
|
|
223
|
+
mockConversation = buildConversation(true)
|
|
224
|
+
mockEnabledFeatures = ['ROLLOUT_MOBILE_manual_archiving']
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
jest.restoreAllMocks()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('hides archiving when the member cannot archive', () => {
|
|
232
|
+
mockConversation = buildConversation(false)
|
|
233
|
+
const { queryByText } = renderScreen()
|
|
234
|
+
|
|
235
|
+
expect(queryByText('Archive conversation…')).toBeNull()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it.each(['QA_MOBILE_manual_archiving', 'ROLLOUT_MOBILE_manual_archiving'])(
|
|
239
|
+
'offers archiving with only %s on',
|
|
240
|
+
flag => {
|
|
241
|
+
mockEnabledFeatures = [flag]
|
|
242
|
+
const { getByText } = renderScreen()
|
|
243
|
+
|
|
244
|
+
expect(getByText('Archive conversation…')).toBeTruthy()
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
it('hides archiving when neither mobile flag is on', () => {
|
|
249
|
+
mockEnabledFeatures = ['ROLLOUT_WEB_manual_archiving']
|
|
250
|
+
const { queryByText } = renderScreen()
|
|
251
|
+
|
|
252
|
+
expect(queryByText('Archive conversation…')).toBeNull()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('asks for confirmation before archiving', () => {
|
|
256
|
+
const { getByText } = renderScreen()
|
|
257
|
+
|
|
258
|
+
fireEvent.press(getByText('Archive conversation…'))
|
|
259
|
+
|
|
260
|
+
expect(Alert.alert).toHaveBeenCalledWith(
|
|
261
|
+
'Archive conversation',
|
|
262
|
+
'This conversation will be archived for everyone. Only leaders can access and unarchive conversations.',
|
|
263
|
+
expect.any(Array)
|
|
264
|
+
)
|
|
265
|
+
expect(mockArchive).not.toHaveBeenCalled()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('archives once confirmed and stays on the details', () => {
|
|
269
|
+
const { getByText } = renderScreen()
|
|
270
|
+
fireEvent.press(getByText('Archive conversation…'))
|
|
271
|
+
|
|
272
|
+
alertButton('Archive')?.onPress?.()
|
|
273
|
+
|
|
274
|
+
expect(mockArchive).toHaveBeenCalledTimes(1)
|
|
275
|
+
expect(mockArchive).toHaveBeenCalledWith(true)
|
|
276
|
+
expect(mockNavigation.dispatch).not.toHaveBeenCalled()
|
|
277
|
+
expect(mockNavigation.goBack).not.toHaveBeenCalled()
|
|
278
|
+
expect(mockNavigation.navigate).not.toHaveBeenCalled()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('does nothing when the confirmation is cancelled', () => {
|
|
282
|
+
const { getByText } = renderScreen()
|
|
283
|
+
fireEvent.press(getByText('Archive conversation…'))
|
|
284
|
+
|
|
285
|
+
alertButton('Cancel')?.onPress?.()
|
|
286
|
+
|
|
287
|
+
expect(mockArchive).not.toHaveBeenCalled()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { HeaderHeightContext } from '@react-navigation/elements'
|
|
2
|
+
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
|
3
|
+
import { render } from '@testing-library/react-native'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
6
|
+
import type { ConversationResource } from '../../../types'
|
|
7
|
+
import { AttachmentActionsScreen, AttachmentActionsScreenProps } from '../attachment_actions_screen'
|
|
8
|
+
|
|
9
|
+
let mockConversation = { id: 1, disabled: false } as ConversationResource
|
|
10
|
+
|
|
11
|
+
jest.mock('@react-navigation/native', () => ({
|
|
12
|
+
...jest.requireActual('@react-navigation/native'),
|
|
13
|
+
useNavigation: () => ({ goBack: jest.fn() }),
|
|
14
|
+
}))
|
|
15
|
+
jest.mock('../../../hooks/use_conversation', () => ({
|
|
16
|
+
useConversation: () => ({ data: mockConversation }),
|
|
17
|
+
}))
|
|
18
|
+
jest.mock('../hooks/useDeleteAttachment', () => ({
|
|
19
|
+
useDeleteAttachment: () => ({ handleDeleteAttachment: jest.fn() }),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
const safeAreaMetrics = {
|
|
23
|
+
frame: { x: 0, y: 0, width: 390, height: 844 },
|
|
24
|
+
insets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const renderScreen = (params: Partial<AttachmentActionsScreenProps['route']['params']> = {}) => {
|
|
28
|
+
const route = {
|
|
29
|
+
key: 'AttachmentActions',
|
|
30
|
+
name: 'AttachmentActions',
|
|
31
|
+
params: {
|
|
32
|
+
attachmentId: 'a1',
|
|
33
|
+
attachmentContentType: 'image/png',
|
|
34
|
+
attachmentUrl: 'https://example.com/a1.png',
|
|
35
|
+
conversation_id: 1,
|
|
36
|
+
canDeleteNonAuthoredMessages: false,
|
|
37
|
+
myMessage: true,
|
|
38
|
+
...params,
|
|
39
|
+
},
|
|
40
|
+
} as AttachmentActionsScreenProps['route']
|
|
41
|
+
|
|
42
|
+
return render(
|
|
43
|
+
<SafeAreaProvider initialMetrics={safeAreaMetrics}>
|
|
44
|
+
<ThemeProvider value={DefaultTheme}>
|
|
45
|
+
<HeaderHeightContext.Provider value={0}>
|
|
46
|
+
<AttachmentActionsScreen route={route} />
|
|
47
|
+
</HeaderHeightContext.Provider>
|
|
48
|
+
</ThemeProvider>
|
|
49
|
+
</SafeAreaProvider>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('AttachmentActionsScreen', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockConversation = { id: 1, disabled: false } as ConversationResource
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('offers Delete on my own attachment', () => {
|
|
59
|
+
const { getByText } = renderScreen()
|
|
60
|
+
|
|
61
|
+
expect(getByText('Delete image')).toBeTruthy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('hides Delete when the conversation is disabled', () => {
|
|
65
|
+
mockConversation = { id: 1, disabled: true, disabledReason: 'locked' } as ConversationResource
|
|
66
|
+
|
|
67
|
+
const { queryByText, getByText } = renderScreen({ canDeleteNonAuthoredMessages: true })
|
|
68
|
+
|
|
69
|
+
expect(queryByText('Delete image')).toBeNull()
|
|
70
|
+
expect(getByText('Open in browser')).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -2,6 +2,7 @@ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
|
2
2
|
import React, { useCallback } from 'react'
|
|
3
3
|
import { Alert, Linking, StyleSheet } from 'react-native'
|
|
4
4
|
import FormSheet, { getFormSheetScreenOptions } from '../../components/primitive/form_sheet'
|
|
5
|
+
import { useConversation } from '../../hooks/use_conversation'
|
|
5
6
|
import { useDeleteAttachment } from './hooks/useDeleteAttachment'
|
|
6
7
|
|
|
7
8
|
export const AttachmentActionsScreenOptions = getFormSheetScreenOptions({
|
|
@@ -28,8 +29,9 @@ export function AttachmentActionsScreen({ route }: AttachmentActionsScreenProps)
|
|
|
28
29
|
myMessage,
|
|
29
30
|
} = route.params
|
|
30
31
|
|
|
32
|
+
const { data: conversation } = useConversation({ conversation_id }, { refetchOnMount: false })
|
|
31
33
|
const attachmentName = getAttachmentLabelName(attachmentContentType)
|
|
32
|
-
const canDelete = myMessage || canDeleteNonAuthoredMessages
|
|
34
|
+
const canDelete = !conversation.disabled && (myMessage || canDeleteNonAuthoredMessages)
|
|
33
35
|
const styles = useStyles()
|
|
34
36
|
|
|
35
37
|
const { handleDeleteAttachment } = useDeleteAttachment({
|
|
@@ -43,6 +43,7 @@ import { ButtonAppearanceUnion } from '../components/display/utils/button_colors
|
|
|
43
43
|
import { useSuspensePaginator, useTheme } from '../hooks'
|
|
44
44
|
import {
|
|
45
45
|
useConversation,
|
|
46
|
+
useConversationArchive,
|
|
46
47
|
useConversationDelete,
|
|
47
48
|
useConversationDisableReplies,
|
|
48
49
|
useConversationMute,
|
|
@@ -50,9 +51,11 @@ import {
|
|
|
50
51
|
} from '../hooks/use_conversation'
|
|
51
52
|
import { availableFeatures, useFeatures } from '../hooks/use_features'
|
|
52
53
|
import { usePreviewAvatarDiameter } from '../hooks/use_preview_avatar_diameter'
|
|
54
|
+
import { useUnarchiveConversationAction } from '../hooks/use_unarchive_conversation_action'
|
|
53
55
|
import { type ConversationResource, MemberResource, isDefined } from '../types'
|
|
54
56
|
import { GroupResource } from '../types/resources/group_resource'
|
|
55
57
|
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../utils'
|
|
58
|
+
import { archivedConversationMessage, isArchivedConversation } from '../utils/archived_conversation'
|
|
56
59
|
import { genderDisplayLabel } from '../utils/gender_display_label'
|
|
57
60
|
import { tokens } from '../vendor/tapestry/tokens'
|
|
58
61
|
// =========================================
|
|
@@ -105,9 +108,14 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
105
108
|
const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params)
|
|
106
109
|
const { mutate: saveTitle } = useConversationUpdate(route.params)
|
|
107
110
|
const { mutate: deleteConversation } = useConversationDelete(route.params)
|
|
111
|
+
const { setArchived, isPending: isArchiving } = useConversationArchive(route.params)
|
|
112
|
+
const unarchiveAction = useUnarchiveConversationAction(conversation)
|
|
108
113
|
const { featureEnabled } = useFeatures()
|
|
109
114
|
const granularNotificationsEnabled = featureEnabled(availableFeatures.granular_notifications_ui)
|
|
110
115
|
const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars)
|
|
116
|
+
const manualArchivingEnabled =
|
|
117
|
+
featureEnabled(availableFeatures.manual_archiving) ||
|
|
118
|
+
featureEnabled(availableFeatures.manual_archiving_rollout)
|
|
111
119
|
const showGenderFilter =
|
|
112
120
|
featureEnabled(availableFeatures.gender_specific_conversations) && !!conversation.genderOption
|
|
113
121
|
const genderLabel = conversation.genderOption ? genderDisplayLabel(conversation.genderOption) : ''
|
|
@@ -118,8 +126,10 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
118
126
|
|
|
119
127
|
const notificationLevelDescription =
|
|
120
128
|
conversation?.conversationMembership?.notificationLevelDescription
|
|
129
|
+
const archived = isArchivedConversation(conversation)
|
|
121
130
|
const canUpdate = conversation.memberAbility?.canUpdate || false
|
|
122
131
|
const canDelete = conversation.memberAbility?.canDelete || false
|
|
132
|
+
const canArchive = manualArchivingEnabled && (conversation.memberAbility?.canArchive || false)
|
|
123
133
|
const isLeader = conversation.memberAbility?.leader || false
|
|
124
134
|
const isDisabled = conversation.disabled ?? false
|
|
125
135
|
const showAdultRequirementNotice = isDisabled && conversation.disabledReason === 'safety_check'
|
|
@@ -203,6 +213,23 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
203
213
|
)
|
|
204
214
|
}, [deleteConversation, navigation, title])
|
|
205
215
|
|
|
216
|
+
const handleArchive = useCallback(() => {
|
|
217
|
+
Alert.alert(
|
|
218
|
+
'Archive conversation',
|
|
219
|
+
'This conversation will be archived for everyone. Only leaders can access and unarchive conversations.',
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
text: 'Cancel',
|
|
223
|
+
style: 'cancel',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
text: 'Archive',
|
|
227
|
+
onPress: () => setArchived(true),
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
}, [setArchived])
|
|
232
|
+
|
|
206
233
|
const HeaderTitle = useCallback(
|
|
207
234
|
(props: HeaderTitleProps) => {
|
|
208
235
|
return (
|
|
@@ -226,7 +253,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
226
253
|
icon: 'general.check',
|
|
227
254
|
accessibilityLabel: 'Done',
|
|
228
255
|
onPress: () => {
|
|
229
|
-
saveTitle({ title: trimmedTitle || conversation.title })
|
|
256
|
+
if (canUpdate) saveTitle({ title: trimmedTitle || conversation.title })
|
|
230
257
|
navigation.goBack()
|
|
231
258
|
},
|
|
232
259
|
})
|
|
@@ -275,25 +302,47 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
275
302
|
showBottomBorder: true,
|
|
276
303
|
sectionInnerStyle: styles.sectionInnerHeaderWithBottomBorder,
|
|
277
304
|
},
|
|
278
|
-
|
|
305
|
+
archived
|
|
279
306
|
? {
|
|
280
|
-
type: SectionTypes.
|
|
307
|
+
type: SectionTypes.view,
|
|
281
308
|
data: {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
309
|
+
children: (
|
|
310
|
+
<>
|
|
311
|
+
<Text variant="tertiary">{archivedConversationMessage(conversation)}</Text>
|
|
312
|
+
{unarchiveAction && (
|
|
313
|
+
<Button
|
|
314
|
+
variant="fill"
|
|
315
|
+
size="lg"
|
|
316
|
+
title={unarchiveAction.label}
|
|
317
|
+
onPress={unarchiveAction.onPress}
|
|
318
|
+
loading={unarchiveAction.loading}
|
|
319
|
+
style={styles.unarchiveButton}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
</>
|
|
323
|
+
),
|
|
286
324
|
},
|
|
287
325
|
showBottomBorder: true,
|
|
288
326
|
}
|
|
289
|
-
:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
327
|
+
: granularNotificationsEnabled
|
|
328
|
+
? {
|
|
329
|
+
type: SectionTypes.navigableSetting,
|
|
330
|
+
data: {
|
|
331
|
+
title: 'Notifications',
|
|
332
|
+
subtitle: notificationLevelDescription,
|
|
333
|
+
onPress: () =>
|
|
334
|
+
navigation.navigate('ConversationNotificationLevelSelect', { conversation_id }),
|
|
335
|
+
},
|
|
336
|
+
showBottomBorder: true,
|
|
337
|
+
}
|
|
338
|
+
: {
|
|
339
|
+
type: SectionTypes.setting,
|
|
340
|
+
data: {
|
|
341
|
+
title: 'Mute',
|
|
342
|
+
rightItem: <Switch value={muted} onValueChange={value => setMuted(value)} />,
|
|
343
|
+
},
|
|
344
|
+
showBottomBorder: true,
|
|
294
345
|
},
|
|
295
|
-
showBottomBorder: true,
|
|
296
|
-
},
|
|
297
346
|
{
|
|
298
347
|
type: canUpdate ? SectionTypes.setting : SectionTypes.hidden,
|
|
299
348
|
data: {
|
|
@@ -304,6 +353,15 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
304
353
|
},
|
|
305
354
|
showBottomBorder: true,
|
|
306
355
|
},
|
|
356
|
+
{
|
|
357
|
+
type: canArchive ? SectionTypes.setting : SectionTypes.hidden,
|
|
358
|
+
data: {
|
|
359
|
+
title: 'Archive conversation…',
|
|
360
|
+
disabled: isArchiving,
|
|
361
|
+
onPress: handleArchive,
|
|
362
|
+
},
|
|
363
|
+
showBottomBorder: canDelete,
|
|
364
|
+
},
|
|
307
365
|
{
|
|
308
366
|
type: canDelete ? SectionTypes.setting : SectionTypes.hidden,
|
|
309
367
|
data: {
|
|
@@ -557,6 +615,7 @@ interface SettingRowProps {
|
|
|
557
615
|
subtitle?: string
|
|
558
616
|
rightItemStyle?: ViewStyle
|
|
559
617
|
appearance?: ButtonAppearanceUnion
|
|
618
|
+
disabled?: boolean
|
|
560
619
|
onPress?: () => void
|
|
561
620
|
}
|
|
562
621
|
|
|
@@ -569,6 +628,7 @@ function SettingRow({
|
|
|
569
628
|
rightItem,
|
|
570
629
|
rightItemStyle = {},
|
|
571
630
|
appearance,
|
|
631
|
+
disabled,
|
|
572
632
|
onPress,
|
|
573
633
|
}: SettingRowProps) {
|
|
574
634
|
const styles = useStyles()
|
|
@@ -579,6 +639,7 @@ function SettingRow({
|
|
|
579
639
|
style={[styles.settingRow, style]}
|
|
580
640
|
textStyle={buttonTextStyle}
|
|
581
641
|
appearance={appearance}
|
|
642
|
+
disabled={disabled}
|
|
582
643
|
onPress={onPress}
|
|
583
644
|
>
|
|
584
645
|
{title}
|
|
@@ -778,6 +839,9 @@ const useStyles = ({ isStart, isEnd }: { isStart?: boolean; isEnd?: boolean } =
|
|
|
778
839
|
deleteButtonText: {
|
|
779
840
|
fontWeight: 'normal',
|
|
780
841
|
},
|
|
842
|
+
unarchiveButton: {
|
|
843
|
+
marginTop: 12,
|
|
844
|
+
},
|
|
781
845
|
settingRowText: {
|
|
782
846
|
lineHeight: 20,
|
|
783
847
|
},
|