@planningcenter/chat-react-native 3.37.0 → 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.
Files changed (59) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +19 -7
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/components/conversation/message_list.d.ts +10 -0
  5. package/build/components/conversation/message_list.d.ts.map +1 -0
  6. package/build/components/conversation/message_list.js +13 -0
  7. package/build/components/conversation/message_list.js.map +1 -0
  8. package/build/components/conversations/conversations.d.ts.map +1 -1
  9. package/build/components/conversations/conversations.js +6 -16
  10. package/build/components/conversations/conversations.js.map +1 -1
  11. package/build/components/conversations/conversations_blank_state.d.ts +8 -0
  12. package/build/components/conversations/conversations_blank_state.d.ts.map +1 -0
  13. package/build/components/conversations/conversations_blank_state.js +25 -0
  14. package/build/components/conversations/conversations_blank_state.js.map +1 -0
  15. package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
  16. package/build/hooks/use_attachment_uploader.js +9 -0
  17. package/build/hooks/use_attachment_uploader.js.map +1 -1
  18. package/build/hooks/use_features.d.ts +1 -0
  19. package/build/hooks/use_features.d.ts.map +1 -1
  20. package/build/hooks/use_features.js +1 -0
  21. package/build/hooks/use_features.js.map +1 -1
  22. package/build/hooks/use_upload_client.d.ts +4 -0
  23. package/build/hooks/use_upload_client.d.ts.map +1 -1
  24. package/build/hooks/use_upload_client.js +13 -1
  25. package/build/hooks/use_upload_client.js.map +1 -1
  26. package/build/jest.js +1 -1
  27. package/build/jest.js.map +1 -1
  28. package/build/screens/age_check/age_check_underage_screen.js +1 -1
  29. package/build/screens/age_check/age_check_underage_screen.js.map +1 -1
  30. package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -1
  31. package/build/screens/avatar_picker/emoji_tab.js +2 -6
  32. package/build/screens/avatar_picker/emoji_tab.js.map +1 -1
  33. package/build/screens/conversation_screen.d.ts.map +1 -1
  34. package/build/screens/conversation_screen.js +3 -7
  35. package/build/screens/conversation_screen.js.map +1 -1
  36. package/build/types/jolt_events/attachment_events.d.ts +14 -0
  37. package/build/types/jolt_events/attachment_events.d.ts.map +1 -0
  38. package/build/types/jolt_events/attachment_events.js +2 -0
  39. package/build/types/jolt_events/attachment_events.js.map +1 -0
  40. package/build/types/jolt_events/index.d.ts +3 -1
  41. package/build/types/jolt_events/index.d.ts.map +1 -1
  42. package/build/types/jolt_events/index.js.map +1 -1
  43. package/package.json +4 -4
  44. package/src/__tests__/hooks/use_attachment_uploader.test.tsx +36 -0
  45. package/src/__tests__/jest.ts +1 -1
  46. package/src/components/conversation/__tests__/message_list.test.tsx +14 -0
  47. package/src/components/conversation/message_form.tsx +21 -7
  48. package/src/components/conversation/message_list.tsx +42 -0
  49. package/src/components/conversations/conversations.tsx +9 -16
  50. package/src/components/conversations/conversations_blank_state.tsx +42 -0
  51. package/src/hooks/use_attachment_uploader.ts +14 -1
  52. package/src/hooks/use_features.ts +1 -0
  53. package/src/hooks/use_upload_client.ts +19 -1
  54. package/src/jest.ts +1 -1
  55. package/src/screens/age_check/age_check_underage_screen.tsx +1 -1
  56. package/src/screens/avatar_picker/emoji_tab.tsx +2 -6
  57. package/src/screens/conversation_screen.tsx +5 -14
  58. package/src/types/jolt_events/attachment_events.ts +14 -0
  59. 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.0",
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": "92b7794327766bd515c3f0b0f4da647b9c06207f"
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
  })
@@ -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 flaggedCount = attachmentUploader?.flaggedAttachmentCount || 0
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 buttonTitle = isSingular ? 'Remove flagged image' : 'Remove flagged images'
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 image was flagged because it doesn't meet our {contentGuidelinesLink}. Please
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 images were flagged because they don't meet our {contentGuidelinesLink}. Please
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
- <View style={styles.listEmpty}>
65
- <BlankState.Root>
66
- <BlankState.Imagery name="general.outlinedTextMessage" />
67
- <BlankState.Content>
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&rsquo;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
@@ -16,6 +16,6 @@
16
16
  */
17
17
  export const jestTransformPackages = [
18
18
  '@planningcenter/chat-react-native',
19
+ '@planningcenter/emoji-keyboard',
19
20
  '@fortawesome',
20
- 'rn-emoji-keyboard',
21
21
  ]
@@ -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
- Your age does not meet the minimum safety requirements to use chat.
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 = (emojiData as EmojisByCategory[]).map(category => ({
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, FlatList, Platform, StyleSheet, View } from 'react-native'
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
- <FlatList
304
- inverted
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.*'