@planningcenter/chat-react-native 3.17.0-rc.1 → 3.17.1-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 (41) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +11 -1
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts +2 -1
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +6 -4
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/reply_shadow_message.js +1 -1
  9. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  10. package/build/contexts/api_provider.d.ts.map +1 -1
  11. package/build/contexts/api_provider.js +4 -1
  12. package/build/contexts/api_provider.js.map +1 -1
  13. package/build/hooks/use_conversation_message.d.ts +2 -1
  14. package/build/hooks/use_conversation_message.d.ts.map +1 -1
  15. package/build/hooks/use_conversation_message.js +5 -2
  16. package/build/hooks/use_conversation_message.js.map +1 -1
  17. package/build/hooks/use_message_draft.d.ts.map +1 -1
  18. package/build/hooks/use_message_draft.js +12 -5
  19. package/build/hooks/use_message_draft.js.map +1 -1
  20. package/build/navigation/index.d.ts +6 -2
  21. package/build/navigation/index.d.ts.map +1 -1
  22. package/build/navigation/index.js +3 -3
  23. package/build/navigation/index.js.map +1 -1
  24. package/build/screens/conversation_screen.d.ts +1 -1
  25. package/build/screens/conversation_screen.d.ts.map +1 -1
  26. package/build/screens/conversation_screen.js +19 -10
  27. package/build/screens/conversation_screen.js.map +1 -1
  28. package/build/screens/message_actions_screen.d.ts +1 -0
  29. package/build/screens/message_actions_screen.d.ts.map +1 -1
  30. package/build/screens/message_actions_screen.js +5 -5
  31. package/build/screens/message_actions_screen.js.map +1 -1
  32. package/package.json +7 -4
  33. package/src/components/conversation/message.tsx +12 -1
  34. package/src/components/conversation/message_form.tsx +8 -2
  35. package/src/components/conversation/reply_shadow_message.tsx +1 -1
  36. package/src/contexts/api_provider.tsx +5 -1
  37. package/src/hooks/use_conversation_message.ts +6 -1
  38. package/src/hooks/use_message_draft.ts +15 -5
  39. package/src/navigation/index.tsx +3 -3
  40. package/src/screens/conversation_screen.tsx +20 -10
  41. package/src/screens/message_actions_screen.tsx +13 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.17.0-rc.1",
3
+ "version": "3.17.1-rc.0",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -16,8 +16,10 @@
16
16
  "test": "expo-module test",
17
17
  "prepublishOnly": "expo-module prepublishOnly"
18
18
  },
19
+ "target": "18",
19
20
  "dependencies": {
20
- "lodash-inflection": "^1.5.0"
21
+ "lodash-inflection": "^1.5.0",
22
+ "react-compiler-runtime": "^1.0.0"
21
23
  },
22
24
  "peerDependencies": {
23
25
  "@planningcenter/datetime-fmt": ">=1.0.0",
@@ -33,7 +35,7 @@
33
35
  "lodash": "*",
34
36
  "moment": ">=2.0.0",
35
37
  "moment-timezone": "*",
36
- "react": "*",
38
+ "react": "^18.0.0 || ^19.0.0",
37
39
  "react-native": "*",
38
40
  "react-native-device-info": "*",
39
41
  "react-native-gesture-handler": "*",
@@ -47,6 +49,7 @@
47
49
  "@testing-library/react-hooks": "^8.0.1",
48
50
  "@types/jest": "^29.5.14",
49
51
  "@typescript-eslint/parser": "^8.32.0",
52
+ "eslint-plugin-react-compiler": "^19.1.0-rc.2",
50
53
  "expo-module-scripts": "^5.0.7",
51
54
  "fast-text-encoding": "^1.0.6",
52
55
  "jest": "^29.7.0",
@@ -56,5 +59,5 @@
56
59
  "react-native-url-polyfill": "^2.0.0",
57
60
  "typescript": "<5.6.0"
58
61
  },
59
- "gitHead": "9cc0a46532ce7bbf7f32bbb5e5873bd5c709b3f0"
62
+ "gitHead": "06e96f7272163e97af39f8d18ef1ed26096c3741"
60
63
  }
@@ -26,6 +26,7 @@ import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_
26
26
  import { Haptic } from '../../utils/native_adapters'
27
27
  import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from './reply_connectors'
28
28
  import { pluralize, REPLIES_FEATURE_ENABLED } from '../../utils'
29
+ import { useConversationMessage } from '../../hooks/use_conversation_message'
29
30
 
30
31
  /** Message
31
32
  * Component for display of a message within a conversation list
@@ -59,6 +60,15 @@ export function Message({
59
60
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
60
61
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
61
62
  useAnimatedMessageBackgroundColor()
63
+ const { message: replyRootMessage } = useConversationMessage({
64
+ conversation_id,
65
+ messageId: message.replyRootId || '',
66
+ enabled: !!message.replyRootId,
67
+ })
68
+
69
+ const replyRootAuthorName = message.replyRootId
70
+ ? replyRootMessage?.author.name
71
+ : message.author.name
62
72
 
63
73
  useEffect(() => {
64
74
  if (pending) {
@@ -93,6 +103,7 @@ export function Message({
93
103
  conversation_id,
94
104
  canDeleteNonAuthoredMessages,
95
105
  inReplyScreen,
106
+ reply_root_author_name: replyRootAuthorName,
96
107
  })
97
108
  }
98
109
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
@@ -120,7 +131,7 @@ export function Message({
120
131
  navigation.navigate('ConversationReply', {
121
132
  conversation_id,
122
133
  reply_root_id: message.id,
123
- // TODO: Add a way to pass the reply root author's name
134
+ reply_root_author_name: message.author.name,
124
135
  })
125
136
  }
126
137
 
@@ -60,6 +60,7 @@ interface MessagesFormRootProps extends ViewProps {
60
60
  conversation: ConversationResource
61
61
  currentlyEditingMessage?: MessageResource | null
62
62
  replyRootId?: string | null
63
+ replyRootAuthorFirstName?: string | null
63
64
  }
64
65
 
65
66
  const MessageFormContext = React.createContext<{
@@ -74,6 +75,7 @@ const MessageFormContext = React.createContext<{
74
75
  currentlyEditingMessage?: MessageResource | null
75
76
  reset: () => void
76
77
  replyRootId?: string | null
78
+ replyRootAuthorFirstName?: string | null
77
79
  }>({
78
80
  text: '',
79
81
  setText: (_text: string) => {},
@@ -85,6 +87,7 @@ const MessageFormContext = React.createContext<{
85
87
  currentlyEditingMessage: null,
86
88
  reset: () => {},
87
89
  replyRootId: undefined,
90
+ replyRootAuthorFirstName: undefined,
88
91
  })
89
92
 
90
93
  const GIPHY_MAX_LENGTH = 50
@@ -95,6 +98,7 @@ function MessageFormRoot({
95
98
  currentlyEditingMessage,
96
99
  children,
97
100
  replyRootId,
101
+ replyRootAuthorFirstName,
98
102
  }: MessagesFormRootProps) {
99
103
  const { giphyApiKey } = useContext(ChatContext)
100
104
  const canGiphy = !!giphyApiKey && !currentlyEditingMessage
@@ -248,6 +252,7 @@ function MessageFormRoot({
248
252
  currentlyEditingMessage,
249
253
  reset,
250
254
  replyRootId,
255
+ replyRootAuthorFirstName,
251
256
  }}
252
257
  >
253
258
  <View style={styles.container}>
@@ -559,14 +564,15 @@ function EditingIndicator() {
559
564
  }
560
565
 
561
566
  function ReplyIndicator() {
562
- const { replyRootId } = React.useContext(MessageFormContext)
567
+ const { replyRootId, replyRootAuthorFirstName } = React.useContext(MessageFormContext)
563
568
  const navigation = useNavigation()
569
+ const title = replyRootAuthorFirstName ? `Reply to ${replyRootAuthorFirstName}` : 'Reply'
564
570
 
565
571
  if (!REPLIES_FEATURE_ENABLED || !replyRootId) return null
566
572
 
567
573
  return (
568
574
  <FormIndicatorRow
569
- title="Reply" // TODO: Get root reply author
575
+ title={title}
570
576
  buttonText="Exit replies"
571
577
  accessibilityHint="Return to the main conversation"
572
578
  iconName="registrations.undo"
@@ -76,7 +76,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
76
76
  navigation.navigate('ConversationReply', {
77
77
  conversation_id,
78
78
  reply_root_id: message.replyRootId,
79
- // TODO: Add a way to pass the reply root author's name
79
+ reply_root_author_name: message.author.name,
80
80
  })
81
81
  }
82
82
 
@@ -37,7 +37,7 @@ export function ApiProvider({ children }: ViewProps) {
37
37
  const { token, env } = useContext(ChatContext)
38
38
  const sessionChanged = useSessionChanged({ token, env })
39
39
 
40
- apiClient = useApiClient()
40
+ const client = useApiClient()
41
41
 
42
42
  useEffect(() => {
43
43
  if (!sessionChanged) return
@@ -45,6 +45,10 @@ export function ApiProvider({ children }: ViewProps) {
45
45
  chatQueryClient.clear()
46
46
  }, [sessionChanged])
47
47
 
48
+ useEffect(() => {
49
+ apiClient = client
50
+ }, [client])
51
+
48
52
  return (
49
53
  <QueryClientProvider client={chatQueryClient}>
50
54
  <PrefetchQueries />
@@ -5,15 +5,20 @@ import { getMessageRequestArgs, getMessageQueryKey } from '../utils/request/get_
5
5
  export const useConversationMessage = ({
6
6
  conversation_id,
7
7
  messageId,
8
+ enabled,
8
9
  }: {
9
10
  conversation_id: number
10
11
  messageId: string
12
+ enabled?: boolean
11
13
  }) => {
12
14
  const {
13
15
  data: message,
14
16
  isError,
15
17
  isLoading,
16
- } = useApiGet<MessageResource>(getMessageRequestArgs({ conversation_id, messageId }))
18
+ } = useApiGet<MessageResource>({
19
+ ...getMessageRequestArgs({ conversation_id, messageId }),
20
+ enabled,
21
+ })
17
22
  const queryKey = getMessageQueryKey({ conversation_id, messageId })
18
23
 
19
24
  return { message, isError, isLoading, queryKey }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react'
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
2
  import { useAsyncStorage } from './use_async_storage'
3
3
  import type { FileAttachment } from '../types/resources/denormalized_attachment_resource_for_create'
4
4
 
@@ -27,17 +27,26 @@ export function useMessageDraft(conversationId: number) {
27
27
  // Filter expired attachments
28
28
  if (storedDraft?.attachments) {
29
29
  const now = Date.now()
30
- storedDraft.attachments = storedDraft.attachments.filter(attachment => {
30
+ const validAttachments = storedDraft.attachments.filter(attachment => {
31
31
  if (!attachment.uploadedAt) return false
32
32
  return now - attachment.uploadedAt < ATTACHMENT_EXPIRY_MS
33
33
  })
34
+
35
+ return {
36
+ ...storedDraft,
37
+ attachments: validAttachments,
38
+ }
34
39
  }
35
40
 
36
41
  return storedDraft
37
42
  })
38
43
 
39
- // Clean up expired drafts on mount
44
+ // Clean up expired drafts only once on mount - React 18 recommended pattern
45
+ const hasCleanedUp = useRef(false)
46
+
40
47
  useEffect(() => {
48
+ if (hasCleanedUp.current) return
49
+
41
50
  const now = Date.now()
42
51
  const validDrafts: Record<string, MessageDraft> = {}
43
52
 
@@ -51,8 +60,9 @@ export function useMessageDraft(conversationId: number) {
51
60
  if (Object.keys(validDrafts).length !== Object.keys(allDrafts).length) {
52
61
  setAllDrafts(validDrafts)
53
62
  }
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- }, []) // I only want this to run on mount
63
+
64
+ hasCleanedUp.current = true
65
+ }, [allDrafts, setAllDrafts])
56
66
 
57
67
  const saveDraft = useCallback(
58
68
  (text: string, attachments: FileAttachment[]) => {
@@ -187,9 +187,9 @@ export const ChatStack = createNativeStackNavigator({
187
187
  },
188
188
  ConversationReply: {
189
189
  screen: ConversationScreen,
190
- options: {
191
- title: 'Reply', // TODO: Get root reply author
192
- },
190
+ options: ({ route }) => ({
191
+ title: (route.params as ConversationRouteProps)?.title ?? 'Reply',
192
+ }),
193
193
  },
194
194
  TeamConversation: {
195
195
  screen: TeamConversationScreen,
@@ -40,7 +40,7 @@ import { REPLIES_FEATURE_ENABLED } from '../utils'
40
40
  export type ConversationRouteProps = {
41
41
  conversation_id: number
42
42
  reply_root_id?: string | null
43
- replyRootAuthor?: string
43
+ reply_root_author_name?: string
44
44
  chat_group_graph_id?: string
45
45
  clear_input?: boolean
46
46
  editing_message_id?: number | null
@@ -55,7 +55,8 @@ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
55
55
  export function ConversationScreen({ route }: ConversationScreenProps) {
56
56
  const styles = useStyles()
57
57
  const navigation = useNavigation()
58
- const { conversation_id, editing_message_id, reply_root_id } = route.params
58
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
59
+ route.params
59
60
  const { data: conversation } = useConversation(route.params)
60
61
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
61
62
  conversation_id,
@@ -73,6 +74,10 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
73
74
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
74
75
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
75
76
  const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
77
+ const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
78
+ const replyHeaderTitle = replyRootAuthorFirstName
79
+ ? `Reply to ${replyRootAuthorFirstName}`
80
+ : 'Reply'
76
81
 
77
82
  const listRef = useRef<FlatList>(null)
78
83
  const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
@@ -89,14 +94,18 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
89
94
  }, [])
90
95
 
91
96
  useEffect(() => {
92
- if (reply_root_id) return
93
-
94
- navigation.setParams({
95
- title: title,
96
- badge: badges?.[0],
97
- deleted: conversation?.deleted,
98
- })
99
- }, [navigation, title, badges, conversation?.deleted, reply_root_id])
97
+ if (reply_root_id) {
98
+ navigation.setParams({
99
+ title: replyHeaderTitle,
100
+ })
101
+ } else {
102
+ navigation.setParams({
103
+ title: title,
104
+ badge: badges?.[0],
105
+ deleted: conversation?.deleted,
106
+ })
107
+ }
108
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle])
100
109
 
101
110
  if (!conversation || conversation.deleted) {
102
111
  return (
@@ -167,6 +176,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
167
176
  {showLeaderDisabledReplyBanner && <LeaderDisabledRepliesBanner />}
168
177
  {canReply ? (
169
178
  <MessageForm.Root
179
+ replyRootAuthorFirstName={replyRootAuthorFirstName}
170
180
  conversation={conversation}
171
181
  replyRootId={reply_root_id}
172
182
  currentlyEditingMessage={currentlyEditingMessage}
@@ -23,13 +23,20 @@ export const MessageActionsScreenOptions = getFormSheetScreenOptions({
23
23
 
24
24
  export type MessageActionsScreenProps = StaticScreenProps<{
25
25
  message_id: string
26
+ reply_root_author_name?: string
26
27
  conversation_id: number
27
28
  canDeleteNonAuthoredMessages?: boolean
28
29
  inReplyScreen?: boolean
29
30
  }>
30
31
 
31
32
  export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
32
- const { conversation_id, message_id, canDeleteNonAuthoredMessages, inReplyScreen } = route.params
33
+ const {
34
+ conversation_id,
35
+ message_id,
36
+ canDeleteNonAuthoredMessages,
37
+ inReplyScreen,
38
+ reply_root_author_name,
39
+ } = route.params
33
40
 
34
41
  const { messages, refetch } = useConversationMessages(
35
42
  { conversation_id },
@@ -46,6 +53,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
46
53
  canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages || false}
47
54
  refetchMessages={refetch}
48
55
  inReplyScreen={inReplyScreen}
56
+ replyRootAuthorName={reply_root_author_name}
49
57
  />
50
58
  )
51
59
  }
@@ -56,12 +64,14 @@ function MessageActionsScreenContent({
56
64
  canDeleteNonAuthoredMessages,
57
65
  refetchMessages,
58
66
  inReplyScreen,
67
+ replyRootAuthorName,
59
68
  }: {
60
69
  message: MessageResource
61
70
  conversation_id: number
62
71
  canDeleteNonAuthoredMessages: boolean
63
72
  refetchMessages: () => void
64
73
  inReplyScreen?: boolean
74
+ replyRootAuthorName?: string
65
75
  }) {
66
76
  const navigation = useNavigation()
67
77
  const apiClient = useApiClient()
@@ -95,10 +105,10 @@ function MessageActionsScreenContent({
95
105
  navigation.navigate('ConversationReply', {
96
106
  conversation_id,
97
107
  reply_root_id: message.replyRootId || message.id,
98
- // TODO: Update title param with reply root author
108
+ reply_root_author_name: replyRootAuthorName,
99
109
  })
100
110
  })
101
- }, [navigation, conversation_id, message.id, message.replyRootId])
111
+ }, [navigation, conversation_id, message.id, message.replyRootId, replyRootAuthorName])
102
112
 
103
113
  const handleCopyPress = () => {
104
114
  Clipboard.setStringAsync(message?.text || attachmentForCopy() || '')