@planningcenter/chat-react-native 4.4.1-qa-staging.8 → 4.4.1-qa-staging.10

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 (55) hide show
  1. package/build/components/conversation/system_message.d.ts.map +1 -1
  2. package/build/components/conversation/system_message.js +85 -74
  3. package/build/components/conversation/system_message.js.map +1 -1
  4. package/build/components/conversations/conversation_preview.js +2 -0
  5. package/build/components/conversations/conversation_preview.js.map +1 -1
  6. package/build/hooks/use_features.d.ts +2 -0
  7. package/build/hooks/use_features.d.ts.map +1 -1
  8. package/build/hooks/use_features.js +2 -0
  9. package/build/hooks/use_features.js.map +1 -1
  10. package/build/hooks/use_filter_dropdown_enabled.d.ts +2 -0
  11. package/build/hooks/use_filter_dropdown_enabled.d.ts.map +1 -0
  12. package/build/hooks/use_filter_dropdown_enabled.js +18 -0
  13. package/build/hooks/use_filter_dropdown_enabled.js.map +1 -0
  14. package/build/types/jolt_events/message_events.d.ts +2 -0
  15. package/build/types/jolt_events/message_events.d.ts.map +1 -1
  16. package/build/types/jolt_events/message_events.js.map +1 -1
  17. package/build/types/resources/conversation.d.ts +1 -1
  18. package/build/types/resources/conversation.d.ts.map +1 -1
  19. package/build/types/resources/conversation.js.map +1 -1
  20. package/build/types/resources/message.d.ts +2 -0
  21. package/build/types/resources/message.d.ts.map +1 -1
  22. package/build/types/resources/message.js.map +1 -1
  23. package/build/utils/date.d.ts +1 -0
  24. package/build/utils/date.d.ts.map +1 -1
  25. package/build/utils/date.js +15 -1
  26. package/build/utils/date.js.map +1 -1
  27. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  28. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +2 -1
  29. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  30. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  31. package/build/utils/request/messages_data_options.js +1 -1
  32. package/build/utils/request/messages_data_options.js.map +1 -1
  33. package/build/utils/system_messages.d.ts +7 -5
  34. package/build/utils/system_messages.d.ts.map +1 -1
  35. package/build/utils/system_messages.js +10 -2
  36. package/build/utils/system_messages.js.map +1 -1
  37. package/package.json +3 -3
  38. package/src/__tests__/hooks/use_filter_dropdown_enabled.test.tsx +42 -0
  39. package/src/__tests__/utils/system_messages.ts +19 -32
  40. package/src/components/conversation/__tests__/system_message.test.tsx +104 -0
  41. package/src/components/conversation/system_message.tsx +20 -6
  42. package/src/components/conversations/__tests__/conversation_preview.test.tsx +70 -0
  43. package/src/components/conversations/conversation_preview.tsx +4 -0
  44. package/src/hooks/use_features.ts +2 -0
  45. package/src/hooks/use_filter_dropdown_enabled.ts +10 -0
  46. package/src/types/jolt_events/message_events.ts +2 -0
  47. package/src/types/resources/conversation.ts +1 -1
  48. package/src/types/resources/message.ts +3 -0
  49. package/src/utils/__tests__/date.test.ts +19 -0
  50. package/src/utils/__tests__/group_messages.test.ts +1 -0
  51. package/src/utils/date.ts +8 -1
  52. package/src/utils/jolt/__tests__/transform_message_event_data_to_message_resource.test.ts +35 -0
  53. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +1 -0
  54. package/src/utils/request/messages_data_options.ts +1 -0
  55. package/src/utils/system_messages.ts +19 -10
@@ -0,0 +1,70 @@
1
+ import { render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import type { ConversationPreviewData, ConversationResource } from '../../../types'
4
+ import { formatDatePreview } from '../../../utils/date'
5
+ import { ConversationPreview } from '../conversation_preview'
6
+
7
+ jest.mock('../conversation_actions', () => ({
8
+ ConversationActions: ({
9
+ children,
10
+ accessibilityLabel,
11
+ }: {
12
+ children: React.ReactNode
13
+ accessibilityLabel?: string
14
+ }) => {
15
+ const { View: MockView } = jest.requireActual('react-native')
16
+ return <MockView accessibilityLabel={accessibilityLabel}>{children}</MockView>
17
+ },
18
+ }))
19
+
20
+ const createdAt = '2026-03-04T15:30:00Z'
21
+
22
+ const conversation = (conversationPreview: ConversationPreviewData): ConversationResource => ({
23
+ type: 'Conversation',
24
+ id: 7,
25
+ title: 'Worship Team',
26
+ conversationPreview,
27
+ createdAt,
28
+ lastMessageCreatedAt: createdAt,
29
+ muted: false,
30
+ repliesDisabled: false,
31
+ unreadCount: 0,
32
+ unreadReaction: false,
33
+ updatedAt: createdAt,
34
+ })
35
+
36
+ const renderPreview = (preview: ConversationPreviewData) =>
37
+ render(<ConversationPreview conversation={conversation(preview)} onPress={jest.fn()} />)
38
+
39
+ describe('ConversationPreview', () => {
40
+ it('prefixes message previews with the author', () => {
41
+ const view = renderPreview({
42
+ kind: 'message',
43
+ authorName: 'Ada Lovelace',
44
+ value: null,
45
+ text: 'See you Sunday',
46
+ createdAt,
47
+ })
48
+
49
+ expect(view.getByText('Ada Lovelace: See you Sunday')).toBeTruthy()
50
+ })
51
+
52
+ it('shows system previews as their text alone', () => {
53
+ const text = 'This conversation was unarchived by Ada Lovelace'
54
+ const view = renderPreview({
55
+ kind: 'system',
56
+ authorName: 'System',
57
+ value: null,
58
+ text,
59
+ createdAt,
60
+ })
61
+
62
+ expect(view.getByText(text)).toBeTruthy()
63
+ expect(view.queryByText('System', { exact: false })).toBeNull()
64
+ expect(
65
+ view.getByLabelText(
66
+ `Select Worship Team, Latest activity: ${text} at ${formatDatePreview(createdAt)}`
67
+ )
68
+ ).toBeTruthy()
69
+ })
70
+ })
@@ -168,6 +168,8 @@ function reactionEmoji(value: ConversationPreviewData['value']): string | null {
168
168
  }
169
169
 
170
170
  function formatConversationPreviewText(preview: ConversationPreviewData) {
171
+ if (preview.kind === 'system') return preview.text
172
+
171
173
  if (preview.kind === 'reaction') {
172
174
  const emoji = reactionEmoji(preview.value)
173
175
  return `${preview.authorName}${emoji ? ` ${emoji}` : ' reacted'} to: "${preview.text}"`
@@ -179,6 +181,8 @@ function formatConversationPreviewText(preview: ConversationPreviewData) {
179
181
  function formatConversationPreviewAccessibilityLabel(preview: ConversationPreviewData) {
180
182
  const date = formatDatePreview(preview.createdAt)
181
183
 
184
+ if (preview.kind === 'system') return `Latest activity: ${preview.text} at ${date}`
185
+
182
186
  if (preview.kind === 'reaction') {
183
187
  return `${preview.authorName} reacted to your message sent ${date} and says ${preview.text}`
184
188
  }
@@ -57,6 +57,8 @@ export const availableFeatures = {
57
57
  dm_search_clear_fix: 'ROLLOUT_MOBILE_dm_search_clear_fix',
58
58
  search: 'QA_MOBILE_search',
59
59
  search_rollout: 'ROLLOUT_MOBILE_search',
60
+ filter_dropdown: 'QA_MOBILE_filter_dropdown',
61
+ filter_dropdown_rollout: 'ROLLOUT_MOBILE_filter_dropdown',
60
62
  manual_archiving: 'QA_MOBILE_manual_archiving',
61
63
  manual_archiving_rollout: 'ROLLOUT_MOBILE_manual_archiving',
62
64
  } as const satisfies Record<string, `${'QA' | 'ROLLOUT'}_${string}`>
@@ -0,0 +1,10 @@
1
+ import { availableFeatures, useFeatures } from './use_features'
2
+
3
+ export function useFilterDropdownEnabled() {
4
+ const { featureEnabled } = useFeatures()
5
+
6
+ return (
7
+ featureEnabled(availableFeatures.filter_dropdown) ||
8
+ featureEnabled(availableFeatures.filter_dropdown_rollout)
9
+ )
10
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
2
2
  import { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'
3
+ import type { SystemTextFormat } from '../resources/message'
3
4
 
4
5
  interface BaseMessageEventData extends Record<string, unknown> {
5
6
  data: {
@@ -21,6 +22,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
21
22
  message_type: string
22
23
  system_event_person_ids: number[] | null
23
24
  system_text_parts: { names: string[]; overflow_count: number; action: string } | null
25
+ system_text_format: SystemTextFormat | null
24
26
  }
25
27
  }
26
28
 
@@ -6,7 +6,7 @@ import { MemberAbilityResource } from './member_ability'
6
6
  import { ReactionCountResource } from './reaction'
7
7
 
8
8
  export interface ConversationPreviewData {
9
- kind: 'reaction' | 'message'
9
+ kind: 'reaction' | 'message' | 'system'
10
10
  authorName: string
11
11
  value: ReactionCountResource['value'] | null
12
12
  text: string
@@ -3,6 +3,8 @@ import { DenormalizedAttachmentResourceForCreate } from './denormalized_attachme
3
3
  import type { PersonResource } from './person'
4
4
  import type { ReactionCountResource } from './reaction'
5
5
 
6
+ export type SystemTextFormat = 'people' | 'actor'
7
+
6
8
  export interface MessageResource {
7
9
  type: 'Message'
8
10
  id: string
@@ -22,6 +24,7 @@ export interface MessageResource {
22
24
  messageType: string
23
25
  personIdsForSystemEvent?: number[] | null
24
26
  systemTextParts?: { names: string[]; overflowCount: number; action: string } | null
27
+ systemTextFormat?: SystemTextFormat | null
25
28
 
26
29
  // Custom Local Properties we set for rendering
27
30
  renderAuthor?: boolean
@@ -0,0 +1,19 @@
1
+ import { formatEventDateTime } from '../date'
2
+
3
+ describe('formatEventDateTime', () => {
4
+ beforeEach(() => {
5
+ jest.useFakeTimers({ now: new Date(2026, 5, 15, 12, 0) })
6
+ })
7
+
8
+ afterEach(() => {
9
+ jest.useRealTimers()
10
+ })
11
+
12
+ it('leaves out the year for dates in the current year', () => {
13
+ expect(formatEventDateTime(new Date(2026, 1, 3, 14, 18))).toBe('Feb 3 at 2:18pm')
14
+ })
15
+
16
+ it('includes the year for dates in other years', () => {
17
+ expect(formatEventDateTime(new Date(2025, 1, 3, 14, 18))).toBe('Feb 3, 2025 at 2:18pm')
18
+ })
19
+ })
@@ -206,6 +206,7 @@ describe('groupMessages — system messages', () => {
206
206
  const messages = [
207
207
  message('sys', {
208
208
  messageType: 'user_joined',
209
+ systemTextFormat: 'people',
209
210
  systemTextParts: { names: ['A'], overflowCount: 0, action: 'joined' },
210
211
  personIdsForSystemEvent: [1],
211
212
  }),
package/src/utils/date.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { date as formatDate } from '@planningcenter/datetime-fmt'
1
+ import { date as formatDate, time as formatTime } from '@planningcenter/datetime-fmt'
2
2
  import dayjs from './dayjs'
3
3
 
4
4
  export type DateProps = string | number | Date
@@ -61,3 +61,10 @@ export function relativeTime(date: DateProps): string {
61
61
  return dayjs(date).format('h:mma')
62
62
  }
63
63
  }
64
+
65
+ export function formatEventDateTime(date: DateProps) {
66
+ const { isThisYear } = getRelativeDateStatus(date)
67
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
68
+ const day = formatDate(date, { style: 'abbreviated', year: !isThisYear, timeZone })
69
+ return `${day} at ${formatTime(date, { timeZone })}`
70
+ }
@@ -0,0 +1,35 @@
1
+ import type { MessageCreatedEvent } from '../../../types/jolt_events/message_events'
2
+ import { isSystemMessage } from '../../system_messages'
3
+ import { transformMessageEventDataToMessageResource } from '../transform_message_event_data_to_message_resource'
4
+
5
+ const archivedEventData: MessageCreatedEvent['data']['data'] = {
6
+ author_avatar: '',
7
+ author_id: 1,
8
+ author_name: 'Wes',
9
+ conversation_id: 1,
10
+ created_at: '2026-01-01T00:00:00Z',
11
+ deleted_at: null,
12
+ organization_id: 1,
13
+ sort_key: '01J0000000000000000000001',
14
+ text: 'This conversation was archived by Wes',
15
+ text_edited_at: null,
16
+ html: '',
17
+ attachments: [],
18
+ reply_count: 0,
19
+ reply_root_id: null,
20
+ message_type: 'conversation_archived',
21
+ system_event_person_ids: [1],
22
+ system_text_parts: null,
23
+ system_text_format: 'actor',
24
+ }
25
+
26
+ describe('transformMessageEventDataToMessageResource', () => {
27
+ it('keeps the system text format so realtime archive events render as system messages', () => {
28
+ const message = transformMessageEventDataToMessageResource({
29
+ data: archivedEventData,
30
+ currentPersonId: 2,
31
+ })
32
+
33
+ expect(isSystemMessage(message)).toBe(true)
34
+ })
35
+ })
@@ -39,5 +39,6 @@ export function transformMessageEventDataToMessageResource({
39
39
  action: data.system_text_parts.action,
40
40
  }
41
41
  : null,
42
+ systemTextFormat: data.system_text_format,
42
43
  }
43
44
  }
@@ -14,6 +14,7 @@ export const getMessageFields = {
14
14
  'message_type',
15
15
  'person_ids_for_system_event',
16
16
  'system_text_parts',
17
+ 'system_text_format',
17
18
  ],
18
19
  Person: ['name', 'avatar'],
19
20
  ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
@@ -1,16 +1,25 @@
1
1
  import type { MessageResource } from '../types'
2
2
 
3
- const SYSTEM_MESSAGE_TYPES = ['user_joined'] as const
4
-
5
- type SystemMessageType = (typeof SYSTEM_MESSAGE_TYPES)[number]
6
-
7
- export interface SystemMessageResource extends MessageResource {
8
- messageType: SystemMessageType
3
+ export interface PeopleSystemMessageResource extends MessageResource {
4
+ systemTextFormat: 'people'
9
5
  systemTextParts: { names: string[]; overflowCount: number; action: string }
10
6
  personIdsForSystemEvent: number[]
11
7
  }
12
8
 
13
- export const isSystemMessage = (message: MessageResource): message is SystemMessageResource =>
14
- SYSTEM_MESSAGE_TYPES.includes(message.messageType as SystemMessageType) &&
15
- message.systemTextParts != null &&
16
- message.personIdsForSystemEvent != null
9
+ export interface ActorSystemMessageResource extends MessageResource {
10
+ systemTextFormat: 'actor'
11
+ text: string
12
+ }
13
+
14
+ export type SystemMessageResource = PeopleSystemMessageResource | ActorSystemMessageResource
15
+
16
+ export const isSystemMessage = (message: MessageResource): message is SystemMessageResource => {
17
+ switch (message.systemTextFormat) {
18
+ case 'people':
19
+ return message.systemTextParts != null && message.personIdsForSystemEvent != null
20
+ case 'actor':
21
+ return !!message.text
22
+ default:
23
+ return false
24
+ }
25
+ }