@planningcenter/chat-react-native 3.37.0-rc.4 → 3.37.1-qa-736.0
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/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +19 -7
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/message_list.d.ts +10 -0
- package/build/components/conversation/message_list.d.ts.map +1 -0
- package/build/components/conversation/message_list.js +13 -0
- package/build/components/conversation/message_list.js.map +1 -0
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +6 -16
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/components/conversations/conversations_blank_state.d.ts +8 -0
- package/build/components/conversations/conversations_blank_state.d.ts.map +1 -0
- package/build/components/conversations/conversations_blank_state.js +25 -0
- package/build/components/conversations/conversations_blank_state.js.map +1 -0
- package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
- package/build/hooks/use_attachment_uploader.js +9 -0
- package/build/hooks/use_attachment_uploader.js.map +1 -1
- package/build/hooks/use_features.d.ts +1 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +1 -0
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_upload_client.d.ts +4 -0
- package/build/hooks/use_upload_client.d.ts.map +1 -1
- package/build/hooks/use_upload_client.js +13 -1
- package/build/hooks/use_upload_client.js.map +1 -1
- package/build/jest.js +1 -1
- package/build/jest.js.map +1 -1
- package/build/screens/age_check/age_check_underage_screen.js +1 -1
- package/build/screens/age_check/age_check_underage_screen.js.map +1 -1
- package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -1
- package/build/screens/avatar_picker/emoji_tab.js +2 -6
- package/build/screens/avatar_picker/emoji_tab.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +3 -7
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/types/jolt_events/attachment_events.d.ts +14 -0
- package/build/types/jolt_events/attachment_events.d.ts.map +1 -0
- package/build/types/jolt_events/attachment_events.js +2 -0
- package/build/types/jolt_events/attachment_events.js.map +1 -0
- package/build/types/jolt_events/index.d.ts +3 -1
- package/build/types/jolt_events/index.d.ts.map +1 -1
- package/build/types/jolt_events/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/hooks/use_attachment_uploader.test.tsx +36 -0
- package/src/__tests__/jest.ts +1 -1
- package/src/components/conversation/__tests__/message_list.test.tsx +14 -0
- package/src/components/conversation/message_form.tsx +21 -7
- package/src/components/conversation/message_list.tsx +42 -0
- package/src/components/conversations/conversations.tsx +9 -16
- package/src/components/conversations/conversations_blank_state.tsx +42 -0
- package/src/hooks/use_attachment_uploader.ts +14 -1
- package/src/hooks/use_features.ts +1 -0
- package/src/hooks/use_upload_client.ts +19 -1
- package/src/jest.ts +1 -1
- package/src/screens/age_check/age_check_underage_screen.tsx +1 -1
- package/src/screens/avatar_picker/emoji_tab.tsx +2 -6
- package/src/screens/conversation_screen.tsx +5 -14
- package/src/types/jolt_events/attachment_events.ts +14 -0
- package/src/types/jolt_events/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.37.
|
|
3
|
+
"version": "3.37.1-qa-736.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "./src/index.tsx",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
|
28
28
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
|
29
|
+
"@planningcenter/emoji-keyboard": "3.37.1-qa-736.0",
|
|
29
30
|
"lodash-inflection": "^1.5.0",
|
|
30
|
-
"react-compiler-runtime": "^1.0.0"
|
|
31
|
-
"rn-emoji-keyboard": "1.7.0"
|
|
31
|
+
"react-compiler-runtime": "^1.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@planningcenter/datetime-fmt": ">=2.0.0",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"react-native-url-polyfill": "^2.0.0",
|
|
73
73
|
"typescript": "~5.9.2"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "85e2929d9a7469bd305a1d534ecb401647f3d39f"
|
|
76
76
|
}
|
|
@@ -11,6 +11,16 @@ import { FileAttachment } from '../../types/resources/denormalized_attachment_re
|
|
|
11
11
|
jest.mock('../../hooks/use_api_client')
|
|
12
12
|
jest.mock('../../hooks/use_upload_client')
|
|
13
13
|
jest.mock('../../hooks/use_chat_configuration')
|
|
14
|
+
jest.mock('../../hooks/use_api', () => ({
|
|
15
|
+
useApiGet: jest.fn().mockReturnValue({ data: { id: 1 } }),
|
|
16
|
+
}))
|
|
17
|
+
const joltEventCallbacks: Record<string, (e: unknown) => void> = {}
|
|
18
|
+
jest.mock('../../hooks/use_jolt', () => ({
|
|
19
|
+
useJoltChannel: jest.fn().mockReturnValue('channel-stub'),
|
|
20
|
+
useJoltEvent: jest.fn((_channel, eventName, callback) => {
|
|
21
|
+
joltEventCallbacks[eventName] = callback
|
|
22
|
+
}),
|
|
23
|
+
}))
|
|
14
24
|
|
|
15
25
|
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
16
26
|
const mockedUseUploadClient = useUploadClient as jest.MockedFunction<typeof useUploadClient>
|
|
@@ -217,4 +227,30 @@ describe('useAttachmentUploader', () => {
|
|
|
217
227
|
expect(result.current.flaggedAttachmentCount).toBe(1)
|
|
218
228
|
})
|
|
219
229
|
})
|
|
230
|
+
|
|
231
|
+
describe('attachment.flagged jolt event', () => {
|
|
232
|
+
it('flips the matching attachment to flagged + error when the event fires', () => {
|
|
233
|
+
const { result } = renderUploader({
|
|
234
|
+
draftAttachments: [draftAttachment('clip.mp4')],
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
act(() => {
|
|
238
|
+
joltEventCallbacks['attachment.flagged']({
|
|
239
|
+
event: 'attachment.flagged',
|
|
240
|
+
data: {
|
|
241
|
+
data: {
|
|
242
|
+
attachment_id: 'att-clip.mp4',
|
|
243
|
+
conversation_id: 1,
|
|
244
|
+
content_type: 'video/mp4',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const flagged = result.current.attachments.find(a => a.id === 'att-clip.mp4')
|
|
251
|
+
expect(flagged?.flagged).toBe(true)
|
|
252
|
+
expect(flagged?.status).toBe('error')
|
|
253
|
+
expect(result.current.flaggedAttachmentCount).toBe(1)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
220
256
|
})
|
package/src/__tests__/jest.ts
CHANGED
|
@@ -4,8 +4,8 @@ describe('jestTransformPackages', () => {
|
|
|
4
4
|
it('exports an array of package patterns', () => {
|
|
5
5
|
expect(jestTransformPackages).toEqual([
|
|
6
6
|
'@planningcenter/chat-react-native',
|
|
7
|
+
'@planningcenter/emoji-keyboard',
|
|
7
8
|
'@fortawesome',
|
|
8
|
-
'rn-emoji-keyboard',
|
|
9
9
|
])
|
|
10
10
|
})
|
|
11
11
|
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { render } from '@testing-library/react-native'
|
|
2
|
+
import { createRef } from 'react'
|
|
3
|
+
import { FlatList } from 'react-native-gesture-handler'
|
|
4
|
+
import { MessageList } from '../message_list'
|
|
5
|
+
|
|
6
|
+
describe('MessageList', () => {
|
|
7
|
+
it('renders with the gesture-handler FlatList so overscroll cannot drag an enclosing sheet', () => {
|
|
8
|
+
const listRef = createRef<FlatList>()
|
|
9
|
+
|
|
10
|
+
const screen = render(<MessageList listRef={listRef} data={[]} renderItem={() => null} />)
|
|
11
|
+
|
|
12
|
+
expect(screen.UNSAFE_getByType(FlatList)).toBeTruthy()
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -31,6 +31,7 @@ import { ConversationResource, MessageResource } from '../../types'
|
|
|
31
31
|
import {
|
|
32
32
|
DenormalizedAttachmentResourceForCreate,
|
|
33
33
|
DenormalizedMessageAttachmentResourceForCreate,
|
|
34
|
+
FileAttachment,
|
|
34
35
|
} from '../../types/resources/denormalized_attachment_resource_for_create'
|
|
35
36
|
import {
|
|
36
37
|
MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
|
|
@@ -340,15 +341,28 @@ function MessageFormAttachments() {
|
|
|
340
341
|
)
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
function getFileType(attachments: FileAttachment[]): 'image' | 'video' | 'file' {
|
|
345
|
+
const kinds = new Set(
|
|
346
|
+
attachments.map(({ file }) => {
|
|
347
|
+
if (file.type.startsWith('image/')) return 'image'
|
|
348
|
+
if (file.type.startsWith('video/')) return 'video'
|
|
349
|
+
return 'file'
|
|
350
|
+
})
|
|
351
|
+
)
|
|
352
|
+
return kinds.size === 1 ? [...kinds][0] : 'file'
|
|
353
|
+
}
|
|
354
|
+
|
|
343
355
|
function FlaggedContentBanner() {
|
|
344
356
|
const styles = useMessageFormStyles()
|
|
345
357
|
const { attachmentUploader } = React.useContext(MessageFormContext)
|
|
346
|
-
const
|
|
358
|
+
const flaggedAttachments = attachmentUploader?.attachments.filter(a => a.flagged) ?? []
|
|
359
|
+
const flaggedCount = flaggedAttachments.length
|
|
347
360
|
|
|
348
361
|
if (flaggedCount === 0) return null
|
|
349
362
|
|
|
350
363
|
const isSingular = flaggedCount === 1
|
|
351
|
-
const
|
|
364
|
+
const fileType = getFileType(flaggedAttachments)
|
|
365
|
+
const buttonTitle = isSingular ? `Remove flagged ${fileType}` : `Remove flagged ${fileType}s`
|
|
352
366
|
|
|
353
367
|
return (
|
|
354
368
|
<View style={styles.flaggedBannerContainer}>
|
|
@@ -356,7 +370,7 @@ function FlaggedContentBanner() {
|
|
|
356
370
|
<BannerPrimitive.StaticLayout>
|
|
357
371
|
<BannerPrimitive.StatusIcon />
|
|
358
372
|
<BannerPrimitive.Content>
|
|
359
|
-
<BannerMessage isSingular={isSingular} />
|
|
373
|
+
<BannerMessage isSingular={isSingular} fileType={fileType} />
|
|
360
374
|
<View style={styles.flaggedBannerButtonRow}>
|
|
361
375
|
<Button
|
|
362
376
|
title={buttonTitle}
|
|
@@ -373,7 +387,7 @@ function FlaggedContentBanner() {
|
|
|
373
387
|
)
|
|
374
388
|
}
|
|
375
389
|
|
|
376
|
-
function BannerMessage({ isSingular }: { isSingular: boolean }) {
|
|
390
|
+
function BannerMessage({ isSingular, fileType }: { isSingular: boolean; fileType: string }) {
|
|
377
391
|
const styles = useMessageFormStyles()
|
|
378
392
|
|
|
379
393
|
const contentGuidelinesLink = (
|
|
@@ -392,13 +406,13 @@ function BannerMessage({ isSingular }: { isSingular: boolean }) {
|
|
|
392
406
|
|
|
393
407
|
return isSingular ? (
|
|
394
408
|
<Text style={styles.flaggedBannerText}>
|
|
395
|
-
An uploaded
|
|
409
|
+
An uploaded {fileType} was flagged because it doesn't meet our {contentGuidelinesLink}. Please
|
|
396
410
|
remove before proceeding.
|
|
397
411
|
</Text>
|
|
398
412
|
) : (
|
|
399
413
|
<Text style={styles.flaggedBannerText}>
|
|
400
|
-
Some uploaded
|
|
401
|
-
remove them before proceeding.
|
|
414
|
+
Some uploaded {fileType}s were flagged because they don't meet our {contentGuidelinesLink}.
|
|
415
|
+
Please remove them before proceeding.
|
|
402
416
|
</Text>
|
|
403
417
|
)
|
|
404
418
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { RefObject } from 'react'
|
|
2
|
+
import { StyleSheet, type FlatListProps } from 'react-native'
|
|
3
|
+
import { FlatList } from 'react-native-gesture-handler'
|
|
4
|
+
import type { EnrichedMessage } from '../../utils/group_messages'
|
|
5
|
+
|
|
6
|
+
const extractItemKey = (item: EnrichedMessage) => String(item.id)
|
|
7
|
+
const maintainVisibleContentPosition = { minIndexForVisible: 0 }
|
|
8
|
+
|
|
9
|
+
type MessageListProps = Pick<
|
|
10
|
+
FlatListProps<EnrichedMessage>,
|
|
11
|
+
| 'data'
|
|
12
|
+
| 'renderItem'
|
|
13
|
+
| 'onScroll'
|
|
14
|
+
| 'onScrollBeginDrag'
|
|
15
|
+
| 'viewabilityConfigCallbackPairs'
|
|
16
|
+
| 'onContentSizeChange'
|
|
17
|
+
| 'onScrollToIndexFailed'
|
|
18
|
+
| 'onEndReached'
|
|
19
|
+
| 'ListHeaderComponent'
|
|
20
|
+
> & {
|
|
21
|
+
listRef: RefObject<FlatList | null>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function MessageList({ listRef, ...props }: MessageListProps) {
|
|
25
|
+
return (
|
|
26
|
+
<FlatList
|
|
27
|
+
inverted
|
|
28
|
+
ref={listRef}
|
|
29
|
+
contentContainerStyle={styles.listContainer}
|
|
30
|
+
maintainVisibleContentPosition={maintainVisibleContentPosition}
|
|
31
|
+
keyExtractor={extractItemKey}
|
|
32
|
+
scrollEventThrottle={64}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const styles = StyleSheet.create({
|
|
39
|
+
listContainer: {
|
|
40
|
+
paddingVertical: 12,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
@@ -3,11 +3,12 @@ import React, { useMemo } from 'react'
|
|
|
3
3
|
import { FlatList, StyleSheet, View } from 'react-native'
|
|
4
4
|
import { useConversationsContext } from '../../contexts/conversations_context'
|
|
5
5
|
import { useTheme } from '../../hooks'
|
|
6
|
+
import { useCanCreateConversations } from '../../hooks/use_chat_permissions'
|
|
6
7
|
import { useConversationsJoltEvents } from '../../hooks/use_conversations_jolt_events'
|
|
7
8
|
import { ConversationResource } from '../../types'
|
|
8
9
|
import { throwResponseError } from '../../utils/response_error'
|
|
9
|
-
import BlankState from '../primitive/blank_state_primitive'
|
|
10
10
|
import { ConversationPreview, ConversationPreviewSkeleton } from './conversation_preview'
|
|
11
|
+
import { ConversationsBlankState } from './conversations_blank_state'
|
|
11
12
|
|
|
12
13
|
interface ConversationsProps {
|
|
13
14
|
ListHeaderComponent?:
|
|
@@ -28,9 +29,11 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
|
|
|
28
29
|
isFetched,
|
|
29
30
|
isError,
|
|
30
31
|
error,
|
|
31
|
-
args: { chat_group_graph_id },
|
|
32
|
+
args: { chat_group_graph_id, group_source_app_name },
|
|
32
33
|
} = useConversationsContext()
|
|
33
34
|
const navigation = useNavigation()
|
|
35
|
+
const canCreateConversations = useCanCreateConversations()
|
|
36
|
+
const isFilterApplied = !!chat_group_graph_id || !!group_source_app_name
|
|
34
37
|
|
|
35
38
|
const showBadges = !chat_group_graph_id
|
|
36
39
|
|
|
@@ -61,14 +64,10 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
|
|
|
61
64
|
refreshing={!isFetched && isRefetching}
|
|
62
65
|
ListHeaderComponent={ListHeaderComponent}
|
|
63
66
|
ListEmptyComponent={
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<BlankState.Heading>No conversations</BlankState.Heading>
|
|
69
|
-
</BlankState.Content>
|
|
70
|
-
</BlankState.Root>
|
|
71
|
-
</View>
|
|
67
|
+
<ConversationsBlankState
|
|
68
|
+
isFilterApplied={isFilterApplied}
|
|
69
|
+
canCreateConversations={canCreateConversations}
|
|
70
|
+
/>
|
|
72
71
|
}
|
|
73
72
|
renderItem={({ item }) => {
|
|
74
73
|
if (item.type === 'loading') {
|
|
@@ -104,12 +103,6 @@ const useStyles = () => {
|
|
|
104
103
|
container: { flex: 1 },
|
|
105
104
|
contentContainer: { paddingVertical: 16 },
|
|
106
105
|
listItem: { color: colors.fillColorNeutral020 },
|
|
107
|
-
listEmpty: {
|
|
108
|
-
flex: 1,
|
|
109
|
-
justifyContent: 'center',
|
|
110
|
-
alignItems: 'center',
|
|
111
|
-
paddingVertical: 32,
|
|
112
|
-
},
|
|
113
106
|
})
|
|
114
107
|
}
|
|
115
108
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { StyleSheet, View } from 'react-native'
|
|
3
|
+
import BlankState from '../primitive/blank_state_primitive'
|
|
4
|
+
|
|
5
|
+
interface ConversationsBlankStateProps {
|
|
6
|
+
isFilterApplied: boolean
|
|
7
|
+
canCreateConversations: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ConversationsBlankState({
|
|
11
|
+
isFilterApplied,
|
|
12
|
+
canCreateConversations,
|
|
13
|
+
}: ConversationsBlankStateProps) {
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.container}>
|
|
16
|
+
<BlankState.Root>
|
|
17
|
+
<BlankState.Imagery name="general.outlinedTextMessage" />
|
|
18
|
+
<BlankState.Content>
|
|
19
|
+
<BlankState.Heading>No conversations yet.</BlankState.Heading>
|
|
20
|
+
{isFilterApplied ? (
|
|
21
|
+
<BlankState.Text>Adjust your filters to find conversations.</BlankState.Text>
|
|
22
|
+
) : canCreateConversations ? (
|
|
23
|
+
<BlankState.Text>Tap the compose button to get started.</BlankState.Text>
|
|
24
|
+
) : (
|
|
25
|
+
<BlankState.Text>
|
|
26
|
+
When your groups or teams start chatting, you’ll find them here.
|
|
27
|
+
</BlankState.Text>
|
|
28
|
+
)}
|
|
29
|
+
</BlankState.Content>
|
|
30
|
+
</BlankState.Root>
|
|
31
|
+
</View>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
container: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
paddingVertical: 32,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { ApiResource } from '../types'
|
|
2
|
+
import { ApiResource, CurrentPersonResource } from '../types'
|
|
3
|
+
import { AttachmentFlaggedEvent } from '../types/jolt_events/attachment_events'
|
|
3
4
|
import {
|
|
4
5
|
FileAttachment,
|
|
5
6
|
FileUploadState,
|
|
6
7
|
NativeAttachmentFile,
|
|
7
8
|
} from '../types/resources/denormalized_attachment_resource_for_create'
|
|
9
|
+
import { useApiGet } from './use_api'
|
|
8
10
|
import { useApiClient } from './use_api_client'
|
|
9
11
|
import { useChatConfiguration } from './use_chat_configuration'
|
|
12
|
+
import { currentPersonRequestArgs } from './use_current_person'
|
|
13
|
+
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
10
14
|
import { useUploadClient } from './use_upload_client'
|
|
11
15
|
|
|
12
16
|
export interface FileError {
|
|
@@ -32,6 +36,15 @@ export function useAttachmentUploader({
|
|
|
32
36
|
const numberOfAttachments = attachments.length
|
|
33
37
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
34
38
|
|
|
39
|
+
const { data: currentPerson } = useApiGet<CurrentPersonResource>(currentPersonRequestArgs)
|
|
40
|
+
const joltChannel = useJoltChannel(`chat.people.${currentPerson?.id}`, Boolean(currentPerson?.id))
|
|
41
|
+
useJoltEvent(joltChannel, 'attachment.flagged', (e: AttachmentFlaggedEvent) => {
|
|
42
|
+
const flaggedId = e.data.data.attachment_id
|
|
43
|
+
setAttachments(prev =>
|
|
44
|
+
prev.map(a => (a.id === flaggedId ? { ...a, flagged: true, status: 'error' } : a))
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
35
48
|
const handleFilesAttached = useCallback(
|
|
36
49
|
(files: NativeAttachmentFile[]) => {
|
|
37
50
|
const fileErrors = {} as FileError
|
|
@@ -42,6 +42,7 @@ export const availableFeatures = {
|
|
|
42
42
|
custom_conversation_avatars: 'ROLLOUT_custom_conversation_avatars',
|
|
43
43
|
jump_to_unread: 'ROLLOUT_jump_to_unread',
|
|
44
44
|
conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
|
|
45
|
+
video_moderation: 'ROLLOUT_MOBILE_video_moderation',
|
|
45
46
|
} as const satisfies Record<string, `ROLLOUT_${string}`>
|
|
46
47
|
|
|
47
48
|
const stableEmptyFeatures: ApiCollection<FeatureResource> = {
|
|
@@ -2,6 +2,7 @@ import { useContext, useMemo } from 'react'
|
|
|
2
2
|
import { ChatContext } from '../contexts/chat_context'
|
|
3
3
|
import { Client } from '../utils'
|
|
4
4
|
import { UploadUri } from '../utils/upload_uri'
|
|
5
|
+
import { availableFeatures, useFeatures } from './use_features'
|
|
5
6
|
|
|
6
7
|
export interface FileForUploadClient {
|
|
7
8
|
uri: string
|
|
@@ -11,6 +12,8 @@ export interface FileForUploadClient {
|
|
|
11
12
|
|
|
12
13
|
export const useUploadClient = () => {
|
|
13
14
|
const { session, onUnauthorizedResponse } = useContext(ChatContext)
|
|
15
|
+
const { featureEnabled } = useFeatures()
|
|
16
|
+
const videoModerationEnabled = featureEnabled(availableFeatures.video_moderation)
|
|
14
17
|
|
|
15
18
|
const uri = useMemo(() => new UploadUri({ session }), [session])
|
|
16
19
|
|
|
@@ -21,14 +24,25 @@ export const useUploadClient = () => {
|
|
|
21
24
|
root: uri.baseUrl,
|
|
22
25
|
defaultHeaders: uri.headers,
|
|
23
26
|
onUnauthorizedResponse,
|
|
27
|
+
videoModerationEnabled,
|
|
24
28
|
}),
|
|
25
|
-
[uri, onUnauthorizedResponse]
|
|
29
|
+
[uri, onUnauthorizedResponse, videoModerationEnabled]
|
|
26
30
|
)
|
|
27
31
|
|
|
28
32
|
return api
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
class UploadClient extends Client {
|
|
36
|
+
private videoModerationEnabled: boolean
|
|
37
|
+
|
|
38
|
+
constructor({
|
|
39
|
+
videoModerationEnabled,
|
|
40
|
+
...rest
|
|
41
|
+
}: ConstructorParameters<typeof Client>[0] & { videoModerationEnabled: boolean }) {
|
|
42
|
+
super(rest)
|
|
43
|
+
this.videoModerationEnabled = videoModerationEnabled
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
async uploadFile(file: FileForUploadClient): Promise<UploadedResource> {
|
|
33
47
|
const formData = new FormData()
|
|
34
48
|
formData.append('file', {
|
|
@@ -40,6 +54,10 @@ class UploadClient extends Client {
|
|
|
40
54
|
const headers: RequestInit['headers'] = { ...this.headers }
|
|
41
55
|
delete headers['Content-Type']
|
|
42
56
|
|
|
57
|
+
if (this.videoModerationEnabled && file.type.startsWith('video/')) {
|
|
58
|
+
headers['X-PCO-Moderate-Video'] = 'true'
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
const response = await fetch(`${this.root}/v2/files`, {
|
|
44
62
|
method: 'POST',
|
|
45
63
|
headers,
|
package/src/jest.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function AgeCheckUnderageScreen({ contactEmail }: AgeCheckUnderageScreenP
|
|
|
31
31
|
|
|
32
32
|
<View style={styles.content}>
|
|
33
33
|
<Heading variant="h3" style={styles.baseText}>
|
|
34
|
-
|
|
34
|
+
Chat is only available for users 13 and older.
|
|
35
35
|
</Heading>
|
|
36
36
|
<Text variant="tertiary" style={styles.baseText}>
|
|
37
37
|
If you submitted the wrong birthdate by accident,{` `}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
+
import { EmojiKeyboard, emojisByCategory, type EmojiType } from '@planningcenter/emoji-keyboard'
|
|
1
2
|
import React, { useCallback } from 'react'
|
|
2
3
|
import { StyleSheet, View } from 'react-native'
|
|
3
|
-
import { EmojiKeyboard, type EmojiType, type EmojisByCategory } from 'rn-emoji-keyboard'
|
|
4
|
-
// rn-emoji-keyboard exposes no public exclusion API, so we reach into its
|
|
5
|
-
// internal src/ tree for the emoji JSON. Version is pinned in package.json
|
|
6
|
-
// — verify this path still resolves before bumping rn-emoji-keyboard.
|
|
7
|
-
import emojiData from 'rn-emoji-keyboard/src/assets/emojis.json'
|
|
8
4
|
import { useTheme } from '../../hooks'
|
|
9
5
|
|
|
10
6
|
const BLOCKED_EMOJIS = new Set(['🖕'])
|
|
11
7
|
|
|
12
|
-
const filteredEmojis =
|
|
8
|
+
const filteredEmojis = emojisByCategory.map(category => ({
|
|
13
9
|
...category,
|
|
14
10
|
data: category.data.filter(e => !BLOCKED_EMOJIS.has(e.emoji)),
|
|
15
11
|
}))
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
useRoute,
|
|
10
10
|
} from '@react-navigation/native'
|
|
11
11
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
-
import { ActivityIndicator,
|
|
12
|
+
import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'
|
|
13
|
+
import type { FlatList } from 'react-native-gesture-handler'
|
|
13
14
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
|
14
15
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
15
16
|
import { Badge, Icon, Text } from '../components'
|
|
@@ -17,6 +18,7 @@ import { EmptyConversationBlankState } from '../components/conversation/empty_co
|
|
|
17
18
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
18
19
|
import { Message } from '../components/conversation/message'
|
|
19
20
|
import { MessageForm } from '../components/conversation/message_form'
|
|
21
|
+
import { MessageList } from '../components/conversation/message_list'
|
|
20
22
|
import {
|
|
21
23
|
ConversationDisabledBanner,
|
|
22
24
|
LeaderMessagesDisabledBanner,
|
|
@@ -85,9 +87,6 @@ export type ConversationRouteProps = {
|
|
|
85
87
|
|
|
86
88
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
87
89
|
|
|
88
|
-
const extractItemKey = (item: EnrichedMessage) => String(item.id)
|
|
89
|
-
const maintainVisibleContentPosition = { minIndexForVisible: 0 }
|
|
90
|
-
|
|
91
90
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
92
91
|
const { conversation_id, message_id, reply_root_id } = route.params
|
|
93
92
|
|
|
@@ -300,16 +299,11 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
300
299
|
{noMessages ? (
|
|
301
300
|
<EmptyConversationBlankState />
|
|
302
301
|
) : (
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
ref={listRef}
|
|
306
|
-
contentContainerStyle={styles.listContainer}
|
|
307
|
-
maintainVisibleContentPosition={maintainVisibleContentPosition}
|
|
302
|
+
<MessageList
|
|
303
|
+
listRef={listRef}
|
|
308
304
|
data={items}
|
|
309
|
-
keyExtractor={extractItemKey}
|
|
310
305
|
onScroll={onScroll}
|
|
311
306
|
onScrollBeginDrag={onScrollBeginDrag}
|
|
312
|
-
scrollEventThrottle={64}
|
|
313
307
|
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
|
|
314
308
|
onContentSizeChange={onContentSizeChange}
|
|
315
309
|
onScrollToIndexFailed={onScrollToIndexFailed}
|
|
@@ -499,9 +493,6 @@ const useStyles = () => {
|
|
|
499
493
|
backgroundColor: navigationTheme.colors.card,
|
|
500
494
|
paddingBottom: bottom,
|
|
501
495
|
},
|
|
502
|
-
listContainer: {
|
|
503
|
-
paddingVertical: 12,
|
|
504
|
-
},
|
|
505
496
|
listHeader: {
|
|
506
497
|
// Just whitespace to provide space where the typing indicator can be
|
|
507
498
|
height: 16,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
|
|
2
|
+
|
|
3
|
+
interface AttachmentFlaggedEventData extends Record<string, unknown> {
|
|
4
|
+
data: {
|
|
5
|
+
attachment_id: string
|
|
6
|
+
conversation_id: number
|
|
7
|
+
content_type: string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AttachmentFlaggedEvent extends CustomMessage {
|
|
12
|
+
event: 'attachment.flagged'
|
|
13
|
+
data: AttachmentFlaggedEventData
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AttachmentFlaggedEvent } from './attachment_events'
|
|
1
2
|
import type {
|
|
2
3
|
ConversationCreatedEvent,
|
|
3
4
|
ConversationDeletedEvent,
|
|
@@ -20,12 +21,14 @@ export type JoltConversationEvent =
|
|
|
20
21
|
export type JoltMessageEvent = MessageCreatedEvent | MessageUpdatedEvent | MessageDeletedEvent
|
|
21
22
|
export type JoltReactionEvent = ReactionCreatedEvent | ReactionDeletedEvent
|
|
22
23
|
export type JoltTypingEvent = TypingBroadcastEvent
|
|
24
|
+
export type JoltAttachmentEvent = AttachmentFlaggedEvent
|
|
23
25
|
|
|
24
26
|
export type CustomJoltEvent =
|
|
25
27
|
| JoltConversationEvent
|
|
26
28
|
| JoltMessageEvent
|
|
27
29
|
| JoltReactionEvent
|
|
28
30
|
| JoltTypingEvent
|
|
31
|
+
| JoltAttachmentEvent
|
|
29
32
|
|
|
30
33
|
export type JoltEventName = CustomJoltEvent['event'] | 'STREAM_USER_UPDATED'
|
|
31
34
|
export type JoltSubscriptionPattern = JoltEventName | 'reaction.*'
|