@planningcenter/chat-react-native 3.30.0 → 3.31.0-rc.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 (61) hide show
  1. package/build/components/conversation/system_message.d.ts +9 -0
  2. package/build/components/conversation/system_message.d.ts.map +1 -0
  3. package/build/components/conversation/system_message.js +93 -0
  4. package/build/components/conversation/system_message.js.map +1 -0
  5. package/build/navigation/index.d.ts +5 -0
  6. package/build/navigation/index.d.ts.map +1 -1
  7. package/build/navigation/index.js +5 -0
  8. package/build/navigation/index.js.map +1 -1
  9. package/build/screens/conversation_screen.d.ts.map +1 -1
  10. package/build/screens/conversation_screen.js +20 -1
  11. package/build/screens/conversation_screen.js.map +1 -1
  12. package/build/screens/index.d.ts +1 -0
  13. package/build/screens/index.d.ts.map +1 -1
  14. package/build/screens/index.js +1 -0
  15. package/build/screens/index.js.map +1 -1
  16. package/build/screens/message_actions_screen.d.ts +1 -0
  17. package/build/screens/message_actions_screen.d.ts.map +1 -1
  18. package/build/screens/message_actions_screen.js +12 -12
  19. package/build/screens/message_actions_screen.js.map +1 -1
  20. package/build/screens/system_message_people_screen.d.ts +9 -0
  21. package/build/screens/system_message_people_screen.d.ts.map +1 -0
  22. package/build/screens/system_message_people_screen.js +68 -0
  23. package/build/screens/system_message_people_screen.js.map +1 -0
  24. package/build/types/jolt_events/message_events.d.ts +7 -0
  25. package/build/types/jolt_events/message_events.d.ts.map +1 -1
  26. package/build/types/jolt_events/message_events.js.map +1 -1
  27. package/build/types/resources/message.d.ts +7 -0
  28. package/build/types/resources/message.d.ts.map +1 -1
  29. package/build/types/resources/message.js.map +1 -1
  30. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  31. package/build/utils/cache/optimistically_create_message.js +1 -0
  32. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  33. package/build/utils/index.d.ts +1 -0
  34. package/build/utils/index.d.ts.map +1 -1
  35. package/build/utils/index.js +1 -0
  36. package/build/utils/index.js.map +1 -1
  37. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  38. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +9 -0
  39. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  40. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  41. package/build/utils/request/messages_data_options.js +3 -0
  42. package/build/utils/request/messages_data_options.js.map +1 -1
  43. package/build/utils/system_messages.d.ts +15 -0
  44. package/build/utils/system_messages.d.ts.map +1 -0
  45. package/build/utils/system_messages.js +5 -0
  46. package/build/utils/system_messages.js.map +1 -0
  47. package/package.json +2 -2
  48. package/src/__tests__/utils/system_messages.ts +79 -0
  49. package/src/components/conversation/system_message.tsx +130 -0
  50. package/src/navigation/index.tsx +8 -0
  51. package/src/screens/conversation_screen.tsx +25 -2
  52. package/src/screens/index.ts +1 -0
  53. package/src/screens/message_actions_screen.tsx +64 -57
  54. package/src/screens/system_message_people_screen.tsx +94 -0
  55. package/src/types/jolt_events/message_events.ts +3 -0
  56. package/src/types/resources/message.ts +3 -0
  57. package/src/utils/cache/optimistically_create_message.ts +1 -0
  58. package/src/utils/index.ts +1 -0
  59. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +9 -0
  60. package/src/utils/request/messages_data_options.ts +3 -0
  61. package/src/utils/system_messages.ts +16 -0
@@ -0,0 +1,130 @@
1
+ import { useNavigation } from '@react-navigation/native'
2
+ import React from 'react'
3
+ import { Pressable, StyleSheet, View } from 'react-native'
4
+ import Animated from 'react-native-reanimated'
5
+ import { useAnimatedMessageBackgroundColor, useTheme } from '../../hooks'
6
+ import { ReactionCountResource } from '../../types/resources/reaction'
7
+ import { Haptic } from '../../utils/native_adapters'
8
+ import { SystemMessageResource } from '../../utils/system_messages'
9
+ import { Text } from '../display'
10
+ import { MessageReaction } from './message_reaction'
11
+
12
+ interface SystemMessageProps {
13
+ message: SystemMessageResource
14
+ conversationId: number
15
+ }
16
+
17
+ export function SystemMessage({ message, conversationId }: SystemMessageProps) {
18
+ const { reactionCounts, systemTextParts, personIdsForSystemEvent } = message
19
+ const { names, overflowCount, action } = systemTextParts
20
+ const styles = useStyles()
21
+ const navigation = useNavigation()
22
+ const { colors } = useTheme()
23
+ const text = systemMessageText(names, overflowCount, action)
24
+ const hasPeople = personIdsForSystemEvent.length > 0
25
+ const hasReactions = reactionCounts.length > 0
26
+ const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
27
+ useAnimatedMessageBackgroundColor()
28
+
29
+ const handlePress = () => {
30
+ navigation.navigate('SystemMessagePeople', {
31
+ conversation_id: conversationId,
32
+ person_ids: personIdsForSystemEvent,
33
+ })
34
+ }
35
+
36
+ const handleLongPress = () => {
37
+ Haptic.impactLight()
38
+ navigation.navigate('MessageActions', {
39
+ message_id: message.id,
40
+ conversation_id: conversationId,
41
+ isSystemMessage: true,
42
+ })
43
+ }
44
+
45
+ const handleReactionLongPress = (reaction: ReactionCountResource) => {
46
+ Haptic.impactLight()
47
+ navigation.navigate('Reactions', {
48
+ message_id: message.id,
49
+ conversation_id: conversationId,
50
+ reaction_value: reaction.value,
51
+ })
52
+ }
53
+
54
+ return (
55
+ <Pressable
56
+ onPress={hasPeople ? handlePress : undefined}
57
+ onLongPress={handleLongPress}
58
+ onPressIn={handleMessagePressIn}
59
+ onPressOut={handleMessagePressOut}
60
+ android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
61
+ accessibilityRole="button"
62
+ accessibilityHint={
63
+ hasPeople
64
+ ? 'Tap to see people. Long press for message actions.'
65
+ : 'Long press for message actions.'
66
+ }
67
+ >
68
+ <Animated.View style={[styles.container, animatedBackgroundColor]}>
69
+ <Text variant="tertiary" style={styles.text}>
70
+ {text}
71
+ </Text>
72
+ {hasReactions && (
73
+ <View style={styles.reactions}>
74
+ {message.reactionCounts.map(reaction => (
75
+ <MessageReaction
76
+ key={reaction.value}
77
+ reaction={reaction}
78
+ onLongPress={handleReactionLongPress}
79
+ message={message}
80
+ conversationId={conversationId}
81
+ />
82
+ ))}
83
+ </View>
84
+ )}
85
+ </Animated.View>
86
+ </Pressable>
87
+ )
88
+ }
89
+
90
+ function systemMessageText(names: string[], overflowCount: number, action: string): string {
91
+ if (overflowCount > 0) {
92
+ const overflowLabel = `${overflowCount} ${overflowCount === 1 ? 'other' : 'others'}`
93
+ return `${names.join(', ')}, and ${overflowLabel} ${action}`
94
+ }
95
+
96
+ switch (names.length) {
97
+ case 0:
98
+ return `Someone ${action}`
99
+ case 1:
100
+ return `${names[0]} ${action}`
101
+ case 2:
102
+ return `${names[0]} and ${names[1]} ${action}`
103
+ // Backend guarantees names.length <= 3 when overflowCount is 0;
104
+ // overflow branch handles 4+ people.
105
+ default:
106
+ return `${names[0]}, ${names[1]}, and ${names[2]} ${action}`
107
+ }
108
+ }
109
+
110
+ const useStyles = () => {
111
+ const { colors } = useTheme()
112
+ return StyleSheet.create({
113
+ container: {
114
+ paddingVertical: 12,
115
+ paddingHorizontal: 16,
116
+ alignItems: 'center',
117
+ },
118
+ text: {
119
+ color: colors.textColorDefaultSecondary,
120
+ textAlign: 'center',
121
+ },
122
+ reactions: {
123
+ flexDirection: 'row',
124
+ flexWrap: 'wrap',
125
+ justifyContent: 'center',
126
+ gap: 4,
127
+ marginTop: 8,
128
+ },
129
+ })
130
+ }
@@ -71,6 +71,10 @@ import { NotificationSettingsScreen } from '../screens/notification_settings_scr
71
71
  import { PreferredAppSelectionScreen } from '../screens/preferred_app_selection_screen'
72
72
  import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
73
73
  import { SendGiphyScreen, SendGiphyScreenOptions } from '../screens/send_giphy_screen'
74
+ import {
75
+ SystemMessagePeopleScreen,
76
+ SystemMessagePeopleScreenOptions,
77
+ } from '../screens/system_message_people_screen'
74
78
  import { TeamConversationScreen } from '../screens/team_conversation_screen'
75
79
  import { ScreenLayoutWithChatAccessGate } from './screenLayout'
76
80
 
@@ -333,6 +337,10 @@ export const ChatStack = createNativeStackNavigator({
333
337
  screen: ReactionsScreen,
334
338
  options: ReactionsScreenOptions,
335
339
  },
340
+ SystemMessagePeople: {
341
+ screen: SystemMessagePeopleScreen,
342
+ options: SystemMessagePeopleScreenOptions,
343
+ },
336
344
  MessageReadReceipts: {
337
345
  screen: MessageReadReceiptsScreen,
338
346
  options: MessageReadReceiptsScreenOptions,
@@ -21,6 +21,7 @@ import {
21
21
  MemberMessagesDisabledBanner,
22
22
  } from '../components/conversation/messages_disabled_banners'
23
23
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
24
+ import { SystemMessage } from '../components/conversation/system_message'
24
25
  import { TypingIndicator } from '../components/conversation/typing_indicator'
25
26
  import { KeyboardView } from '../components/display/keyboard_view'
26
27
  import BlankState from '../components/primitive/blank_state_primitive'
@@ -40,6 +41,7 @@ import { ConversationBadgeResource } from '../types/resources/conversation_badge
40
41
  import { getRelativeDateStatus } from '../utils/date'
41
42
  import dayjs from '../utils/dayjs'
42
43
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
44
+ import { isSystemMessage } from '../utils/system_messages'
43
45
 
44
46
  export type ConversationRouteProps = {
45
47
  conversation_id: number
@@ -188,6 +190,10 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
188
190
  )
189
191
  }
190
192
 
193
+ if (isSystemMessage(item)) {
194
+ return <SystemMessage message={item} conversationId={conversation_id} />
195
+ }
196
+
191
197
  return (
192
198
  <Message
193
199
  {...item}
@@ -293,6 +299,25 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
293
299
  const prevMessage = ms[i + 1]
294
300
  const nextMessage = ms[i - 1]
295
301
  const date = dayjs(message.createdAt).format('YYYY-MM-DD')
302
+
303
+ const prevMessageIsDateSeparator =
304
+ prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')
305
+
306
+ if (isSystemMessage(message)) {
307
+ message.myLatestInConversation = false
308
+ message.lastInGroup = true
309
+ message.renderAuthor = false
310
+ message.nextRendersAuthor = nextMessage?.renderAuthor
311
+ message.isReplyShadowMessage = false
312
+ message.nextIsReplyShadowMessage = false
313
+ message.threadPosition = null
314
+ enrichedMessages.push(message)
315
+ if (prevMessageIsDateSeparator) {
316
+ enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
317
+ }
318
+ return
319
+ }
320
+
296
321
  const inThread = message.replyRootId !== null
297
322
  const nextMessageInThread = nextMessage?.replyRootId !== null
298
323
  const threadRoot = message.replyRootId === message.id
@@ -307,8 +332,6 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
307
332
  const nextMessageMoreThan5Minutes =
308
333
  nextMessage &&
309
334
  new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
310
- const prevMessageIsDateSeparator =
311
- prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')
312
335
  const nextMessageIsDateSeparator =
313
336
  nextMessage && date !== dayjs(nextMessage.createdAt).format('YYYY-MM-DD')
314
337
  const insertReplyShadowMessage =
@@ -20,3 +20,4 @@ export * from './conversation_select_recipients/conversation_select_group_recipi
20
20
  export * from './conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen'
21
21
  export * from './attachment_actions/attachment_actions_screen'
22
22
  export * from './conversations/conversations_screen'
23
+ export * from './system_message_people_screen'
@@ -28,6 +28,7 @@ export type MessageActionsScreenProps = StaticScreenProps<{
28
28
  conversation_id: number
29
29
  canDeleteNonAuthoredMessages?: boolean
30
30
  inReplyScreen?: boolean
31
+ isSystemMessage?: boolean
31
32
  }>
32
33
 
33
34
  export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
@@ -37,6 +38,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
37
38
  canDeleteNonAuthoredMessages,
38
39
  inReplyScreen,
39
40
  reply_root_author_name,
41
+ isSystemMessage,
40
42
  } = route.params
41
43
 
42
44
  const { messages, refetch } = useConversationMessages(
@@ -55,6 +57,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
55
57
  refetchMessages={refetch}
56
58
  inReplyScreen={inReplyScreen}
57
59
  replyRootAuthorName={reply_root_author_name}
60
+ isSystemMessage={isSystemMessage}
58
61
  />
59
62
  )
60
63
  }
@@ -66,6 +69,7 @@ function MessageActionsScreenContent({
66
69
  refetchMessages,
67
70
  inReplyScreen,
68
71
  replyRootAuthorName,
72
+ isSystemMessage,
69
73
  }: {
70
74
  message: MessageResource
71
75
  conversation_id: number
@@ -73,6 +77,7 @@ function MessageActionsScreenContent({
73
77
  refetchMessages: () => void
74
78
  inReplyScreen?: boolean
75
79
  replyRootAuthorName?: string
80
+ isSystemMessage?: boolean
76
81
  }) {
77
82
  const navigation = useNavigation()
78
83
  const apiClient = useApiClient()
@@ -240,65 +245,67 @@ function MessageActionsScreenContent({
240
245
  />
241
246
  ))}
242
247
  </View>
243
- <View style={styles.actions}>
244
- {!inReplyScreen && (
248
+ {!isSystemMessage && (
249
+ <View style={styles.actions}>
250
+ {!inReplyScreen && (
251
+ <FormSheet.Action
252
+ onPress={handleReplyPress}
253
+ title="Reply to message"
254
+ iconName="registrations.undo"
255
+ accessibilityHint="Navigates to the reply screen"
256
+ accessibilityRole="link"
257
+ />
258
+ )}
245
259
  <FormSheet.Action
246
- onPress={handleReplyPress}
247
- title="Reply to message"
248
- iconName="registrations.undo"
249
- accessibilityHint="Navigates to the reply screen"
250
- accessibilityRole="link"
260
+ onPress={handleCopyPress}
261
+ title="Copy text"
262
+ iconName="services.fileCopy"
263
+ accessibilityHint="Copies text and links to clipboard"
251
264
  />
252
- )}
253
- <FormSheet.Action
254
- onPress={handleCopyPress}
255
- title="Copy text"
256
- iconName="services.fileCopy"
257
- accessibilityHint="Copies text and links to clipboard"
258
- />
259
- {showReportMessageAction && (
260
- <FormSheet.Action
261
- onPress={handleReportPress}
262
- title="Report message"
263
- iconName="chat.reportMessageO"
264
- accessibilityHint="Opens a form to report this message"
265
- />
266
- )}
267
- {message?.mine && (
268
- <FormSheet.Action
269
- onPress={() => handleEditPress()}
270
- title="Edit message"
271
- iconName="accounts.editor"
272
- accessibilityHint="Opens existing text in the message form input."
273
- />
274
- )}
275
- {message?.mine && (
276
- <FormSheet.Action
277
- onPress={() => handleViewReadReceiptsPress()}
278
- title="View read receipts"
279
- iconName="general.checkPerson"
280
- accessibilityHint="Opens a modal with a list of people who read your message."
281
- />
282
- )}
283
- {message?.mine && hasExpandedLink && (
284
- <FormSheet.Action
285
- onPress={() => handleRemoveLinkPreviewConfirm()}
286
- title="Remove link preview"
287
- iconName="general.brokenLink"
288
- accessibilityHint="Removes an expanded link preview from the message."
289
- />
290
- )}
291
- {(message?.mine || canDeleteNonAuthoredMessages) && (
292
- <FormSheet.Action
293
- onPress={() => handleDeleteConfirm()}
294
- title="Delete message"
295
- iconName="publishing.trash"
296
- appearance="danger"
297
- disabled={isPending}
298
- accessibilityHint="Opens a confirmation alert to delete this message permanently."
299
- />
300
- )}
301
- </View>
265
+ {showReportMessageAction && (
266
+ <FormSheet.Action
267
+ onPress={handleReportPress}
268
+ title="Report message"
269
+ iconName="chat.reportMessageO"
270
+ accessibilityHint="Opens a form to report this message"
271
+ />
272
+ )}
273
+ {message?.mine && (
274
+ <FormSheet.Action
275
+ onPress={() => handleEditPress()}
276
+ title="Edit message"
277
+ iconName="accounts.editor"
278
+ accessibilityHint="Opens existing text in the message form input."
279
+ />
280
+ )}
281
+ {message?.mine && (
282
+ <FormSheet.Action
283
+ onPress={() => handleViewReadReceiptsPress()}
284
+ title="View read receipts"
285
+ iconName="general.checkPerson"
286
+ accessibilityHint="Opens a modal with a list of people who read your message."
287
+ />
288
+ )}
289
+ {message?.mine && hasExpandedLink && (
290
+ <FormSheet.Action
291
+ onPress={() => handleRemoveLinkPreviewConfirm()}
292
+ title="Remove link preview"
293
+ iconName="general.brokenLink"
294
+ accessibilityHint="Removes an expanded link preview from the message."
295
+ />
296
+ )}
297
+ {(message?.mine || canDeleteNonAuthoredMessages) && (
298
+ <FormSheet.Action
299
+ onPress={() => handleDeleteConfirm()}
300
+ title="Delete message"
301
+ iconName="publishing.trash"
302
+ appearance="danger"
303
+ disabled={isPending}
304
+ accessibilityHint="Opens a confirmation alert to delete this message permanently."
305
+ />
306
+ )}
307
+ </View>
308
+ )}
302
309
  </FormSheet.Root>
303
310
  )
304
311
  }
@@ -0,0 +1,94 @@
1
+ import { StaticScreenProps } from '@react-navigation/native'
2
+ import React, { memo } from 'react'
3
+ import { Platform, StyleSheet, View } from 'react-native'
4
+ import { FlatList } from 'react-native-gesture-handler'
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
6
+ import { Avatar, Text } from '../components'
7
+ import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
8
+ import { useSuspenseGet } from '../hooks'
9
+ import { MemberResource } from '../types'
10
+
11
+ export const SystemMessagePeopleScreenOptions = getFormSheetScreenOptions({
12
+ sheetAllowedDetents: Platform.select({
13
+ android: [0.5, 0.94],
14
+ default: [0.5, 1],
15
+ }),
16
+ headerTitle: 'People',
17
+ })
18
+
19
+ export type SystemMessagePeopleScreenProps = StaticScreenProps<{
20
+ conversation_id: number
21
+ person_ids: number[]
22
+ }>
23
+
24
+ export function SystemMessagePeopleScreen({ route }: SystemMessagePeopleScreenProps) {
25
+ const { conversation_id, person_ids } = route.params
26
+ const styles = useStyles()
27
+
28
+ const { data: members } = useSuspenseGet<MemberResource[]>({
29
+ url: `/me/conversations/${conversation_id}/members`,
30
+ data: {
31
+ fields: {
32
+ Member: ['id', 'name', 'avatar'],
33
+ },
34
+ where: {
35
+ id: person_ids,
36
+ },
37
+ limit: person_ids.length,
38
+ },
39
+ })
40
+
41
+ const orderById = Object.fromEntries(person_ids.map((id, i) => [id, i]))
42
+ const sortedMembers = [...members].sort((a, b) => (orderById[a.id] ?? 0) - (orderById[b.id] ?? 0))
43
+
44
+ return (
45
+ <FormSheet.Root contentStyle={styles.formSheetContent}>
46
+ <FlatList
47
+ data={sortedMembers}
48
+ contentContainerStyle={styles.contentContainer}
49
+ keyExtractor={item => item.id.toString()}
50
+ renderItem={({ item }) => <Person person={item} />}
51
+ />
52
+ </FormSheet.Root>
53
+ )
54
+ }
55
+
56
+ type PersonProps = Pick<MemberResource, 'id' | 'name' | 'avatar'>
57
+
58
+ const Person = memo(({ person }: { person: PersonProps }) => {
59
+ const styles = useStyles()
60
+
61
+ return (
62
+ <View style={styles.personRow}>
63
+ <Avatar sourceUri={person.avatar} size="sm" />
64
+ <Text variant="tertiary" numberOfLines={2} style={styles.personName}>
65
+ {person.name}
66
+ </Text>
67
+ </View>
68
+ )
69
+ })
70
+
71
+ const useStyles = () => {
72
+ const { bottom } = useSafeAreaInsets()
73
+
74
+ return StyleSheet.create({
75
+ formSheetContent: {
76
+ paddingTop: 16,
77
+ },
78
+ contentContainer: {
79
+ paddingTop: 8,
80
+ paddingBottom: bottom + 24,
81
+ },
82
+ personRow: {
83
+ flexDirection: 'row',
84
+ alignItems: 'center',
85
+ gap: 8,
86
+ paddingVertical: 8,
87
+ paddingHorizontal: 16,
88
+ flex: 1,
89
+ },
90
+ personName: {
91
+ flex: 1,
92
+ },
93
+ })
94
+ }
@@ -18,6 +18,9 @@ interface BaseMessageEventData extends Record<string, unknown> {
18
18
  idempotent_key?: string | null
19
19
  reply_count: number
20
20
  reply_root_id: string | null
21
+ message_type: string
22
+ system_event_person_ids: number[] | null
23
+ system_text_parts: { names: string[]; overflow_count: number; action: string } | null
21
24
  }
22
25
  }
23
26
 
@@ -17,6 +17,9 @@ export interface MessageResource {
17
17
  reactionCounts: ReactionCountResource[]
18
18
  replyCount: number
19
19
  replyRootId?: string | null
20
+ messageType: string
21
+ personIdsForSystemEvent?: number[] | null
22
+ systemTextParts?: { names: string[]; overflowCount: number; action: string } | null
20
23
 
21
24
  // Custom Local Properties we set for rendering
22
25
  renderAuthor?: boolean
@@ -34,6 +34,7 @@ export function optimisticallyCreateMessage({
34
34
  const optimisticMessage: MessageResource = {
35
35
  ...message,
36
36
  type: 'Message',
37
+ messageType: 'user',
37
38
  id,
38
39
  text,
39
40
  html: '', // Will be filled by server
@@ -10,3 +10,4 @@ export * from './reaction_constants'
10
10
  export * from './destructure_chat_group_graph_id'
11
11
  export * from './convert_attachments_for_create'
12
12
  export * from './assert_keys_are_numbers'
13
+ export * from './system_messages'
@@ -29,5 +29,14 @@ export function transformMessageEventDataToMessageResource({
29
29
  reactionCounts: [],
30
30
  replyCount: data.reply_count || 0,
31
31
  replyRootId: data.reply_root_id || null,
32
+ messageType: data.message_type,
33
+ personIdsForSystemEvent: data.system_event_person_ids,
34
+ systemTextParts: data.system_text_parts
35
+ ? {
36
+ names: data.system_text_parts.names,
37
+ overflowCount: data.system_text_parts.overflow_count,
38
+ action: data.system_text_parts.action,
39
+ }
40
+ : null,
32
41
  }
33
42
  }
@@ -10,6 +10,9 @@ export const getMessageFields = {
10
10
  'reaction_counts',
11
11
  'reply_count',
12
12
  'reply_root',
13
+ 'message_type',
14
+ 'person_ids_for_system_event',
15
+ 'system_text_parts',
13
16
  ],
14
17
  Person: ['name', 'avatar'],
15
18
  ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
@@ -0,0 +1,16 @@
1
+ import { MessageResource } from '../types'
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
9
+ systemTextParts: { names: string[]; overflowCount: number; action: string }
10
+ personIdsForSystemEvent: number[]
11
+ }
12
+
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