@planningcenter/chat-react-native 3.5.0-rc.0 → 3.5.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 (235) hide show
  1. package/build/components/conversation/attachments/audio_attachment.d.ts +2 -2
  2. package/build/components/conversation/attachments/audio_attachment.d.ts.map +1 -1
  3. package/build/components/conversation/attachments/audio_attachment.js +2 -2
  4. package/build/components/conversation/attachments/audio_attachment.js.map +1 -1
  5. package/build/components/conversation/attachments/expanded_link.d.ts +2 -2
  6. package/build/components/conversation/attachments/expanded_link.d.ts.map +1 -1
  7. package/build/components/conversation/attachments/expanded_link.js +2 -2
  8. package/build/components/conversation/attachments/expanded_link.js.map +1 -1
  9. package/build/components/conversation/attachments/generic_file_attachment.d.ts +2 -2
  10. package/build/components/conversation/attachments/generic_file_attachment.d.ts.map +1 -1
  11. package/build/components/conversation/attachments/generic_file_attachment.js +2 -2
  12. package/build/components/conversation/attachments/generic_file_attachment.js.map +1 -1
  13. package/build/components/conversation/attachments/giphy_attachment.d.ts +2 -2
  14. package/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -1
  15. package/build/components/conversation/attachments/giphy_attachment.js +18 -5
  16. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
  17. package/build/components/conversation/attachments/image_attachment.d.ts +2 -2
  18. package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
  19. package/build/components/conversation/attachments/image_attachment.js +3 -3
  20. package/build/components/conversation/attachments/image_attachment.js.map +1 -1
  21. package/build/components/conversation/attachments/video_attachment.d.ts +2 -2
  22. package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -1
  23. package/build/components/conversation/attachments/video_attachment.js +2 -2
  24. package/build/components/conversation/attachments/video_attachment.js.map +1 -1
  25. package/build/components/conversation/empty_conversation_blank_state.js +1 -1
  26. package/build/components/conversation/empty_conversation_blank_state.js.map +1 -1
  27. package/build/components/conversation/message.d.ts +5 -2
  28. package/build/components/conversation/message.d.ts.map +1 -1
  29. package/build/components/conversation/message.js +11 -9
  30. package/build/components/conversation/message.js.map +1 -1
  31. package/build/components/conversation/message_attachments.d.ts +3 -2
  32. package/build/components/conversation/message_attachments.d.ts.map +1 -1
  33. package/build/components/conversation/message_attachments.js +9 -9
  34. package/build/components/conversation/message_attachments.js.map +1 -1
  35. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  36. package/build/components/conversations/conversation_actions.js +1 -1
  37. package/build/components/conversations/conversation_actions.js.map +1 -1
  38. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  39. package/build/components/conversations/conversation_preview.js +13 -3
  40. package/build/components/conversations/conversation_preview.js.map +1 -1
  41. package/build/components/conversations/conversations.js +3 -3
  42. package/build/components/conversations/conversations.js.map +1 -1
  43. package/build/components/display/action_button.d.ts +4 -1
  44. package/build/components/display/action_button.d.ts.map +1 -1
  45. package/build/components/display/action_button.js +20 -5
  46. package/build/components/display/action_button.js.map +1 -1
  47. package/build/components/display/avatar.d.ts +4 -1
  48. package/build/components/display/avatar.d.ts.map +1 -1
  49. package/build/components/display/avatar.js +3 -2
  50. package/build/components/display/avatar.js.map +1 -1
  51. package/build/components/display/avatar_group.d.ts +4 -1
  52. package/build/components/display/avatar_group.d.ts.map +1 -1
  53. package/build/components/display/avatar_group.js +6 -3
  54. package/build/components/display/avatar_group.js.map +1 -1
  55. package/build/components/display/blank_state.d.ts +7 -2
  56. package/build/components/display/blank_state.d.ts.map +1 -1
  57. package/build/components/display/blank_state.js +6 -5
  58. package/build/components/display/blank_state.js.map +1 -1
  59. package/build/components/display/child_notice.d.ts +2 -1
  60. package/build/components/display/child_notice.d.ts.map +1 -1
  61. package/build/components/display/child_notice.js +4 -4
  62. package/build/components/display/child_notice.js.map +1 -1
  63. package/build/components/display/heading.d.ts +2 -3
  64. package/build/components/display/heading.d.ts.map +1 -1
  65. package/build/components/display/heading.js.map +1 -1
  66. package/build/components/display/icon.d.ts +2 -1
  67. package/build/components/display/icon.d.ts.map +1 -1
  68. package/build/components/display/icon.js +3 -0
  69. package/build/components/display/icon.js.map +1 -1
  70. package/build/components/display/icon_button.d.ts +4 -0
  71. package/build/components/display/icon_button.d.ts.map +1 -1
  72. package/build/components/display/icon_button.js +32 -0
  73. package/build/components/display/icon_button.js.map +1 -1
  74. package/build/components/display/image_attachment_preview.d.ts +14 -0
  75. package/build/components/display/image_attachment_preview.d.ts.map +1 -0
  76. package/build/components/display/image_attachment_preview.js +42 -0
  77. package/build/components/display/image_attachment_preview.js.map +1 -0
  78. package/build/components/display/index.d.ts +2 -0
  79. package/build/components/display/index.d.ts.map +1 -1
  80. package/build/components/display/index.js +2 -0
  81. package/build/components/display/index.js.map +1 -1
  82. package/build/components/display/keyboard_view.d.ts +2 -1
  83. package/build/components/display/keyboard_view.d.ts.map +1 -1
  84. package/build/components/display/keyboard_view.js +13 -11
  85. package/build/components/display/keyboard_view.js.map +1 -1
  86. package/build/components/display/person.d.ts.map +1 -1
  87. package/build/components/display/person.js +4 -1
  88. package/build/components/display/person.js.map +1 -1
  89. package/build/components/display/platform_modal_header_buttons.d.ts +15 -0
  90. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -0
  91. package/build/components/display/platform_modal_header_buttons.js +43 -0
  92. package/build/components/display/platform_modal_header_buttons.js.map +1 -0
  93. package/build/components/display/video_attachment_preview.d.ts +8 -0
  94. package/build/components/display/video_attachment_preview.d.ts.map +1 -0
  95. package/build/components/display/video_attachment_preview.js +71 -0
  96. package/build/components/display/video_attachment_preview.js.map +1 -0
  97. package/build/components/group_conversation_list.d.ts +1 -1
  98. package/build/components/group_conversation_list.d.ts.map +1 -1
  99. package/build/components/group_conversation_list.js +8 -6
  100. package/build/components/group_conversation_list.js.map +1 -1
  101. package/build/components/page/error_boundary.d.ts.map +1 -1
  102. package/build/components/page/error_boundary.js +4 -1
  103. package/build/components/page/error_boundary.js.map +1 -1
  104. package/build/components/primitive/avatar_primitive.d.ts +6 -1
  105. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  106. package/build/components/primitive/avatar_primitive.js +24 -3
  107. package/build/components/primitive/avatar_primitive.js.map +1 -1
  108. package/build/contexts/chat_context.d.ts +1 -0
  109. package/build/contexts/chat_context.d.ts.map +1 -1
  110. package/build/contexts/chat_context.js +5 -3
  111. package/build/contexts/chat_context.js.map +1 -1
  112. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +14 -196
  113. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts.map +1 -1
  114. package/build/hooks/groups/use_group_members_for_new_conversation.js +2 -2
  115. package/build/hooks/groups/use_group_members_for_new_conversation.js.map +1 -1
  116. package/build/hooks/use_app_state.d.ts +2 -0
  117. package/build/hooks/use_app_state.d.ts.map +1 -0
  118. package/build/hooks/use_app_state.js +16 -0
  119. package/build/hooks/use_app_state.js.map +1 -0
  120. package/build/hooks/use_conversation.d.ts.map +1 -1
  121. package/build/hooks/use_conversation.js +7 -1
  122. package/build/hooks/use_conversation.js.map +1 -1
  123. package/build/hooks/use_conversations_actions.d.ts +9 -9
  124. package/build/hooks/use_conversations_actions.js +4 -4
  125. package/build/hooks/use_conversations_actions.js.map +1 -1
  126. package/build/hooks/use_conversations_cache.d.ts.map +1 -1
  127. package/build/hooks/use_conversations_cache.js +8 -0
  128. package/build/hooks/use_conversations_cache.js.map +1 -1
  129. package/build/hooks/use_giphy.js.map +1 -1
  130. package/build/hooks/use_jolt.d.ts +2 -2
  131. package/build/hooks/use_jolt.d.ts.map +1 -1
  132. package/build/hooks/use_jolt.js +30 -7
  133. package/build/hooks/use_jolt.js.map +1 -1
  134. package/build/hooks/use_report_bug_action.d.ts +5 -0
  135. package/build/hooks/use_report_bug_action.d.ts.map +1 -0
  136. package/build/hooks/use_report_bug_action.js +30 -0
  137. package/build/hooks/use_report_bug_action.js.map +1 -0
  138. package/build/navigation/index.d.ts +5 -0
  139. package/build/navigation/index.d.ts.map +1 -1
  140. package/build/navigation/index.js +5 -0
  141. package/build/navigation/index.js.map +1 -1
  142. package/build/screens/attachment_actions/attachment_actions_screen.d.ts +6 -2
  143. package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
  144. package/build/screens/attachment_actions/attachment_actions_screen.js +16 -16
  145. package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
  146. package/build/screens/attachment_actions/hooks/useDeleteAttachment.d.ts +10 -0
  147. package/build/screens/attachment_actions/hooks/useDeleteAttachment.d.ts.map +1 -0
  148. package/build/screens/attachment_actions/hooks/useDeleteAttachment.js +29 -0
  149. package/build/screens/attachment_actions/hooks/useDeleteAttachment.js.map +1 -0
  150. package/build/screens/bug_report_screen.d.ts +5 -0
  151. package/build/screens/bug_report_screen.d.ts.map +1 -0
  152. package/build/screens/bug_report_screen.js +237 -0
  153. package/build/screens/bug_report_screen.js.map +1 -0
  154. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  155. package/build/screens/conversation_details_screen.js +29 -10
  156. package/build/screens/conversation_details_screen.js.map +1 -1
  157. package/build/screens/conversation_new/components/form_list.d.ts +3 -1
  158. package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
  159. package/build/screens/conversation_new/components/form_list.js +6 -2
  160. package/build/screens/conversation_new/components/form_list.js.map +1 -1
  161. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  162. package/build/screens/conversation_new/components/groups_form.js +3 -6
  163. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  164. package/build/screens/conversation_new/components/services_form.js +1 -1
  165. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  166. package/build/screens/conversation_screen.d.ts.map +1 -1
  167. package/build/screens/conversation_screen.js +3 -5
  168. package/build/screens/conversation_screen.js.map +1 -1
  169. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  170. package/build/screens/conversations/conversations_screen.js +7 -2
  171. package/build/screens/conversations/conversations_screen.js.map +1 -1
  172. package/build/screens/design_system_screen.d.ts.map +1 -1
  173. package/build/screens/design_system_screen.js +29 -4
  174. package/build/screens/design_system_screen.js.map +1 -1
  175. package/build/types/resources/denormalized_attachment_resource.d.ts +2 -2
  176. package/build/types/resources/denormalized_attachment_resource.js.map +1 -1
  177. package/build/types/resources/member_ability.d.ts +1 -0
  178. package/build/types/resources/member_ability.d.ts.map +1 -1
  179. package/build/types/resources/member_ability.js.map +1 -1
  180. package/build/utils/request/conversation.d.ts +2 -1
  181. package/build/utils/request/conversation.d.ts.map +1 -1
  182. package/build/utils/request/conversation.js.map +1 -1
  183. package/package.json +2 -2
  184. package/src/components/conversation/attachments/audio_attachment.tsx +3 -3
  185. package/src/components/conversation/attachments/expanded_link.tsx +3 -8
  186. package/src/components/conversation/attachments/generic_file_attachment.tsx +3 -3
  187. package/src/components/conversation/attachments/giphy_attachment.tsx +23 -8
  188. package/src/components/conversation/attachments/image_attachment.tsx +4 -4
  189. package/src/components/conversation/attachments/video_attachment.tsx +3 -3
  190. package/src/components/conversation/empty_conversation_blank_state.tsx +1 -1
  191. package/src/components/conversation/message.tsx +20 -8
  192. package/src/components/conversation/message_attachments.tsx +18 -11
  193. package/src/components/conversations/conversation_actions.tsx +1 -0
  194. package/src/components/conversations/conversation_preview.tsx +24 -3
  195. package/src/components/conversations/conversations.tsx +3 -3
  196. package/src/components/display/action_button.tsx +32 -4
  197. package/src/components/display/avatar.tsx +17 -2
  198. package/src/components/display/avatar_group.tsx +19 -3
  199. package/src/components/display/blank_state.tsx +21 -8
  200. package/src/components/display/child_notice.tsx +7 -4
  201. package/src/components/display/heading.tsx +2 -2
  202. package/src/components/display/icon.tsx +4 -1
  203. package/src/components/display/icon_button.tsx +32 -0
  204. package/src/components/display/image_attachment_preview.tsx +74 -0
  205. package/src/components/display/index.ts +2 -0
  206. package/src/components/display/keyboard_view.tsx +16 -17
  207. package/src/components/display/person.tsx +4 -1
  208. package/src/components/display/platform_modal_header_buttons.tsx +83 -0
  209. package/src/components/display/video_attachment_preview.tsx +105 -0
  210. package/src/components/group_conversation_list.tsx +14 -7
  211. package/src/components/page/error_boundary.tsx +4 -1
  212. package/src/components/primitive/avatar_primitive.tsx +42 -4
  213. package/src/contexts/chat_context.tsx +6 -3
  214. package/src/hooks/groups/use_group_members_for_new_conversation.ts +6 -4
  215. package/src/hooks/use_app_state.ts +20 -0
  216. package/src/hooks/use_conversation.ts +7 -1
  217. package/src/hooks/use_conversations_actions.ts +5 -5
  218. package/src/hooks/use_conversations_cache.ts +7 -0
  219. package/src/hooks/use_giphy.ts +2 -2
  220. package/src/hooks/use_jolt.ts +38 -9
  221. package/src/hooks/use_report_bug_action.ts +37 -0
  222. package/src/navigation/index.tsx +5 -0
  223. package/src/screens/attachment_actions/attachment_actions_screen.tsx +35 -16
  224. package/src/screens/attachment_actions/hooks/useDeleteAttachment.tsx +46 -0
  225. package/src/screens/bug_report_screen.tsx +334 -0
  226. package/src/screens/conversation_details_screen.tsx +41 -7
  227. package/src/screens/conversation_new/components/form_list.tsx +17 -1
  228. package/src/screens/conversation_new/components/groups_form.tsx +6 -5
  229. package/src/screens/conversation_new/components/services_form.tsx +3 -1
  230. package/src/screens/conversation_screen.tsx +9 -5
  231. package/src/screens/conversations/conversations_screen.tsx +11 -1
  232. package/src/screens/design_system_screen.tsx +68 -3
  233. package/src/types/resources/denormalized_attachment_resource.ts +2 -2
  234. package/src/types/resources/member_ability.ts +1 -0
  235. package/src/utils/request/conversation.ts +2 -1
@@ -18,6 +18,8 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
18
18
  const conversationQueryKey = getRequestQueryKey(conversationsRequestArgs)
19
19
 
20
20
  const fetchConversation = async (id: number) => {
21
+ if (!id) return
22
+
21
23
  const { data: argsData } = conversationsRequestArgs
22
24
  const { data } = await apiClient.chat.get<ApiResource<ConversationResource>>({
23
25
  url: `/me/conversations/${id}`,
@@ -31,6 +33,8 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
31
33
  }
32
34
 
33
35
  const updateOrCreate = async (conversation: ConversationResource) => {
36
+ if (!conversation.id) return
37
+
34
38
  queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
35
39
  updateOrCreateRecordInPagesData({
36
40
  data: prev,
@@ -53,11 +57,14 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
53
57
 
54
58
  const fetchAndUpdateOrCreate = async ({ id }: { id: number }) => {
55
59
  const conversation: ConversationResource = await fetchConversation(id).catch(c => c)
60
+ if (!conversation.id) return
56
61
 
57
62
  updateOrCreate(conversation)
58
63
  }
59
64
 
60
65
  const handleConversationDestroy = ({ id }: { id: number }) => {
66
+ if (!id) return
67
+
61
68
  queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
62
69
  deleteRecordInPagesData({
63
70
  data: prev,
@@ -19,8 +19,8 @@ interface GiphyGif {
19
19
 
20
20
  interface GiphyVariant {
21
21
  url: string
22
- width: number
23
- height: number
22
+ width: string
23
+ height: string
24
24
  size: string
25
25
  frames: string
26
26
  }
@@ -8,10 +8,11 @@ import {
8
8
  JoltSubscription,
9
9
  } from '@planningcenter/jolt-client/dist/types/JoltSubscription'
10
10
  import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
11
- import { useEffect, useMemo, useState } from 'react'
11
+ import { useCallback, useEffect, useMemo } from 'react'
12
12
  import { useChatContext } from '../contexts/chat_context'
13
13
  import { ApiResource } from '../types'
14
14
  import { Client, Uri } from '../utils'
15
+ import { useAppState } from './use_app_state'
15
16
 
16
17
  interface JoltResponse {
17
18
  type: 'JoltToken'
@@ -118,23 +119,51 @@ export const useJoltClient = (): JoltClient | undefined => {
118
119
  return joltClient
119
120
  }
120
121
 
121
- export function useJoltChannel(channelName: string) {
122
- const [joltChannel, setJoltChannel] = useState<JoltSubscription>()
122
+ export function useJoltChannel(channelName: string): JoltSubscription | undefined | null {
123
123
  const jolt = useJoltClient()
124
+ const appState = useAppState()
125
+ const queryClient = useQueryClient()
124
126
 
125
- useEffect(() => {
126
- if (!jolt) return () => {}
127
- setJoltChannel(jolt.subscribe(channelName))
128
- return () => jolt.unsubscribe(channelName)
127
+ const handleSubscribe = useCallback(async () => {
128
+ if (!jolt) return null
129
+
130
+ // If the subscription already exists, we don't need to subscribe again
131
+ const alreadySubscribed = jolt.subscriptions.find(c => c.channel === channelName)
132
+ if (alreadySubscribed) return alreadySubscribed
133
+
134
+ return jolt.subscribe(channelName)
129
135
  }, [channelName, jolt])
130
136
 
131
- return joltChannel
137
+ const { data: subscription } = useQuery<JoltSubscription | null>({
138
+ queryKey: ['jolt-subscription', channelName],
139
+ queryFn: handleSubscribe,
140
+ enabled: Boolean(jolt) && appState !== 'background',
141
+ })
142
+
143
+ const handleUnsubscribe = useCallback(() => {
144
+ queryClient.removeQueries({
145
+ queryKey: ['jolt-subscription', channelName],
146
+ exact: true,
147
+ })
148
+
149
+ jolt?.unsubscribe(channelName)
150
+ }, [queryClient, channelName, jolt])
151
+
152
+ useEffect(() => {
153
+ if (appState !== 'background') return handleUnsubscribe
154
+
155
+ handleUnsubscribe()
156
+
157
+ return () => null
158
+ }, [appState, handleUnsubscribe])
159
+
160
+ return subscription
132
161
  }
133
162
 
134
163
  type UserCallbackFn<T> = (_event: T) => void
135
164
 
136
165
  export function useJoltEvent<T extends CustomMessage>(
137
- channel: JoltSubscription | undefined,
166
+ channel: JoltSubscription | undefined | null,
138
167
  eventName: string,
139
168
  callback: UserCallbackFn<T>
140
169
  ) {
@@ -0,0 +1,37 @@
1
+ import { useMutation } from '@tanstack/react-query'
2
+ import { useApiClient } from './use_api_client'
3
+ import DeviceInfo from 'react-native-device-info'
4
+ const brand = DeviceInfo.getBrand()
5
+ const model = DeviceInfo.getModel()
6
+ const systemName = DeviceInfo.getSystemName()
7
+ const systemVersion = DeviceInfo.getSystemVersion()
8
+ const readableVersion = DeviceInfo.getReadableVersion()
9
+ const appName = DeviceInfo.getApplicationName()
10
+
11
+ export const useReportBugAction = () => {
12
+ const apiClient = useApiClient()
13
+
14
+ return useMutation({
15
+ mutationFn: async ({
16
+ description,
17
+ attachmentIds,
18
+ }: {
19
+ description: string
20
+ attachmentIds: string[]
21
+ }) => {
22
+ return apiClient.chat.post({
23
+ url: `/me/report_bug`,
24
+ data: {
25
+ data: {
26
+ type: '',
27
+ attributes: {
28
+ description,
29
+ device_info: `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,
30
+ attachment_ids: attachmentIds,
31
+ },
32
+ },
33
+ },
34
+ })
35
+ },
36
+ })
37
+ }
@@ -33,6 +33,7 @@ import { ConversationSelectGroupRecipientsScreen } from '../screens/conversation
33
33
  import { ConversationSelectTeamsILeadRecipientsScreen } from '../screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen'
34
34
  import { AttachmentActionsScreenOptions } from '../screens/attachment_actions/attachment_actions_screen'
35
35
  import { AttachmentActionsScreen } from '../screens/attachment_actions/attachment_actions_screen'
36
+ import { BugReportScreen, BugReportScreenOptions } from '../screens/bug_report_screen'
36
37
 
37
38
  export const NewConversationStack = createNativeStackNavigator({
38
39
  initialRouteName: 'ConversationSelectRecipients',
@@ -179,6 +180,10 @@ export const ChatStack = createNativeStackNavigator({
179
180
  screen: ReactionsScreen,
180
181
  options: ReactionsScreenOptions,
181
182
  },
183
+ BugReport: {
184
+ screen: BugReportScreen,
185
+ options: BugReportScreenOptions,
186
+ },
182
187
  NotFound: {
183
188
  screen: NotFound,
184
189
  options: {
@@ -1,30 +1,44 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
3
  import { Linking } from 'react-native'
4
- import { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'
5
4
  import ActionsFormSheet, {
6
5
  BaseActionsFormSheetOptions,
7
6
  } from '../../components/primitive/actions_form_sheet'
7
+ import { useDeleteAttachment } from './hooks/useDeleteAttachment'
8
8
 
9
9
  export const AttachmentActionsScreenOptions = BaseActionsFormSheetOptions
10
10
 
11
11
  export type AttachmentActionsScreenProps = StaticScreenProps<{
12
- attachment: DenormalizedAttachmentResource
12
+ attachmentId: string
13
+ attachmentContentType: string
14
+ attachmentUrl: string
15
+ conversation_id: number
16
+ canDeleteNonAuthoredMessages: boolean
17
+ myMessage: boolean
13
18
  }>
14
19
 
15
20
  export function AttachmentActionsScreen({ route }: AttachmentActionsScreenProps) {
16
21
  const navigation = useNavigation()
17
- const { attachment } = route.params
22
+ const {
23
+ attachmentId,
24
+ attachmentContentType,
25
+ attachmentUrl,
26
+ conversation_id,
27
+ canDeleteNonAuthoredMessages,
28
+ myMessage,
29
+ } = route.params
18
30
 
19
- const attachmentName = getAttachmentLabelName(attachment)
31
+ const attachmentName = getAttachmentLabelName(attachmentContentType)
32
+ const canDelete = myMessage || canDeleteNonAuthoredMessages
20
33
 
21
- const handleSaveAttachment = () => {
22
- if (attachment.type === 'giphy') {
23
- Linking.openURL(attachment.titleLink)
24
- } else {
25
- Linking.openURL(attachment.attributes.url)
26
- }
34
+ const { handleDeleteAttachment } = useDeleteAttachment({
35
+ conversation_id,
36
+ attachmentId,
37
+ attachmentName,
38
+ })
27
39
 
40
+ const handleOpenInBrowser = () => {
41
+ Linking.openURL(attachmentUrl)
28
42
  navigation.goBack()
29
43
  }
30
44
 
@@ -36,20 +50,25 @@ export function AttachmentActionsScreen({ route }: AttachmentActionsScreenProps)
36
50
  accessibilityRole="link"
37
51
  accessibilityLabel={`Open ${attachmentName} in browser`}
38
52
  accessibilityHint={`${attachmentName} can be downloaded and shared through the browser.`}
39
- onPress={handleSaveAttachment}
53
+ onPress={handleOpenInBrowser}
40
54
  />
55
+ {canDelete && (
56
+ <ActionsFormSheet.Action
57
+ appearance="danger"
58
+ iconName="publishing.trash"
59
+ title={`Delete ${attachmentName}`}
60
+ onPress={handleDeleteAttachment}
61
+ />
62
+ )}
41
63
  </ActionsFormSheet.Root>
42
64
  )
43
65
  }
44
66
 
45
- function getAttachmentLabelName(attachment: DenormalizedAttachmentResource) {
46
- if (attachment.type === 'giphy') return 'giphy'
47
- if (attachment.type === 'ExpandedLink') return 'link'
48
-
49
- const { contentType } = attachment.attributes
67
+ function getAttachmentLabelName(contentType: string) {
50
68
  if (contentType.startsWith('image/')) return 'image'
51
69
  if (contentType.startsWith('video/')) return 'video'
52
70
  if (contentType.startsWith('audio/')) return 'audio'
71
+ if (contentType === 'application/pdf') return 'PDF'
53
72
  if (contentType.startsWith('application/')) return 'file'
54
73
 
55
74
  return ''
@@ -0,0 +1,46 @@
1
+ import { Alert } from 'react-native'
2
+ import { useNavigation } from '@react-navigation/native'
3
+ import { useMutation } from '@tanstack/react-query'
4
+ import { useApiClient } from '../../../hooks/use_api_client'
5
+ import { useCallback } from 'react'
6
+ import { useConversationMessages } from '../../../hooks/use_conversation_messages'
7
+
8
+ type UseDeleteAttachmentProps = {
9
+ conversation_id: number
10
+ attachmentId: string | undefined
11
+ attachmentName: string
12
+ }
13
+
14
+ export function useDeleteAttachment({
15
+ conversation_id,
16
+ attachmentId,
17
+ attachmentName,
18
+ }: UseDeleteAttachmentProps) {
19
+ const apiClient = useApiClient()
20
+ const navigation = useNavigation()
21
+ const { refetch } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
22
+
23
+ const deleteAttachment = useCallback(() => {
24
+ const url = `/me/conversations/${conversation_id}/message_attachments/${attachmentId}`
25
+
26
+ return apiClient.chat.delete({ url })
27
+ }, [apiClient, conversation_id, attachmentId])
28
+
29
+ const { mutate: handleDeleteAttachment } = useMutation({
30
+ mutationFn: deleteAttachment,
31
+ onSuccess: () => {
32
+ refetch()
33
+ navigation.goBack()
34
+ },
35
+ onError: () => {
36
+ Alert.alert(
37
+ 'Oops',
38
+ `We were unable to delete this ${attachmentName} attachment. Please try again.`
39
+ )
40
+ },
41
+ })
42
+
43
+ return {
44
+ handleDeleteAttachment,
45
+ }
46
+ }
@@ -0,0 +1,334 @@
1
+ import React, { useCallback, useLayoutEffect, useState } from 'react'
2
+ import { View, StyleSheet, TextInput, Linking, ScrollView } from 'react-native'
3
+ import type {
4
+ NativeStackNavigationOptions,
5
+ NativeStackScreenProps,
6
+ } from '@react-navigation/native-stack'
7
+ import { useNavigation } from '@react-navigation/native'
8
+ import {
9
+ Button,
10
+ Spinner,
11
+ Text,
12
+ TextInlineButton,
13
+ ImageAttachmentPreview,
14
+ BlankState,
15
+ } from '../components'
16
+ import { useTheme } from '../hooks'
17
+ import { useUploadClient } from '../hooks/use_upload_client'
18
+ import { ImagePicker, ImagePickerResult, platformFontWeightBold } from '../utils'
19
+ import { useReportBugAction } from '../hooks/use_report_bug_action'
20
+ import {
21
+ HeaderCancelButton,
22
+ HeaderSubmitButton,
23
+ } from '../components/display/platform_modal_header_buttons'
24
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
25
+ import { DefaultLoading } from '../components/page/loading'
26
+ import { startsWith } from 'lodash'
27
+ import { VideoAttachmentPreview } from '../components/display/video_attachment_preview'
28
+
29
+ const MAX_DESCRIPTION_LENGTH = 5000
30
+
31
+ export const BugReportScreenOptions = ({
32
+ navigation,
33
+ }: NativeStackScreenProps<any>): NativeStackNavigationOptions => ({
34
+ presentation: 'modal',
35
+ title: 'Report a bug',
36
+ headerLeft: () => <HeaderCancelButton title="Cancel" onPress={() => navigation.goBack()} />,
37
+ })
38
+
39
+ interface Attachment {
40
+ id: string
41
+ name: string
42
+ type: string
43
+ }
44
+
45
+ export function BugReportScreen() {
46
+ const styles = useStyles()
47
+ const navigation = useNavigation()
48
+ const [description, setDescription] = useState('')
49
+ const uploadApi = useUploadClient()
50
+ const [uploading, setUploading] = useState(false)
51
+ const [attachment, setAttachment] = useState<Attachment | null>(null)
52
+ const [uploadError, setUploadError] = useState<string | null>(null)
53
+ const mutation = useReportBugAction()
54
+ const { mutate, status } = mutation
55
+ const formValid = description.trim().length > 0 && status === 'idle' && !uploading
56
+ const [imagePreviewURI, setImagePreviewURI] = useState<string>('')
57
+
58
+ const nearMaxDescription = description.length >= MAX_DESCRIPTION_LENGTH - 100
59
+ const isImageAttachment = startsWith(attachment?.type, 'image/')
60
+
61
+ const handleSubmit = useCallback(() => {
62
+ mutate({
63
+ description,
64
+ attachmentIds: attachment ? [attachment.id] : [],
65
+ })
66
+ }, [attachment, description, mutate])
67
+
68
+ const handleRemoveAttachment = useCallback(() => {
69
+ setAttachment(null)
70
+ setImagePreviewURI('')
71
+ }, [])
72
+
73
+ function uploadImagePickerResult(result: ImagePickerResult) {
74
+ if (result.canceled || result.assets.length === 0) {
75
+ setUploading(false)
76
+ return
77
+ }
78
+
79
+ const asset = result.assets[0]
80
+
81
+ uploadApi
82
+ ?.uploadFile({
83
+ uri: asset.uri,
84
+ name: asset.fileName as string,
85
+ type: asset.mimeType as string,
86
+ })
87
+ .then(uploadedResource => {
88
+ setUploading(false)
89
+ setImagePreviewURI(asset.uri)
90
+ setAttachment({
91
+ id: uploadedResource.id,
92
+ name: asset.fileName || 'File',
93
+ type: asset.mimeType as string,
94
+ })
95
+ })
96
+ .catch(() => {
97
+ setUploading(false)
98
+ setUploadError(`Failed to upload attachment`)
99
+ })
100
+ }
101
+
102
+ function pickImage() {
103
+ setUploading(true)
104
+ return ImagePicker.openImageLibraryAsync().then(result => uploadImagePickerResult(result))
105
+ }
106
+
107
+ useLayoutEffect(() => {
108
+ navigation.setOptions({
109
+ // eslint-disable-next-line react/no-unstable-nested-components
110
+ headerRight: () => (
111
+ <HeaderSubmitButton title="Submit" onPress={handleSubmit} disabled={!formValid} />
112
+ ),
113
+ })
114
+ }, [formValid, handleSubmit, navigation])
115
+
116
+ if (status === 'pending') {
117
+ return (
118
+ <View style={[styles.container, styles.fullHeight]}>
119
+ <DefaultLoading />
120
+ </View>
121
+ )
122
+ }
123
+
124
+ if (status === 'success') {
125
+ return (
126
+ <BlankState
127
+ iconName="general.checkCircle"
128
+ iconStyle={styles.successIcon}
129
+ title="Thank you!"
130
+ titleStyle={styles.successTitle}
131
+ subtitle="We appreciate you taking the time to help improve chat! We'll take a look at the issue you reported."
132
+ subtitleStyle={styles.successSubtitle}
133
+ buttonProps={{
134
+ title: 'Return to chat',
135
+ onPress: navigation.goBack,
136
+ size: 'lg',
137
+ variant: 'fill',
138
+ }}
139
+ />
140
+ )
141
+ }
142
+
143
+ if (status === 'error') {
144
+ return (
145
+ <View style={styles.container}>
146
+ <Text>
147
+ This is embarrassing, we can't submit your bug report right now. If you still need help or
148
+ would like to submit this bug report another way please{' '}
149
+ <TextInlineButton
150
+ accessibilityRole="link"
151
+ onPress={() =>
152
+ Linking.openURL('https://support.planningcenteronline.com/hc/en-us/requests/new')
153
+ }
154
+ >
155
+ contact our help center
156
+ </TextInlineButton>
157
+ .
158
+ </Text>
159
+ <Button title="Return to chat" onPress={navigation.goBack} variant="fill" size="lg" />
160
+ </View>
161
+ )
162
+ }
163
+
164
+ return (
165
+ <ScrollView contentContainerStyle={styles.container}>
166
+ <Text style={styles.description}>
167
+ Thanks for helping us improve chat! Please provide details about the issue you’ve
168
+ encountered. We read every submission and your feedback helps us improve the experience.
169
+ </Text>
170
+ <View style={styles.textInputContainer}>
171
+ <TextInput
172
+ style={styles.textInput}
173
+ multiline
174
+ placeholder="Details about the problem"
175
+ value={description}
176
+ onChangeText={setDescription}
177
+ maxLength={MAX_DESCRIPTION_LENGTH}
178
+ />
179
+ {nearMaxDescription && (
180
+ <Text variant="footnote">
181
+ {description.length}/{MAX_DESCRIPTION_LENGTH}
182
+ </Text>
183
+ )}
184
+ </View>
185
+
186
+ <View style={styles.attachmentSection}>
187
+ <View style={styles.attachmentHeader}>
188
+ <Text style={styles.attachmentLabel}>
189
+ Attachment <Text style={styles.subLabel}>(optional)</Text>
190
+ </Text>
191
+ {uploading && <Spinner />}
192
+ </View>
193
+ {uploadError && <Text style={styles.attachmentErrorText}>{uploadError}</Text>}
194
+ {attachment ? (
195
+ <View style={styles.attachmentPreviewContainer}>
196
+ {isImageAttachment ? (
197
+ <ImageAttachmentPreview
198
+ uri={imagePreviewURI}
199
+ fileName={attachment.name}
200
+ onClosePress={handleRemoveAttachment}
201
+ />
202
+ ) : (
203
+ <VideoAttachmentPreview
204
+ name={attachment.name}
205
+ onClosePress={handleRemoveAttachment}
206
+ />
207
+ )}
208
+ </View>
209
+ ) : (
210
+ <Button
211
+ title="Attach a screenshot"
212
+ accessibilityHint="Opens your device's image gallery"
213
+ iconNameLeft="general.paperclip"
214
+ onPress={pickImage}
215
+ size="sm"
216
+ variant="outline"
217
+ style={styles.attachButton}
218
+ disabled={uploading || Boolean(attachment)}
219
+ />
220
+ )}
221
+ </View>
222
+
223
+ <View style={styles.footer}>
224
+ <Text variant="footnote">
225
+ We can’t respond to every submission, but we may reach out if we have additional
226
+ questions.
227
+ </Text>
228
+
229
+ <Text variant="footnote">
230
+ For details on how we process your data and ensure its security, please refer to our{' '}
231
+ <TextInlineButton
232
+ accessibilityRole="link"
233
+ variant="footnote"
234
+ onPress={() => Linking.openURL('https://www.planningcenter.com/privacy')}
235
+ >
236
+ Privacy Policy
237
+ </TextInlineButton>
238
+ .
239
+ </Text>
240
+ </View>
241
+ </ScrollView>
242
+ )
243
+ }
244
+
245
+ const useStyles = () => {
246
+ const { colors } = useTheme()
247
+ const { bottom } = useSafeAreaInsets()
248
+ return StyleSheet.create({
249
+ container: {
250
+ padding: 16,
251
+ paddingBottom: bottom + 16,
252
+ gap: 24,
253
+ },
254
+ fullHeight: {
255
+ flex: 1,
256
+ },
257
+ centeredText: {
258
+ textAlign: 'center',
259
+ },
260
+ description: {
261
+ color: colors.textColorDefaultSecondary,
262
+ },
263
+ textInputContainer: {
264
+ borderTopWidth: 1,
265
+ borderBottomWidth: 1,
266
+ borderColor: colors.borderColorDefaultBase,
267
+ paddingVertical: 12,
268
+ paddingHorizontal: 16,
269
+ marginHorizontal: -16,
270
+ gap: 8,
271
+ },
272
+ textInput: {
273
+ color: colors.textColorDefaultPrimary,
274
+ fontSize: 16,
275
+ textAlignVertical: 'top',
276
+ minHeight: 120,
277
+ maxHeight: 200,
278
+ },
279
+ attachmentSection: {
280
+ gap: 8,
281
+ },
282
+ attachmentHeader: {
283
+ flexDirection: 'row',
284
+ gap: 4,
285
+ alignItems: 'center',
286
+ },
287
+ attachmentLabel: { fontWeight: platformFontWeightBold },
288
+ subLabel: { fontWeight: 'normal', color: colors.textColorDefaultSecondary },
289
+ attachmentPreviewContainer: {
290
+ gap: 4,
291
+ },
292
+ attachmentErrorText: {
293
+ color: colors.statusErrorText,
294
+ },
295
+ attachButton: {
296
+ alignSelf: 'flex-start',
297
+ },
298
+ videoPreviewContainer: {
299
+ flexDirection: 'row',
300
+ justifyContent: 'space-between',
301
+ alignItems: 'center',
302
+ gap: 16,
303
+ },
304
+ videoPreviewTextContainer: {
305
+ flexDirection: 'row',
306
+ gap: 4,
307
+ alignItems: 'center',
308
+ flexShrink: 1,
309
+ },
310
+ videoPreviewText: {
311
+ flexShrink: 1,
312
+ color: colors.textColorDefaultSecondary,
313
+ },
314
+ videoPreviewFileIcon: {
315
+ color: colors.iconColorDefaultSecondary,
316
+ },
317
+ videoPreviewRemoveButtonIcon: {
318
+ color: colors.statusErrorIcon,
319
+ },
320
+ footer: { gap: 8 },
321
+ successIcon: {
322
+ color: colors.statusSuccessIcon,
323
+ fontSize: 48,
324
+ },
325
+ successTitle: {
326
+ fontSize: 24,
327
+ marginBottom: 4,
328
+ },
329
+ successSubtitle: {
330
+ fontSize: 16,
331
+ marginBottom: 4,
332
+ },
333
+ })
334
+ }