@planningcenter/chat-react-native 3.36.1 → 3.36.2-qa-726.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 (171) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +3 -3
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/display/action_button.d.ts +3 -1
  5. package/build/components/display/action_button.d.ts.map +1 -1
  6. package/build/components/display/action_button.js +8 -1
  7. package/build/components/display/action_button.js.map +1 -1
  8. package/build/components/display/index.d.ts +1 -0
  9. package/build/components/display/index.d.ts.map +1 -1
  10. package/build/components/display/index.js +1 -0
  11. package/build/components/display/index.js.map +1 -1
  12. package/build/components/index.d.ts +2 -0
  13. package/build/components/index.d.ts.map +1 -1
  14. package/build/components/index.js +2 -0
  15. package/build/components/index.js.map +1 -1
  16. package/build/components/page/component_error_boundary.d.ts +4 -0
  17. package/build/components/page/component_error_boundary.d.ts.map +1 -0
  18. package/build/components/page/component_error_boundary.js +8 -0
  19. package/build/components/page/component_error_boundary.js.map +1 -0
  20. package/build/components/page/error_boundary.d.ts +13 -10
  21. package/build/components/page/error_boundary.d.ts.map +1 -1
  22. package/build/components/page/error_boundary.js +20 -90
  23. package/build/components/page/error_boundary.js.map +1 -1
  24. package/build/components/page/page_error_boundary.d.ts +4 -0
  25. package/build/components/page/page_error_boundary.d.ts.map +1 -0
  26. package/build/components/page/page_error_boundary.js +80 -0
  27. package/build/components/page/page_error_boundary.js.map +1 -0
  28. package/build/hooks/groups/use_group_chat_conversation_payload.d.ts +168 -0
  29. package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -0
  30. package/build/hooks/groups/use_group_chat_conversation_payload.js +23 -0
  31. package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -0
  32. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +0 -4
  33. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts.map +1 -1
  34. package/build/hooks/groups/use_group_members_for_new_conversation.js +6 -18
  35. package/build/hooks/groups/use_group_members_for_new_conversation.js.map +1 -1
  36. package/build/hooks/groups/use_groups_conversation_create.js +1 -1
  37. package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
  38. package/build/hooks/index.d.ts +2 -1
  39. package/build/hooks/index.d.ts.map +1 -1
  40. package/build/hooks/index.js +2 -1
  41. package/build/hooks/index.js.map +1 -1
  42. package/build/hooks/services/use_find_or_create_services_conversation.d.ts +11 -3
  43. package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
  44. package/build/hooks/services/use_find_or_create_services_conversation.js +10 -14
  45. package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
  46. package/build/hooks/services/use_services_chat_conversation_payload.d.ts +164 -0
  47. package/build/hooks/services/use_services_chat_conversation_payload.d.ts.map +1 -0
  48. package/build/hooks/services/use_services_chat_conversation_payload.js +16 -0
  49. package/build/hooks/services/use_services_chat_conversation_payload.js.map +1 -0
  50. package/build/hooks/services/use_team_members_for_new_conversation.d.ts.map +1 -1
  51. package/build/hooks/services/use_team_members_for_new_conversation.js +11 -4
  52. package/build/hooks/services/use_team_members_for_new_conversation.js.map +1 -1
  53. package/build/hooks/use_conversation_validate.d.ts +12 -0
  54. package/build/hooks/use_conversation_validate.d.ts.map +1 -0
  55. package/build/hooks/use_conversation_validate.js +28 -0
  56. package/build/hooks/use_conversation_validate.js.map +1 -0
  57. package/build/hooks/use_enrich_people.d.ts +13 -0
  58. package/build/hooks/use_enrich_people.d.ts.map +1 -0
  59. package/build/hooks/use_enrich_people.js +25 -0
  60. package/build/hooks/use_enrich_people.js.map +1 -0
  61. package/build/hooks/use_features.d.ts +9 -6
  62. package/build/hooks/use_features.d.ts.map +1 -1
  63. package/build/hooks/use_features.js +1 -0
  64. package/build/hooks/use_features.js.map +1 -1
  65. package/build/hooks/use_jolt.d.ts +2 -1
  66. package/build/hooks/use_jolt.d.ts.map +1 -1
  67. package/build/hooks/use_jolt.js.map +1 -1
  68. package/build/hooks/use_product_analytics.d.ts +7 -1
  69. package/build/hooks/use_product_analytics.d.ts.map +1 -1
  70. package/build/hooks/use_product_analytics.js +4 -0
  71. package/build/hooks/use_product_analytics.js.map +1 -1
  72. package/build/index.d.ts +3 -2
  73. package/build/index.d.ts.map +1 -1
  74. package/build/index.js +2 -1
  75. package/build/index.js.map +1 -1
  76. package/build/navigation/screenLayout.d.ts.map +1 -1
  77. package/build/navigation/screenLayout.js +5 -3
  78. package/build/navigation/screenLayout.js.map +1 -1
  79. package/build/screens/conversation_details_screen.js +1 -1
  80. package/build/screens/conversation_details_screen.js.map +1 -1
  81. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  82. package/build/screens/conversation_new/components/groups_form.js +14 -1
  83. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  84. package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
  85. package/build/screens/conversation_new/components/services_form.js +20 -2
  86. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  87. package/build/screens/conversation_screen.d.ts.map +1 -1
  88. package/build/screens/conversation_screen.js +2 -2
  89. package/build/screens/conversation_screen.js.map +1 -1
  90. package/build/screens/conversations/conversations_screen.js +2 -2
  91. package/build/screens/conversations/conversations_screen.js.map +1 -1
  92. package/build/screens/team_conversation_screen.d.ts.map +1 -1
  93. package/build/screens/team_conversation_screen.js +6 -3
  94. package/build/screens/team_conversation_screen.js.map +1 -1
  95. package/build/types/jolt_events/index.d.ts +2 -0
  96. package/build/types/jolt_events/index.d.ts.map +1 -1
  97. package/build/types/jolt_events/index.js.map +1 -1
  98. package/build/types/resources/conversation_validate.d.ts +10 -0
  99. package/build/types/resources/conversation_validate.d.ts.map +1 -0
  100. package/build/types/resources/conversation_validate.js +2 -0
  101. package/build/types/resources/conversation_validate.js.map +1 -0
  102. package/build/types/resources/index.d.ts +1 -0
  103. package/build/types/resources/index.d.ts.map +1 -1
  104. package/build/types/resources/index.js +1 -0
  105. package/build/types/resources/index.js.map +1 -1
  106. package/build/utils/cache/messages_cache.d.ts +1 -1
  107. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  108. package/build/utils/client/instrumented_fetch.d.ts +2 -0
  109. package/build/utils/client/instrumented_fetch.d.ts.map +1 -0
  110. package/build/utils/client/instrumented_fetch.js +64 -0
  111. package/build/utils/client/instrumented_fetch.js.map +1 -0
  112. package/build/utils/client/request_helpers.d.ts.map +1 -1
  113. package/build/utils/client/request_helpers.js +2 -1
  114. package/build/utils/client/request_helpers.js.map +1 -1
  115. package/build/utils/native_adapters/log.d.ts +11 -0
  116. package/build/utils/native_adapters/log.d.ts.map +1 -1
  117. package/build/utils/native_adapters/log.js +9 -0
  118. package/build/utils/native_adapters/log.js.map +1 -1
  119. package/build/utils/performance_tracking.d.ts +1 -1
  120. package/build/utils/performance_tracking.d.ts.map +1 -1
  121. package/build/utils/performance_tracking.js.map +1 -1
  122. package/build/utils/request/get_chat_configuration.d.ts +1 -1
  123. package/build/utils/request/get_chat_configuration.d.ts.map +1 -1
  124. package/build/utils/request/get_features.d.ts +1 -1
  125. package/build/utils/request/get_features.d.ts.map +1 -1
  126. package/build/utils/request/get_message.d.ts +1 -1
  127. package/build/utils/request/get_message.d.ts.map +1 -1
  128. package/build/utils/request/get_messages.d.ts +1 -1
  129. package/build/utils/request/get_messages.d.ts.map +1 -1
  130. package/package.json +3 -2
  131. package/src/__tests__/hooks/use_conversation_validate.test.tsx +117 -0
  132. package/src/__tests__/hooks/use_enrich_people.test.tsx +95 -0
  133. package/src/components/conversation/message.tsx +6 -4
  134. package/src/components/display/action_button.tsx +18 -0
  135. package/src/components/display/index.ts +1 -0
  136. package/src/components/index.tsx +2 -0
  137. package/src/components/page/__tests__/component_error_boundary.test.tsx +46 -0
  138. package/src/components/page/__tests__/error_boundary.test.tsx +93 -0
  139. package/src/components/page/__tests__/page_error_boundary.test.tsx +77 -0
  140. package/src/components/page/component_error_boundary.tsx +13 -0
  141. package/src/components/page/error_boundary.tsx +34 -118
  142. package/src/components/page/page_error_boundary.tsx +112 -0
  143. package/src/hooks/groups/use_group_chat_conversation_payload.ts +38 -0
  144. package/src/hooks/groups/use_group_members_for_new_conversation.ts +9 -23
  145. package/src/hooks/groups/use_groups_conversation_create.ts +1 -1
  146. package/src/hooks/index.ts +2 -1
  147. package/src/hooks/services/use_find_or_create_services_conversation.ts +27 -24
  148. package/src/hooks/services/use_services_chat_conversation_payload.ts +26 -0
  149. package/src/hooks/services/use_team_members_for_new_conversation.ts +18 -7
  150. package/src/hooks/use_conversation_validate.ts +45 -0
  151. package/src/hooks/use_enrich_people.ts +35 -0
  152. package/src/hooks/use_features.ts +5 -2
  153. package/src/hooks/use_jolt.ts +2 -1
  154. package/src/hooks/use_product_analytics.ts +13 -3
  155. package/src/index.tsx +3 -2
  156. package/src/navigation/screenLayout.tsx +6 -3
  157. package/src/screens/conversation_details_screen.tsx +1 -1
  158. package/src/screens/conversation_new/components/groups_form.tsx +17 -1
  159. package/src/screens/conversation_new/components/services_form.tsx +26 -2
  160. package/src/screens/conversation_screen.tsx +2 -1
  161. package/src/screens/conversations/conversations_screen.tsx +2 -2
  162. package/src/screens/team_conversation_screen.tsx +6 -6
  163. package/src/types/jolt_events/index.ts +3 -0
  164. package/src/types/resources/conversation_validate.ts +11 -0
  165. package/src/types/resources/index.ts +1 -0
  166. package/src/utils/client/__tests__/instrumented_fetch.test.ts +84 -0
  167. package/src/utils/client/instrumented_fetch.ts +69 -0
  168. package/src/utils/client/request_helpers.ts +2 -1
  169. package/src/utils/native_adapters/__tests__/log.test.ts +62 -0
  170. package/src/utils/native_adapters/log.ts +22 -0
  171. package/src/utils/performance_tracking.ts +1 -1
@@ -1,137 +1,53 @@
1
- import { useNavigation } from '@react-navigation/native'
2
- import { useQueryErrorResetBoundary } from '@tanstack/react-query'
3
- import React, { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
4
- import { onAuthRefresh } from '../../utils/auth_events'
1
+ import React, { PropsWithChildren } from 'react'
2
+ import { Log } from '../../utils/native_adapters/configuration'
3
+ import { ReportErrorScope } from '../../utils/native_adapters/log'
5
4
  import { ResponseError } from '../../utils/response_error'
6
- import BlankState from '../primitive/blank_state_primitive'
5
+
6
+ export type ErrorBoundaryFallback =
7
+ | React.ReactNode
8
+ | ((error: Error, reset: () => void) => React.ReactNode)
9
+
10
+ export type ErrorBoundaryProps = {
11
+ scope?: ReportErrorScope
12
+ screenName?: string
13
+ fallback?: ErrorBoundaryFallback
14
+ }
7
15
 
8
16
  type ErrorBoundaryState = {
9
- error: ResponseError | Error | TypeError | null
10
- unsubscriber: () => void
17
+ error: Error | null
11
18
  }
12
19
 
13
- class ErrorBoundary extends React.Component<PropsWithChildren<{ onReset?: () => void }>> {
14
- state: ErrorBoundaryState = {
15
- error: null,
16
- unsubscriber: () => {},
17
- }
20
+ export class ErrorBoundary extends React.Component<
21
+ PropsWithChildren<ErrorBoundaryProps>,
22
+ ErrorBoundaryState
23
+ > {
24
+ state: ErrorBoundaryState = { error: null }
18
25
 
19
- componentDidCatch(error: any) {
20
- this.handleError(error)
26
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
27
+ return { error }
21
28
  }
22
29
 
23
- handleError(error: any) {
24
- this.setState({ error })
30
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
31
+ if (error instanceof ResponseError) return
32
+
33
+ Log.reportError(error, {
34
+ componentStack: errorInfo.componentStack ?? undefined,
35
+ scope: this.props.scope,
36
+ screenName: this.props.screenName,
37
+ tags: { team: 'chat', package: 'chat-react-native' },
38
+ })
25
39
  }
26
40
 
27
41
  handleReset = () => {
28
- this.props.onReset?.()
29
42
  this.setState({ error: null })
30
43
  }
31
44
 
32
45
  render() {
33
- if (this.state.error) {
34
- return <ErrorView error={this.state.error} onReset={this.handleReset} />
35
- } else {
36
- return this.props.children
37
- }
38
- }
39
- }
46
+ if (!this.state.error) return this.props.children
40
47
 
41
- function ErrorView({ error, onReset }: { error: Error | ResponseError; onReset: () => void }) {
42
- const { reset } = useQueryErrorResetBoundary()
48
+ const { fallback } = this.props
49
+ if (fallback === undefined) return null
43
50
 
44
- const handleReset = useCallback(() => {
45
- reset()
46
- onReset()
47
- }, [reset, onReset])
48
-
49
- useEffect(() => handleReset, [handleReset])
50
-
51
- if (error instanceof ResponseError) {
52
- return <ResponseErrorView response={error.response} onReset={handleReset} />
53
- }
54
-
55
- if (isNetworkError(error)) {
56
- return (
57
- <ErrorContent
58
- heading={'Problem connecting!'}
59
- body={'Check your internet connection and try again.'}
60
- />
61
- )
51
+ return typeof fallback === 'function' ? fallback(this.state.error, this.handleReset) : fallback
62
52
  }
63
-
64
- return <ErrorContent heading={'Oops!'} body={'Something unexpected happened.'} />
65
53
  }
66
-
67
- function isNetworkError(error: ResponseError | Error | TypeError | null) {
68
- const isError = error instanceof Error
69
- const networkFailedMessages = [
70
- 'Network request failed',
71
- 'Network request timed out',
72
- 'Failed to fetch',
73
- ]
74
-
75
- if (!isError) return false
76
-
77
- return new RegExp(networkFailedMessages.join('|'), 'i').test(error.message)
78
- }
79
-
80
- function ResponseErrorView({ response, onReset }: { response: Response; onReset: () => void }) {
81
- const { status } = response
82
-
83
- const heading = useMemo(() => {
84
- switch (status) {
85
- case 403:
86
- return 'Permission required'
87
- case 404:
88
- return 'Content not found'
89
- default:
90
- return 'Oops!'
91
- }
92
- }, [status])
93
-
94
- const body = useMemo(() => {
95
- switch (status) {
96
- case 403:
97
- return 'Contact your administrator for access.'
98
- case 404:
99
- return 'If you believe something should be here, please reach out to your administrator.'
100
- default:
101
- return 'Something unexpected happened.'
102
- }
103
- }, [status])
104
-
105
- useEffect(() => {
106
- if (status !== 401) return
107
-
108
- return onAuthRefresh(onReset)
109
- }, [status, onReset])
110
-
111
- return <ErrorContent heading={heading} body={body} />
112
- }
113
-
114
- function ErrorContent({ heading, body }: { heading: string; body: string }) {
115
- const navigation = useNavigation()
116
-
117
- return (
118
- <BlankState.Root>
119
- <BlankState.Imagery name="people.noTextMessage" />
120
- <BlankState.Content>
121
- <BlankState.Heading>{heading}</BlankState.Heading>
122
- <BlankState.Text>{body}</BlankState.Text>
123
- </BlankState.Content>
124
- <BlankState.Button
125
- title="Go back"
126
- onPress={navigation.goBack}
127
- size="md"
128
- accessibilityRole="link"
129
- />
130
- <BlankState.TextButton onPress={() => navigation.navigate('BugReport')}>
131
- Report a bug
132
- </BlankState.TextButton>
133
- </BlankState.Root>
134
- )
135
- }
136
-
137
- export default ErrorBoundary
@@ -0,0 +1,112 @@
1
+ import { useNavigation } from '@react-navigation/native'
2
+ import { useQueryErrorResetBoundary } from '@tanstack/react-query'
3
+ import React, { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
4
+ import { onAuthRefresh } from '../../utils/auth_events'
5
+ import { ResponseError } from '../../utils/response_error'
6
+ import BlankState from '../primitive/blank_state_primitive'
7
+ import { ErrorBoundary, ErrorBoundaryFallback, ErrorBoundaryProps } from './error_boundary'
8
+
9
+ const renderPageFallback: ErrorBoundaryFallback = (error, reset) => (
10
+ <ErrorView error={error} reset={reset} />
11
+ )
12
+
13
+ export function PageErrorBoundary({ children, ...props }: PropsWithChildren<ErrorBoundaryProps>) {
14
+ return (
15
+ <ErrorBoundary scope="screen" fallback={renderPageFallback} {...props}>
16
+ {children}
17
+ </ErrorBoundary>
18
+ )
19
+ }
20
+
21
+ function ErrorView({ error, reset }: { error: Error; reset: () => void }) {
22
+ const { reset: resetQueries } = useQueryErrorResetBoundary()
23
+
24
+ const handleReset = useCallback(() => {
25
+ resetQueries()
26
+ reset()
27
+ }, [resetQueries, reset])
28
+
29
+ useEffect(() => handleReset, [handleReset])
30
+
31
+ if (error instanceof ResponseError) {
32
+ return <ResponseErrorView response={error.response} onReset={handleReset} />
33
+ }
34
+
35
+ if (isNetworkError(error)) {
36
+ return (
37
+ <ErrorContent
38
+ heading={'Problem connecting!'}
39
+ body={'Check your internet connection and try again.'}
40
+ />
41
+ )
42
+ }
43
+
44
+ return <ErrorContent heading={'Oops!'} body={'Something unexpected happened.'} />
45
+ }
46
+
47
+ function isNetworkError(error: Error) {
48
+ const networkFailedMessages = [
49
+ 'Network request failed',
50
+ 'Network request timed out',
51
+ 'Failed to fetch',
52
+ ]
53
+
54
+ return new RegExp(networkFailedMessages.join('|'), 'i').test(error.message)
55
+ }
56
+
57
+ function ResponseErrorView({ response, onReset }: { response: Response; onReset: () => void }) {
58
+ const { status } = response
59
+
60
+ const heading = useMemo(() => {
61
+ switch (status) {
62
+ case 403:
63
+ return 'Permission required'
64
+ case 404:
65
+ return 'Content not found'
66
+ default:
67
+ return 'Oops!'
68
+ }
69
+ }, [status])
70
+
71
+ const body = useMemo(() => {
72
+ switch (status) {
73
+ case 403:
74
+ return 'Contact your administrator for access.'
75
+ case 404:
76
+ return 'If you believe something should be here, please reach out to your administrator.'
77
+ default:
78
+ return 'Something unexpected happened.'
79
+ }
80
+ }, [status])
81
+
82
+ useEffect(() => {
83
+ if (status !== 401) return
84
+
85
+ return onAuthRefresh(onReset)
86
+ }, [status, onReset])
87
+
88
+ return <ErrorContent heading={heading} body={body} />
89
+ }
90
+
91
+ function ErrorContent({ heading, body }: { heading: string; body: string }) {
92
+ const navigation = useNavigation()
93
+
94
+ return (
95
+ <BlankState.Root>
96
+ <BlankState.Imagery name="people.noTextMessage" />
97
+ <BlankState.Content>
98
+ <BlankState.Heading>{heading}</BlankState.Heading>
99
+ <BlankState.Text>{body}</BlankState.Text>
100
+ </BlankState.Content>
101
+ <BlankState.Button
102
+ title="Go back"
103
+ onPress={navigation.goBack}
104
+ size="md"
105
+ accessibilityRole="link"
106
+ />
107
+ <BlankState.TextButton onPress={() => navigation.navigate('BugReport')}>
108
+ Report a bug
109
+ </BlankState.TextButton>
110
+ </BlankState.Root>
111
+ )
112
+ }
@@ -0,0 +1,38 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { ApiResource, ResourceObject } from '../../types'
3
+ import { useApiClient } from '../use_api_client'
4
+
5
+ const STALE_TIME_MS = 25 * 60 * 1000 // under Groups' 30-min server-side expiry
6
+
7
+ interface Props {
8
+ groupId: number
9
+ enabled?: boolean
10
+ }
11
+
12
+ export function useGroupChatConversationPayload({ groupId, enabled = true }: Props) {
13
+ const apiClient = useApiClient()
14
+ const url = `/me/groups/${groupId}/chat_conversation_payload`
15
+
16
+ const { data, ...rest } = useQuery({
17
+ queryKey: ['groups', url],
18
+ queryFn: () =>
19
+ apiClient.groups.post<ApiResource<GroupChatConversationPayload>>({
20
+ url,
21
+ data: {
22
+ data: {
23
+ type: 'GroupChatConversationPayload',
24
+ attributes: { title: '' },
25
+ },
26
+ },
27
+ }),
28
+ staleTime: STALE_TIME_MS,
29
+ enabled,
30
+ })
31
+
32
+ return { payload: data?.data.value, ...rest }
33
+ }
34
+
35
+ interface GroupChatConversationPayload extends ResourceObject {
36
+ type: 'GroupChatConversationPayload'
37
+ value: string
38
+ }
@@ -1,6 +1,7 @@
1
- import { MemberBadge, MemberResource, ResourceObject } from '../../types'
1
+ import { useMemo } from 'react'
2
+ import { MemberResource, ResourceObject } from '../../types'
2
3
  import { GroupsGroupMemberResource } from '../../types/resources/groups/groups_member_resource_with_person'
3
- import { useCurrentPerson } from '../use_current_person'
4
+ import { useEnrichPeople } from '../use_enrich_people'
4
5
  import { useSuspensePaginator } from '../use_suspense_api'
5
6
 
6
7
  type UseSuspensePaginatorResult<T extends ResourceObject> = ReturnType<
@@ -16,10 +17,6 @@ export type GroupMembersForNewConversationResult = Omit<
16
17
  childMembers: MemberResource[]
17
18
  }
18
19
 
19
- /**
20
- * This is specifically for the new conversation screen because we assign
21
- * the "Conversation owner" badge to the current person.
22
- */
23
20
  export function useGroupMembersForNewConversation({
24
21
  id,
25
22
  gender,
@@ -27,7 +24,6 @@ export function useGroupMembersForNewConversation({
27
24
  id: number
28
25
  gender: string | null
29
26
  }) {
30
- const currentPerson = useCurrentPerson()
31
27
  const response = useSuspensePaginator<GroupsGroupMemberResource>({
32
28
  url: `/me/groups/${id}/memberships`,
33
29
  data: {
@@ -43,12 +39,16 @@ export function useGroupMembersForNewConversation({
43
39
  })
44
40
 
45
41
  const { data: memberships = [] } = response
42
+ const personIds = useMemo(() => memberships.map(m => +m.person.id), [memberships])
43
+ const enrichmentMap = useEnrichPeople({ personIds, groupId: id })
44
+
46
45
  const members: MemberResource[] = memberships.map(membership => {
47
46
  const { person } = membership
47
+ const enrichment = enrichmentMap.get(+person.id)
48
48
  return {
49
49
  type: 'Member',
50
50
  avatar: person.avatarUrl,
51
- badges: buildBadges(membership, currentPerson.id),
51
+ badges: enrichment?.badges ?? [],
52
52
  child: person.child,
53
53
  gender: person.gender,
54
54
  id: +person.id,
@@ -57,6 +57,7 @@ export function useGroupMembersForNewConversation({
57
57
  role: membership.role,
58
58
  }
59
59
  })
60
+
60
61
  const adultMembers = members.filter(member => !member.child)
61
62
  const childMembers = members.filter(member => member.child)
62
63
 
@@ -67,18 +68,3 @@ export function useGroupMembersForNewConversation({
67
68
  childMembers,
68
69
  }
69
70
  }
70
-
71
- function buildBadges(
72
- membership: GroupsGroupMemberResource,
73
- currentPersonId: number
74
- ): MemberBadge[] {
75
- const { person } = membership
76
- const badges = []
77
- if (membership.role === 'leader') {
78
- badges.push({ title: 'Leader' })
79
- }
80
- if (person.id === currentPersonId) {
81
- badges.push({ title: 'Conversation owner' })
82
- }
83
- return badges
84
- }
@@ -24,7 +24,7 @@ export function useGroupsConversationCreate({ groupId, title, genderId, onSucces
24
24
  url: `/me/groups/${groupId}/chat_conversation_payload`,
25
25
  data: {
26
26
  data: {
27
- type: '',
27
+ type: 'GroupChatConversationPayload',
28
28
  attributes: {
29
29
  title,
30
30
  },
@@ -1,6 +1,7 @@
1
1
  export * from './use_animated_message_background_color'
2
- export * from './use_api_client'
2
+ export { useApiClient } from './use_api_client'
3
3
  export * from './use_api'
4
+ export * from './use_app_state'
4
5
  export * from './use_async_storage'
5
6
  export * from './use_at_font_scale_breakpoint'
6
7
  export * from './use_chat_permissions'
@@ -26,14 +26,7 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
26
26
  const apiClient = useApiClient()
27
27
 
28
28
  const teamAndPlanParams: TeamAndPlanParams = useMemo(
29
- () =>
30
- omitBy(
31
- {
32
- team_id: teamIds?.join(',') || null,
33
- plan_id: planId,
34
- },
35
- isNil
36
- ),
29
+ () => buildTeamAndPlanParams(teamIds, planId),
37
30
  [teamIds, planId]
38
31
  )
39
32
 
@@ -49,16 +42,23 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
49
42
  onSuccess: ({ conversation, created }) => {
50
43
  onSuccess?.(conversation, { created })
51
44
  },
52
- mutationFn: async () => findOrCreateServicesConversation(apiClient, teamAndPlanParams),
45
+ mutationFn: async () =>
46
+ findOrCreateServicesConversation({ apiClient, teamIds: teamIds ?? [], planId }),
53
47
  })
54
48
 
55
49
  return { ...mutation, selectionHasConversation, isLoadingConversationCheck }
56
50
  }
57
51
 
58
- export const findOrCreateServicesConversation = async (
59
- apiClient: ApiClient,
60
- teamAndPlanParams: TeamAndPlanParams
61
- ): Promise<{ conversation: ConversationResource; created: boolean }> => {
52
+ export const findOrCreateServicesConversation = async ({
53
+ apiClient,
54
+ teamIds,
55
+ planId,
56
+ }: {
57
+ apiClient: ApiClient
58
+ teamIds: number[]
59
+ planId?: number
60
+ }): Promise<{ conversation: ConversationResource; created: boolean }> => {
61
+ const teamAndPlanParams = buildTeamAndPlanParams(teamIds, planId)
62
62
  const foundConversations = await getGroupIdsFromServices(apiClient, teamAndPlanParams)
63
63
  .then(res => res.data.groupIdentifiers)
64
64
  .then(groupIdentifiers => findConversationWithExactTeams(apiClient, groupIdentifiers))
@@ -69,24 +69,27 @@ export const findOrCreateServicesConversation = async (
69
69
  return { conversation: foundConversation, created: false }
70
70
  }
71
71
 
72
- return fetchServicesPayload(apiClient, teamAndPlanParams)
72
+ return getServicesChatConversationPayload({ apiClient, teamIds, planId })
73
73
  .then(res => res.data.payload)
74
74
  .then(payload => createConversation(apiClient, payload))
75
75
  .then(res => ({ conversation: res.data, created: true }))
76
76
  .catch(throwResponseError)
77
77
  }
78
78
 
79
- export const fetchServicesPayload = (
80
- apiClient: ApiClient,
81
- teamAndPlanParams: TeamAndPlanParams
82
- ) => {
79
+ export const getServicesChatConversationPayload = ({
80
+ apiClient,
81
+ teamIds,
82
+ planId,
83
+ }: {
84
+ apiClient: ApiClient
85
+ teamIds: number[]
86
+ planId?: number
87
+ }) => {
83
88
  return apiClient.services.get({
84
- url: `/chat`,
89
+ url: '/chat',
85
90
  data: {
86
- ...teamAndPlanParams,
87
- fields: {
88
- Chat: ['payload'],
89
- },
91
+ ...buildTeamAndPlanParams(teamIds, planId),
92
+ fields: { Chat: ['payload'] },
90
93
  },
91
94
  }) as Promise<ApiResource<ServicesChatPayloadResource>>
92
95
  }
@@ -157,7 +160,7 @@ export const checkIfConversationWithGroupExists = (
157
160
  export const buildTeamAndPlanParams = (teamIds: number[] = [], planId?: number) => {
158
161
  return omitBy(
159
162
  {
160
- team_id: teamIds.join(','),
163
+ team_id: teamIds.length ? teamIds.join(',') : null,
161
164
  plan_id: planId,
162
165
  },
163
166
  isNil
@@ -0,0 +1,26 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useApiClient } from '../use_api_client'
3
+ import { getServicesChatConversationPayload } from './use_find_or_create_services_conversation'
4
+
5
+ const STALE_TIME_MS = 55 * 60 * 1000 // under Services' 1-hour server-side expiry
6
+
7
+ interface Props {
8
+ teamIds: number[]
9
+ planId?: number
10
+ enabled?: boolean
11
+ }
12
+
13
+ export function useServicesChatConversationPayload({ teamIds, planId, enabled = true }: Props) {
14
+ const apiClient = useApiClient()
15
+ const sortedTeamIds = [...teamIds].sort((a, b) => a - b)
16
+
17
+ const { data, ...rest } = useQuery({
18
+ queryKey: ['services', '/chat', { teamIds: sortedTeamIds, planId }],
19
+ queryFn: () =>
20
+ getServicesChatConversationPayload({ apiClient, teamIds: sortedTeamIds, planId }),
21
+ staleTime: STALE_TIME_MS,
22
+ enabled: enabled && sortedTeamIds.length > 0,
23
+ })
24
+
25
+ return { payload: data?.data.payload, ...rest }
26
+ }
@@ -2,6 +2,7 @@ import { isNil, omitBy } from 'lodash'
2
2
  import { useMemo } from 'react'
3
3
  import { MemberResource, TeamPeopleResource, TeamPersonResponseItem } from '../../types'
4
4
  import { useApiGet } from '../use_api'
5
+ import { useEnrichPeople } from '../use_enrich_people'
5
6
 
6
7
  interface Props {
7
8
  teamIds: number[]
@@ -23,14 +24,24 @@ export function useTeamMembersForNewConversation({ teamIds, planId }: Props) {
23
24
  })
24
25
 
25
26
  const people = data?.people || stableEmptyPersonArray
27
+ const personIds = useMemo(() => people.map(p => p.id), [people])
28
+ const enrichmentMap = useEnrichPeople({ personIds })
26
29
 
27
- const members: MemberResource[] = useMemo(() => {
28
- return people.map(person => ({
29
- ...person,
30
- type: 'Member',
31
- gender: null,
32
- }))
33
- }, [people])
30
+ const members: MemberResource[] = useMemo(
31
+ () =>
32
+ people.map(person => {
33
+ const enrichedBadges = enrichmentMap.get(person.id)?.badges ?? []
34
+ const existingTitles = new Set(person.badges.map(b => b.title))
35
+ const newBadges = enrichedBadges.filter(b => !existingTitles.has(b.title))
36
+ return {
37
+ ...person,
38
+ type: 'Member',
39
+ gender: null,
40
+ badges: [...person.badges, ...newBadges],
41
+ }
42
+ }),
43
+ [people, enrichmentMap]
44
+ )
34
45
 
35
46
  return {
36
47
  members,
@@ -0,0 +1,45 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { ApiResource, ConversationValidateResource } from '../types'
3
+ import { useApiClient } from './use_api_client'
4
+
5
+ interface Props {
6
+ payload: string | undefined
7
+ isLoadingPayload?: boolean
8
+ genderId?: string | null
9
+ enabled?: boolean
10
+ }
11
+
12
+ export function useConversationValidate({
13
+ payload,
14
+ isLoadingPayload = false,
15
+ genderId,
16
+ enabled = true,
17
+ }: Props) {
18
+ const apiClient = useApiClient()
19
+
20
+ const { data, isLoading } = useQuery({
21
+ queryKey: ['chat', '/me/conversation_validate', { payload, genderId }],
22
+ queryFn: () =>
23
+ apiClient.chat.post<ApiResource<ConversationValidateResource>>({
24
+ url: '/me/conversation_validate',
25
+ data: {
26
+ data: {
27
+ type: 'ConversationValidate',
28
+ attributes: {
29
+ payload: payload!,
30
+ ...(genderId ? { gender_id: genderId } : {}),
31
+ },
32
+ },
33
+ },
34
+ }),
35
+ enabled: enabled && payload != null,
36
+ retry: false,
37
+ })
38
+
39
+ const warnings = data?.data.warnings ?? []
40
+
41
+ return {
42
+ warnings,
43
+ validationPending: enabled && (isLoadingPayload || isLoading),
44
+ }
45
+ }
@@ -0,0 +1,35 @@
1
+ import { keepPreviousData, useQuery } from '@tanstack/react-query'
2
+ import { useMemo } from 'react'
3
+ import { ApiCollection, ResourceObject } from '../types'
4
+ import { useApiClient } from './use_api_client'
5
+
6
+ export interface PersonEnrichmentResource extends ResourceObject {
7
+ type: 'PersonEnrichment'
8
+ id: string
9
+ badges: { title: string }[]
10
+ }
11
+
12
+ export function useEnrichPeople({ personIds, groupId }: { personIds: number[]; groupId?: number }) {
13
+ const apiClient = useApiClient()
14
+
15
+ const { data } = useQuery<ApiCollection<PersonEnrichmentResource>>({
16
+ queryKey: ['enrich_people', [...personIds].sort((a, b) => a - b), groupId],
17
+ queryFn: () =>
18
+ apiClient.chat.post<ApiCollection<PersonEnrichmentResource>>({
19
+ url: '/enrich_people',
20
+ data: {
21
+ data: {
22
+ type: 'PersonEnrichment',
23
+ attributes: {
24
+ person_ids: personIds,
25
+ ...(groupId !== undefined ? { group_id: groupId } : {}),
26
+ },
27
+ },
28
+ },
29
+ }),
30
+ enabled: personIds.length > 0,
31
+ placeholderData: keepPreviousData,
32
+ })
33
+
34
+ return useMemo(() => new Map(data?.data.map(e => [+e.id, e]) ?? []), [data])
35
+ }
@@ -5,6 +5,8 @@ import type { FeatureResource } from '../types/resources/feature_resource'
5
5
  import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
6
6
  import { useApiClient } from './use_api_client'
7
7
 
8
+ type FeatureName = (typeof availableFeatures)[keyof typeof availableFeatures]
9
+
8
10
  export function useFeatures() {
9
11
  const apiClient = useApiClient()
10
12
  const requestArgs = getFeaturesRequestArgs()
@@ -22,7 +24,7 @@ export function useFeatures() {
22
24
  const features = data.data
23
25
 
24
26
  const featureEnabled = useCallback(
25
- (featureName: string) =>
27
+ (featureName: FeatureName) =>
26
28
  features.some(feature => feature.name === featureName && feature.enabled),
27
29
  [features]
28
30
  )
@@ -39,7 +41,8 @@ export const availableFeatures = {
39
41
  granular_notifications_ui: 'ROLLOUT_granular_notification_preferences_ui',
40
42
  custom_conversation_avatars: 'ROLLOUT_custom_conversation_avatars',
41
43
  jump_to_unread: 'ROLLOUT_jump_to_unread',
42
- }
44
+ conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
45
+ } as const satisfies Record<string, `ROLLOUT_${string}`>
43
46
 
44
47
  const stableEmptyFeatures: ApiCollection<FeatureResource> = {
45
48
  data: [],